从选题到上线,聊聊我用SpringBoot做高校防诈骗宣传平台的完整过程
每年到了毕业设计季,总有人问我选题怎么定。说实话,与其纠结那些烂大街的“图书管理系统”“学生选课系统”,不如找一个既有现实意义、又能在技术栈上完整展示你水平的题目。我今天要分享的,就是这样一个项目:基于SpringBoot的高校防诈骗宣传平台。
这个项目解决的是什么问题?很简单——高校里电信网络诈骗频发,新生被骗学费、兼职刷单被骗生活费,这些事每年都在发生。学校虽然做安全教育,但形式基本停留在讲座签到和传单发放,学生根本记不住。而这个平台要做的,就是把反诈教育从“被动听”变成“主动看、互动聊”,通过案例库、学习记录、社区问答,让防骗知识真正进脑子。
技术上的定位也很有意思。它不是一个纯管理后台,而是一个“内容平台+学习系统+社区互动”三合一的Web应用,非常适合用SpringBoot做后端、Vue做前端的经典前后端分离架构来落地。做完这个项目,SpringBoot、MyBatis-Plus、Spring Security、Redis、数据库设计这些毕业设计高频考核点基本全覆盖,面试时也有的聊。
适合谁?正在做毕业设计的学生、想练手SpringBoot全家桶的初学者、以及想给学校做一套反诈宣传系统的技术爱好者。这篇博文我会把从需求分析、表结构设计、核心接口实现到部署避坑的全过程讲清楚,里面的代码和思路你都可以直接拿去改。
1. 项目定位与整体设计思路
1.1 核心需求解析:这个平台到底要做什么
动手写代码之前,我习惯先把需求想透。防诈骗宣传平台听起来简单,但拆开一看,它至少有四个角色视角:普通学生、系统管理员、内容审核者(可能是辅导员或保卫处老师)、以及游客。
学生的需求是什么?不是上去看一堆长篇大论的防骗手册,而是能快速了解最新的诈骗套路,看到真实案例,最好还能做几道题检验一下自己会不会被骗。管理员的需求是什么?能发布案例、审核社区内容、看统计数据,知道哪个系的学生学习积极性高、哪种诈骗案例阅读量大。辅导员的潜在需求呢?大概是能追踪学生的学习进度,发现哪些学生长期不学习,定向提醒。
这就决定了系统不能只做一个“文章发布系统”。我最终把功能拆成了四个核心模块:用户管理模块、案例学习模块、互动社区模块、后台统计模块。用户可以注册登录、浏览分类案例、搜索特定骗局、参与答题测试、发帖提问、收藏评论。管理端则负责内容审核、用户管理、数据大屏和公告发布。
这个定位很关键。很多同学做毕业设计喜欢堆功能,CMS功能加了一堆,结果核心场景根本不清晰。我这个项目从头到尾围绕“反诈教育闭环”来设计——案例浏览是输入,答题反馈是检验,社区讨论是巩固,管理统计是支撑。所有功能都能回答一个问题:它服务于哪个教育环节?
1.2 技术选型权衡:为什么敲定SpringBoot全家桶
技术选型这部分,我直接说结论,再解释为什么。
后端我用的是SpringBoot 2.7.18,搭配Java 8。这个组合是当下毕业设计最稳妥的选择,不是因为版本新,而是因为资料多、坑少。SpringBoot 3.0虽然已经出了很久,但它基于Jakarta命名空间,很多老教程直接跑不通,对毕设新手来说属于自找麻烦。
持久层选了MyBatis-Plus而不是原生MyBatis。原因很简单:单表CRUD不用写SQL,自带分页插件和条件构造器,代码量能少一半。毕业设计最怕代码写不完还给崩了,能省时间的工具就是好工具。复杂联表查询再手写SQL也不迟。
权限认证用的Spring Security + JWT。很多同学看到Spring Security就头疼,觉得配置复杂。但在这个项目里我只用了它最核心的两个能力:过滤器链做JWT校验、注解做接口权限控制。如果你实在搞不定Spring Security,退一步用拦截器+Redis也可以,但答辩时老师大概率会问“你如何保证接口安全”,用Spring Security在回答上会从容很多。
数据库用的MySQL 8.0,缓存用的Redis。Redis在这个项目里不是必选项,但我用它做了两件很提效的事:统计热门案例的浏览量、存储验证码。后面我会详细讲实现。
前端我选的是Vue 3 + Element Plus + Axios,构建工具用Vite。如果你前端基础薄,也可以直接用Thymeleaf做服务端渲染,一个SpringBoot工程搞定,省去跨域和联调烦恼。但这里我强烈建议学一下前后端分离,这本身就是毕设答辩的一个加分展示点。
1.3 系统架构与目录结构规划
做前后端分离项目,最忌讳的就是想到哪写到哪。我的一贯做法是先把工程结构定死,后面填代码就不容易乱。
后端的核心包结构如下:
com.campus.antifraud ├── config // 配置类:安全配置、Redis配置、跨域配置、拦截器 ├── controller // 接口层:接收请求、参数校验、返回统一结果 ├── service // 业务层:核心逻辑都在这一层 │ └── impl ├── mapper // 数据访问层:MyBatis-Plus的Mapper接口 ├── entity // 实体类:对应数据库表 ├── dto // 前端交互对象:登录DTO、案例查询DTO等 ├── vo // 返回对象:统一封装给前端展示的数据结构 ├── utils // 工具类:JWT生成解析、Redis工具、脱敏工具 ├── exception // 全局异常处理器 └── common // 通用类:统一返回结果Result、状态码枚举这个结构是标准的分层架构。Controller只做参数接收和结果返回,不写业务逻辑;Service层写业务规则;Mapper层只做数据读写——各层职责单一,答辩时老师问“你这个项目是怎么分层的,为什么这么分层”,你照着这个结构说就行。
有个细节要提醒:实体类千万别直接返回给前端。比如用户表有密码字段,你说你没写查询密码的接口,但万一哪天漏了呢?正确做法是新建VO对象,把需要展示的字段拷贝过去再返回。这不仅是安全习惯,也是代码规范的一部分。
2. 数据库建模与核心表结构设计
2.1 从用户到案例:数据表的整体规划
数据库设计是毕业设计的半壁江山,很多同学一上来就建表,做到后面发现字段不够用、表关系不对,推倒重来。我建议把所有表放在一起全局规划,一次性想清楚。
平台总共设计了7张核心表:
| 表名 | 说明 | 核心字段 |
|---|---|---|
| t_user | 用户表 | id, username, password, nickname, avatar, role, dept_id, status |
| t_case | 案例表 | id, title, type_id, content, cover, author_id, views, likes, status |
| t_case_type | 案例分类表 | id, name, sort |
| t_comment | 评论表 | id, case_id, user_id, content, parent_id, create_time |
| t_like_record | 点赞记录表 | id, user_id, case_id, create_time |
| t_answer | 答题记录表 | id, user_id, score, answer_json, create_time |
| t_notice | 公告表 | id, title, content, create_time |
用户表要区分角色。我用了最简单的方案:role字段,0表示学生,1表示管理员,2表示审核员。不要搞太复杂的权限模型,毕设场景下RBAC用role字段完全够用。dept_id关联的是学院或系别,这个字段很有用,后面统计“哪个学院学习完成率高”就靠它。
2.2 案例表的字段设计细节
案例表是核心内容表,字段设计直接决定功能好不好做。我自己吃过亏,最初版本少设计了两个字段,后面代码写得很难受。
第一个容易漏的字段是status。案例不是发出来就能上线的,管理员要审核,审核通过才展示给学生。status值为0是待审核、1是已发布、2是已下架。别小看这个字段,它让“内容审核”功能成为可能,也避免了学生直接看到还没编辑完的半成品。
第二个容易漏的是summary。列表页不需要展示全文,只需要一段摘要。如果数据库没有summary字段,列表接口就要把content全文查出来再截取,既浪费IO又麻烦。我在设计时就加上summary,列表页直接select摘要字段,详情页才查全文。
第三个细节是封面图字段cover。大学校园里网速再好,一个纯文字的页面还是劝退的。封面图让每个案例卡片看起来更加美观。文件上传方案后面会讲,表结构里先预留这个字段。
likes字段我做了冗余设计。如果一个案例的点赞数要实时统计,每次都要count点赞记录表,数据量大了效率有隐患。所以我在案例表里存一个likes总量,用户点赞时用事务同时更新点赞记录表和likes字段。这种“空间换时间”的冗余设计,在答辩时可以作为一个加分亮点讲给老师听。
2.3 MyBatis-Plus逆向工程与建表实战
表结构确定之后,建表我推荐直接在Navicat或者命令行执行SQL,然后用MyBatis-Plus的代码生成器自动生成entity、mapper和service。这里有一个很多人不知道的细节:生成器生成的实体类默认用驼峰命名映射下划线字段,需要在配置文件里开启map-underscore-to-camel-case才算完整,不过MyBatis-Plus默认就开着。
核心建表SQL我贴几个关键的,你们可以参考:
CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint DEFAULT 0 COMMENT '0学生 1管理员 2审核员', `dept_id` bigint DEFAULT NULL COMMENT '学院ID', `status` tinyint DEFAULT 1 COMMENT '1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';注意几个点:密码字段长度至少100,因为BCrypt加密后的字符串很长;所有表都加create_time,便于管理后台排序;字符集用utf8mb4而不是utf8,因为要存emoji和生僻字,虽然我不用emoji但学生评论里可能带。
其实每一张张表都设计个编号,还有存储引擎——InnoDB必须的,撑事务和外键。毕业设计如果用了MyISAM,答辩被问到事务处理就很尴尬了。
3. 核心功能实现与代码解析
3.1 用户注册登录与JWT鉴权
用户模块是系统的门面,也是Spring Security的主要应用场景。注册逻辑本身不复杂:前端传username、password、nickname,后端先查重,再用BCrypt加密入库,默认角色是学生。注意密码绝对不能明文存,这一点任何老师都会问,BCrypt是标准答案。
登录的核心代码逻辑如下:
@Service public class AuthServiceImpl implements AuthService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Override public LoginVO login(LoginDTO dto) { // 1. 根据用户名查询用户 User user = userMapper.selectOne( new LambdaQueryWrapper<User>() .eq(User::getUsername, dto.getUsername())); // 2. 用户不存在或密码不匹配 if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 3. 用户被禁用 if (user.getStatus() == 0) { throw new BusinessException("账号已被禁用,请联系管理员"); } // 4. 生成JWT String token = JwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole()); // 5. 组装返回对象,注意脱敏 LoginVO vo = new LoginVO(); vo.setToken(token); vo.setUserInfo(convertToUserVO(user)); return vo; } }JWT工具类核心就三件事:生成token、解析token、校验过期时间。我用了jjwt库,一个被广泛使用的Java JWT库。密钥放到application.yml里,注意别硬编码在代码中,否则每次重启密钥就失效了,已登录的用户全部要重新登录。
Spring Security的配置是这个模块的难点。核心逻辑是:放行登录、注册接口,其他接口全部走JWT过滤器,过滤器里解析token并把用户信息放进SecurityContext。这样后面的接口通过注解就能拿到当前用户。具体配置不贴全量代码了,重点说一个踩坑点:Spring Security默认开启CSRF防护,前后端分离模式下必须显式关闭,否则所有POST请求都会403,我见过好多人在这个地方卡了一整天才发现是CSRF的问题。
3.2 案例发布、审核与分类检索
案例模块是平台的核心内容流。设计时我把“内容展示”和“内容生产”分成了两个视角:学生看到的是已经审核通过的案例列表;管理员和审核员看到的是带状态的案例管理列表,待审核的优先排在最前面。
案例列表接口是个典型的分页查询加条件过滤场景,用MyBatis-Plus的条件构造器非常顺手:
public Page<CaseVO> getCasePage(CaseQueryDTO dto) { Page<Case> page = new Page<>(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapper<Case> wrapper = new LambdaQueryWrapper<>(); // 只查询已审核通过的案例 wrapper.eq(Case::getStatus, 1); // 按分类筛选 if (dto.getTypeId() != null) { wrapper.eq(Case::getTypeId, dto.getTypeId()); } // 关键词搜索(标题或摘要) if (StringUtils.hasText(dto.getKeyword())) { wrapper.and(w -> w.like(Case::getTitle, dto.getKeyword()) .or().like(Case::getSummary, dto.getKeyword())); } // 最新优先 wrapper.orderByDesc(Case::getCreateTime); Page<Case> result = caseMapper.selectPage(page, wrapper); // 实体转VO,关联查询分类名称和作者昵称 return convertToVOPage(result); }这里有个性能细节:列表接口不要查content字段,但MyBatis-Plus的selectPage是select *的。解决方案是在实体类的content字段上标注@TableField(select = false),或者查询时手动wrapper.select(Case::getId, Case::getTitle, Case::getSummary, ...)。我用的是后者,更显式一些。这个细节虽然小,但能体现你对性能和字段控制的意识,在答辩时提一句,老师会觉得你考虑得很全面。
案例详情的浏览计数,我用Redis做了优化:
public CaseDetailVO getCaseDetail(Long caseId) { // 1. 查出详情 Case caseInfo = caseMapper.selectById(caseId); // 2. Redis中自增浏览量 String key = "case:views:" + caseId; Long views = redisTemplate.opsForValue().increment(key); // 3. 如果缓存中没有值,取数据库的值作为初始值 if (views == 1) { redisTemplate.opsForValue().set(key, caseInfo.getViews() + 1); views = caseInfo.getViews() + 1L; } // 4. 异步写回数据库(或定期批量更新) caseInfo.setViews(views.intValue()); // 返回详情... }理论上浏览量可以先放Redis,定期批量刷回数据库,降低频繁update对MySQL的压力。不过毕设项目量级不大,直接在读详情时同步increment然后update数据库也完全扛得住。但如果你用Redis做了,记得在答辩时说清楚为什么这样设计——高并发场景下数据库写压力大,Redis是内存操作,扛写能力远强于磁盘型数据库。这个思路本身就是加分项。
3.3 互动社区:评论、点赞与举报
社区功能让平台从单向宣传变成双向互动,具体拆开就是评论和点赞两个业务。
评论表是自关联的,支持楼中楼。parent_id为0表示一级评论,非0表示对某条评论的回复。查询评论列表时,我采用的方式是:先查一级评论,再根据一级评论的id批量查询回复,然后组装成树形结构。这种方式比一次性查全部再在内存中递归组装要清晰得多。
点赞功能的关键是防重复点。核心做法是加唯一索引:t_like_record表里user_id和case_id建立联合唯一索引。用户点击点赞时,先insert点赞记录,如果触发了DuplicateKeyException说明已经点过了,再执行取消点赞逻辑。这个手段比先select再判断要更靠谱,因为在高并发下select判断存在竞态条件——两个请求同时查到不存在,然后都执行insert,还好唯一索引兜底,保证不会出现两条重复数据。这种利用数据库约束处理并发问题的思路,也是面试和答辩的经典考点。
举报功能我做得相对简单:一个t_report表,用户提交举报原因,管理后台展示待处理举报列表,处理完成后状态置为1。虽然业务逻辑简单,但它补全了“社区治理”这个模块的完整性,让系统不只是一个发文章的工具。
3.4 答题功能:让学习效果有反馈
纯看案例是没办法确认学生到底学会没有的。所以我加了一个答题自测功能:从题库里随机抽10道题,学生作答后实时计算得分,成绩低于60分就推荐重新学习相关分类的案例。
题库我设计在代码里管理,没有单独建表。核心原因:题目数量不大,而且每道题关联案例分类的id,用代码维护比数据库维护更适合毕设场景。如果你后续想扩展,可以把题库存到数据库,加上题目管理界面,这算是系统的一个扩展方向。
答题模块的代码结构就是三个接口:获取随机题目、提交答案、查询历史成绩。提交答案时接收用户的选择项数组,和正确答案比对返回分数。响应速度要求高,基本没有复杂逻辑。
3.5 管理后台与统计展示
后台管理系统我用了一套独立的路由和页面,管理员登录后可以看到:用户管理、案例审核、评论管理、数据统计四个大板块。
数据统计是我个人比较看重的功能。我做了三个维度的展示:注册用户总数和每日新增趋势、案例发布量分类占比、各学院学生学习完成率排名。前面两个数据都是简单的count查询,写起来不费劲。关键是第三个——各学院学习完成率,需要联表查询:
public List<DeptRankVO> getDeptStudyRank() { // 统计每个学院的学生总数、已学习人数、平均答题得分 return deptMapper.selectStudyRank(); }对应的SQL大概长这样:
SELECT d.name AS dept_name, COUNT(DISTINCT u.id) AS total_students, COUNT(DISTINCT CASE WHEN a.id IS NOT NULL THEN u.id END) AS studied_students, IFNULL(AVG(a.score), 0) AS avg_score FROM t_dept d LEFT JOIN t_user u ON u.dept_id = d.id AND u.role = 0 LEFT JOIN t_answer a ON a.user_id = u.id GROUP BY d.id ORDER BY studied_students DESC这个SQL能直接把各个学院的参与度拉出来。我给前端配了一个ECharts柱状图,效果非常直观。这个页面做完之后,整个系统的完整度瞬间提升了一个档次——你的平台不只是看文章的地方,它真的在驱动各个学院去做反诈学习这件事。
4. 前后端联调与部署中的典型问题
4.1 跨域问题:前后端联调第一坑
前端跑在localhost:5173,后端跑在localhost:8080,端口不同,浏览器按同源策略直接拦截。前后端分离项目遇到跨域问题是100%的,解决方案是在后端配置CorsFilter,允许指定来源访问。
我在Spring Security的配置类里加了CorsConfigurationSource的Bean,设置allowedOriginPatterns为http://localhost:5173,允许的方法包括GET、POST、PUT、DELETE,允许携带凭证。只开放这一个前端来源,其他来源一律不放行,这是基本的安全意识。
有一个细节要提醒:如果你用了Spring Security,CORS配置要放在SecurityFilterChain之前生效,否则安全过滤器会先于跨域配置拦截请求。具体做法是调用http.cors()开启Spring Security的CORS支持。不少人在跨域配置上踩坑就是漏了这个。
4.2 文件上传的本地存储与访问映射
案例封面、用户头像都是图片上传需求。我用了最简单的方案:本地磁盘存储 + 虚拟路径映射。
后端接收MultipartFile后,把文件写入服务器的一个指定目录,文件名用UUID重命名防止冲突,然后返回一个可访问的URL。为了让前端能通过URL访问到这个文件,需要配置静态资源映射:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = "file:" + uploadDir + "/"; registry.addResourceHandler("/upload/**").addResourceLocations(uploadPath); } }这里uploadDir是上传目录的绝对路径。用绝对路径是个关键点——相对路径在不同的启动方式下会指向不同的位置,用IDE启动和用java -jar启动的目录不同,容易出幺蛾子。
部署时有人会问:为什么不用OSS?原因很简单,毕设环境不一定有云服务资源,而且OSS需要配置密钥、开通服务,会增加不少工作量。本地存储虽然low一点,但能完整跑通流程。答辩时如果被问到,你可以说“生产环境可以替换为对象存储服务,接口层面不需要改动”,这个回答已经证明你了解这个技术演进路径了。
4.3 部署上线:从jar包到服务器运行
项目做完之后,部署上线也是毕设验收的必经环节。后端打包前先把application.yml里的数据库连接改成线上配置,然后执行mvn clean package -DskipTests,在target目录下拿到jar包。服务器上执行命令:
nohup java -jar antifraud-platform.jar --server.port=8080 > app.log 2>&1 &前端构建需要先安装依赖然后打包:
npm install npm run build构建产物在dist目录,是一个纯静态文件集合。我用Nginx托管这些静态文件,同时配置反向代理把/api开头的请求转发到后端8080端口:
server { listen 80; server_name your-domain.com; location / { root /var/www/antifraud; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行是Vue路由history模式必须的,否则刷新页面时会404。这个Nginx配置就是我在实测中最常用的,直接复制改改域名就能用。
4.4 开发期高频报错与解决方案速查
我整理了开发这个项目时容易踩的坑,也是后来学弟学妹问得最多的几个问题,直接给结论:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求接口403 | Spring Security CSRF未关闭 | 在SecurityConfig中显式关闭csrf |
| 登录后接口返回401 | JWT过期或过滤器未放行 | 检查token有效期,检查SecurityConfig放行路径 |
| 中文乱码 | MySQL连接URL缺编码参数 | jdbcUrl加characterEncoding=utf8mb4 |
| 返回的时间格式为数字 | Jackson未配置日期格式 | 配置spring.jackson.date-format=yyyy-MM-dd HH:mm:ss |
| MyBatis-Plus批量插入失败 | 实体类主键策略不对 | 主键用@TableId(type = IdType.AUTO) |
| 前端上传图片返回404 | 静态资源映射未生效 | 检查虚拟路径和实际路径是否匹配 |
表里第一行就是我前面说的CSRF问题,第三行中文乱码也值得多说两句。现在MySQL 8.0默认字符集就是utf8mb4,但如果连接URL没显式指定,某些情况下仍然可能乱码。连接串里加上serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4基本能一次到位。
另外一个容易被忽略的问题:SpringBoot版本和MyBatis-Plus版本的兼容性。如果你的SpringBoot是2.7.x,MyBatis-Plus要用3.5.x;如果是SpringBoot 3.x,必须用MyBatis-Plus 3.5.4以上版本,否则启动直接报错。这种版本匹配问题在毕业设计季实在太常见了,建议大家建项目时一次把版本全部锁死。
5. 项目扩展与答辩准备建议
项目做到这里,已经是一个功能完整、能跑通前后端闭环的系统。但对于毕业设计来说,真正拉开差距的往往是“增量”——你有没有做一些基础需求之外、能让老师眼前一亮的东西?我给出几个低成本高回报的扩展思路。
第一个是把验证码换成滑块验证或者图形验证码。登录页加一个图形验证码,后端生成图片base64返回前端,校验时对比Redis中存储的值。这个功能不难,但能让登录模块更完整,也能体现你对“接口防刷”有认知。我在项目里用的是Redis存储验证码,超时时间设置60秒,过期自动失效。
第二个是数据统计页增加“最新骗局预警”模块,通过爬虫或者人工录入的方式把最新的诈骗手法在首页置顶展示。这个模块做成一个接口就行,管理后台可以手动标记某条案例为“紧急预警”,平台首页立即置顶显示。虽然逻辑不复杂,但它回应了“高校防诈骗”这个核心场景的时效性需求。
第三个是添加一个“学习证书”功能:学生学完指定数量的案例,答对指定数量的题目,自动生成一张电子证书。这是一种游戏化激励手段,能明显提升学习完成率。实现也不难:用一个进度表记录学习轨迹,满足条件后生成一张带用户昵称、学院名称、时间的证书图片。
答辩准备的话,我从自己参加过的答辩中总结出三个必考点:一是项目背景和意义——为什么做这个系统,对比现有方案有什么改进;二是技术选型——为什么用SpringBoot而不用SSM,为什么用Redis,为什么做前后端分离;三是个人贡献——哪些模块是你独立设计的,哪些遇到了困难怎么解决。回答的核心思路是把“做了什么”讲清楚固然重要,但更重要的是把“为什么这么做”讲清楚。
最后再分享一个我个人的实操感受:做毕设遇到卡壳的时候,不要一个人闷头调试超过两小时。先休息一下,让脑子冷静下来,很多时候问题出在一个非常愚蠢的拼写错误上。我在做这个项目时,光是排查一个“日期格式返回变成数字”的问题就花了半天,最后才发现是Jackson配置少写了一个格式化的注解。这些小坑,踩过一次就能记住一辈子。