首页 > 其他分享 >Springboot 实现简易短链功能

Springboot 实现简易短链功能

时间:2025-01-08 10:12:00浏览次数:6  
标签:COMMENT return Springboot shortUrl param 简易 item 短链

1. 什么是 URL 短链

URL 短链,就是把原来较长的网址,转换成比较短的网址。我们可以在短信和微博里可以经常看到短链的身影。如下图:

image

上图所示短信中,蓝色链接就是一条短链。 用户点击蓝色的短链,就可以在浏览器中看到它对应的原网址

那么为什么要做这样的转换呢?来看看短链带来的好处:

  • 在微博, 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.效果

短链页面

image

统计页面

image

标签:COMMENT,return,Springboot,shortUrl,param,简易,item,短链
From: https://www.cnblogs.com/Linzj5950/p/18659118

相关文章

  • 基于SpringBoot的斯诺克球馆预约购票管理系统
    作者:计算机学姐开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码精品专栏:Java精选实战项目源码、Python精选实战项目源码、大数据精选......
  • 2025毕设springboot 《计算机网络》课程学习网站论文+源码
    系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,教育领域正经历着深刻的变革。在线学习作为一种新兴的教育模式,以其灵活、便捷的特点,受到了广大师生的青睐。特别是在《计算机网络》这类理论与实践并重的课程中,学生往往需要在课外时间进行深入的自主学习和实......
  • 2025毕设springboot 《花间故里》论文+源码
    系统程序文件列表开题报告内容研究背景《花间故里》这一毕业设计题目源于对现代都市人精神需求的深刻洞察。随着生活节奏的加快,人们愈发向往自然与宁静,渴望在繁忙之余寻得一处心灵的栖息地。鲜花,作为大自然的使者,不仅美化环境,更以其独特的韵味抚慰人心。然而,传统花卉市场受......
  • 基于SpringBoot在线课程管理系统的设计与实现-毕业设计-附源码
    文末获取源码和万字论文,制作不易,感谢点赞支持。图片无法加载的情况可看文末私我获取基于SpringBoot在线课程管理系统的设计与实现摘要本文首先介绍了在线课程管理系统的现状及开发背景,然后论述了系统的设计目标、系统需求、总体设计方案以及系统的详细设计和实现,最后对......
  • 基于java的SpringBoot/SSM+Vue+uniapp的工贸学生信息管理系统的详细设计和实现(源码+l
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • springboot毕设 基于JavaWeb的博客网 程序+论文
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展和信息时代的到来,个人表达与知识分享已成为网络文化中不可或缺的一部分。博客,作为一种集个人日记、文章发布、观点交流于一......
  • SpringBootWeb案例-1(day10)
    准备工作需求&环境搭建需求说明环境搭建步骤:准备数据库表(dept、emp)创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)配置文件application.properties中引入mybatis的配置信息,准备对应的实体类准备对应的Mapper、Service(接口、实现......
  • springboot基于Hadoop的影片推荐系统
    收藏关注不迷路!!......
  • SpringBoot企业公文管理系统9i9to(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表员工,领导,归档人员,行政人员,公文种类,公文信息,督办人员,督办催办,公文进度,公文完结,公文归档开题报告内容一、研究背景与意义随着信息技术的快速发展,企业与......
  • SpringBoot企业工资管理系统r9a51(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表员工,财务,项目名称,财务项目,工资信息开题报告内容一、研究背景在现代企业中,薪资管理是人力资源管理的重要组成部分,直接影响到员工的工作积极性和企业的运营成......