ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue短链接流量数据分析与可视化系统实战解析

SpringBoot+Vue短链接流量数据分析与可视化系统实战解析 做短链接流量分析这几年我见过太多项目只做“短链接跳转”而把数据统计当成摆设。标题里这个“短流量数据分析与可视化abo信息管理系统”实际就是一套把短链接生成、访问流量采集、统计分析、可视化展示串起来的完整闭环SpringBoot做后端接口和数据聚合Vue做前端图表看板MySQL存短链和埋点日志三件套一拼再配上定时汇总任务就是一个可以直接上手改、直接跑起来用的管理端项目。这篇文章我会从数据库设计、后端统计链路、前端可视化对接、本地运行部署四个方向把这个系统拆开来讲包括每一张表为什么这么建、访问日志怎么落库才不拖慢跳转、ECharts图表怎么跟接口数据对齐以及我在实际部署中踩过的一些坑。适合正在做Java课程设计、想练手全栈项目、或者确实有短链统计需求的同学直接照着抄。1. 项目整体架构与分析思路1.1 系统定位不只是“发个短链接”很多初学者看到“短流量数据分析与可视化”第一反应就是做短链接——把一个长URL压缩成短码用户访问时302跳回去完事。但真正有价值的部分在“流量”这两个字上每一次短链被点击访问来自哪个IP、哪个地区、什么设备、什么浏览器、几点几分这些数据攒下来之后能做什么能看出推广渠道效果、用户活跃时段、访问趋势走向。这个系统取名“ABO信息管理系统”更像是给内部运营或课程设计定制的管理后台。它要承载的核心职责有三块短码管理创建、禁用、删除、流量采集每次点击留痕、数据可视化PV/UV、来源、设备、趋势在一个页面里看全。数据流是典型的“产生—采集—存储—聚合—展示”链路用户访问短链产生日志后端异步写入MySQL定时任务把明细分记录汇总成每日统计Vue前端从聚合接口拿数据画图。1.2 为什么选SpringBoot Vue MySQL这套组合这套组合几乎是当前Java全栈项目的“标准答案”之所以它烂大街是因为它每个角色都踩在舒适区里SpringBoot负责把后端繁琐的配置收敛掉。内嵌Tomcat、自动装配、Starter机制三分钟就能起一个带接口的Web服务做这种管理端系统再合适不过。Vue负责把数据变成人看得懂的东西。尤其可视化部分需要大量DOM操作和图表更新Vue的响应式机制让数据和视图绑定后端返回新数据页面图表自动跟着变不需要手动操作DOM。MySQL负责把关系型数据存得明明白白。短链、用户、日志、汇总表之间有关系查询统计需要聚合MySQL的索引和GROUP BY完全扛得住这种量级——这个系统的访问量每天撑死几千几万条远没到需要上时序数据库或者列式存储的程度。有一个关键点必须说清楚这套组合真正值钱的地方不是某个框架多高级而是前后端如何约定接口格式、数据如何建模、统计如何分层。框架只是工具设计思路才是能迁移到任何项目里的东西。1.3 功能地图管理端要覆盖哪些页面拆开来看这个系统前端页面大概需要这么几个登录页基于用户表的账号密码认证区分管理员和普通用户。工作台/总览页顶部放核心指标卡片今日PV、今日UV、总链接数、近7日趋势中间放访问趋势折线图下面放渠道来源和终端分布。短链管理页表格展示所有短码支持新建短链、复制跳转地址、禁用、删除列表里能看到每个链接的累计访问量。访问明细页按时间范围查某条短链的全部点击记录分页展示IP、地区、UA、访问时间。统计分析页选一个短链、选一个时间范围看它的详细图表支持折线和柱状切换。这些页面不需要花里胡哨的设计Element UI的表格、卡片、表单组件拖上去配合ECharts的折线图、饼图、柱状图视觉效果和实用性就都有了。2. 数据库设计短链接统计的根基2.1 建表思路四张表不多不少这个项目的表结构我强烈建议按“实体表 明细流水表 汇总表”三层来设计而不是把所有内容塞进一张大表。第一张是用户表sys_userCREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录名, password varchar(128) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(64) DEFAULT NULL COMMENT 姓名, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色0管理员 1普通用户, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码一定不要存明文。这个项目如果接了Spring Security直接用BCryptPasswordEncoder如果只想轻量实现至少也要做加盐的SHA-256哈希千万别拿MD5裸存现在MD5撞库太容易了。第二张是短链信息表link_infoCREATE TABLE link_info ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL COMMENT 短码即链接后面的唯一标识, original_url varchar(2048) NOT NULL COMMENT 原始长链接, title varchar(128) DEFAULT NULL COMMENT 链接名称/备注, user_id bigint(20) NOT NULL COMMENT 创建人, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0禁用 1启用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, expire_time datetime DEFAULT NULL COMMENT 过期时间空为永久, PRIMARY KEY (id), UNIQUE KEY uk_short_code (short_code), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;短码是这张表的灵魂要全局唯一并且禁用short_code上的普通索引必须建唯一索引因为每次跳转都要WHERE short_code ?这样查。original_url用varchar(2048)是因为有些推广链接带一堆utm参数动不动就几百上千字符。第三张是访问明细流水表link_access_log这张表是流量分析的“原料”。我见过不少人把访问记录做得特别简陋只存一个访问时间和IP到后面想看设备分布、来源渠道的时候直接傻眼。所以在建表阶段就要把埋点字段想全CREATE TABLE link_access_log ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL COMMENT 被访问的短码, ip varchar(64) DEFAULT NULL COMMENT 访问者IP, region varchar(64) DEFAULT NULL COMMENT 解析后的地区, device_type varchar(16) DEFAULT NULL COMMENT 设备PC/MOBILE/TABLET, browser varchar(32) DEFAULT NULL COMMENT 浏览器类型, referer varchar(512) DEFAULT NULL COMMENT 来源页面, access_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 访问时间, PRIMARY KEY (id), KEY idx_short_code_time (short_code, access_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个设计细节值得强调表里没有存original_url只存short_code。为什么一是减少冗余跳转查询时本来就要先用short_code查到original_url日志表跟着存code就行二是统计时按code分组能精确到每一条短链不会因为URL里参数顺序不同而分成两行。第四张是每日汇总表link_daily_statsCREATE TABLE link_daily_stats ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL, stat_date date NOT NULL COMMENT 统计日期, pv int(11) NOT NULL DEFAULT 0 COMMENT 访问次数, uv int(11) NOT NULL DEFAULT 0 COMMENT 访客数同IP去重, ip_count int(11) NOT NULL DEFAULT 0 COMMENT 独立IP数, PRIMARY KEY (id), UNIQUE KEY uk_code_date (short_code, stat_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张汇总表可以没有直接让前端拿link_access_log实时GROUP BY也可以。但一旦明细数据到几十万条GROUP BY的响应时间会肉眼可见地变慢。有了每日汇总表查询趋势图就是SELECT * FROM link_daily_stats WHERE short_code? AND stat_date BETWEEN ? AND ?直接秒回。这就是典型的空间换时间思路用定时任务每天凌晨跑一次汇总白天所有报表查询全走汇总表。2.2 索引与查询优化小表也要用心很多人在小项目里从不考虑索引但碰到统计类查询就原形毕露。这个项目虽然数据量不大但有几个索引必须建link_info.short_code唯一索引跳转查询和日志关联都要用它。link_access_log的复合索引(short_code, access_time)因为所有趋势查询都是“查某条链接某段时间的日志”这个复合索引直接命中不用回表太多。link_daily_stats的(short_code, stat_date)唯一索引既保证同一短链同一天只有一个汇总记录又加速趋势查询。另外建表统一用utf8mb4而不是utf8。原因很简单MySQL的utf8最多只有3字节根本存不下emoji和部分生僻字。用户UA字符串里偶尔会带特殊符号用utf8会直接报错或被替换成问号这种坑我在实际项目里踩过初始化SQL里顺手就把字符集定好能省掉后面无数个编码问题。3. SpringBoot后端从接口到统计链路的实现3.1 工程搭法与分层结构后端建议按标准的Controller → Service → Mapper三层来拆包我习惯的项目包结构是这样com.abo.shortlink ├── controller # 接口层只做参数接收和结果包装 ├── service # 业务逻辑层短链操作、统计查询都在这里 ├── mapper # MyBatis/MyBatis-Plus的数据访问接口 ├── entity # 数据库实体 ├── vo # 前端展示对象聚合统计结果 ├── config # 跨域、拦截器、异步线程池配置 ├── interceptor # 短链访问拦截器 └── task # 定时汇总任务Controller层要薄业务逻辑别往里面堆。比如短链创建时要做“生成短码 → 判重 → 插入 → 返回完整短链”一串动作拆一个ShortLinkService.createShortLink()方法Controller只有一行调用。这样后面接单元测试、加缓存、改逻辑都方便。数据库访问可以用MyBatis-Plus它能省掉大量单表CRUD的XML。这个项目里link_info、sys_user的增删改查直接用MyBatis-Plus内置方法只有link_access_log的聚合统计、link_daily_stats的按日期批量查询才需要手写SQL。手写SQL时优先用Select注解SQL短的话没必要搞XML文件。3.2 前端配置端口、数据源、时间区application.yml里几个关键配置直接列出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/abo_shortlink?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl有几个坑要重点说MySQL 8.x 必须用com.mysql.cj.jdbc.Driver老写法com.mysql.jdbc.Driver虽然还能用但控制台会打一大段警告。serverTimezoneAsia/Shanghai不能少。很多新手连接时报“The server time zone value is unrecognized”就是因为JVM和MySQL时区不一致。**useSSLfalse**是给本地调试用的SSL握手会增加连接耗时本地项目没有敏感数据关掉更省事。密码按自己本地MySQL的实际情况填root密码不对的话后面一步都跑不了。3.3 短链生成从URL到短码的算法选择短链生成是这个项目最有“算法感”的地方。常见方案有三类直接UUID太长生成出来的短链根本没有“短”的意义。雪花ID 进制转换用全局唯一ID做Base62编码短且唯一性能好但不依赖第三方工具的话要在代码里维护递增序列。哈希截断把长URL做MD5取前8位做短码但存在碰撞风险需要查库判重重了就在原串加盐重新哈希。我在这个项目里推荐方案二的变体使用雪花算法生成一个分布式自增ID然后做62进制10位数字26个小写字母26个大写字母转换得到一个6~8位的短码。这样完全不需要查库判重——ID本身就是唯一的只要保证雪花ID不重复短码就一定不重复。private static final String BASE62 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; public static String encode62(long num) { StringBuilder sb new StringBuilder(); while (num 0) { sb.append(BASE62.charAt((int) (num % 62))); num / 62; } return sb.reverse().toString(); }用这个方案还要注意一个细节雪花ID是long转成字符串后按62进制转换出来的短码不会带http://之类容易混淆的字符非常干净。3.4 短链跳转拦截器一次点击一次留痕短链跳转的实现方式有两种一是GetMapping(/s/{shortCode})写一个Controller方法二是用拦截器拦截/s/*。两者实现效果差不多但拦截器方式更直观地表达了“这是一个被统一定义的访问入口”这层语义。核心逻辑就是两件事查link_info得到原始URL然后写入一条访问日志。但这两件事的执行顺序有讲究先写日志后重定向。因为重定向之后Handler就返回了如果先重定向再写日志日志逻辑还没跑完就被SpringMVC的返回给截断了异步执行容易被容器提前回收。Component public class ShortLinkInterceptor extends HandlerInterceptorAdapter { Autowired private LinkAccessLogService accessLogService; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); String shortCode uri.substring(uri.lastIndexOf(/) 1); // 查短链不存在或禁用则返回404 LinkInfo linkInfo linkInfoService.getByShortCode(shortCode); if (linkInfo null || linkInfo.getStatus() 0) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return false; } // 异步记录访问日志 accessLogService.recordAsync(shortCode, request); // 重定向到原地址 response.sendRedirect(linkInfo.getOriginalUrl()); return false; } }记日志的理由不只是“有了数据才能做报表”它本身还是判断链接质量、排查恶意访问的重要依据。通过短时间内同一IP反复请求次数还能顺带做一个简单的访客频率分析这些东西都是原始长链接完全提供不了的。3.5 异步落库让跳转响应不背锅这里必须专门讲一下“异步”这两个字。如果跳转请求在重定向前同步执行INSERT INTO link_access_log那么数据库一旦有锁竞争或者网络抖动用户点击短链后就要白等几十甚至几百毫秒体验极差。解决办法是异步写入。最简单的做法是Spring的Async注解Async(logExecutor) public void recordAsync(String shortCode, HttpServletRequest request) { // 组装日志实体插入数据库 }但这里有个小坑要注意Async默认用的线程池是SimpleAsyncTaskExecutor它每次都new一个线程执行高并发场景下线程数会爆炸。所以我建议在配置类里显式定义一个线程池Bean(logExecutor) public ThreadPoolTaskExecutor logExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(16); executor.setQueueCapacity(1000); executor.setThreadNamePrefix(log-); executor.setWaitForTasksToCompleteOnShutdown(true); executor.initialize(); return executor; }量再大一点老鸟会选择把日志先塞进内存队列定时批量刷库避免每次点击都打一次数据库。小区块可以使用BlockingQueue每次offer进队列然后一个Scheduled任务每5秒poll一批批量insert。这种设计在这个项目里略微超前但理解了“写日志不能阻塞主流程”这个原则后后面不管换Redis队列还是换消息队列思路都是通的。3.6 统计接口给前端提供什么格式的数据前端可视化图表需要后端提供几个标准结构的数据。趋势图接口GET /api/stats/trend?shortCodexxxdays7返回{ dates: [2025-01-01, 2025-01-02], pv: [120, 150], uv: [30, 42], ipCount: [28, 39] }前端拿这个直接塞给ECharts的折线图不需要再做任何加工。这里有个更细的优化如果图表要显示24小时分布或者按小时的热度可以改成line_info表里加一个hour字段或者干脆在汇总表里拆成pv_00到pv_23的24列看业务需求来定。来源分布接口GET /api/stats/source?shortCodexxx返回{ data: [ {name: 直接访问, value: 340}, {name: 百度, value: 126}, {name: CSDN, value: 98} ] }这里的来源怎么定义从referer字段提取域名空referer归为“直接访问”。ECharts饼图直接吃{name, value}数组格式对齐非常重要很多时候前端图表不显示不是图表配置错了而是后端字段名对不上——name写成了sourceName前端取不到。设备分布、地区分布同理基本都是查明细表GROUP BY后返回{name, value}数组。这几组接口代码很简单无非是SELECT device_type AS name, COUNT(*) AS value FROM link_access_log WHERE short_code #{shortCode} GROUP BY device_type3.7 定时汇总任务凌晨跑批白天快查每日汇总用Scheduled就能搞定Scheduled(cron 0 10 0 * * ?) public void dailyStatsTask() { // 查询昨天的所有访问记录按 short_code 日期分组聚合 // 写入 link_daily_stats已存在则更新 }Cron表达式0 10 0 * * ?表示每天0点10分执行留10分钟的缓冲防止刚好有跨天的记录还没落库。汇总逻辑建议用一条SQLINSERT INTO link_daily_stats (short_code, stat_date, pv, uv, ip_count) SELECT short_code, DATE(access_time), COUNT(*), COUNT(DISTINCT ip), COUNT(DISTINCT CASE WHEN ip IS NOT NULL THEN ip END) FROM link_access_log WHERE access_time BETWEEN CONCAT(CURDATE() - INTERVAL 1 DAY, 00:00:00) AND CONCAT(CURDATE(), 00:00:00) GROUP BY short_code, DATE(access_time) ON DUPLICATE KEY UPDATE pvVALUES(pv), uvVALUES(uv), ip_countVALUES(ip_count);这个SQL同时完成了插入和更新因为(short_code, stat_date)是唯一索引已经存在就直接覆盖。第一次跑任务可能觉得没必要但一个月后当明细表涨到几十万行时你会感谢当初那个凌晨跑批的自己。4. Vue前端把数据变成看得懂的图表4.1 工程初始化与目录规划Vue前端建议用Vue 2 Element UI ECharts这套组合稳定性高网上资料全配起来不折腾。用Vue CLI创建vue create abo-web cd abo-web npm install element-ui echarts axios前端目录里把“功能模块”和“通用组件”分开src ├── api # 所有接口请求统一封装 ├── router # 路由表 ├── views # 页面级组件 │ ├── Dashboard.vue │ ├── LinkManage.vue │ └── StatsDetail.vue ├── components # 通用组件图表卡片等 ├── utils # request.js axios封装 └── App.vueAPI层统一封装是个很容易被忽略的好习惯。我见过很多新手在页面里直接写axios.get(http://localhost:8080/api/...)改一个接口地址要全局搜索替换效率极低。正确做法是在src/utils/request.js里统一创建axios实例配好baseURL和拦截器然后每个接口对应一个函数import request from /utils/request export function getTrend(shortCode, days) { return request({ url: /stats/trend, method: get, params: { shortCode, days } }) }页面组件里只调用函数不直接碰axios。这样做最直观的好处是后端接口加个统一前缀或者让请求携带token改一处就行全项目跟着生效。4.2 页面加载时的数据获取时机问题Vue页面里拉取数据的常规做法是放在created里。但这里有一个经验对于Dashboard这种需要多组接口并行的页面用Promise.all并发请求比逐个await串行快了不止一倍。async loadData() { const [overviewRes, trendRes, sourceRes, deviceRes] await Promise.all([ getOverview(), getTrend(, 7), getSource(), getDevice() ]) this.overview overviewRes.data this.trendData trendRes.data this.sourceData sourceRes.data this.deviceData deviceRes.data }每个接口都是独立的查询没有互相依赖串行相当于白白等着服务器处理完一个再发下一个非常浪费时间。Promise.all会让四个请求同时打到后端总体耗时约等于最慢的那个接口体验好很多。4.3 ECharts图表对接配置项和数据分离ECharts的使用有个高频误区把配置项和数据写死在一起一旦数据变了整个图表要重新setOption一遍。正确做法是把数据和配置项分开数据变了只更新series里的data。以趋势折线图为例模板里初始化图表const chart echarts.init(this.$refs.trendChart) this.chart chart拿到数据后this.chart.setOption({ tooltip: { trigger: axis }, legend: { data: [PV, UV] }, xAxis: { type: category, data: this.trendData.dates }, yAxis: { type: value }, series: [ { name: PV, type: line, data: this.trendData.pv, smooth: true }, { name: UV, type: line, data: this.trendData.uv, smooth: true } ] })要刷新数据时不需要重置整个option只需要this.chart.setOption({ xAxis: { data: this.trendData.dates }, series: [{ data: this.trendData.pv }, { data: this.trendData.uv }] })ECharts的setOption本质是合并式更新你只传变化的片段它会自动和原来的配置合并。这点和Vue的响应式更新设计非常像——只关心变化的部分不推倒重来。还有两个细节要提醒页面里用了v-if控制图表容器的显示时初始化必须在DOM渲染完成之后否则this.$refs.trendChart拿不到DOM节点。组件销毁时要执行this.chart.dispose()或echarts.getInstanceByDom(this.$refs.trendChart)?.dispose()释放资源否则路由切换后图表实例泄漏页面多了会卡顿。4.4 实时刷新定时轮询还是WebSocket可视化管理端常见需求是“图表自动更新”。这个系统可以选两种方案定时轮询setInterval每60秒重新请求一次趋势接口实现简单几十个在线用户毫无压力。WebSocket后端推送最新日志数据实时性更强但要处理连接管理和消息推送逻辑复杂度上了一个台阶。短链接分析场景里PV/UV趋势没有秒级实时性要求1分钟级别的延迟完全可接受。我强烈建议用定时轮询mounted() { this.timer setInterval(() { this.loadData() }, 60000) }, beforeDestroy() { clearInterval(this.timer) }记得在beforeDestroy里清掉定时器这个坑我见过太多次——页面都关掉了接口还在每60秒请求一次后端日志里全是无意义的请求记录。4.5 访问明细列表分页和筛选访问明细页用Element UI的el-table展示后端接口设计成分页模式select * from link_access_log where short_code #{shortCode} and access_time between #{startTime} and #{endTime} order by access_time desc limit #{offset}, #{pageSize}前端分页组件的当前页和每页条数绑定到queryParams.pagination切换时重新调接口。这里要注意的是时间筛选要往后端传Unix时间戳或标准字符串不要在前端拿到全量数据再filter数据量一大前端直接卡死。给表格加一个导出CSV功能其实也是顺手的事前端拿到当前查询条件下的所有数据或分批拉取用Blob生成文件这个功能很多运营场景要用源码头link之后能不能分享CSV就看这里做不做。5. 本地环境准备与运行5.1 MySQL安装与初始化这套系统对MySQL版本没有特别挑剔的要求5.7和8.x都能跑。我推荐直接上MySQL 8.0不仅性能好窗口函数等新语法以后做复杂分析也方便。Windows用户安装流程很简单官网下载MySQL Installer一路Next选Developer Default期间会让你设置root密码记好它。Linux用户用包管理器sudo apt update sudo apt install mysql-server sudo systemctl start mysql sudo mysql_secure_installation装完之后要注意一个经典报错ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。这个报错通常意味着MySQL服务没启动或者socket文件路径不对。排查路径很固定# 查看服务状态 systemctl status mysql # 没启动就启动 systemctl start mysql # 看socket文件在哪 mysql -uroot -p -h 127.0.0.1 -P 3306用TCP方式-h 127.0.0.1连接能绕开socket文件的问题很多时候能帮你判断到底是服务没起还是socket路径不对。初始化数据库mysql -uroot -p init.sqlinit.sql里面就是前面那四张表的建表语句可以加一条管理员账号的INSERT方便登录后台INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, $2a$10$..., 管理员, 0);记得这个密码是BCrypt加密后的值别直接写明文。5.2 SpringBoot配置与启动用IDEA打开后端工程等待Maven把依赖下载完。如果下载很慢把Maven的settings.xml里换成阿里云镜像。启动前确认三件事application.yml里的MySQL地址、账号、密码和本地一致。JDK版本和pom.xml里的java.version一致常见的坑是本地JDK8而项目要求JDK11。Redis相关配置如果项目里有就确认Redis启动了我这个版本没依赖Redis所以不用管。直接运行main方法里带SpringBootApplication的类控制台出现“Started Application”就说明后端起来了。验证接口直接用浏览器访问http://localhost:8080/api/link/list如果能返回JSON说明数据库连接也通了。5.3 Node环境与Vue启动Vue项目运行前先确认Node版本Vue CLI 5需要Node 12以上。如果版本不对强烈建议用nvm管理Node版本不要硬装版本切换方便很多。nvm install 16.20.2 nvm use 16.20.2 cd abo-web npm install npm run servenpm install如果卡住不动八成是npm源的问题换成淘宝镜像npm config set registry https://registry.npmmirror.com启动成功后会显示App running at Local: http://localhost:8081/浏览器打开就是这个管理端项目。默认端口是8081是因为Vue CLI会自动避开后端8080端口前后端不在同一端口天然触发跨域问题见下文。5.4 前后端联调与跨域处理Vue前端跑8081SpringBoot后端跑8080前端页面的axios请求直接跨域了。跨域不解决页面打开是白纸一张所有接口请求都被浏览器CORS策略拦下。后端处理最省事的方式是加全局跨域配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个细节addAllowedOrigin(*)在allowCredentials(true)时会报错必须用addAllowedOriginPattern(*)通配。这个错误不实际跑一遍不容易遇到我当年就是被它坑了小半天。前端axios的baseURL配成http://localhost:8080// request.js const service axios.create({ baseURL: http://localhost:8080, timeout: 10000 })这样前端所有接口请求都会落到后端跨域交给CorsFilter解决。后端日志能看到请求进来前端控制台也能见到返回的JSON数据联调就算成功了。6. 常见问题与排查技巧实录6.1 问题速查表现象根因解决办法连MySQL报ERROR 2002MySQL服务没启动或socket路径不对systemctl start mysql或用-h 127.0.0.1指定TCP连接连MySQL报The server time zone value时区不一致URL加serverTimezoneAsia/Shanghai启动SpringBoot报Driver not foundMySQL驱动类写错MySQL8用com.mysql.cj.jdbc.Driver前端请求接口全是CORS报错跨域未配置后端加CorsFilter注意allowCredentials与addAllowedOriginPattern组合Vuenpm install卡住npm源访问慢改用npmmirror镜像源页面图表空白、控制台报ECharts没初始化DOM未渲染完成拿不到ref把echarts.init放到nextTick里跳转短链一直404link_info表里没这个code或状态为禁用检查短码是否入库、状态字段是否为正趋势图数据全是0定时汇总任务没跑或者跑的数据是昨天的手动执行一次汇总SQL确认link_daily_stats有数据6.2 排查技巧按链路逐步验证后端联调阶段出问题最容易的做法是沿着“浏览器 → 前端 → 后端 → MySQL”这条链路逐步确认浏览器F12打开开发者工具看Network面板里接口请求是否发出返回状态码多少。如果请求根本没发出问题在前端配置baseURL、路由、代码报错如果发了但401/403问题在认证授权如果持续pending直到超时问题大概率在后端。打开后端控制台看有没有对应的接口访问日志。MyBatis开启了StdOutImpl日志的话会打印每一条SQL。如果后端没收到请求八成是跨域拦截或路径不对。拿到SQL后直接在Navicat或命令行执行确认SQL本身没问题。能排除数据库层面的错。这套排查思路听起来朴素但它能快速缩小故障范围。我见过太多人上来就怀疑$_GET没传对、Vue路由炸了结果查到最后是数据库密码错了——一开始就把链路从头到尾走一遍几秒钟就能定位。6.3 部署到服务器时的补充坑本地跑通之后如果还想部署到测试服务器会多出几个问题后端打包mvn clean package -DskipTests项目里如果集成了SpringBoot的Maven插件打出来是可直接java -jar运行的fat jar。前端打包npm run build生成dist目录。有两种部署方式一是把dist文件扔到Nginx的html目录Nginx反向代理/api到后端的8080端口二是把dist文件挂到SpringBoot的static目录下让后端自己托管静态资源。个人建议用Nginx方式部署更灵活前后端更新互不影响。数据库迁移本地测试完的MySQL数据用mysqldump导出服务器上导入mysqldump -uroot -p abo_shortlink abo_shortlink_backup.sql mysql -uroot -p -h 服务器IP abo_shortlink abo_shortlink_backup.sql服务器防火墙千万记得开放后端端口和Nginx端口不然外部访问不到本地却一切正常最容易让人怀疑人生。6.4 给新手的几个避坑提示最后说点实在的。这类全栈项目我建议你把“跑起来”和“改明白”分开对待第一遍先照着步骤把数据库、后端、前端全部跑通不要急着改代码。哪怕是照着敲一遍也能让你对整个项目产生体感认知哪个端口对哪个页面、哪个接口返回哪个图表、短链跳转的请求链路如何流转。第二遍再动手改换一种短码生成算法、加一个图表、改一下汇总任务的时间粒度。改的过程才是真正学东西的过程。我做这个项目时最深的体会是这些代码框架本身都很成熟难点全在“业务怎么建模”这个层面——日志怎么存才不臃肿、统计数据怎么算才高效、图表数据怎么组织和接口对齐。这些经验从短链接系统出发放到管理后台、BI看板、消息队列项目里底层的思路完全通用。先把这套系统里的数据流和设计取舍吃透下次碰到类似的“数据采集 展示”需求你的第一直觉就会准很多。
返回列表