1. 什么是 URL 短链
URL 短链,就是把原来较长的网址,转换成比较短的网址。我们可以在短信和微博里可以经常看到短链的身影。如下图:
上图所示短信中,蓝色链接就是一条短链。 用户点击蓝色的短链,就可以在浏览器中看到它对应的原网址
那么为什么要做这样的转换呢?来看看短链带来的好处:
- 在微博, Twitter 这些限制字数的应用中,短链带来的好处不言而喻: 网址短、美观、便于发布、传播,可以写更多有意义的文字;
- 在短信中,如果含长网址的短信内容超过 70 字,就会被拆成两条发送,而用短链则可能一条短信就搞定,如果短信量大也可以省下不少钱;
- 我们平常看到的二维码,本质上也是一串 URL ,如果是长链,对应的二维码会密密麻麻,扫码的时候机器很难识别,而短链则不存在这个问题;
- 出于安全考虑,不想让有意图的人看到原始网址。
2. 库表设计
短链表
CREATE TABLE `short_url` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`long_url` varchar(500) DEFAULT NULL COMMENT '长链接',
`short_url` varchar(30) NOT NULL COMMENT '短链接',
`title` varchar(100) DEFAULT NULL COMMENT '短链名称',
`deleted` int(11) DEFAULT '0' COMMENT '逻辑删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`punter` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_short_url` (`short_url`) USING BTREE,
KEY `idx_long_url` (`long_url`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='短链列表';
访问表
CREATE TABLE `short_url_access` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`short_url` varchar(30) DEFAULT NULL COMMENT '短链接',
`access_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '访问时间',
`ip` varchar(30) DEFAULT NULL COMMENT '访问ip',
`device` varchar(30) DEFAULT NULL COMMENT '设备型号',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_short_url` (`short_url`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='短链访问列表';
3.相关代码
只贴核心代码,其他代码自行编写
重定向Controller
/**
* 短链重定向
*
* @param mav 视图
* @param shortUrl 短链
* @return 重定向
*/
@RequestMapping(value = {"/redirect/{shortUrl}", "/redirect"})
public ModelAndView redirect(@PathVariable(value = "shortUrl", required = false) String shortUrl, ModelAndView mav,
HttpServletRequest httpServletRequest) {
return shortUrlService.redirect(mav, shortUrl, httpServletRequest);
}
重定向Service实现
/**
* 重定向根路径
*/
final String BASE_URL = "";
/**
* 路径错误地址
*/
final String ERROR_URL = "";
/**
* 重定向前缀
*/
final String REDIRECT_PREFIX = "redirect:";
/**
* 重定向
*
* @param mav 视图模型信息
* @param shortUrl 短链地址
* @param httpServletRequest 请求信息
* @return 结果
*/
@Override
public ModelAndView redirect(ModelAndView mav, String shortUrl, HttpServletRequest httpServletRequest) {
ShortUrl shortUrlData = getOne(new LambdaQueryWrapper<ShortUrl>().eq(ShortUrl::getShortUrl, BASE_URL + shortUrl));
// 成功重定向到该地址
if (Objects.nonNull(shortUrlData)) {
// 插入访问记录
shortUrlAccessService.save(new ShortUrlAccess()
.setShortUrl(BASE_URL + shortUrl)
.setIp(IpUtils.getIpAddr(httpServletRequest))
.setDevice(getDeviceInfo(httpServletRequest.getHeader("User-Agent"))));
mav.setViewName(REDIRECT_PREFIX + shortUrlData.getLongUrl());
return mav;
}
mav.setViewName(REDIRECT_PREFIX + ERROR_URL);
return mav;
}
/**
* 创建短链
*
* @return 短链
*/
private String createShortUrl() {
List<String> shortUrlList = list(new LambdaQueryWrapper<ShortUrl>()
.select(ShortUrl::getShortUrl)).stream()
.map(item -> item.getShortUrl().replace(BASE_URL, ""))
.collect(Collectors.toList());
// 这里的逻辑先简单判断,如果后期用户多了需要加锁
String shortUrl = IdUtil.nanoId(6);
if (shortUrlList.contains(shortUrl)) {
int i = 0;
int max = 100;
do {
if (i == max) {
throw new ServiceException("生成短链失败,请稍后重试");
}
shortUrl = IdUtil.nanoId(6);
i++;
} while (!shortUrlList.contains(shortUrl));
}
return shortUrl;
}
/**
* 获取设备信息
*
* @param userAgent 请求头信息
* @return 结果
*/
public String getDeviceInfo(String userAgent) {
if (userAgent.contains("iPhone")) {
return "iPhone";
} else if (userAgent.contains("Android")) {
return "Android";
} else if (userAgent.contains("Windows")) {
return "windows";
}
return "未知设备";
}
统计访问数据
/**
* 查询访问信息
*
* @param shortUrlAccess 对象
* @return 结果
*/
@Override
public List<ShortUrlAccessInfoVO> queryAccessInfo(ShortUrlAccess shortUrlAccess) {
Map<String, Object> params = shortUrlAccess.getParams();
DateTime beginAccessTime = DateUtil.parseDate(params.get("beginAccessTime").toString());
DateTime endAccessTime = DateUtil.parseDate(params.get("endAccessTime").toString());
// 初始化参数,判断单位为小时或者天
long between = DateUtil.between(beginAccessTime, endAccessTime, DateUnit.DAY);
boolean isHour = Objects.equals(1L, between);
Function<? super ShortUrlAccess, ? extends String> groupFunction;
Function<String, String> formatFunction;
LinkedList<String> timeList;
if (isHour) {
groupFunction = item -> String.valueOf(DateUtil.hour(item.getAccessTime(), true));
formatFunction = item -> item + "点";
timeList = IntStream.rangeClosed(0, 23)
.mapToObj(String::valueOf)
.collect(Collectors.toCollection(LinkedList::new));
} else {
groupFunction = item -> DateUtil.format(item.getAccessTime(), DatePattern.NORM_DATE_PATTERN);
formatFunction = Function.identity();
timeList = DateUtil.rangeToList(beginAccessTime, DateUtil.offsetDay(endAccessTime, -1), DateField.DAY_OF_MONTH).stream()
.map(item -> DateUtil.format(item, DatePattern.NORM_DATE_PATTERN))
.collect(Collectors.toCollection(LinkedList::new));
}
return this.buildAccessInfo(shortUrlAccess, groupFunction, formatFunction, beginAccessTime, endAccessTime, timeList);
}
/**
* 生成访问数据
*
* @param shortUrlAccess 对象
* @param groupFunction 分组函数
* @param formatFunction 格式化函数
* @param beginAccessTime 开始时间
* @param endAccessTime 结束时间
* @param timeList 时间列表
* @return 结果
*/
private List<ShortUrlAccessInfoVO> buildAccessInfo(ShortUrlAccess shortUrlAccess,
Function<? super ShortUrlAccess, ? extends String> groupFunction,
Function<String, String> formatFunction,
DateTime beginAccessTime,
DateTime endAccessTime,
LinkedList<String> timeList) {
// 指定短链指定时间段内的访问记录
List<ShortUrlAccess> shortUrlAccessList = list(new LambdaQueryWrapper<ShortUrlAccess>()
.eq(ShortUrlAccess::getShortUrl, shortUrlAccess.getShortUrl())
.between(ShortUrlAccess::getAccessTime, beginAccessTime, endAccessTime));
// 生成不同ip第一次访问的时间集合,用于后面求uv
Map<String, Optional<ShortUrlAccess>> firstAccessMap = shortUrlAccessList.stream()
.collect(Collectors.groupingBy(ShortUrlAccess::getIp, Collectors.minBy(Comparator.comparing(ShortUrlAccess::getAccessTime))));
// 小时/日分组
Map<String, List<ShortUrlAccess>> groupMap = shortUrlAccessList.stream()
.collect(Collectors.groupingBy(groupFunction));
return timeList.stream()
.map(index -> {
// 获取当前小时访问记录
List<ShortUrlAccess> accessList = groupMap.getOrDefault(index, Collections.emptyList());
// 如果ip第一次访问则算一次访客,否则不记
Integer uv = accessList.stream()
.map(item -> {
Optional<ShortUrlAccess> firstAccess = firstAccessMap.get(item.getIp());
return Objects.equals(item.getId(), firstAccess.orElse(new ShortUrlAccess()).getId()) ? 1 : 0;
})
.reduce(Integer::sum).orElse(0);
// 封装返回值
ShortUrlAccessInfoVO vo = new ShortUrlAccessInfoVO();
vo.setTime(formatFunction.apply(index));
vo.setPv(accessList.size());
vo.setUv(uv);
return vo;
})
.collect(Collectors.toList());
}
4.效果
短链页面
标签:COMMENT,return,Springboot,shortUrl,param,简易,item,短链 From: https://www.cnblogs.com/Linzj5950/p/18659118统计页面