ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue的短链接流量分析与可视化系统实战

基于SpringBoot+Vue的短链接流量分析与可视化系统实战 做短链接流量分析这个事我一开始是被临时拉去救火的。业务方要做一场裂变活动投放了一堆带参数链接结果后台只能看到打开人数来源渠道、设备分布、时段趋势全是一团黑。市面上的第三方统计平台要么收费贵要么数据落地格式不自由。想自己搞一个吧翻了一圈开源项目不是太重动不动上KafkaFlink全家桶就是只有前端大屏展示数据完全不落地。最后我决定用当前最顺手的那套组合——SpringBootVueMyBatisMySQL前后端分离用最短的开发周期搭一个能真正跑到线上的“短流量数据分析与可视化ABO系统”。这里ABO不是别的就是Analysis-Business-Optimization分析、业务、优化把链路数据从采集到展示再推回业务决策走完一个完整循环。这篇把整个项目的需求拆解、技术选型、核心模块实现、部署教程以及我实际踩过的坑一次性整理完整。适合已经有Java基础和Vue入门经验的开发者或者正在做前后端分离项目实战练手的人。1. 项目由来与需求拆解短流量分析到底要分析什么1.1 为什么盯上“短流量”这个细分场景一般谈到数据分析大家第一反应是埋点、用户行为日志、漏斗转化那是个大工程。但短流量不太一样。它的本质是“每一次点击都是带了明确来源诉求的”比如短信里的短链接、海报上的短链二维码、社群分享出来的短链接。这些流量不像站内浏览那么混沌它们的生命周期很短数据量不会大到离线计算的程度但是维度属性非常清晰谁点的、从哪个渠道来的、用什么设备、在哪个时段点的、停留了多久。这些字段凑在一起足够支撑运营做快速决策。而且更有意思的是短流量的用户路径天然就能对应上“链—点—览”三段结构。链接生成、点击访问、落地页浏览每一段都可以量化。如果做成长链加参数虽然也能查但推广素材里经常被截断体验也差。所以做这个系统第一步需求就定死了一定要有短链生成能力一定要能记录每次点击的元信息最后通过时间维度和属性维度聚合出可视化报表。这些需求不复杂但对数据一致性要求不低点击就是事实不能丢也不能重复统计。1.2 功能清单与数据流转逻辑需求最终收敛成四个模块短链管理、点击采集、数据聚合、可视化展示。短链管理负责把原始长链接压缩成短码。点击采集是核心埋点接口用户每次访问短链都会走一次重定向重定向之前在服务端把请求头里的User-Agent、Referer、IP以及URL里携带的渠道参数记录下来。数据聚合分实时和离线两层实时的话我用了简单的本地缓存做分钟级计数离线报表则由定时任务按小时跑批把明细数据聚合成小时表、日报表。可视化展示用Vue写了一套Dashboard包含整体趋势、渠道占比、设备分布、最近点击实时滚动几个组件。整个数据流转的核心链路是这样的前端短链地址发起请求 → SpringBoot拦截器解析短码 → 异步写入点击流水 → 重定向到原始长链 → 定时任务聚合到统计表 → 前端ECharts图表拉取聚合接口渲染。这里最需要注意的点是“重定向和写日志必须解耦”否则点击一多接口延迟就上去了。我的做法是把点击流水的写入丢进一个线程池异步处理主线程只做一次短码DB查询和302跳转。这样写既有实时性又不阻塞链路。1.3 前后端分离的总体架构设计这个项目的架构在部署形态上走的是标准的前后端分离前端Vue项目独立开发调试通过代理访问后端接口生产环境有两种可选一种是把Vue打包后的dist目录扔进SpringBoot的static资源下做成单Jar部署另一种是用Nginx托管静态文件、反向代理后端接口。我最后实际线上用的是第二种因为后续还要在这个域名下挂别的服务网关层独立出来会更灵活。后端按包结构划分成controller、service、mapper、entity、common、config几层。短链模块在controller里直接返回短码和完整短链地址点击分析模块提供趋势、排行、实时三个大类接口。数据库操作全部走MyBatis手写SQL的比例高一些因为统计场景里的多表关联、分组聚合、时间序列补零用注解SQL表达起来勉强XML里写动态SQL才舒服。2. 技术选型SpringBootVueMyBatisMySQL这套组合的理由2.1 后端为什么还是SpringBoot最稳有些项目喜欢一上来就上Spring Cloud全家桶但短流量分析这种场景业务量级也就是日百万级点击连CDN都还没参与进来微服务带来的收益基本为零反倒增加部署和运维成本。SpringBoot单应用足以扛住这个量级而且开发效率最高。我选的版本是SpringBoot 2.7.x。很多人喜欢追新上来就SpringBoot 3.x。但是3.x强制要求JDK 17而且javax命名空间改成jakarta网上大量老教程的代码直接跑不通。对大多数中小项目来说2.7 JDK 1.8的组合最稳定云服务器上CentOS自带的JDK版本也不用折腾。如果后续真需要流量再上涨升级路径也明确接入Nginx负载均衡、加Redis做缓存、再考虑拆服务。2.2 前端选Vue而不是React的真实原因团队之前的技术栈里Vue和React都有用过但这个项目选Vue3有非常实在的理由一是ECharts对Vue3的封装生态更成熟vue-echarts组件用起来比在React里手动管理chart实例要顺手很多二是Vue的模板语法对后端转前端的同事很友好模板里可以直接写v-for、v-if不需要像JSX那样在render函数里绕逻辑三是数据可视化场景里Vue的响应式数据天生和图表组件契合接口返回的新数据集丢进去图表自动刷新。脚手架我用的是Vite而不是Vue CLI速度快了不是一点半点。这里提醒一句网上很多教程还在用Vue CLI创建项目新开项目直接npm create vitelatest然后选择vue模板就行。2.3 MyBatis在统计场景下的灵活性和坑MyBatis和MyBatis-Plus之间我选了前者准确说是只加了通用Mapper插件没有用Plus的LambdaQueryWrapper。原因很直接统计报表的SQL几乎都是动态拼条件、按维度分组、子查询嵌套MyBatis-Plus的封装在这种场景下反而绕。比如“查询最近14天每天各渠道PV”如果全部用MP的Wrapper去套代码能写出一本书来。而用XML动态SQL一句if判断一个维度直观到不行。MyBatis的缓存机制这里得单独提一下。默认一级缓存是SqlSession级别的二级缓存默认不开启。统计系统的数据实时性要求其实不高报表做到分钟级已经足够所以我在统计查询Mapper上开启了二级缓存并且把缓存过期时间设置成60秒。这样热点报表接口的数据库压力小了很多。但要注意如果有写操作同时改统计表缓存很容易脏读。所以我的实践是报表查询走二级缓存明细点击流水查询强制刷新两条路互不干扰。这一块会在第7章踩坑部分再展开讲。2.4 为什么不用若依这类前后端分离脚手架很多人看到SpringBootVueMyBatisMySQL就条件反射想起若依框架。确实RuoYi这种脚手架把权限、用户、菜单、代码生成全都做好了拿来改改就能跑。但这个项目我坚持不用的原因有两个。第一若依体系过于完整自带一套RBAC权限模型和定时任务界面对短流量分析这种纯内部工具来说是负担光删菜单就得删半天。第二项目里统计逻辑有大量自定义的SQL聚合脚手架带的通用CRUD接口在这块派不上用场。需要啥能力自己写一层service就完事了。这套逻辑也跟团队风格有关项目越贴近业务越不要套重框架保持代码透明、可控排查问题时不需要先弄懂框架的拦截链。3. 数据库设计短链表与点击日志表的字段推敲3.1 核心表结构与建表SQL再来拆库表。整整一个项目跑起来只需要四张表短链信息表、点击明细表、小时聚合表、用户表。这个设计是按数据流向来的短链表管映射点击明细表管事实聚合表管分析用户表管后台登录。字段上没有刻意做垂直拆分一切以查询路径最短为第一原则。短链信息表作用是一文定音短码和长链接的映射关系再加上创建人、有效期、状态。我加了一个visit_count栏目作为冗余计数每次点击异步更新一次。有人会问这个字段和明细表count(*)不是重复吗确实是冗余但价值巨大短链列表页可以直接用这个字段排序展示不需要每次都去SUM明细表。点击明细表是体量最大的表每产生一次点击就写一条。字段包括短码、IP、User-Agent、浏览器类型、操作系统、设备类型、渠道来源、访问时间。渠道来源我并没有做复杂解析而是约定短链生成时携带channel参数保存到短链接表访问的时候跟随短码一起读取出来写入明细。这个设计简单可靠不需要像独立埋点那样搞一套归因引擎。聚合表是小时表和日报表二合一只保存短码、维度类型、维度值、PV数量、独立访客数、统计时间。独立访客我用IPUA做了个Hash值去重虽然不及Cookie精确但这个场景已经够了。下面给出核心建表SQL。CREATE TABLE short_url ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL COMMENT 短码, long_url varchar(2048) NOT NULL COMMENT 原始链接, channel varchar(64) DEFAULT COMMENT 渠道标识, title varchar(128) DEFAULT COMMENT 活动名称, visit_count int(11) DEFAULT 0 COMMENT 累计点击数, expire_time datetime DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1有效 0失效, create_time datetime DEFAULT CURRENT_TIMESTAMP, creator varchar(64) DEFAULT , PRIMARY KEY (id), UNIQUE KEY uk_short_code (short_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短链映射表; CREATE TABLE click_log ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL, ip varchar(64) DEFAULT , user_agent varchar(512) DEFAULT , browser varchar(32) DEFAULT , os varchar(32) DEFAULT , device varchar(16) DEFAULT 1 COMMENT 1PC 2移动端, channel varchar(64) DEFAULT , uv_hash varchar(64) DEFAULT , click_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_code_time (short_code,click_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT点击明细表; CREATE TABLE stat_hourly ( id bigint(20) NOT NULL AUTO_INCREMENT, short_code varchar(16) NOT NULL, stat_date date NOT NULL, stat_hour tinyint(4) NOT NULL, dimension varchar(16) NOT NULL COMMENT channel/device/browser, dimension_value varchar(64) NOT NULL, pv int(11) DEFAULT 0, uv int(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_query_line (short_code,stat_date,stat_hour,dimension,dimension_value) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT小时聚合表;3.2 聚合表不设自增值的考虑说实话我本来也想在最前面直接放一张时间维度的总表统计每天的总PVUV。但后来想想报表页面的趋势图、渠道占比图、设备分布图本质上都是“同一时间范围、同一个short_code、按不同维度分组SUM”。所以一张聚合表用dimension字段区分分组主题反而最省事。查询趋势图就是筛选dimension不可用直接按小时SUM查询渠道占比就是筛选dimensionchannel按dimension_value分组。一张表通吃查询SQL写得简单索引也建得少。不过这个设计带来的问题是写入量增加。原本一条点击数据在一个小时维度上只需要写一条记录现在按channel、device、browser三个维度极端情况下要写三条聚合记录。但这个量级对MySQL来说毫无压力而且我实际观察下来单条点击记录的三个维度大概率落在3条聚合记录以内这个成本完全能接受。3.3 索引设计的实战经验索引设计上除了唯一索引uk_short_code我最看重的是idx_code_time。这个索引的字段顺序是短码在前、时间在后因为所有报表查询的第一步永远是按短码圈定数据范围然后再加时间条件缩小范围。如果反过来建多小时索引MySQL的索引前缀匹配特性会使时间条件没法快速定位虽然也能走索引但效果差一个量级。有一点很多初学者容易忽略聚合表的唯一索引uk_query_line一定要建成唯一索引而不是普通索引。光看字段名可能觉得这就是为了查重实际上聚合任务重跑时要用INSERT IGNORE或者ON DUPLICATE KEY UPDATE来幂等写入。如果没有唯一索引兜底重跑两次数据就翻倍了。4. 后端核心功能与实现链路4.1 短链生成算法与防碰撞短码生成我踩过一轮坑最开始用的UUID截取8位做短码结果跑到1万条就开始出现碰撞。后来改成雪花算法转62进制又发现生成的短码太长落在URL里难看。最后采用的自增ID Base62混淆映射每个短链生成时插入短链表拿到自增主键然后把这个主键转成62进制字符串长度基本控制在6~8位。这个方案的天然优势是主键唯一转换结果必然唯一根本不用做碰撞检测。Base62转换的逻辑也很简单就是用0-9a-zA-Z共62个字符做进制转换。注意生成之后可以再做一次字符混淆比如把第一位字母随机大小写防止短码太规律被人批量抓取。下面是我实际用的转换工具类核心代码。public class Base62Util { private static final String BASE62 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; public static String fromDecimal(long num) { StringBuilder sb new StringBuilder(); while (num 0) { sb.append(BASE62.charAt((int) (num % 62))); num / 62; } return sb.reverse().toString(); } }4.2 点击埋点接口的异步化设计点击重定向接口是整个系统里QPS最高的入口。我把逻辑分成三段第一段查缓存拿长链接映射第二段构造点击明细Entity第三段异步落库并重定向。这里的缓存策略是Caffeine本地缓存过期时间10分钟短链映射查一次DB之后基本不再查第二次。点击明细的落库我单独搞了一个线程池核心线程数8、最大线程数16、阻塞队列长度2000。线程池满了以后的处理策略用的是CallerRunsPolicy宁可让请求链路本身来写这条日志也不能因为队列溢出丢了点击数据。这属于降级处理的一种保证真实性比保证响应速度更重要。异步任务里同时做三件事插入click_log、更新short_url的visit_count、把聚合增量丢进一个内存计数Map等待定时任务冲刷到stat_hourly。4.3 统计接口与动态SQL的编写经验报表接口用MyBatis动态SQL落地整体思路基本相同换维度就拼SQL。以“渠道占比”接口为例前端传shortCode、startTime、endTime三个参数后端在stat_hourly表里筛dimensionchannel然后按dimension_value分组SUM。注意SQL的黑魔法不在聚合而在“按小时补零”。如果某一天某个渠道一单点击都没有前端ECharts画出来的趋势折线会直接断掉。补零逻辑我放在Java内存里做后端把数据库里已有的记录查出来之后在循环里填充缺少的时间点PV UV置0。这个方案比SQL里做递归连接表简单一百倍也容易理解。还有一个比较实用的写法是统一接口返回结构时间字段全部格式化好再传给前端。统计数据里最容易出现时区误差我在Service层集中用LocalDateTime操作转JSON时配上全局时间格式。这块配置如果没做好你会发现图表上最新数据永远缺一小时也就是第7章要讲的坑之一。4.4 权限控制和参数校验的简化内部工具不需要做太重的权限体系我用拦截器加一个简单的Token机制。用户登录之后服务端生成Token存进Redis前端请求头携带Token拦截器校验通过则放行。这个项目不引入Shiro或者Spring Security因为对于纯内部可视化系统它们太重了学习成本反而高。参数校验方面短链创建接口一定得做URL白名单校验防止有人拿短链接口跳转到钓鱼站点。做法是用Hutool的UrlValidator类判断协议和域名至少保证只能是http/https且不拦截内网地址段。5. 前端可视化Vue3ECharts从零搭建仪表盘5.1 工程初始化和环境配置前端部分我用Vite创建Vue3工程用npm安装依赖。这里先给一段初始化和安装依赖的命令省得再去翻文档。npm create vitelatest short-dashboard -- --template vue cd short-dashboard npm install npm install vue-router4 pinia axios echarts vue-echarts安装完依赖后第一件事是配好路由。可视化页面有一张总览大屏我配了一个根路径直接指向Dashboard组件。路由用的history模式理论上更美观但要注意生产环境部署时如果没配Nginx的try_files刷新二级页面会404。这个问题网上问的人非常多第6章部署部分会给出对应配置。5.2 开发环境跨域配置与接口层封装前后端分离开发中最烦的就是跨域。开发环境下后端在8080端口前端在5173端口直接fetch必然被CORS拦。解决办法是Vite的server.proxy配置把所有以/api开头的请求代理到后端的接口地址。为什么以/api开头就是特意给代理留的标记后端Controller统一加了这个路径前缀。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })接口层我封装了axios实例在request拦截器里统一加上Token头response拦截器里统一处理错误码。这里有个小细节后端返回的JSON字段名用驼峰前端axios拿到的也是驼峰中间不需要afterRequest做字段转换。但如果后端哪天改成下划线命名就得加映射这个坑先记下。5.3 核心图表组件落地明细总览Dashboard我分了四个区块第一块是整体趋势折线图显示所选时间范围内每天的PV和UV第二块是渠道来源饼图显示各个渠道占比第三块是设备分布环形图区分PC和移动端第四块是最近点击的实时滚动表格。每个区块都由一个接口驱动前端用Promise.all并发请求所有数据到齐后一次性更新图表。ECharts组件的使用上我不推荐在data里存chart实例因为Vue3的响应式代理会试图劫持ECharts实例的内部属性引发怪异问题。正确做法是在模板里用ref或者vue-echarts组件让组件库自己去管理实例生命周期。图表容器必须设置固定高度否则初始化时宽度为0图表画不出来。这是初学者最常踩的坑。5.4 实时刷新策略与性能取舍实时刷新不能整页刷新否则图表会闪。我用setInterval每隔10秒请求一次最近点击接口只更新滚动表格区域的数据。趋势图和占比图的数据刷新频率设置成60秒一次避免频繁拉接口造成后端无谓压力。前端轮询的坑在于组件销毁时要清理定时器否则路由切换后定时器还在跑控制台会爆一堆警告。这里用onUnmounted钩子清理即可。6. 完整部署教程从源码到可访问的线上服务6.1 环境准备与版本搭配部署前先把环境准备清楚。我线上用的是一台2C4G的云服务器系统是CentOS 7。软件版本搭配上JDK用的1.8对应SpringBoot 2.7.xMySQL用的5.7.44Nginx用的1.20Node.js只在构建前端时用到16.20版本够用。这个组合是国内服务器最稳的一档千万别在生产装MySQL 8.0然后连接方式不换后面踩坑一节会说详细。MySQL安装这块建议直接用rpm包安装不要用源码编译。具体步骤是下载对应版本的rpm包rpm -ivh安装然后初始化并启动服务。网上很多教程让改my.cnf的character_set_serverutf8mb4这一步必须做而且要在初始化之前改好否则建出来的库默认排序规则不对中文索引和排序会有很奇怪的行为。6.2 后端打包与启动命令后端打包只需要在项目根目录执行Maven打包命令。Maven的配置里我把最终产物名设置成short-analysis.jar方便脚本里引用。打包前记得检查application.yml数据库连接串、Redis地址、日志路径都要改成生产环境的值。mvn clean package -DskipTests nohup java -jar short-analysis.jar --spring.profiles.activeprod /data/logs/short.log 21 nohup启动是Linux服务最朴素的实践。有人会用systemd写service但内部工具不必上那么重。启动完之后立刻看日志确认端口和数据库连接正常curl一下健康检查接口。6.3 前端构建与两种部署形态前端部署有两个选择。第一个选择是把dist目录里的静态文件复制到Nginx的html目录再用Nginx配置反向代理转发/api请求给后端Java服务。第二个选择是把dist目录整个复制到SpringBoot的src/main/resources/static目录下重新打包最终只用一个Jar跑前端和后端。前面说了我线上用的是第一种方案好处是静态文件走Nginx性能更好后面扩容时前端可以挂CDN。前端构建命令是npm run build产物在dist目录。Nginx的关键配置直接看下面这段。server { listen 80; server_name analysis.example.com; location / { root /data/www/dist; index index.html; try_files $uri $uri/ /index.html; } 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那行就是为了解决刷新页面404的问题。如果缺了这行浏览器访问/about路由时Nginx会去磁盘找about目录找不到就返回404。加了try_files之后所有不存在的路径都回退到index.html由前端路由接管页面渲染。6.4 初始化数据库与定时任务验证数据库初始化和定时任务一起说。项目里我写了一个schema.sql放在resources/db目录首次启动时用Spring的sql.init机制自动执行建表语句。但生产环境我建议关掉自动执行手动到服务器上用mysql命令来导入更可控。定时任务方面后端用Spring自带的Scheduled注解实现了两个任务每10分钟把内存里的聚合计数刷进stat_hourly表每1小时执行一次全量重算。验证定时任务是否正常最简单的方法是看聚合表的最新记录时间是不是和当前时间匹配同时看应用日志有没有异常。7. 部署与开发中踩过的坑完整排查链路记录7.1 MySQL连接失败的完整排查过程这个坑是上生产环境时遇到的。本地Windows开发环境一切正常代码部署到Linux服务器后起后端就报Communications link failure。当时第一反应是数据库地址写错了检查application.yml发现IP和端口都对。然后尝试在服务器上mysql -h127.0.0.1 -uroot -p连接居然也连不上报错是Access denied。后来才反应过来是MySQL 5.7安装时root用户默认只允许localhost登录而Java应用用JDBC连接时的host是127.0.0.1跟localhost不完全是一个授权条目。解决方法是手动创建授权用户。CREATE USER short_userlocalhost IDENTIFIED BY ComplexPwd123!; GRANT ALL PRIVILEGES ON short_db.* TO short_userlocalhost; FLUSH PRIVILEGES;那个报错还有个常见变形就是MySQL 8.0的caching_sha2_password认证插件问题。旧的JDBC驱动不支持这个插件报错是Unable to load authentication plugin。解决方法是下载最新版mysql-connector-java或者在MySQL里把认证插件改成mysql_native_password。我建议这俩方案都做尤其是云数据库默认就是8.0完全跑不了老驱动。7.2 端口冲突与IDEA启动参数配置有一次本地起服务时发现8080端口被一个Java进程占用idea启动日志报Web server failed to start看到这句基本可以确定是端口被占。排查命令是netstat -ano | findstr 8080找到占用进程的PID后taskkill /PID /F解决。更规范的做法是后端启动配置里带上--server.port8083这样临时指定端口避免和本机其他服务冲突。IDEA里配置SpringBoot的运行参数时一定注意是Program arguments而不是VM options这两个填错位置效果完全不同填到VM options里启动会被JVM当成非法参数直接拒绝。7.3 前后端联调CORS问题和Token丢失开发环境配了Vite代理后前端理论上不会遇到CORS问题。但我有一次绕过代理直接请求了后端地址结果浏览器报CORS error后端接口虽然返回正常数据但被浏览器拦截。这里要理解CORS是浏览器的安全策略不是后端的强制限制。生产环境如果Nginx代理配置正确根本不需要在后端开启CORS。如果硬要在开发环境放行后端写一个CorsFilterallowOrigin设成具体的前端地址就行不要用星号否则带Cookie的请求依然会被拦。Token丢失这个坑体现在登录之后前端跳转路由刷新页面Token就没了。原因是我把Token放在内存变量里刷新后JS重新加载变量自然清空。正确做法是存在localStorage或者sessionStorage里请求拦截器每次从storage里取。这个坑很初级但阵容不齐的团队里最容易踩到而且表现诡异登录状态一会儿有一会儿没有。7.4 MyBatis缓存与脏读问题的实践复盘前面提到统计Mapper开了二级缓存但我一开始把所有Mapper都开了结果出了大问题短链点击量接口每次查询的累计点击数和明细对不上。排查思路是先用日志打印SQL发现第一次查询打印了SQL第二次查询直接走缓存没打印SQL但此时明细表里已经新增了点击数统计结果还是旧值。这就是典型的脏读缓存命中了基于旧数据的查询结果。解决方案如下只有统计报表Mapper开启二级缓存点击流水和短链映射Mapper关闭。同时在统计Mapper的flushInterval里设置成60000毫秒让数据最多延迟一分钟。这里还顺带理解了MyBatis框架里一级缓存的生命周期。一级缓存是SqlSession级别的如果同一个SqlSession里先查后写再查会存在旧值复用的问题。Spring管理的Mapper实际上每次都创建新SqlSession除非用Transactional包着所以一级缓存问题在这个项目里不严重但知道原理是好事。7.5 时区问题导致统计结果差8小时这个坑是在定时任务上线第二天发现的。前一天18点到24点的统计数据显示为0但点击明细表里明明有数据。查看了stat_hourly表发现数据写进去的时间是第二天凌晨2点到8点整整差了8个小时。这就是Java应用默认时区和MySQL时区不一致导致的。CentOS系统时区是UTC而MySQL连接串里的serverTimezone没指定JDBC驱动就用系统时区解析DATETIME最后数据落库整体偏移。修复方案是在JDBC连接串上明确指定serverTimezoneAsia/Shanghai同时应用启动参数加-Duser.timezoneAsia/Shanghai。这里注意MySQL 8.0版本自带的时区表默认内容很少如果连接的时候指定Asia/Shanghai报错需要用mysql_tzinfo_to_sql命令导入系统时区表或者干脆在MySQL配置里default-time-zone8:00。这三种方案我最后一起上了确保所有环节一致。7.6 前端图表初始化和刷新时的心得图表初始化那个坑是这样的接口数据还没返回时ECharts容器已经渲染但尺寸为0数据回来后图表显示空白。排查时发现路Chart的option设置没问题手动resize一下就能显示这就是典型的“数据驱动渲染时容器尺寸未就绪”。解决方案不是很复杂在组件mounted后先对图表实例调用一次resize或者给图表的wrapper设置一个最小高度。常用做法是给容器固定的高度比如dashboard-trend样式里直接height: 320px问题就没了。另一个心得是接口数据格式和图表数据格式一定要在前端做适配层不要把后端返回的JSON直接塞给ECharts。ECharts的series.data接受数组对象但后端返回的分组统计结果往往是Map或者List嵌套直接塞过去会报错。我在utils/chartAdapter.js里写了一套数据转换函数专门把后端聚合结果转成ECharts需要的格式。这样后端接口只管数据语义前端图表只管展示职责清晰。8. 上线后的数据校验与后续扩展建议系统上线后第一件事不是看图表漂不漂亮而是校验数据准确性。我最常用的一套校验逻辑是拿聚合表的24小时SUM值和点击明细表按天的COUNT比较误差超过1%就要查问题。误差来源一般有两个一是定时任务还没跑完统计已经被前端拉走二是去重UV的逻辑出错。校验脚本用一条SQL就能搞定。SELECT (SELECT COUNT(*) FROM click_log WHERE click_time 2024-01-01 00:00:00 AND click_time 2024-01-02 00:00:00) AS detail_pv, (SELECT IFNULL(SUM(pv), 0) FROM stat_hourly WHERE stat_date 2024-01-01) AS agg_pv;两列数值对不上就说明链路有问题需要逐个环节排查。后续扩展方向上如果数据量真的涨到千万级点击、每天上百万条明细那再考虑把click_log按月分区或者把统计模块抽出来用Flink做实时计算。老实说以我目前线上这个量级MySQL单库单表加定时聚合完全扛得住不需要盲目上大数据组件毕竟技术栈越复杂排查问题成本越高。这个系统做下来最大的体会是前后端分离项目的复杂度不在于某个单独的框架有多难而在于数据从采集到存储再到展示的每一环都要保持语义一致和格式统一。谁能在这些环节上做得细致谁的项目就能稳定跑下去。
返回列表