校园里最不缺的就是闲置物品——毕业生走的时候成箱成箱地扔书、扔小电器,新生开学又大包小包地买新的。这个循环里其实藏着一个很典型的需求:需要一个让本校学生自己挂、自己买、当面交易的平台。用SpringBoot框架做一套校园交易系统,恰好是这个需求最常见的落地方式,也是Java方向练手、做毕业设计、写简历项目的高频选题。这篇文章我就把这套系统从需求拆解、数据库设计到后端实现、部署避坑的完整过程梳理一遍,把里面真正值钱的细节和踩过的坑都展开讲清楚,给准备做类似项目的朋友一个能直接抄作业的参考。
1. 项目背景与核心需求拆解
1.1 校园场景下的真实痛点在哪里
很多人一上来就照着闲鱼、转转那种通用二手平台去设计功能,这是个挺常见的误区。校园交易和公开二手市场最大的区别在于交易半径:买方和卖方几乎肯定生活在一个校区里,商品的交接方式是线下当面交付,而不是快递。这个差异决定了整个系统不需要复杂的物流跟踪、不需要担保交易、甚至不需要第三方支付对接,省掉这些之后,核心链路其实非常短。
另一个校园场景特有的痛点是信任。闲鱼上陌生人之间的信任靠芝麻信用、评价体系慢慢积累,但校园里天然有学号、院系、宿舍楼这种信息可以做背书。所以一套合格的校园交易系统,用户注册时最好有学号或者校园卡的校验环节,商品详情页也要展示卖家的基本信息(比如所在校区、年级),让买家在联系卖家之前就对对方有个基本判断。这些东西做进去之后,整个产品才有“校园感”,不然就是套壳闲鱼。
还有一个容易被忽略的点是时效性。校园交易的商品生命周期非常短,毕业季的旧书、换季的洗衣机、期末前甩卖的复习资料,可能一周之内就要出手。系统里的商品如果长期挂着没人管,会严重影响用户的信任和使用体验。所以下架机制、商品状态流转(在售、已预约、已售出、已下架)需要从一开始就设计好,而不是后期补丁式地加。
1.2 功能范围怎么划才是最合理的
根据上面的场景分析,我用一个原则来圈定功能边界:凡是线下能解决的事情,系统里就不做重流程。比如不实现在线支付、不做物流跟踪、不做平台的担保介入,只做信息撮合和沟通触达。
基于这个原则,核心功能我分成了四个模块:
| 模块 | 核心功能点 | 设计说明 |
|---|---|---|
| 用户模块 | 学号注册、登录、个人信息、我的发布、我的收藏 | 注册时校验学号格式,登录用JWT无状态会话 |
| 商品模块 | 发布闲置、商品列表、分类筛选、关键词搜索、商品详情、图片上传 | 核心业务模块,状态机驱动上下架流转 |
| 交易模块 | 我要预约、订单生成、卖家确认交易、交易完成、评价 | 用订单表记录线下交易意向,不涉及资金流 |
| 管理后台 | 商品审核、用户管理、举报处理、数据统计 | 单用户角色,管理员独立接口前缀 |
把交易模块做成“预约制”而不是“下单支付制”,这是个很重要的取舍。学生看到商品后发起预约,卖家那边收到意向提醒,双方在线下碰面、验货、交钱、确认完成。系统只负责记录状态和留痕,这样既保证了交易的可追溯,又不至于把复杂度推到支付网关和资金账户上去。
1.3 给这个系统定义明确的角色边界
整个系统就三种角色,千万别再加什么平台运营、超级管理员之类的花活。普通用户负责发布商品、预约、评价;管理员负责审核和治理。技术实现上,管理员和普通用户用同一套用户表,通过一个role字段区分(0-普通用户,1-管理员),权限控制放在拦截器里做,而不是单独建一张权限表——对于一个校园级别的项目,RBAC权限模型属于过度设计。
角色边界清楚了之后,接口的粒度才能定下来。普通用户能调的接口、管理员能调的接口、未登录能调的接口,三层分开梳理,写代码的时候心里有谱,做安全校验的时候也不会漏。
2. 技术选型与整体架构设计
2.1 为什么这个项目就该用SpringBoot
选SpringBoot做这个系统,不单是因为它流行、好写简历,更关键的是它和这个项目的规模、需求匹配度非常高。校园交易系统属于典型的中小型Web应用,接口数量大概在三十到五十个之间,数据量短期内不可能达到千万级别,这种体量用SpringBoot的单体应用架构是最务实的方案——一个应用搞定所有接口,打成jar包就能跑,部署成本极低。
SpringBoot带来的核心收益其实是起步零配置。不需要像传统SSH那样写一堆XML配置文件,内嵌Tomcat,一个main方法就能把整个服务拉起来。这对单人开发或者两三人小组做课程设计来说,节省的时间是非常可观的。另外SpringBoot的生态足够成熟,集成MyBatis-Plus、Redis、JWT这些常用组件都有现成的starter,遇到问题一搜就能找到答案,这一点对学习者来说比技术本身更值钱。
多模块拆分的想法在校园交易系统这种体量下是弊大于利的。Maven多模块工程确实能让代码结构更清晰,但模块之间的依赖管理、打包顺序、本地联调的配置成本都会上升。我个人的建议是:单模块 + 按包名分层,等这个项目真的膨胀到需要拆分的程度再说。
2.2 完整技术栈明细与选型理由
这里给出我当时用的技术栈清单,每一项都带选型理由,方便你对照着做决策:
| 分层 | 技术选型 | 版本建议 | 选型理由 |
|---|---|---|---|
| 核心框架 | SpringBoot | 2.7.x | 比2.5多了不少工具类增强,又不像3.x那样强制要求JDK17 |
| 持久层 | MyBatis-Plus | 3.5.x | 单表CRUD不用手写SQL,分页插件即插即用 |
| 数据库 | MySQL | 8.0 | InnoDb引擎,utf8mb4字符集,兼容性优于5.7 |
| 缓存 | Redis | 6.x | 存验证码、存token黑名单、缓存热点商品列表 |
| 鉴权 | JWT + 拦截器 | jjwt 0.11.x | 无状态会话,适合前后端分离部署 |
| 前端 | Vue3 + Element Plus | 最新稳定 | 后台管理界面开发效率极高 |
| 接口文档 | knife4j | 4.x | 在线调试方便,写实验报告时还能直接截图 |
| 构建工具 | Maven | 3.8+ | 普及度最高,遇到问题好排查 |
这里特别提醒一句,SpringBoot版本不要追新。我见过不少同学直接上SpringBoot 3.x,结果JDK版本不匹配、javax改成jakarta、各种starter不支持,光修环境就花了两天。这个体量的项目用2.7.x完全够用,稳定压倒一切。JDK用1.8还是11,取决于你本机和课程的实际情况,但SpringBoot 2.7.x配JDK8是经过千锤百炼的组合。
2.3 单体应用的三层代码结构
代码结构我用的是经典的三层架构,包名规则适合教学演示也适合后续扩展:
com.campus.trade ├── controller // 接口层,只做参数接收和结果封装 ├── service // 业务层,事务边界都在这一层 │ └── impl ├── mapper // 数据访问层,继承BaseMapper ├── entity // 数据库实体类 ├── dto // 前端交互的数据传输对象 ├── vo // 视图对象,接口返回给前端的结构 ├── config // 配置类(跨域、拦截器、Redis等) ├── common // 统一返回结果、异常处理、常量、工具类 └── CampusTradeApplication.java这个结构的核心原则是单向依赖:controller依赖service,service依赖mapper,禁止反向调用。很多新手写代码喜欢在controller里直接注入mapper,图省事,但这种写法一旦业务复杂起来就是灾难。坚持三层分离,后面加需求、改功能的时候你就能体会到好处了——改业务逻辑不用动接口,加接口不用动数据库。
DTO和VO的区分也很重要。实体类(entity)对应数据库的字段,DTO对应前端传来的参数,VO对应接口返回给页面的结构。三张皮分开之后,才不会出现前端多传了一个字段就把数据库给污染了的情况。
3. 数据库设计:校园交易系统的表结构拆解
3.1 核心表设计与字段说明
数据库是整个系统的地基,这部分设计好了后面能省一半的返工时间。我按功能域拆成六张核心表,外加两张辅助表:
-- 用户表 CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `student_no` VARCHAR(20) NOT NULL COMMENT '学号', `password` VARCHAR(100) NOT NULL COMMENT '加密后的密码', `nickname` VARCHAR(30) COMMENT '昵称', `avatar` VARCHAR(255) COMMENT '头像URL', `role` TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', `status` TINYINT DEFAULT 1 COMMENT '0-禁用 1-正常', `contact` VARCHAR(50) COMMENT '联系方式', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';用户表的两个设计细节值得说。第一,student_no要建唯一索引,这个字段登录时要用、注册时要查重,不加索引会把接口拖慢;第二,create_time和update_time用默认值,不要每次insert的时候手动set,既容易漏也容易出错,数据库自动维护是最省心的。
-- 商品表 CREATE TABLE `product` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '发布者ID', `title` VARCHAR(50) NOT NULL, `description` TEXT COMMENT '详细描述', `price` DECIMAL(10,2) NOT NULL COMMENT '价格', `original_price` DECIMAL(10,2) COMMENT '原价', `category_id` INT COMMENT '分类ID', `images` VARCHAR(1000) COMMENT '图片URL,多张用逗号分隔', `status` TINYINT DEFAULT 1 COMMENT '1-在售 2-预约中 3-已售出 4-下架 5-审核中', `view_count` INT DEFAULT 0 COMMENT '浏览数', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status_time` (`status`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';商品表的status状态字段是整个交易链路的核心。列表页只查status=1的数据,预约后改成2,卖家确认后改成3,用户主动下架改成4,刚发布的走审核改成5。状态流转要用常量类管理,不要飘字符串,不然改一个状态全项目找引用能找哭。
3.2 订单表、收藏表与消息表设计
订单表承接的是“预约-成交”链路:
CREATE TABLE `trade_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `product_id` BIGINT NOT NULL, `seller_id` BIGINT NOT NULL, `buyer_id` BIGINT NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-待确认 1-已确认 2-已完成 3-已取消', `remark` VARCHAR(255) COMMENT '预约留言', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易订单表';order_no的生成规则我用的时间戳加随机数:new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + (int)((Math.random()*9+1)*100000)。也有人喜欢用雪花ID,但在这个系统里分布式部署都不一定用得上,23位的雪花ID反而显得臃肿。
收藏表比较简单,唯一索引可以防止重复收藏;消息表本质上是一个站内信,我实现了买家咨询卖家的功能,字段就是sender_id、receiver_id、content、is_read。如果不想做站内信,直接留手机号、微信号让双方线下联系也是可行的,看你的需求文档怎么写的。
3.3 数据一致性与并发控制的关键设计
校园交易系统最容易出问题的点是并发预约。如果两个人同时对同一件商品发起预约,怎么保证不会撞车?最简单可靠的做法是用商品状态做乐观锁:
// 预约商品时,只更新状态=1的在售商品 boolean success = productMapper.update(null, new LambdaUpdateWrapper<Product>() .eq(Product::getId, productId) .eq(Product::getStatus, 1) // 关键条件:状态必须是1 .set(Product::getStatus, 2)); if (!success) { throw new BizException("商品已被预约或已下架"); }这个update语句利用了数据库行锁的原子性:同时到达的两个请求,只有一条update能匹配status=1的行,另一条匹配不到就更新0行。这就天然解决了超卖问题,完全不需要引入分布式锁。等商品成交之后再把status改成3。这个思路在商品秒杀、库存扣减场景里也经常被用到,是MySQL并发控制的基础操作,值得记下来。
关于事务,预约操作需要两步:更新商品状态 + 创建订单记录。这两步必须放在同一个@Transactional里。但要注意,事务只在service层生效,同一个类内部方法自调用不经过代理,@Transactional会失效,这是个经典坑,后面常见问题里还会提到。
4. 后端核心模块实现与关键代码
4.1 统一响应结构与全局异常处理
前后端分离的开发模式下,接口返回结构的统一是最基本也最容易被忽略的事。我定了这样一个返回格式:
public class Result<T> { private Integer code; // 200成功,4xx客户端错误,5xx服务端错误 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(HttpStatus.OK.value()); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }配套的还有全局异常处理。用@RestControllerAdvice统一拦截业务异常和系统异常,业务异常返回4xx业务码,系统异常打印日志后返回500。这样前端永远都在处理同一个JSON结构,不需要为某个接口的特殊返回写一堆if-else。我见过有些项目每个接口自己拼返回体,code一会儿是字符串一会儿是数字,前端对接起来简直想骂人。
4.2 JWT登录鉴权与拦截器实现
我用JWT做登录态管理,流程是:用户登录成功之后,后端生成一个带学号和用户ID的token返回给前端,前端存在localStorage里,每次请求带着token走,后端拦截器校验通过后放行。
生成token的代码很直接:
String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("studentNo", user.getStudentNo()) .claim("nickname", user.getNickname()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 有效期24小时 .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器里做三件事:放行登录、注册、商品列表、商品详情这些匿名接口;校验token是否存在;校验token是否过期。Util包里的方法从上文取得登录用户ID,避免在controller里反复从token里解析。
这里有必要提醒一下:JWT的密钥要放在配置文件里,不要硬编码到代码中。以后项目提交到公开仓库,密钥泄露意味着任何人都能伪造token,这是个很低级但非常危险的安全漏洞。
4.3 商品发布与多图片上传处理
图片上传是校园交易系统里最有细节的模块。前端支持一次传多张、限制单张大小、后端返回图片URL列表回显。我用的是本地存储方案:配置一个上传目录映射为静态资源路径,生产环境则放到云存储上。
spring: servlet: multipart: max-file-size: 5MB max-request-size: 50MB mvc: static-path-pattern: /static/** resources: static-locations: classpath:/static/, file:${upload.dir}/上传工具类负责两件事:生成不重复的文件名(UUID+原始后缀),按日期分目录保存(比如/upload/2025/07/xxx.jpg)。这样即使一年下来图片很多,单个目录的文件数量也不会失控,管理起来方便。
校验图片格式一定要做白名单校验。我在实际项目里看到过有人直接保存扩展名,结果被上传了含jsp的畸形文件名,虽然不是严重问题但很危险。白名单只认jpg、jpeg、png、gif、webp五种,大小不能超过5MB,前端后端双重校验。
4.4 商品列表的分页查询与缓存优化
商品列表是访问量最大的接口。全部商品按时间排序一路查到底的做法只适用于数据量很小的演示项目,正确做法是结合MyBatis-Plus分页插件:
Page<ProductVO> page = new Page<>(current, size); LambdaQueryWrapper<Product> query = new LambdaQueryWrapper<>(); query.eq(Product::getStatus, 1) .like(StringUtils.hasText(keyword), Product::getTitle, keyword) .eq(categoryId != null, Product::getCategoryId, categoryId) .orderByDesc(Product::getCreateTime);关键字搜索用like就够了,校园场景的数据量走不到Elasticsearch那一层。真正值得缓存的是首页的热门商品——用Redis存一个热门商品ID列表,每十分钟刷新一次,能明显降低数据库压力。缓存穿透的问题在Redis里查不到的商品可以缓存一个空值,或者用布隆过滤器,但作为课程设计级别项目,做好空值缓存已经足够。
4.5 预约下单与订单状态流转
预约的接口逻辑是整个后端的核心。我把它完整梳理一遍:
@Transactional public OrderVO createOrder(OrderDTO dto) { // 第二步:校验商品是否为在售状态 Product product = productMapper.selectById(dto.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException(400, "商品不存在或已下架"); } if (product.getUserId().equals(currentUserId())) { throw new BizException(400, "不能购买自己发布的商品"); } // 第三步:原子性更新商品状态 boolean lock = productMapper.update(null, new LambdaUpdateWrapper<Product>() .eq(Product::getId, dto.getProductId()) .eq(Product::getStatus, 1) .set(Product::getStatus, 2)); if (!lock) { throw new BizException(400, "手慢了一步,商品已被预约"); } // 第四步:创建订单记录 TradeOrder order = new TradeOrder(); order.setOrderNo(generateOrderNo()); order.setProductId(product.getId()); order.setSellerId(product.getUserId()); order.setBuyerId(currentUserId()); order.setRemark(dto.getRemark()); tradeOrderMapper.insert(order); return orderVO; }注意我给步骤加了序号——第一步是参数校验和用户校验,核心就是“状态条件更新”,这是并发安全的关键。订单创建后,商品状态改成2(预约中),卖家在自己的“我发布的”列表里能看到预约消息,点“确认交易”之后状态变成3(已售出),订单状态同步变成已完成。
5. 前端对接要点与接口规范
5.1 接口设计给前端省了多少事
后端设计的接口质量直接决定前端开发的效率。我做这套系统的时候,全站采用RESTful风格,资源用名词复数,方法用HTTP谓词表达动作:GET /api/products(列表)、POST /api/products(发布)、PUT /api/products/{id}(更新)、DELETE /api/products/{id}(删除)。预约是POST /api/orders,收藏是POST /api/favorites/{productId}。
分页接口我统一返回这样的结构:
{ "code": 200, "message": "操作成功", "data": { "records": [ { "id": 1, "title": "九成新高数课本" } ], "total": 35, "size": 10, "current": 1 } }前端拿这个结构直接可以驱动分页组件,不需要二次加工。
5.2 跨域配置与请求封装
前后端分离必然遇到跨域问题。开发环境最简单的方式是后端开启全放行的CORS配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意一个细节:allowCredentials设为true的时候,allowedOrigins不能写"*",必须用allowedOriginPatterns(" * "),这是SpringBoot 2.4以后的行为变化。我见过有人照着老教程抄了一个Credentials+Origin的组合导致跨域一直失败的,排查了半天。
前端axios封装里需要做两件事:请求拦截器统一加token,响应拦截器统一处理code。token从localStorage取,401跳登录页,200以上非200的错误码弹提示。这套东西封装好了,业务代码里就不用每个请求都写一遍错误处理了。
5.3 从列表到详情页关键交互流程
商品列表到详情页的跳转,除了路由参数的传递,还要关注联动的交互逻辑。列表展示的是简要卡片(图片、标题、价格、成色),点击进入详情页时要展示:完整描述、卖家信息(昵称、头像、是否本校学生)、商品状态按钮、收藏和预约按钮。
一个很重要的交互细节是商品状态的实时感知。用户从列表点进详情的时候商品还是在售的,提交预约的时候可能已经被别人抢了,这时后端的原子更新会兜底返回一个“手慢了一步”的错误。前端拿到这个错误要友好提示用户,而不是给一个刺眼的报错弹窗。
详情页的浏览量加一操作,我用的是异步接口。前端页面加载后fire-and-forget调用PUT /api/products/{id}/view,后端在update语句里直接view_count = view_count + 1,不走先查后改的两步逻辑,避免丢更新。
6. 部署上线与常见问题排查
6.1 Maven打包与Docker部署全流程
这个项目我最终用Docker部署到服务器上,整个流程捋一遍:
# 1. 本地打包(跳过测试) mvn clean package -DskipTests # 2. 在服务器上准备Dockerfile FROM openjdk:8-jdk-alpine VOLUME /tmp ARG JAR_FILE=target/campus-trade-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]MySQL和Redis用docker-compose一起编排,数据目录挂载到宿主机保证容器重启数据不丢。宝塔面板里也可以直接用Docker管理器操作,但很多坑在于端口映射。我当时把一个常见坑列出来了,对新手特别有用——
数据库容器端口映射到宿主机3307,应用里配置的JDBC地址就要对应改成jdbc:mysql://服务器IP:3307/campus_trade,别让两边端口不一致导致连不上。还有数据库容器的时区参数,不然Java和MySQL之间会有8小时时差,查出来的时间总是差八个小时。
6.2 高频踩坑清单与解决方案
做这套系统过程中我收集了一批高频问题,这里做一个问题快速定位表:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 请求报403 | 拦截器把匿名请求拦截了 | 检查放行路径白名单,/api/public/开头的路径要放行 |
| 图片上传成功但访问404 | 静态资源映射没配置 | 检查静态映射路径和文件实际存储路径是否一致 |
| 中文乱码 | JDBC连接串缺字符集参数 | URL加characterEncoding=utf8&serverTimezone=Asia/Shanghai |
| 点击预约永远提示商品不存在 | 事务内先查后改,查到的是脏数据 | 换成状态条件更新,一条update解决问题 |
| Redis连不上导致登录失败 | 忘记配置Redis或密码错误 | 本地开发时可以临时把缓存降级为内存实现 |
| 导出的jar启动闪退 | 没有指定mainClass | 检查pom.xml中spring-boot-maven-plugin配置 |
| SpringBoot3.x用javax报错 | 包名迁移到jakarta | 别折腾,老老实实降回2.7.x |
6.3 代码提交前必须注意的安全项
项目做完要提交代码、写文档、可能在公开平台晒源码,这个环节最容易被忽略但也最容易出事故。我在交代码之前会做一遍三查:
第一查配置文件。application.yml里的数据库密码、Redis密码、JWT密钥,全部改成环境变量引用或者删掉,绝对不要明文留在仓库里。就算只放在自己的GitHub仓库,只要你设置成公开,就会被爬虫扫到——这种案例太多了,只要搜一下数据库密码的语法就能看到一批实锤。
第二查日志输出。有没有把自己的token、学号等敏感信息打出来?日志里不能有密码,加密后的要带上哪怕调试都不行。这是一个底线问题。
第三查上传文件的类型校验。上传接口有没有校验文件后缀?有没有对图片内容做合法性判断?这部分补位的关键操作如果缺失会被攻击者塞个脚本文件。安全下线、程序输出零风险才是真正能公开出去的东西。
7. 实操总结与个人经验分享
这套校园交易系统从设计到上线,我自己前后花了大概三周的时间,实际写代码的时间只占了六成,剩下四成全在需求梳理和排查问题上。如果只给一条最重要的建议,我会说:动手之前把数据库表结构和状态流转图画清楚,后面写代码的速度会快一倍。
第二个经验是,做这类项目不用追求技术的“大而全”。很多同学喜欢把Redis、RabbitMQ、Elasticsearch、分布式锁都堆上去,觉得技术栈越新越有含金量,但面试官或者答辩老师问几个为什么,很容易就露馅了。这个项目里Redis只负责了缓存和token管理,消息队列根本没有引入——因为业务场景里根本没有异步消息需求,硬加一个MQ纯属为了技术而技术。
最后分享一个小技巧:给项目写在线接口文档用的knife4j,其实不止是给前端同学看的,答辩的时候把接口文档页打开,对着文档讲业务逻辑,比贴着一页页的代码讲效率高得多。老师一眼能看到你设计了哪些接口、业务是不是完整、流程是不是闭环,这种工程化思维是非常加分的。
后续如果要往深扩展,可以在预约功能上加上消息推送,或者在商品审核上引入图片识别,但那是毕业后的故事了,当前这套系统的核心链路已经足够闭合。
如果你正准备做校园交易系统,建议先拿着这篇文章把需求和表结构捋顺,然后挑一个模块完整实现,其余模块照此推进。源码部分如果你需要参考的话,可以在相关资源列表里找到。祝你的项目一次通过测试跑通,少熬夜少踩坑。