ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Spring Boot+Vue的短链接管理系统:架构、算法与部署实战

基于Spring Boot+Vue的短链接管理系统:架构、算法与部署实战 简介基于Spring BootVue前后端分离架构实现的短链接生成管理系统完整包含源码与数据库脚本面向正在学习SpringBoot、MyBatis、JWT、Spring Security及Vue技术整合的学生或初级开发者适用于课程设计、毕业设计或需要搭建短链接服务的内部工具场景。系统除登录认证外还提供短链接生成与管理、访问日志、统计面板等功能可统计总连接数、今日及昨日点击量和IP数、近七日访问趋势并通过ECharts展示操作系统、浏览器及访问量图表同时集成用户、角色、菜单等权限管理模块。资源包为zip压缩格式共321个文件约1.19MB主要包含97个vue页面、74个js脚本、86个svg图标、18个java类、5个yml配置及SQL脚本等前端页面与后端代码分离便于按模块阅读和部署附带启动和打包脚本可辅助快速运行。通过这套代码还可以学习JWT认证、Spring Security权限控制、MyBatis持久化及Vue与ECharts图表整合便于理解真实项目中的前后端协作方式。目前已有77人学习下载适合作为完整前后端分离项目案例进行二次开发或学习参考。1. 短链接生成管理系统到底是个什么样的项目从一次实际营销截流场景说起前两年给一个做线下地推的客户搭过一套投放追踪系统核心需求很简单把一大批带着渠道参数的长链接缩短方便印在传单和二维码上同时还能统计每个渠道扫进来多少人。当时现成的短链服务不是不能用而是数据在别人手里想按渠道、按批次、按失效日期做自定义管理就得自己造一个轮子。这就是标题里这个项目要做的事用 Spring Boot 提供后端接口和跳转服务用 Vue 搭建管理后台通过前后端分离架构把「生成短码、管理链接、统计访问」整条链路串起来。对正在做毕设、求职项目或者公司内部工具的人来说它最大的价值不是那个 62 进制的算法而是一个完整的、能部署能交付的工程骨架。源码加数据库的交付形态本质上是把「能跑」作为最低标准真正要看的反而是架构选型和表设计是否经得起追问。2. 前后端分离架构与数据库设计这套系统为什么不能做成单体 JSP市面上大量短链接教程还停留在 JSP Servlet 的阶段页面渲染交给后端模板管理端和跳转服务耦合在同一个 Web 应用里。标题既然明确了前后端分离那就要把「分离」的边界画清楚Spring Boot 只负责输出 JSON、处理跳转、保存数据Vue 负责页面渲染和用户操作两者通过 HTTP 接口通信。开发环境下前端用 Vite 代理避免跨域生产环境下用 Nginx 托管前端静态文件并反向代理 API 请求。这套结构的好处是跳转服务和管理后台可以独立扩容但代价是部署多了一个 Nginx 节点排查问题时要分清是前端路由问题还是后端接口问题。2.1 技术选型与分离边界谁管页面、谁管 API、浏览器怎么串联前后端分离的「分离」不是简单地把文件分开而是职责分离。Spring Boot 这边要做的只有三件事提供短码生成接口、提供短码跳转接口、提供管理端的增删改查接口。Vue 那边负责的是登录、短链接列表展示、创建表单、统计图表。浏览器访问流程是用户输入短链域名下的短码Nginx 把请求转发给 Spring Boot后者查数据库找到原始长链接返回 302 重定向浏览器跳到目标地址。我见过不少「伪前后端分离」的做法把 Vue 打包后的 dist 目录丢进 Spring Boot 的 static 目录然后让 Spring Boot 同时托管静态页面和 API。这种方案在开发时省事但一旦要独立部署管理端或者给跳转服务做多实例扩容就会遇到静态资源被服务端代码捆绑的问题。真正的分离架构应该是 Vue 的 dist 目录由 Nginx 托管Spring Boot 只暴露 /api 前缀的接口这样跳转服务可以横向扩展前端页面更新也不需要重启后端。开发环境的联通靠 Vite 的 proxy 配置解决。Vue 开发服务器跑在 5173 端口所有 /api 开头的请求代理到后端的 8080 端口浏览器里看起来是同源请求规避了跨域。这个配置要写对否则前端页面能打开、但所有接口都报 404 或者 CORS 错误。在调试接口时我一般先打开浏览器的 Network 面板看请求是否发出去、响应状态码是什么而不是急着查后端日志。2.2 数据库表结构设计三张核心表与索引的取舍短链接系统的数据量级通常不大但它有一个特点读多写少、跳转请求高频、管理操作低频。基于这个特点表结构设计要有针对性。交付包里带的数据库脚本一般包含三张核心表。第一张是短链接映射表负责记录短码、原始长链接、创建时间、过期时间、状态第二张是访问日志表记录每次跳转的 IP、User-Agent、访问时间第三张是用户表用来做管理端的登录认证。如果系统要支持多渠道投放还需要一个渠道字段或者单独的渠道表。短码字段必须加唯一索引这是防止并发生成重复短码的最后一道防线。过期时间字段要建普通索引因为后台管理页的列表查询经常会按「已过期/未过期」筛选。访问日志表不要和映射表放在同一个频繁查询的路径里它可以接受稍微慢一点的写入因为点击统计走的是异步落库。下面是核心表的建表 SQL这套结构可以直接拿来初始化数据库-- 短链接映射表 CREATE TABLE url_map ( id bigint NOT NULL AUTO_INCREMENT COMMENT 自增主键, short_code varchar(16) NOT NULL COMMENT 短码用于拼短链, long_url varchar(2048) NOT NULL COMMENT 原始长链接, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0有效 1失效, expire_time datetime DEFAULT NULL COMMENT 过期时间NULL表示永久有效, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_short_code (short_code), KEY idx_expire_time (expire_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短链接映射表; -- 访问日志表 CREATE TABLE visit_log ( id bigint NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL COMMENT 被访问的短码, ip varchar(64) DEFAULT NULL COMMENT 访问者IP, user_agent varchar(512) DEFAULT NULL COMMENT 浏览器UA, visit_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 访问时间, PRIMARY KEY (id), KEY idx_short_code_time (short_code, visit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT访问日志表;这段 SQL 有几个细节值得注意。long_url用varchar(2048)而不是text因为大多数投放链接不会超过这个长度避免在索引和查询时出现隐式转换问题。short_code用varchar(16)是因为 62 进制编码在 long 类型范围内最长也就 11 位左右留 16 位是为了将来扩展短码池。visit_log的联合索引(short_code, visit_time)是为了支撑「查某个短码的访问趋势」这类高频统计查询单独查 short_code 时走左前缀也能命中。初始化数据库时要注意 utf8mb4 字符集否则存入 emoji 或特殊符号的长链接会出现乱码。MySQL 8.0 默认字符集已经是 utf8mb4但如果是 5.7 的老库建表时一定要显式指定。导入交付包里的 SQL 脚本后建议先跑一遍SHOW CREATE TABLE url_map确认索引和字符集都正确再启动 Spring Boot 应用。2.3 后端配置与实践技巧从 application.yml 到表同步策略Spring Boot 的配置集中在application.yml里。需要关注三个点数据源配置、MyBatis 映射配置、server 端口设置。数据源配置要注意时区参数否则数据库服务和应用服务器所在时区不一致时过期时间会偏移 8 小时导致链接提前失效或者永远不过期。我踩过这个坑查了半天发现是连接串少了serverTimezoneAsia/Shanghai。spring: datasource: url: jdbc:mysql://localhost:3306/short_link?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 server: port: 8080 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true参数说明serverTimezoneAsia/Shanghai解决时区偏移map-underscore-to-camel-case开启驼峰映射让数据库的create_time直接映射到实体的createTime字段省去手写 ResultMaplog-impl开启 SQL 日志调试时能看到 MyBatis 实际执行的语句。Redis 配置在初始阶段可以不启用但后续做高频短码缓存和短码池预热时会用到。在实际交付的源码包里数据库脚本一般包含初始管理员账号密码是 BCrypt 加密后的值。拿到源码第一件事是改默认密码不要带着初始账号上线。第二步是把数据库连接信息改成自己的环境变量不要把生产库密码直接写死在application.yml里。这两个习惯看着不起眼但在答辩或技术评审时会被追问。3. 短链生成与访问跳转62 进制算法、冲突策略和状态机短链接系统的核心功能是「把长链接变成短链接访问短链接时跳转到长链接」。这个过程的算法并不复杂但要把它做扎实需要考虑生成效率、冲突处理、过期失效三个问题。很多文章只贴一段 62 进制转换代码就算完事但实际生产环境中短码生成是高并发入口一旦出现重复短码或者生成性能瓶颈整个跳转服务都会受影响。3.1 自增 ID 转 62 进制从 Long 到短码的完整实现短链生成的常见做法是发号器生成一个全局唯一的自增 ID把这个 ID 转成 62 进制字符串数字 大小写字母作为短码。为什么用 62 进制而不是 10 进制因为同样的数字范围62 进制能大幅缩短字符串长度。比如 10 位数以内的自增 ID用 62 进制编码后通常只有 6-8 位落在短码 16 个字符的字段里绰绰有余。下面是一个标准的 Base62 编码实现核心是除模取余、反向排列public class Base62Encoder { private static final String BASE62_CHARS 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; public static String encode(long value) { if (value 0) { return String.valueOf(BASE62_CHARS.charAt(0)); } StringBuilder sb new StringBuilder(); while (value 0) { int remainder (int) (value % 62); sb.append(BASE62_CHARS.charAt(remainder)); value / 62; } return sb.reverse().toString(); } public static long decode(String shortCode) { long result 0; for (char c : shortCode.toCharArray()) { int index BASE62_CHARS.indexOf(c); result result * 62 index; } return result; } }参数说明BASE62_CHARS的排列顺序直接决定了编码结果一旦发布上线就不能改动顺序否则历史短码全部失效。encode里每次取模得到当前位的值从字符表中找到对应字符value / 62相当于右移一位最后reverse()是因为低位先被计算出来需要反转才能得到正确的从左到右表示。decode是反向操作用于 URL 请求解析时把短码还原成 ID 查询数据库。这段代码有一个容易被忽略的边界条件value 0时直接返回字符表第一位如果不做这个判断while 循环不会进入拿到的是空字符串。另外要注意的是 Java 的long是 64 位有符号整数发号器生成的 ID 不能为负数否则value % 62的结果可能是负数导致字符表下标越界。3.2 短码冲突的三种应对策略查重、唯一索引与短码池预热62 进制编码本身不会产生冲突但两个不同的长链接可能通过某种哈希算法得到相同的短码。如果系统用「长链接哈希 - 短码」的方式生成冲突概率会随数据量上升。更稳健的做法是把「发号器生成 ID - 转短码」作为主链路因为自增 ID 天然唯一不会冲突。但并发场景下仍然可能遇到问题如果短码是在 Spring Boot 应用内存里生成的两台服务器同时拿到相邻的 ID各自生成短码后插入数据库数据库的唯一索引会挡住重复插入但应用层需要处理这个异常。常见的兜底策略是捕获主键冲突异常后重新生成一个短码重试重试次数控制在 2-3 次超过就放弃。下面是一个带重试的生成逻辑public String generateShortCode(String longUrl) { for (int retry 0; retry 3; retry) { long id idGenerator.nextId(); String shortCode Base62Encoder.encode(id); try { urlMapMapper.insert(shortCode, longUrl); return shortCode; } catch (DuplicateKeyException e) { // 短码冲突重试一次 } } throw new RuntimeException(短码生成失败请稍后重试); }参数说明idGenerator.nextId()是发号器可以用数据库自增主键、Redis INCR、或者雪花算法。数据库自增最简单但每次插入都要等数据库返回主键高并发下吞吐有限Redis INCR 每秒十万级适合短链这种读多写少的场景雪花算法完全不依赖外部存储适合多实例部署。短码池预热这个技巧在极客风文章里经常看到实际做法是在系统空闲时预生成一批短码放到 Redis 队列里请求进来时直接从队列里取。这个方案能把生成接口的响应时间从几十毫秒压到几毫秒代价是预生成的短码无法保证时间连续性——如果你需要从短码上直接看出先后顺序就不适合用这种方案。3.3 跳转链路的实现与状态机301 还是 302这是一个数据问题短链接的表结构里有一个status字段和expire_time字段跳转接口的逻辑就是「查短码 - 判断状态和过期时间 - 重定向」。判断逻辑不复杂但重定向的状态码选择会直接影响统计数据的准确性。用 301 还是 302 的取舍是短链接系统最容易想当然的地方。301 是永久重定向浏览器会缓存这个跳转结果第二次访问时直接走本地缓存不再请求短链服务器——这意味着统计系统永远看不到第二次访问。302 是临时重定向每次访问都会经过短链服务器统计才完整。短链接系统的本质是追踪投放效果所以要选 302。如果做的是品牌短链、不关心统计301 可以减轻服务器压力但在这个项目里选 302。GetMapping(/{shortCode}) public void redirect(PathVariable String shortCode, HttpServletResponse response) { UrlMap urlMap urlMapMapper.selectByShortCode(shortCode); if (urlMap null) { response.setStatus(HttpStatus.NOT_FOUND.value()); return; } if (urlMap.getStatus() 1 || (urlMap.getExpireTime() ! null urlMap.getExpireTime().before(new Date()))) { response.setStatus(HttpStatus.GONE.value()); return; } response.setStatus(HttpStatus.FOUND.value()); response.setHeader(Location, urlMap.getLongUrl()); }这段代码里HttpStatus.FOUND就是 302Location头是跳转目标。映射表的status字段和expire_time字段共同决定链接是否可用这两个字段在管理端有对应的操作手动失效、设置过期时间、恢复有效。把这两者的判断组合起来就构成了短链接的生命周期状态机创建时有效到了过期时间自动失效被手动停用则立即失效重新启用则状态恢复。这种设计不用定时任务去批量扫表改状态而是请求时实时判断简单且不会漏掉任何一条。跳转接口要注意编码问题。有的长链接带中文参数或者空格存入数据库后浏览器直接跳转可能报错。标准做法是在存储对长链接做 URL 编码处理跳转前检查是否包含非法字符。如果不做这个处理线下海报上的短码扫出来是乱码运营那边就要找你喝茶了。4. Vue 管理端与后端接口对接从开发联调到生产部署管理端的核心功能是登录、短链接列表展示、创建新短链、失效与删除、查看访问统计。Vue 工程负责把这些页面拼起来通过 axios 调后端接口。这一章的重点不是教 Vue 基础语法而是把前后端联调的几个关键节点理清楚。4.1 用 Vite 创建 Vue 工程并配置 axios 请求实例Vue 工程脚手架推荐用 Vite创建命令是npm create vitelatest short-link-admin -- --template vue然后安装路由和状态管理依赖。前后端联调的第一步是配置环境变量把 API 地址抽离出来避免在代码里写死。// .env.development VITE_API_BASE_URL/api // .env.production VITE_API_BASE_URLhttps://your-domain.com/api环境变量的作用是在开发和生产环境自动切换 API 地址。开发环境用相对路径/api配合 Vite 的代理转发到后端 8080生产环境用真实域名。axios 实例封装如下import axios from axios const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { // 登录失效跳回登录页 window.location.href /login } return Promise.reject(error) } ) export default request参数说明baseURL从环境变量读取开发和生产自动切换请求拦截器把登录后存在 localStorage 的 token 加到请求头响应拦截器统一处理 401 跳转登录页。这里要注意一个常见的翻车点如果在拦截器里直接操作window.location.href跳转Vue Router 的状态会丢失刷新后路由栈就断了。更好的是用router.push(/login)但需要把 router 引入来避免循环依赖。登录接口返回 token 后前端要持久化到 localStorage 还是 sessionStorage取决于系统对「记住登录状态」的要求。管理后台一般用 sessionStorage 就够了关闭浏览器即失效减少 token 泄露风险。用 localStorage 的便利是下次打开免登录但安全性差一些。创建短链接的页面是一个典型的表单页输入长链接、选择是否设置过期时间、提交后展示生成的短码。这里有个细节生成短码的接口应该支持批量生成否则运营人员在后台一条条录入链接会崩溃。批量接口的入参是一个长链接数组返回短码数组对应关系靠数组下标维持。4.2 Nginx 部署配置托管静态文件、反向代理与路由回退Vue 工程npm run build后生成 dist 目录里面是纯静态文件。生产环境部署时把 dist 放到 Nginx 的 html 目录下配置反向代理把/api请求转发到 Spring Boot。核心配置如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/short-link-admin; index index.html; # 前端路由回退防止 Vue Router history 模式刷新 404 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段配置里try_files $uri $uri/ /index.html是 Vue Router history 模式的命脉。如果去掉这一行用户访问https://your-domain.com/dashboard时Nginx 会去找物理文件dashboard找不到就返回 404。加上这一行后Nginx 会在文件不存在时回退到index.html由 Vue Router 接管路由。location /api/里的proxy_pass末尾有没有斜杠差别很大。proxy_pass http://127.0.0.1:8080/api/会把请求路径原样转发到后端如果写成proxy_pass http://127.0.0.1:8080;则/api/short-code会变成/api/short-code还是/short-code取决于具体配置容易踩坑。我习惯在前后端把 API 路径统一定义前端请求/api/short-code后端 Controller 的 RequestMapping 也带/api前缀后端再把/api独立成 context-path这样转发规则最简单。还有一个生产环境常见的坑如果 Spring Boot 返回的 JSON 里包含中文字符而 Nginx 没有配置charset utf-8浏览器可能显示乱码。前端请求头里加Accept: application/json;charsetutf-8可以缓解更好的做法是在 Nginx 的 server 块加上charset utf-8;。5. 上线前后的避坑清单最容易翻车的三个位置每次给这类系统做上线前检查翻车点都集中在数据库、跳转状态码、前端路由这三个地方。5.1 短码查询超时问题出在索引类型而不是 SQL 语句现象短码列表页刷不出来接口响应时间高达几秒数据库 CPU 飙升。原因短码字段虽然建了唯一索引但如果表的字符集是utf8mb4而查询条件是utf8或者utf8mb4_binMySQL 可能无法使用索引触发全表扫描。另外short_code如果被设计成VARCHAR(16)且带索引但查询时传入了Long类型的短码 IDMySQL 会做隐式类型转换索引同样失效。解决排查时先执行EXPLAIN SELECT * FROM url_map WHERE short_code abc123查看type字段是否为const或ref。如果是ALL说明索引没生效。修复方式是统一字符集和排序规则并且在 Mapper 查询时显式传入字符串类型。我一般还会在短链跳转的查询上加一层 Redis 缓存热点短码根本不走数据库压力天然分散。5.2 301 重定向导致统计大量丢失换 302 后立即恢复现象管理后台的点击量和实际投放数据对不上差了一倍以上。原因跳转接口初期用了HttpStatus.MOVED_PERMANENTLY301浏览器第一次访问短码后把跳转缓存住了。用户第二次点击时浏览器直接走本地缓存短链服务器根本没收到请求自然记不到日志里。这和浏览器、App 内嵌 WebView 的缓存策略都有关微信内置浏览器对 301 缓存尤其激进。解决把跳转状态码统一改为HttpStatus.FOUND302。改完后可以在浏览器开发者工具里观察 Network 面板每次访问短码都应该看到请求发出并返回 302不能从disk cache直接读取。这个改动一行代码但直接影响整个统计系统的可信度。5.3 Vue 页面刷新后 404Nginx 缺少 try_files 回退现象从列表页点击进入详情页正常但在详情页按 F5 刷新直接白屏 404。原因Vue Router 用的是createWebHistory模式刷新时浏览器请求的是/detail/abc123这个真实路径但 dist 目录下没有这个物理文件Nginx 返回 404。开发环境 Vite 内置的 dev server 自动处理了回退所以开发时发现不了问题。解决在 Nginx 的 location 块里配置try_files $uri $uri/ /index.html;让所有不存在的物理路径都回退到单页应用的入口。改完配置后用nginx -t检查语法再nginx -s reload生效。验证方法是刷新任意一个二级路由页面能正常显示就算通过。6. 从「短链接生成」到「可运营的短链中台」批量导入、二维码与统计补全如果把标题里的「管理系统」再往前推一步真正能落地的短链接平台至少还需要三个能力批量导入长链接、二维码生成、访问统计可视化。这三个能力不需要改架构现有表结构就能支撑。批量导入用 Excel 文件上传后端用 EasyExcel 解析逐行插入url_map表。注意数据校验长链接为空的行跳过、重复长链接可以映射到同一个短码提前在业务层做查重、批量插入要分批执行单批 500 条为宜避免一次性拼接超大 SQL。二维码生成不需要额外服务引入zxing库后端把短链转成二维码图片返回 Base64 字符串前端img标签直接渲染。访问统计的补全思路是点击日志落库后定时任务按short_code聚合生成日报表按日、按渠道统计访问量。不要直接在前端查visit_log全表去 Count数据量上去后必然卡死。聚合表加一个visit_date字段按天去重查询时走(short_code, visit_date)联合索引。验证这套系统是否达标我通常跑三条命令curl -I http://localhost:8080/abc123看返回头是否含Location且状态码为 302curl -X POST http://localhost:8080/api/short-code -H Content-Type: application/json -d {longUrl:https://example.com}验证生成接口返回短码再登录管理后台创建一条带过期时间的短链等过期后访问确认返回 410。这三步走完核心链路就闭环了。我现在的习惯是给任何短链接项目都先画一张「请求路径图」浏览器到 Nginx、Nginx 到 Spring Boot、Spring Boot 到 MySQL/Redis、302 回浏览器。每一跳都标出端口和配置位置排障时按图索骥比对着报错日志瞎猜快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表