我做校园类项目前后带过不少学弟学妹,二手交易这个题目几乎每年都有人选,但真正能把源码跑起来、开题写明白、答辩讲清楚的并不多。大部分人的困境不是不会写代码,而是不知道这个项目的“边界”在哪——功能堆太多做不完,堆太少又显得没内容,最后交上去的东西既不像个能用的系统,也不像一份合格的毕业设计。这篇就围绕校园二手交易网站这个题目,把源码结构、技术选型、数据库设计、核心功能实现、开题报告写法、部署演示这几块掰开揉碎讲一遍,从零基础到已经动手写了一半的人都能拿去对照。开题部分我按实际评审关注的逻辑来拆,不写那种满篇“具有重要意义”的空话。
1. 先把这个项目的真实边界划清楚
1.1 校园场景和闲鱼这类平台到底差在哪
很多人一上手就说“我要做一个校园版闲鱼”,这个定位一出来,后面所有的设计都会跑偏。社会化的二手平台解决的是陌生人之间的低频大额交易,而校园二手交易的核心特征是高频、小额、强信任、短周期。一本书、一个台灯、一辆毕业甩卖的自行车,价格几十块,买卖双方大概率住同一栋楼或者同一个校区,交易方式是当面交付,付款方式很多时候是当面转账甚至现金。这四条特征决定了整个系统的重心根本不在“交易担保”,而在“信息撮合效率”和“身份可信”。
我见过太多同学照着电商系统的套路去设计,做了一整套购物车、优惠券、多规格SKU、物流跟踪、退款仲裁流程,结果一个学期过去只完成了商品发布,剩下的全是半成品。这就是没把场景吃透的后果。校园场景里,购物车基本是伪需求——没人会把三本书加进购物车一起结算,因为每件商品的卖家都不一样。物流模块更是完全不需要,同城当面交易根本不存在快递环节。把这些砍掉,你省下的时间足够把搜索、筛选、消息通知、信用评价这几块做到能拿得出手的程度。
另一个经常被忽略的点是周期性。校园二手交易有明显的时间波峰:开学季、毕业季、考研季。毕设答辩如果安排在春季,正好可以拿毕业季的真实需求当业务背景讲,这个细节写进开题报告的“研究背景”里,比泛泛而谈“二手交易市场规模逐年增长”要扎实得多。
1.2 该做什么、不该做什么,列个清单
我给几个学弟整理过一份功能取舍清单,原则很简单:核心链路必须跑通,边缘功能可以做但不能影响主流程。
必做部分围绕“发布—浏览—联系—成交—评价”这条主线展开。用户注册登录、商品发布(含多图上传)、分类浏览、关键词搜索、商品详情页、站内留言或联系方式展示、下单/预定、订单状态流转、完成后互评,这是最小闭环。少了其中任何一环,演示时就会卡壳,答辩老师一问“买家怎么联系卖家”你答不上来就很尴尬。
加分项是可以做但要控制工作量的部分:收藏、浏览历史、举报违规、求购专区、按距离排序(需要位置信息,成本较高,建议只做校区筛选)、简易后台管理(用户管理、商品审核、举报处理)。这些功能的共同特点是独立性强,做不完直接关掉入口不影响主线。
明确不要做的部分:在线支付(涉及资质,校园项目直接做会被质疑合规性)、物流对接、复杂促销、多语言。尤其是支付,我的建议是做成“线下当面交易+状态标记”,系统只记录订单达成,不碰资金流。这个取舍一定要在开题报告的“研究内容”里写清楚,反而是加分项——说明你懂业务边界。
2. 技术选型:别把简单项目做复杂
2.1 三套主流方案的实际对比
学生项目最常见的坑就是技术栈选择被“流行度”绑架。我见过一个二手交易网站,用户量预期不超过两百,硬是拆成了六个微服务,最后部署的时候光内存就不够,答辩前一天还在调服务注册中心。这种失败完全是自己给自己找的。
| 方案 | 技术组合 | 开发周期(单人) | 适合人群 | 主要风险 |
|---|---|---|---|---|
| 方案A | Spring Boot + MyBatis-Plus + MySQL + Vue3 | 4~6周 | 有 Java 基础,想做主流后端 | 前后端分离调试耗时,跨域配置容易卡 |
| 方案B | SSM(Spring+SpringMVC+MyBatis)+ JSP/Thymeleaf | 5~7周 | 课程学的是老技术栈 | 模板渲染调试繁琐,页面交互偏弱 |
| 方案C | Node.js(Express/Koa)+ MySQL + 原生前端 | 3~5周 | 熟悉 JS,想快点出成果 | 后端工程化能力体现不足,答辩时容易被追问 |
我个人的建议是方案A。原因不在于它“先进”,而在于它的可解释性最强。答辩时老师问“你的分层是怎么设计的”,你能清晰地说出 Controller 负责参数校验和路由、Service 承载业务逻辑和事务、Mapper 负责数据访问,这套分层逻辑是教科书级的,讲起来毫不费力。方案C虽然开发快,但很多评审会认为后端深度不够,尤其是当你的题目被归到“计算机科学与技术”而不是“软件工程”的时候。
方案B如果是课程强制要求那就没办法,但要注意一个问题:JSP 在现代浏览器和容器版本上兼容性坑不少,用 Thymeleaf 会稳一些,而且它和 Spring Boot 集成更顺。
2.2 为什么单体架构才是这个项目的正确答案
单体架构不是“落后”,而是匹配当前规模的最优解。你的并发量预估是几十到几百,一台 2 核 4G 的云服务器足够跑起来,数据库和应用部署在同一台机器上,出了问题看日志一目了然。微服务带来的收益——独立部署、技术异构、弹性伸缩——在这个规模下一条都用不上,但成本是实打实的:服务间调用要处理超时和重试、链路追踪要额外搭、本地开发要起一堆进程。
我在实际项目里的做法是逻辑分层、物理单体:代码层面严格按模块划分(用户模块、商品模块、订单模块、消息模块),每个模块内部再分层,但打包出来就是一个 jar。这样既保证了代码可读性,又避免了运维复杂度。将来真要拆分,因为模块边界清晰,改造成本也不高。这个思路可以原封不动写进开题报告的“技术路线”部分,是很标准的表述。
2.3 中间件的取舍:哪些能省,哪些不能省
Redis 要不要上?我的答案是:如果要展示登录态和热点商品缓存,可以上;如果只是为了“显得技术栈丰富”,别上。校园项目的会话量很小,用 Spring Session 存数据库或者干脆用 JWT 无状态令牌都能解决,多引入一个中间件就多一个部署和排查的成本。
文件存储反而是一个必须认真对待的点。商品图片是这个系统的核心内容,如果直接把图片以二进制存进数据库,表会迅速膨胀,查询性能肉眼可见地下降。正确做法是图片存磁盘或对象存储,数据库只存访问路径。本地开发时存到项目外的目录(比如/data/upload/),通过 Nginx 或 Spring 的静态资源映射对外提供访问。这个细节在很多学生的代码里是错的,答辩时被问到“图片怎么存的”答“存数据库里了”,基本就露馅了。
还有一个常被忽略的是全局异常处理和参数校验。用@RestControllerAdvice统一捕获异常、用@Valid做参数校验,这两个东西加起来不到一百行代码,但能让你的接口在异常输入下不返回一大坨堆栈信息。答辩演示的时候如果因为输入了一个空标题就白屏,观感非常差。
3. 数据库设计:这个项目的真正骨架
3.1 核心表结构和字段取舍
数据库设计得好,后面写代码是顺水推舟;设计得烂,每加一个功能都要改表。我按实际项目经验给一套可以直接用的结构。
用户表要区分普通用户和管理员,用角色字段控制权限。手机号做唯一索引,因为校园场景里手机号是最主要的联系方式。还要留一个campus字段记录所属校区,这个字段在筛选时非常有用。
CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '加密后密码', nickname VARCHAR(32) NOT NULL, phone VARCHAR(20) UNIQUE COMMENT '手机号,用于联系', campus VARCHAR(50) COMMENT '所属校区', avatar VARCHAR(255), credit_score INT DEFAULT 100 COMMENT '信用分,初始100', role TINYINT DEFAULT 0 COMMENT '0普通 1管理员', status TINYINT DEFAULT 1 COMMENT '1正常 0封禁', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;商品表是整个系统的核心,字段设计上要有几个前瞻性考虑。status不能只做“上架/下架”两态,实际业务里至少需要:待审核、在售、已预定、已售出、已下架。view_count记录浏览量,用于详情页展示和热度排序。off_shelf_time记录下架时间,方便做超期自动下架。
CREATE TABLE t_goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, seller_id BIGINT NOT NULL, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2) COMMENT '原价,用于展示折扣', cover_image VARCHAR(255), images VARCHAR(1000) COMMENT '多图,逗号分隔或JSON', campus VARCHAR(50), condition_level TINYINT COMMENT '成色 1全新 2九成新 3有使用痕迹', status TINYINT DEFAULT 1 COMMENT '0待审核 1在售 2已预定 3已售出 4已下架', view_count INT DEFAULT 0, off_shelf_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_status (category_id, status), KEY idx_seller (seller_id), KEY idx_title (title) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表要特别注意一点:同一件商品在“已预定”状态下不能被第二个人下单。这个约束靠代码判断会有并发问题,正确做法是在数据库层加唯一索引做兜底。
CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, goods_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT '1待确认 2已确认 3已完成 4已取消', remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, KEY idx_buyer (buyer_id), KEY idx_seller (seller_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.2 几个当时踩过的设计问题
多图存储用逗号分隔还是 JSON?我一开始用逗号拼接存字符串,后来要做“删除其中一张图”的时候发现得写一堆字符串切割逻辑,还容易出错。改用 JSON 字段后(MySQL 5.7+ 支持),读写都清爽很多。如果你的 MySQL 版本较低,那就单独建一张t_goods_image表,一图一行,是最规范的做法。
分类要不要做成两级?单级分类足够。校园二手商品的品类本来就不多,教材、数码、生活用品、运动器材、代步工具、其他,六个大类能覆盖九成场景。做两级分类,用户发布时要在两个下拉框里点来点去,体验反而变差。真要细分,用标签更适合,“考研资料”“四六级”“宿舍神器”这种标签比分类灵活得多。
信用分怎么算?我见过一些毕设里信用分是随机生成的,一看就假。比较合理的做法是初始 100 分,每完成一笔订单加 2 分上限 150,被举报核实扣 20 分,低于 80 分限制发布。这套规则简单,但在开题报告的“研究内容”里写出来,显得你确实想过业务闭环。
3.3 索引和数据量预估的细节
索引不是越多越好。t_goods表上我加了三个索引:分类+状态联合索引用于列表页筛选,卖家 ID 索引用于“我的发布”,标题索引用于搜索。注意标题索引用的是普通 B+ 树索引,LIKE '%关键词%'这种前后模糊匹配是走不上的,这也是为什么很多人说“我加了索引怎么还慢”。
数据量不大的情况下(几千条商品)根本感觉不到差异,但如果开题报告里被问到“如何优化搜索性能”,你可以回答两条:一是数据量小的时候直接全表扫描加LIKE足够,二是数据量上来后引入全文索引(MySQL 的FULLTEXT)或独立的搜索引擎。这个回答说明你知道当前的方案和它的边界在哪,比硬套一个 Elasticsearch 强得多。
4. 核心功能实现的实操要点
4.1 登录与密码处理,别在这种地方犯低级错误
密码必须加盐哈希存储,用 BCrypt 是最省事的选择,Spring Security 自带BCryptPasswordEncoder,一行代码搞定。绝对不要用 MD5,更不要明文。我在帮人看代码时真的见过明文存密码的,这个如果被评审发现,基本就是一票否决。
登录态的选择上,前后端分离项目用 JWT 比较顺手。核心逻辑是登录成功返回一个 token,前端存到 localStorage,之后每个请求在请求头里带上Authorization。后端写一个拦截器统一校验,把用户 ID 解析出来放进 ThreadLocal,业务代码直接取,不用每个方法都传一遍。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { String token = req.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BizException(401, "未登录"); } Claims claims = JwtUtil.parse(token); UserContext.set(Long.valueOf(claims.get("uid").toString())); return true; } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception e) { UserContext.clear(); } }注意afterCompletion里必须清理 ThreadLocal,否则线程池复用会导致用户信息串号。这个问题在本地测试时几乎发现不了,上线后偶尔出现“看到别人订单”的诡异现象,排查起来极其痛苦。我自己就栽过一次,后来养成习惯,凡是 ThreadLocal 一律配套写remove。
4.2 商品发布:图片上传是坑最多的地方
图片上传的完整链路是:前端选文件 → 后端接收 → 校验类型和大小 → 重命名(防止重名覆盖和路径穿越)→ 存盘 → 返回访问 URL。这五步里每一步都有坑。
类型校验不能只看扩展名,要读文件头。一个改了后缀的脚本文件如果只按扩展名校验就会被放行。简单做法是用ImageIO.read()尝试读取,读不出来就不是图片。大小限制在配置文件里设置,Spring Boot 默认单文件 1MB、单请求 10MB,商品图一般 2MB 上限比较合适,超过的话在application.yml里改:
spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB文件名一定要重命名,用 UUID 加原扩展名。如果直接用用户上传的文件名,一方面会重名覆盖,另一方面如果有人上传一个叫../../config.yml的文件,可能写到意料之外的目录。
多图上传时前端用 FormData 一次带多个文件,后端用MultipartFile[]接收,循环处理后把路径拼成 JSON 数组存进images字段。这里有个细节:如果其中某一张失败,应该整体回滚,不要让商品创建成功但图片缺一张。
4.3 搜索与筛选,把用户最常用的路径做顺
搜索框的默认行为要设计好。我建议是多字段联合匹配,标题或描述里包含关键词就命中。SQL 层面就是WHERE (title LIKE ? OR description LIKE ?) AND status = 1。性能不用担心,几千条数据下毫秒级返回。
筛选条件的组合要用动态 SQL。MyBatis 里用<if>标签,或者用 MyBatis-Plus 的LambdaQueryWrapper链式拼接,后者代码更清爽:
LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Goods::getStatus, 1); if (StringUtils.isNotBlank(keyword)) { wrapper.and(w -> w.like(Goods::getTitle, keyword).or().like(Goods::getDescription, keyword)); } if (categoryId != null) wrapper.eq(Goods::getCategoryId, categoryId); if (StringUtils.isNotBlank(campus)) wrapper.eq(Goods::getCampus, campus); if (minPrice != null) wrapper.ge(Goods::getPrice, minPrice); if (maxPrice != null) wrapper.le(Goods::getPrice, maxPrice); wrapper.orderByDesc(Goods::getCreateTime);注意排序规则要给用户可选项:最新发布、价格从低到高、价格从高到低。默认按发布时间倒序,这是二手交易里最符合直觉的排序方式,因为新发布的商品往往性价比更高、卖家也更活跃。
分页要记得做,别一次性把几百条数据全返回。用 MyBatis-Plus 的Page对象配合分页插件,两行代码搞定。前端做滚动加载或者页码跳转都行,页码更简单,也更好演示。
4.4 订单状态机,这块逻辑乱整个系统就乱
订单状态流转必须定义清楚,而且要集中管理,不能每个接口里各写各的判断。我建议定义一个枚举加一张流转规则表,代码里用一个方法统一做状态校验和推进。
| 当前状态 | 允许的操作 | 目标状态 | 触发方 |
|---|---|---|---|
| 待确认 | 卖家确认 | 已确认 | 卖家 |
| 待确认 | 取消 | 已取消 | 买家/卖家 |
| 已确认 | 完成交易 | 已完成 | 买卖双方任一确认后由卖家确认 |
| 已确认 | 取消 | 已取消 | 买家/卖家 |
| 已完成 | 互相评价 | 已完成 | 双方 |
| 已取消 | 无 | - | - |
关键点是状态推进要加乐观锁。两个客户端同时点“确认”,如果不加控制可能会重复触发。做法是在t_order表加一个version字段,更新时带上版本号条件:
boolean ok = orderMapper.update(null, new LambdaUpdateWrapper<Order>() .eq(Order::getId, orderId) .eq(Order::getStatus, OrderStatus.WAIT_CONFIRM.getCode()) .set(Order::getStatus, OrderStatus.CONFIRMED.getCode()) .set(Order::getVersion, version + 1)) > 0; if (!ok) throw new BizException("订单状态已变更,请刷新");商品状态要跟订单状态联动。买家下单成功,商品立刻从“在售”改成“已预定”,避免被别人重复下单;订单取消则退回“在售”;订单完成则商品改为“已售出”。这个联动逻辑放在 Service 层的方法里,加上@Transactional保证一致性。我特意提这一点是因为见过太多项目里商品状态和订单状态各走各的,最后出现“已售出的商品还能下单”这种明显 bug。
4.5 站内消息,做轻不做重
完整的即时通讯系统对这个项目来说太重了,需要长连接、消息存储、已读未读、离线推送。我的建议是做一个简化版的站内信:买家在商品详情页点“联系卖家”,跳转到一个对话页,消息按会话分组存储,双方进入页面时轮询拉取新消息(三秒一次就够用),或者干脆做成留言板形式,不需要实时。
数据库设计上,一张t_message表记录发送方、接收方、商品 ID、内容、是否已读、时间。查询会话列表就是按(from_id, to_id, goods_id)分组取最新一条,稍微复杂一点但 SQL 写得出来。如果觉得麻烦,直接用“留言”模式——留言展示在商品详情页下方,所有人可见,这样连会话管理都省了,实现成本极低,演示效果也不差。
我个人的取舍是:做留言,不做私聊。理由很实在,校园二手交易的沟通量本身就不大,很多交易在详情页留言两三条就敲定了。省下来的时间投到搜索和筛选上,收益更高。
5. 开题报告怎么写才不会被退回
5.1 开题的核心逻辑:证明这件事值得做、你能做
开题报告不是走流程的表格,它的本质是回答两个问题:这个题目有研究价值吗,你有能力在给定时间内完成吗。所有章节都是围绕这两点展开的。理解了这个,你就知道为什么有些开题会被退回——要么选题太泛(“做一个电商平台”),要么技术路线含糊(“采用先进技术实现”),要么进度安排明显做不完。
选题背景部分,别去堆宏观数据。写校园场景的具体痛点:毕业生离校时大量物品无法带走、低年级学生置办生活用品成本高、现有社交群组交易信息刷屏且无法检索、交易双方身份不明缺乏信任基础。这四条每一条都能对应到你系统里的一个功能模块,逻辑是闭合的。这种写法比“随着互联网的快速发展”强一百倍。
5.2 国内外研究现状怎么写才不像凑字数
这一节最容易写成产品介绍大全 —— 罗列一堆平台名称然后说“功能强大”。这种写法评审一眼就能看出你没读过文献。比较靠谱的结构是:先讲二手交易模式的研究脉络(从分类信息到社区化交易),再讲技术层面的研究(推荐算法在二手交易中的应用、信用评价机制),最后落到“校园垂直场景的研究相对较少”,从而引出你的选题。
文献要真读几篇。不需要多,八到十二篇就够,其中至少三到五篇是近三年的期刊论文。知网上搜“二手交易”“校园电商”“信用评价”这些关键词,挑几篇核心期刊的,读摘要和结论,知道人家解决了什么问题、用了什么方法。写作的时候用自己的话概括,别整段翻译。这一节能写到八百到一千字,整份开题的厚度就有了。
5.3 研究内容和技术路线,写细一点反而好交差
研究内容要按模块列,每个模块写清楚做什么、用什么方法、达到什么效果。比如“商品检索模块:支持关键词模糊匹配与多条件组合筛选,采用 MyBatis 动态 SQL 实现条件拼接,通过分类与状态联合索引优化查询效率”。这么一句话,信息量比“实现商品搜索功能”大得多,而且每一句都是你能兑现的。
技术路线用文字加流程图描述(注意,开题文档里的流程图用 Visio 或 draw.io 画成图片插入,不要用代码生成)。写清楚开发模型是瀑布还是迭代、每个阶段产出什么。我建议写“迭代式开发”,因为实际情况就是你做一块测一块,写瀑布反而和事实不符,答辩时被追问细节容易露馅。
进度安排必须是可执行的。给一份参考:
| 阶段 | 时间跨度 | 主要任务 | 交付物 |
|---|---|---|---|
| 需求分析与设计 | 第1-2周 | 调研、用例梳理、库表设计 | 需求文档、ER图 |
| 环境搭建与框架集成 | 第3周 | 项目骨架、依赖配置、基础工具类 | 可运行的空白工程 |
| 用户与商品模块 | 第4-5周 | 注册登录、发布、列表、详情 | 主线功能可演示 |
| 订单与消息模块 | 第6-7周 | 下单流转、留言、评价 | 完整业务闭环 |
| 后台与优化 | 第8周 | 管理端、异常处理、性能调优 | 可交付版本 |
| 测试与论文撰写 | 第9-12周 | 功能测试、文档整理、答辩准备 | 论文、演示材料 |
这里有个经验:每个阶段的交付物都要是可验证的。“完成用户模块”这种描述没法验收,“注册登录流程可用,密码加密存储,登录态可保持”就是可以当场演示的。答辩老师如果翻到这一页,看到的是具体成果而不是模糊承诺,印象分完全不同。
6. 部署上线与答辩演示的实战细节
6.1 服务器准备和基础环境
一台 2 核 4G 的云服务器足够。系统装 Ubuntu 或者 CentOS 都行,我更习惯 Ubuntu,软件源更新方便。需要装的东西:JDK 17(或者 8,看你的 Spring Boot 版本)、MySQL 8、Nginx。Node 只在本地打包前端时需要,服务器上不用装。
MySQL 装完后要做三件事:改字符集为 utf8mb4、建独立的业务账号(别用 root 连应用)、配置允许本地连接。字符集这个问题会在存中文和 emoji 时暴露,商品描述里带个表情符号就报错,很影响演示。
安全组要放开 80 端口(Web 访问)和 22 端口(SSH),数据库的 3306 千万别对外开放,只允许本机访问。这一点写进论文的“系统部署”章节是加分项,说明你有基本的安全意识。
6.2 Nginx 配置和前后端分离的部署方式
前端打包成静态文件,后端打成 jar 包用nohup或者systemd后台运行。Nginx 负责静态资源和反向代理。
server { listen 80; server_name _; client_max_body_size 20m; location / { root /var/www/school-market; 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; } location /upload/ { alias /data/upload/; expires 7d; } }try_files ... /index.html这行是关键。前端用 history 模式路由时,直接访问/goods/123这种路径,Nginx 找不到对应文件就返回 404。加上这行回退到index.html,让前端路由接管。这个坑我在第一次部署时踩过,本地开发一切正常,上线后刷新页面就白屏,找了两个小时。
后端的启动脚本写成systemd服务,比nohup稳,宕机能自动拉起:
[Unit] Description=school-market After=network.target [Service] User=www WorkingDirectory=/opt/app ExecStart=/usr/bin/java -jar -Xms256m -Xmx512m school-market.jar --spring.profiles.active=prod Restart=on-failure [Install] WantedBy=multi-user.target-Xmx512m这个参数别设太大,4G 的机器还要跑 MySQL 和 Nginx,堆内存给到 512M 到 1G 之间比较合适。设成 2G 反而容易触发系统 OOM。
6.3 答辩演示清单,提前一天过一遍
演示环节翻车的方式就那么几种,提前规避掉。准备一份数据:至少 20 条商品、5 个用户、3 个不同状态的订单。空系统演示是最忌讳的,点进去一个商品都没有,效果直接减半。数据要真实一点,商品标题写“高数教材(第七版)”“宿舍小台灯”,别写“测试1”“aaa”。
演示路径提前走三遍:注册登录 → 发布商品 → 换账号搜索到该商品 → 下单 → 卖家确认 → 完成 → 互评。这条链路七步,控制在三分钟内讲完。每一步说什么话提前写好,别临场组织语言。
准备两个浏览器(或者一个正常窗口一个无痕窗口),用来演示买卖双方的不同视角。这个技巧很多人都没想到,但效果很好,能直观展示订单状态的双向变化。
7. 常见问题排查速查表
7.1 环境和依赖类问题
这类问题占了新手卡壳原因的七成以上,而且往往一卡就是一整天。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
启动报Table 'xxx' doesn't exist | 数据库没建表或连错库 | 检查application.yml的库名,执行建表脚本 |
| 中文乱码 | 数据库或连接串字符集不是 utf8mb4 | 连接串加?useUnicode=true&characterEncoding=utf8mb4 |
| 前端请求 404 | 代理路径不匹配 | 检查前端 baseURL 和 Nginx 的location /api/是否一致 |
| 请求跨域被拦截 | 本地开发时前后端端口不同 | 后端配置CorsFilter或在WebMvcConfigurer里加映射 |
| 打包后页面空白 | 静态资源路径配置错误 | Vue 项目publicPath设为./或与部署路径一致 |
上传报FileSizeLimitExceeded | 超过默认大小限制 | 调spring.servlet.multipart的配置 |
| 时间差 8 小时 | 数据库时区或 JDBC 时区参数问题 | 连接串加serverTimezone=Asia/Shanghai |
| 端口被占用 | 上次进程没停干净 | lsof -i:8080找到 PID 后 kill |
7.2 业务逻辑类的隐蔽 bug
有几类问题不报错,但现象很怪,排查起来最费劲。
商品列表少了几条数据。检查筛选条件里是不是把status写成了等于 1,而实际上待审核(0)的商品也应该在某些场景下可见。还有一种是LEFT JOIN写成了INNER JOIN,导致没有分类的商品被过滤掉。
并发下单同一件商品都能成功。这就是前面提到的,靠代码先查后改是有时间窗口的。要么给商品表加乐观锁版本号,要么在下单时用UPDATE t_goods SET status = 2 WHERE id = ? AND status = 1判断影响行数,影响行数为 0 就说明被人抢先了。
分页总数不对。八成是在分页插件之外自己又写了一层 count 查询,两个条件不一致。用 MyBatis-Plus 的分页插件就不用管这个,它自动生成 count 语句。
修改商品后列表页没变化。浏览器缓存或者前端没重新拉数据。开发阶段可以在接口上加时间戳参数,或者直接开 DevTools 的禁用缓存。
提醒一句:所有涉及金额、库存、状态的写操作,都要想清楚并发场景。校园项目的并发量虽然小,但答辩时被问“两个人同时下单怎么办”,你能说出乐观锁或者条件更新,这个回答的分量很重。
7.3 一个反面案例的处理过程
有位同学的项目,症状是“发布商品成功后,我的发布列表里有,但首页搜不到”。他先怀疑是搜索的 SQL 写错了,改了半天没效果;又怀疑是缓存,清了缓存还是不行。最后定位到问题是商品发布时status默认值取了 0(待审核),而首页查询条件是status = 1。
这个案例的教训是:排查问题要从数据入手,不要从代码入手。正确顺序是先查数据库,看那条记录的每个字段值是什么,和查询条件对比,差异在哪里,一眼就能看出来。上来就翻代码,等于在几百行里找一个可能不存在的错误。
我一般的排查顺序是:确认数据对不对 → 确认 SQL 单独执行返回什么 → 确认接口实际返回什么 → 最后看代码逻辑。这四步走完,九成的 bug 都能定位。这个方法论写进论文的“系统测试”章节,也比单纯罗列测试用例更有说服力。
最后分享一个小技巧。开题和论文里,凡是提到“创新点”或者“特色”的地方,不要写“界面美观”“操作简便”这种谁都能说的话,写具体一点:比如“针对校园场景的强信任特征,设计了基于手机号与校区绑定的身份校验机制”“考虑到毕业季的商品集中发布潮,实现了按校区的分区展示与就近排序”。这些描述有场景、有方法、有针对性,评审一看就知道你确实在思考业务,而不是在套模板。