去年这个时候某同学拿着他做的校园二手平台交易系统来找我看源码能跑开题报告也交了初稿结果答辩前一晚被问了一句“你这个商品下架了订单还在待确认系统怎么处理”当场卡住。这个场景我印象太深了因为校园二手平台看着简单真正的难点全在状态流转、权限边界和字段设计这些不显眼的地方。我从那之后陆续帮人改过五六个类似的项目有的用 Spring Boot 搭有的用 Python 那套框架也见过用小程序端加云开发硬凑出来的。踩的坑基本一样只是换了个语言而已。这篇东西我打算把校园二手平台交易系统从头到尾拆一遍不做成产品说明书而是按我实际改代码、补需求、救答辩的思路走。包括整体架构怎么定、数据库那几张表到底怎么设计才不返工、商品发布和图片上传里有多少细节要注意、订单状态机为什么必须画出来、站内沟通怎么防止绕过平台还有源码拿到手之后怎么改成自己的、开题报告怎么写才不像从模板里抠的、部署演示当天怎么不翻车。适合正在做毕设的、刚接了个外包小单的、以及想拿这套东西练前后端分离的读者。基础差一点没关系关键步骤我都写了为什么这么选。1. 先弄清楚校园二手场景到底特殊在哪很多人一上来就抄综合电商的逻辑把商品、SKU、购物车、优惠券、支付流水全堆上去结果做到一半发现根本用不上时间全浪费在没人看的功能上。校园二手平台的业务密度其实很低但它的场景约束又特别强不搞清楚这几条后面表结构会反复推倒重来。1.1 和综合二手电商的核心差异最根本的差异是商品唯一性。综合电商一款手机可以有一千件库存校园二手平台一台二手自行车就是一件卖掉就没了。这一条直接决定了你不需要库存字段、不需要扣减库存的锁、不需要考虑并发超卖但你需要一个“重复下单”的防护机制因为两个买家同时点“我要了”是真实会发生的。第二个差异是交易半径极短。买卖双方大概率在同一个校区交易方式是当面看货、当面付款平台不碰钱。这意味着订单系统不需要接支付只需要记录“谁跟谁约了什么时候在哪看货”。很多同学硬要接个第三方支付沙箱答辩时被问“真实场景里学生愿意走平台付款吗”反而答不上来。第三个差异是用户身份可控。校园场景可以要求邮箱域名或者邀请码用户质量比开放平台高一个量级所以你的举报、风控模块不需要做得像大厂那么重简单几招就够用了。第四个差异是生命周期短、数据量小。一个学校的活跃用户可能就几千人商品表一年也就几万条。这个量级下MySQL 单表加合适的索引完全扛得住不要一开始就上搜索引擎、上分库分表那是给自己找罪受。提示如果你在开题报告里写了“高并发”“海量数据”然后又写用单机 MySQL答辩老师很容易顺着问下去。数据量级老老实实按校园真实规模写反而显得你懂业务。1.2 功能边界必须做的和看着很酷但没必要的我把功能分成三档这样你在排期的时候心里有数。优先级功能模块说明大致工作量占比必须做用户注册登录、商品发布、商品列表与检索、商品详情、收藏、订单创建与状态流转、个人中心缺任何一个系统都是残的60%应该做图片多图上传与压缩、站内留言、举报与下架、信用评价、分类与标签直接决定答辩时的完整度25%加分项敏感词过滤、相似商品推荐、数据看板、消息未读数、后台审核工作台做一两个讲清楚原理就够15%我见过太多人把时间全砸在推荐算法上结果“买家点了想要之后怎么联系卖家”这个主链路是断的。答辩老师不傻主链路不通加分项再多也没用。所以排期一定要按主链路先通、再补细节、最后挑加分项的顺序来。还有一类功能要想清楚再动手站内聊天。如果你打算做完整的即时通讯WebSocket 长连接、消息已读未读、离线消息、消息漫游这一套下来工作量顶半个系统。校园项目里更务实的做法是做“留言板式”的站内信买家在商品详情页留言卖家回复双方都能看到配合一个简单的未读角标就够了。等主链路稳了再考虑升级。2. 技术选型和整体架构怎么定技术选型这件事我的观点很明确选你讲得清楚的不选看起来最先进的。答辩现场问的是“为什么这么选”不是“你用了什么新技术”。我见过用微服务拆了五个模块做二手平台的光是本地跑起来就要开一堆服务演示的时候一个端口没起来整个页面白屏。2.1 后端框架单体 Spring Boot 足够撑住主推 Java 这套原因很实在网上资料最多、报错一搜就有答案、答辩老师最熟悉、简历上写着也不算减分。Spring Boot 3 或者 2.7 都行JDK 17 配 3.xJDK 8 配 2.7别搞混版本对不上第一关就卡住。如果你的环境就是装不上 JDK或者你更熟 Python那么 Django 加 Django REST Framework 也完全可行同样能做出前后端分离的结构只是生态里现成的后台管理、权限框架更薄一点。选哪个不重要重要的是你能把“分层”讲明白。我用得比较顺手的后端组合是这样的Spring Boot做 Web 层和容器MyBatis-Plus做持久层单表 CRUD 基本不用写 SQL省下来的时间去做业务校验MySQL 8存业务数据字符集统一 utf8mb4Redis可选用来存登录令牌和热门商品缓存不用也能跑用了能讲JWT做无状态登录前端每次请求带在请求头里本地磁盘或对象存储放商品图片校园项目用本地磁盘完全够我特意把持久层说细一点。MyBatis-Plus 的价值在于它把分页、条件构造、逻辑删除这些重复劳动都封装了但它有个坑条件构造器写多了之后业务逻辑会散落在 Controller 里。我的做法是强制分层Controller 只做参数校验和响应包装Service 里做状态判断和事务Mapper 只做数据访问。这一条在开题报告里写出来是能加分的因为它说明你想过工程结构。// Service 层做状态校验不放在 Controller Service public class OrderServiceImpl implements OrderService { Override Transactional(rollbackFor Exception.class) public Long create(Long itemId, Long buyerId) { Item item itemMapper.selectById(itemId); if (item null || item.getStatus() ! ItemStatus.ON_SALE.getCode()) { throw new BizException(商品已下架或已售出); } if (item.getOwnerId().equals(buyerId)) { throw new BizException(不能购买自己发布的商品); } // 一物一件用版本号防止两个人同时下单 int affected itemMapper.lockForOrder(itemId, item.getVersion()); if (affected 0) { throw new BizException(手慢了商品刚被别人拍下); } Order order new Order(); order.setItemId(itemId); order.setBuyerId(buyerId); order.setSellerId(item.getOwnerId()); order.setStatus(OrderStatus.WAIT_CONFIRM.getCode()); order.setOrderNo(OrderNoGenerator.next()); orderMapper.insert(order); return order.getId(); } }上面这段里lockForOrder对应一条带版本条件的更新语句写成 SQL 就是update item set status 2, version version 1 where id ? and version ? and status 1。影响行数为 0 就说明状态已经变了直接抛业务异常。这个手法不复杂但它精确解决了“两个人同时点想要”的问题答辩时这么讲比背概念有说服力。-- item 表关键字段与索引 CREATE TABLE item ( id BIGINT NOT NULL AUTO_INCREMENT, owner_id BIGINT NOT NULL COMMENT 发布者, category_id INT NOT NULL COMMENT 分类, title VARCHAR(80) NOT NULL, description VARCHAR(2000) DEFAULT NULL, price DECIMAL(10,2) NOT NULL, ori_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价用于展示折扣, cover_url VARCHAR(255) DEFAULT NULL, campus VARCHAR(30) DEFAULT NULL COMMENT 校区用于筛选, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在售 2锁定 3已售 4下架 5审核中, view_count INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_cate_time (status, category_id, create_time), KEY idx_owner (owner_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引这块说一句实在话idx_status_cate_time这个联合索引的顺序不是随便排的。列表页最常见的查询是“在售 某个分类 按时间倒序”把选择性最低但一定会出现在条件里的status放最左然后category_id最后create_time让排序也能走索引避免 filesort。如果分类筛选是可选的那还要考虑再加一个(status, create_time)的索引因为不筛分类时联合索引用不上第二列。这种细节在开题里写不进太多但答辩问“你的索引怎么设计的”能答上来就是加分。2.2 前后端分离的结构与目录约定前端我建议 Vue 3 加 Vite配一个成熟组件库。理由不是它多先进而是它的构建速度快、热更新稳改一行代码秒级生效做毕设这种反复调整的场景体验差别很大。路由用 vue-router请求封装用 axios状态管理看情况用户信息和令牌这种全局状态用 Pinia 存一下就行别把整个商品列表都塞进去。目录我习惯这么分campus-market/ ├── server/ 后端 │ ├── src/main/java/com/demo/market/ │ │ ├── controller/ 只做入参校验和响应 │ │ ├── service/ 业务逻辑与事务边界 │ │ ├── mapper/ 数据访问 │ │ ├── entity/ 与表一一对应 │ │ ├── dto/ 出入参对象不要直接用 entity │ │ ├── enums/ 状态枚举全项目唯一来源 │ │ ├── config/ 跨域、拦截器、上传配置 │ │ └── common/ 统一响应、异常、工具类 │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ 复杂查询的 XML └── web/ 前端 ├── src/api/ 按模块拆的接口文件 ├── src/views/ 页面 ├── src/components/ 复用组件 ├── src/stores/ Pinia └── src/utils/request.js axios 实例与拦截器有一条经验值得单独拎出来DTO 和 Entity 一定要分开。我见过太多人图省事Controller 直接接收 Entity、直接返回 Entity结果某个字段忘了脱敏用户手机号、内部状态码全暴露给前端了。分开之后出参用什么字段是你说了算。这一条不难但坚持下来的人不多。2.3 数据库表设计的关键取舍表不用多九张能覆盖全部主流程。我按依赖顺序列一下表名作用关键字段设计要点user用户昵称、头像、联系方式、信用分、状态联系方式单独存默认不对外返回category分类名称、排序、父级两级就够别做无限极item商品标题、价格、状态、版本号、校区状态和版本号是核心item_image商品图片商品ID、图片地址、排序单独存表支持多图favorite收藏用户ID、商品ID联合唯一索引防重复收藏order订单订单号、商品、买家、卖家、状态、约定地点时间状态机是重点message留言商品ID、发送者、接收者、内容、已读按商品维度聚合简化实现review评价订单ID、评分、内容一单一评订单ID唯一索引report举报目标类型、目标ID、理由、处理状态类型字段做成通用几个具体取舍讲一下。价格用 DECIMAL 不用 DOUBLE。这个不用多解释浮点数做金额迟早出问题DECIMAL(10,2)是标准做法。前端传过来是字符串还是数字要注意Java 侧用BigDecimal接收别用double。状态字段用 TINYINT 存枚举值但代码里必须有枚举类。数据库里存 1、2、3 是为了索引和存储效率业务代码里必须用ItemStatus.ON_SALE这种写法禁止在代码里出现魔法数字。这一条我在给别人改代码时见得最多的毛病一旦状态多了谁也不知道 3 是已售还是下架。逻辑删除标记单独一列deleted。用户删掉自己的发布其实是下架加标记删除数据库记录要留着因为可能已经关联了订单。物理删除会让历史订单查不到商品信息这个坑一旦踩了改起来要动一大片。时间字段统一用 DATETIME应用层统一时区。数据库、JVM、前端三处时区不一致是新手最容易被坑的地方。我的做法是数据库连接串里显式指定时区实体类用LocalDateTime返回给前端统一格式化成yyyy-MM-dd HH:mm:ss。# application.yml 里容易被忽略的几项 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/campus_market?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse servlet: multipart: max-file-size: 5MB # 单张图上限和前端校验保持一致 max-request-size: 20MB # 一次多图上传的总上限 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/ShanghaiserverTimezone这一项如果漏了插入的时间和查出来的时间能差 8 小时而且现象很诡异数据库里看着是对的接口返回是错的。这类问题的排查方法我会在最后一部分细说。3. 核心模块的落地实现与细节主链路跑通的关键就三段发布、找货、成交。每一段都有几个不做就会返工的点我按实际开发的顺序讲。3.1 商品发布与图片上传的完整处理商品发布看着是填表单实际上后端要做四件事字段校验、图片落盘、关联写入、状态初始化。顺序错了会留下脏数据。先说图片。前端选完图先做一次压缩再上传这是最省事也最有效的优化。一张手机原图动辄四五兆直接传不仅慢服务端还要处理超大文件。前端用 canvas 压到长边 1280、质量 0.8通常能压到 300KB 以内画质在商品展示场景下完全够看。服务端接收之后我建议做的处理是重命名不要用原始文件名中文名、空格、特殊符号在 Linux 上会让你怀疑人生。用 UUID 或者雪花 ID 加扩展名。按日期分目录/upload/2025/06/13/xxx.jpg单目录文件数不会爆炸后期备份和迁移也方便。校验类型和大小不要只信前端服务端要重新判断扩展名和 Content-Type同时限制大小。生成缩略图列表页加载原图会很慢生成一张 400 宽的缩略图详情页再加载原图。这一步用现成的图像处理库几行代码就能搞定。PostMapping(/upload) public ResultListString upload(RequestParam(files) MultipartFile[] files) { if (files null || files.length 0) { return Result.fail(请选择图片); } if (files.length 9) { return Result.fail(最多上传9张); } ListString urls new ArrayList(); String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); for (MultipartFile file : files) { String ext FileUtil.getExtension(file.getOriginalFilename()); if (!ALLOW_EXT.contains(ext.toLowerCase())) { return Result.fail(仅支持 jpg、png、webp); } String name IdUtil.fastSimpleUUID() . ext; File dest new File(uploadRoot, datePath / name); FileUtil.mkParentDirs(dest); file.transferTo(dest); // 生成缩略图失败不阻断主流程 try { Thumbnails.of(dest).size(400, 400).keepAspectRatio(true) .toFile(new File(dest.getParent(), thumb_ name)); } catch (Exception ignored) { } urls.add(publicBase / datePath / name); } return Result.ok(urls); }这里有个隐蔽的问题图片先传上去用户填完表单点发布或者干脆关掉页面不发了磁盘上就留下一堆孤儿文件。校园项目不用做得多复杂一个定时任务每天扫一遍把创建时间超过 24 小时且没被任何商品引用的文件清掉就行。这个细节如果你在答辩时主动提出来是很能体现工程思维的。再说字段校验。标题长度、价格范围、描述长度这些用注解校验就够但有两个业务校验必须写在 Service 里一是同一用户短时间内不能重复发布完全相同的标题和描述防止刷屏二是价格上限要合理二手平台标个 99999 元一看就是乱填我一般限制在 10 万以内并给出提示。商品初始状态如果你是直接发布上架初始状态就是“在售”如果要做人工审核就先落到“审核中”审核通过再改成“在售”。校园项目里我倾向于不做审核但保留举报下架的能力这样开发量小又能体现内容治理的思路。3.2 商品检索别一上来就上搜索引擎列表页的查询需求无非是关键词、分类、校区、价格区间、排序方式、分页。数据量在几万条这个级别MySQL 完全够用关键是把查询写对。关键词搜索用LIKE %关键词%有个致命问题前置通配符导致索引失效全表扫描。数据量小的时候你感觉不到数据量一上来就明显卡。可行的优化路径有两条第一条是全文索引。MySQL 8 的 InnoDB 支持全文索引对中文需要配分词器配置起来有点门槛但效果比 LIKE 好很多。如果你的标题和描述是中文这一步可能要折腾一下。第二条是标签化替代模糊搜。让用户在发布时选标签列表页用标签精确匹配加分类筛选把模糊搜索降级成“精确条件组合”。这一招在校园项目里特别实用因为二手商品的品类其实很集中教材、数码、生活用品、自行车标签覆盖度很高。不管你选哪条分页这里有两个细节深分页问题。LIMIT 100000, 20这种写法越翻越慢因为要扫描前 10 万条再丢掉。校园项目里用户翻不到那么深但如果你要做“加载更多”的交互改用游标分页更稳WHERE id 上一页最后一条的id ORDER BY id DESC LIMIT 20。总数统计。每次列表都COUNT(*)一次成本不低。可以接受的做法是首页查总数翻页时不再重复查或者总数超过一定值就显示“1000”。-- 列表查询条件可选用动态 SQL 拼接注意排序字段要能走索引 SELECT id, title, price, cover_url, campus, create_time FROM item WHERE deleted 0 AND status 1 AND (#{categoryId} IS NULL OR category_id #{categoryId}) AND (#{campus} IS NULL OR campus #{campus}) AND (#{minPrice} IS NULL OR price #{minPrice}) ORDER BY create_time DESC LIMIT #{offset}, #{size};注意这种#{} IS NULL OR的写法虽然方便但它会让优化器放弃部分索引。如果发现慢查询正确做法是在 XML 里用if动态拼 SQL条件不成立就不拼这一段。这是我看了很多份“能跑但很慢”的代码之后总结出来的。还有一个体验细节浏览数view_count。每次进详情页都update ... set view_count view_count 1在高频访问下会造成行锁竞争。校园项目量小无所谓但如果你想做得讲究一点可以用 Redis 计数定时回写数据库。这个点在答辩时讲出来比单纯说“我做了浏览量统计”要有分量。3.3 订单状态机整个系统最该画清楚的地方我开头提到的那个卡住的场景根子就在这儿。订单状态如果没有明确定义和流转规则代码里就会到处是if (status 1 || status 2)这种判断最后谁也说不清。我一般定义六个状态并且明确每个状态能做什么、不能做什么状态码状态名触发动作可流转到说明10待卖家确认买家下单20 / 90商品同时被锁定为“已锁定”20待当面交易卖家同意30 / 90双方可见联系方式约定地点时间30交易完成买家确认收货40商品状态改为已售40已评价双方互评—终态90已取消任一方取消或超时—商品恢复在售关键规则有三条写进代码之前先写进注释和文档规则一状态只能单向流转不允许回退。已取消的订单不能复活要交易就重新下单。这条规则能挡掉 90% 的诡异数据。规则二商品状态与订单状态必须联动且写在一个事务里。下单时商品从“在售”变“锁定”订单取消时商品从“锁定”变回“在售”交易完成时变“已售”。这三处如果分开写、没有事务保护一旦中间报错就会出现“商品锁着但没有有效订单”的僵尸数据。规则三超时自动取消。待卖家确认超过 24 小时待当面交易超过 7 天系统自动取消并释放商品。实现方式可以是定时任务扫表也可以是延迟队列。校园项目用定时任务最省事每小时跑一次。public enum OrderStatus { WAIT_CONFIRM(10, 待卖家确认), WAIT_DEAL(20, 待当面交易), FINISHED(30, 交易完成), REVIEWED(40, 已评价), CANCELED(90, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } // 允许的流转关系集中维护改动只改这一处 private static final MapOrderStatus, SetOrderStatus ALLOW Map.of( WAIT_CONFIRM, Set.of(WAIT_DEAL, CANCELED), WAIT_DEAL, Set.of(FINISHED, CANCELED), FINISHED, Set.of(REVIEWED), REVIEWED, Set.of(), CANCELED, Set.of() ); public static boolean canTransit(OrderStatus from, OrderStatus to) { return ALLOW.getOrDefault(from, Set.of()).contains(to); } }这张流转关系表看着简单但它是我改别人代码时最先找的东西。如果找不到说明这个项目的状态管理基本靠猜。你在自己的项目里把它单独拎出来写成一个类所有状态变更都走canTransit校验代码质量立刻就上来一个台阶。还有一个边界要想清楚卖家拒绝订单。卖家不同意卖订单取消商品恢复在售。这和买家取消是同一个终点但原因不同。所以我建议订单表里再加一个cancel_reason字段记录是超时、买家取消、卖家拒绝还是举报下架将来做数据统计或者申诉的时候用得上。3.4 站内沟通与联系方式保护这块是校园二手平台最容易被忽略、又最能体现“你想过真实场景”的地方。如果商品详情页直接把卖家手机号明文展示出来那平台就退化成一个公告板双方加个联系方式就绕过去了你的订单系统、评价体系全成了摆设。比较务实的做法是分级展示未产生订单关系时详情页只展示“站内留言”入口联系方式打码比如显示138****5678。订单进入“待当面交易”状态后双方才能看到完整联系方式。同时提供“复制联系方式”按钮但做一次性限制或者记录查看日志。技术上实现不复杂就是 DTO 里控制字段是否返回配合一个“当前用户与该商品的关系”判断。但这个设计思路要讲清楚联系方式是交易关系的产物不是商品信息的附属品。这句话在答辩时说出来比念十行功能列表都管用。留言功能的表设计我前面提过按商品维度聚合不做完整会话。查询时按商品 ID 拉全部留言按时间正序展示买家提问在前、卖家回复在后。已被删除的商品留言也不再展示。SELECT m.id, m.content, m.create_time, m.from_id, m.to_id, u.nickname, u.avatar FROM message m LEFT JOIN user u ON u.id m.from_id WHERE m.item_id #{itemId} AND m.deleted 0 ORDER BY m.create_time ASC;这里有个坑留言和商品的权限关系。如果商品已经下架但订单还在走流程双方可能还需要沟通。我的处理是留言板跟着订单走订单相关的沟通放在订单详情页商品留言板只服务于售前咨询。两者分开逻辑清爽很多。3.5 信用、评价与举报的几个落地要点评价系统我最常见的错误设计是“一个订单能评无数次”。解决办法很简单评价表里给order_id加唯一索引数据库层面直接挡住。评分用 1 到 5 的整数用户信用分可以简单算成所有评价的平均分乘以 20映射到 0 到 100 的区间别搞复杂的加权公式讲不清楚。举报功能要做得“够用”。表里存目标类型商品、用户、留言、目标 ID、举报理由、补充说明、处理状态。后台有个简单的列表页能标记已处理就行。真正需要注意的是举报后的商品状态被举报的商品不一定要立刻下架可以标记一个“审核中”的中间态累计三次有效举报自动下架。这个设计既不误伤又能控制风险。加分项里我最推荐做的是未读角标。实现成本很低留言表加个read_flag个人中心查一下未读数。演示的时候能看到红点观感提升明显性价比极高。4. 源码拿到手之后怎么改成自己的这一节是给那些手里已经有一份现成代码、需要交付或答辩的读者的。直接改名字交上去风险不小我见过不止一次因为代码风格前后不一致被追问的情况。4.1 先摸清依赖关系和启动路径拿到源码第一步不是改代码是把它跑起来。顺序应该是装好 JDK 和 MySQL、建库导数据、改配置文件里的连接信息、启动后端、启动前端、打开浏览器走一遍主流程。这一步跑不通后面全是空谈。跑通之后从数据库往上读。我的习惯是打开建表脚本把每个表和实体类一一对上再看 Controller 里有哪些接口最后顺着一个接口比如商品列表从 Controller 读到 Mapper把整条链路走一遍。走完这一条剩下的模块结构基本一样很快就能摸清。常见的启动失败原因就那么几个JDK 版本不匹配导致语法报错、数据库连不上、端口被占用、前端代理地址写错。逐个排查就行我在最后一部分整理了速查表。4.2 改造的优先顺序改造不要东一榔头西一棒子按这个顺序第一步换皮。项目名、包名、页面标题、logo、配色、默认头像这些是肉眼可见的换完立刻像你自己的东西。注意重命名包名的时候用 IDE 的重构功能别手动改容易漏。第二步调表结构和字段。加一两个符合校园场景的字段比如商品表加“校区”和“可议价”标记用户表加“宿舍楼”或者“学院”。加字段的同时记得同步改实体、DTO、前端表单和列表展示四处都要改漏一处就报错。第三步补一个自己的功能。这一步是拉开差距的关键。建议挑一个工作量在半天到一天的功能比如“商品上下架切换”、“收藏列表导出”、“按校区筛选”、“浏览历史”。功能不难但你有完整的从表到接口到页面的实现过程答辩被问到细节时能展开讲。第四步改前端样式和交互。组件库默认样式辨识度太高改一下主题色、圆角、卡片间距整体观感就变了。列表页加个骨架屏加载态空列表加个插画占位这些细节很便宜但显得完整。提示改造过程中一定要用版本管理工具每完成一步提交一次。我见过改包名改到一半项目起不来又没有备份只能从头再来的情况。4.3 哪些代码必须自己重写有几处代码我建议不要留原样因为它们是答辩最容易被追问的点登录鉴权逻辑。令牌怎么生成、怎么校验、过期怎么处理你必须能讲清楚每一行。这块如果说不明白会给人“项目不是自己写的”的印象。订单状态流转。前面说过的状态机哪怕原代码是散落的 if你也应该重构成集中维护的枚举。图片上传。文件存哪、怎么命名、大小怎么限制这些是实操问题讲得出细节最加分。其余的部分比如通用返回类、工具类、分页封装保留没问题这些本来就是重复劳动。5. 开题报告怎么写才不像套模板标题里带“开题”说明这块是刚需。我见过太多开题报告背景部分写三段行业规模数据技术路线写一堆框架名字可行性分析写“技术成熟、资料丰富”一看就是从同一个模板改的。5.1 选题背景要落到具体场景不要一上来讲二手交易市场规模多少亿那些数据和你这个系统没关系。有效的写法是从一个具体的痛点切入。比如你可以这样组织校园里每年毕业季有大量闲置物品需要流转目前主要靠群聊和楼道张贴信息分散、更新不及时、交易双方缺乏基本信任已有的综合类二手平台面向全社会商品信息与校园场景不匹配同城交易的沟通成本高因此需要一个面向单一校区、信息集中、身份可控的二手信息与交易撮合系统。这段话说清楚了三件事需求从哪来、现有方案为什么不够、你要做什么。比堆砌行业数据有用得多。而且这段话里的每一句都能对应到你后面的功能设计逻辑是闭合的。5.2 研究内容与技术路线怎么写才具体“研究内容”这一节最忌讳写成功能清单。写成清单显得你只是在做增删改查。更好的写法是按“问题—方案”的方式组织比如针对商品信息与买家匹配效率低的问题设计基于分类、校区、价格区间的多维筛选与标签检索方案针对一物一件场景下可能出现的重复下单问题设计基于状态字段与版本号的并发控制方案针对交易过程中状态不透明的问题设计订单状态机并对状态流转进行约束针对交易双方信任缺失问题设计评价与举报机制。每一条都指向一个具体问题解决方案也是具体的技术手段这样老师一看就知道你想过。技术路线部分可以放一张表把层次、技术、用途写清楚层次技术选型承担职责表现层Vue 3 组件库页面渲染、表单交互、状态展示接口层Spring Boot 拦截器路由、参数校验、统一鉴权、异常处理业务层Service 枚举 事务状态流转、业务规则校验持久层MyBatis-Plus MySQL数据访问、索引优化支撑本地文件存储 / 缓存图片存储、热点数据缓存进度安排建议按周排并且留出至少两周的缓冲。真实的开发节奏一定会有返工把缓冲写进去说明你有工程经验。阶段时间主要任务交付物需求与设计第 1-2 周场景调研、功能边界、表结构设计需求说明、E-R 图环境与骨架第 3 周工程搭建、登录鉴权打通可运行的空壳主链路开发第 4-7 周发布、检索、订单可演示的主流程细节完善第 8-9 周留言、评价、举报、后台功能完整版测试与文档第 10-11 周用例测试、修复、撰写文档测试记录、论文初稿缓冲与答辩准备第 12 周演示脚本、环境冻结演示包5.3 答辩现场最容易被问的几个问题我整理了几个高频问题提前想好答案能少出很多汗。问题一你的系统和现成的二手平台有什么区别不要答“功能更简单”要答“面向单一校区、身份可控、交易半径短、线下当面交易为主因此不需要复杂的支付和物流体系”。问题二一物一件两个人同时下单怎么办这就是版本号那套方案讲清楚“条件更新 影响行数判断 事务”即可。问题三图片存在服务器本地服务器挂了怎么办老实回答“当前是单机存储扩展方向是接入对象存储并做冗余”比硬编一个不存在的方案强。问题四数据量大了怎么办分层回答先加索引、再读写分离、再考虑缓存和检索服务并说明当前数据量级不需要。问题五你的测试怎么做的准备好测试用例表覆盖正常流程、边界值、异常流程三类哪怕只测了二十条说清楚就行。6. 部署演示与问题排查实录最后这部分是纯实操。答辩那天系统能不能跑起来比代码写得多漂亮重要得多。6.1 打包部署的完整流程后端打成可执行 jar前端打包成静态文件交给 Nginx这是最稳的组合。# 后端打包跳过测试加快速度 mvn clean package -DskipTests # 启动指定配置文件日志重定向 nohup java -jar market-server.jar --spring.profiles.activeprod app.log 21 # 前端打包 npm run buildNginx 的配置有几个关键点少一个就出问题server { listen 80; server_name localhost; # 前端静态资源history 路由必须配 try_files location / { root /opt/web/dist; 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; client_max_body_size 20m; # 不加这行多图上传会报 413 } # 图片直接由 Nginx 提供减轻后端压力 location /upload/ { alias /opt/upload/; expires 7d; } }try_files那一行是必须的。前端用 history 模式做路由用户直接访问/item/123或者刷新页面后端没有对应的静态文件就会 404。加上这一行找不到文件就回退到 index.html由前端路由接管。client_max_body_size也必须有。Nginx 默认只允许 1MB 的请求体多图上传直接 413而后端日志里什么都看不到很容易误以为是代码问题。6.2 常见问题速查与定位思路现象可能原因排查方法处理页面能开但接口全部 404代理路径不匹配或前缀重复浏览器网络面板看实际请求地址检查 proxy_pass 末尾斜杠与前端 baseURL接口 401 循环跳登录令牌没带上或校验失败看请求头是否携带、令牌是否过期拦截器放行登录接口统一处理过期跨域报错开发环境代理未配或后端未放行看控制台报错的具体来源开发用代理生产同域不需要跨域上传 413Nginx 请求体限制后端无日志、Nginx 有错误日志调大 client_max_body_size上传后图片裂图路径拼接错误或目录权限直接在浏览器访问图片地址检查静态映射与实际目录时间差 8 小时时区配置不一致对比数据库值、接口值、页面值连接串与序列化统一时区中文乱码字符集不统一查表字符集与连接串参数全部统一 utf8mb4列表页越来越慢深分页或索引缺失开启慢查询日志改游标分页、补联合索引启动报端口占用旧进程未退出查端口占用进程结束进程或换端口前端刷新 404history 模式未配回退直接访问子路由配 try_files再补几个不在表里但很典型的。数据库连接池耗尽。现象是系统跑一会儿就全部接口超时日志里一堆获取连接超时。原因通常是某处慢查询占着连接不放或者代码里有未关闭的流。排查方法是看连接池的活跃连接数和慢查询日志。我的习惯是把连接池最大连接数设成 20 左右校园项目完全够设太大反而掩盖问题。事务里做了远程调用或者文件操作。比如在Transactional方法里上传图片、发消息一旦这些操作慢事务就一直开着数据库锁也一直不放。正确做法是把文件操作放到事务外面先存文件再入库。逻辑删除和唯一索引冲突。收藏表加了(user_id, item_id)唯一索引用户取消收藏用了逻辑删除再收藏一次就报唯一键冲突。解决办法是把删除标记也放进唯一索引或者取消收藏直接物理删除。这个坑我踩过排查了很久。定时任务重复执行。如果部署了多实例定时任务会同时跑导致订单被重复取消。单机部署没这个问题但如果你的项目以后可能多实例至少要加一个分布式锁或者只让一台执行。注意答辩演示前一定要把环境冻结。演示前一天不要再改代码、不要再装新依赖、不要再调数据库。我见过演示当天早上更新了一个依赖结果本地起不来只能现场重装。最后分享一个演示的小技巧。提前准备好一份种子数据包含两三个用户、十几个商品、两个不同状态的订单这样演示的时候不用现场造数据直接就能展示列表、详情、下单、状态流转的完整链路。种子数据里刻意留一两个边界情况比如一个已下架商品、一个已取消订单讲的时候顺便说明“系统对这种情况的处理是……”节奏会很顺。这套东西做下来说实话工作量不小但逻辑是清楚的。我自己反复改这类项目的体会是真正拉开差距的从来不是用了多少技术而是你有没有把“一物一件怎么防重复下单”“订单状态怎么约束”“联系方式什么时候才能看到”这几个具体问题想透。想透了代码写起来快讲起来也稳。
阅读完成 · 觉得有帮助?