毕设校园二手交易平台实战:从单体架构到高可用微服务的演进路径
关键词:毕设校园二手交易平台、Spring Boot、Vue3、Redis、OSS、JWT、防超卖、Clean Code
1. 背景痛点:学生项目最容易踩的五个坑
做校园二手交易平台,很多同学第一反应是“把商品挂上去、能下单就行”。结果答辩现场被老师三连问:
- “同时 100 个人抢 10 台相机,库存怎么保证不超卖?”
- “图片全扔在
static/upload里,GitHub 一同步就冲突,怎么办?” - “登录态用 Session,重启服务就踢所有人下线,合理吗?”
把真实踩坑记录拉出来,高频问题集中在:
- SQL 注入:手写 SQL 拼接
"SELECT * FROM item WHERE id=" + id。 - 无幂等下单:前端连点两次“立即购买”,订单表瞬间重复。
- 本地文件存储:Windows 路径
D:\\pics\\在 Linux 服务器直接 404。 - 无事务边界:扣库存、写订单、减余额三条 SQL 各自为政,崩了无法回滚。
- 日志缺失:上线后用户反馈 500,服务器却找不到任何请求记录。
这些问题在本地 1 个用户测试时全都不会暴露,一旦放到公网,就是“社死”现场。下面给出一条从“能跑”到“能扛”的实战路线,全部代码已开源在 GitHub,文末附地址。
2. 技术选型:为什么不是 Flask 而是 Spring Boot?
| 维度 | Spring Boot | Flask/Django | 备注 |
|---|---|---|---|
| 依赖注入 | 原生支持 | 三方库 | 毕设周期短,少踩一个坑是一个 |
| 事务&锁 | @Transactional+ Redisson | 手动实现 | 防超卖场景刚需 |
| 生态成熟 | Alibaba Spring Cloud 全套 | 社区零散 | OSS、Nacos 直接 starter 引入 |
| 就业面 | 国内 70% 岗 JD 要求 | 偏外企 | 简历更吃香 |
前端选型更直接:Vue3 组合式 API 相比 Vue2:
- 用
reactive+watchEffect把“搜索-分页-购物车”三块数据做成一个useTrade组合函数,代码量下降 30%。 <script setup>单文件即可写完组件,无需export default {},对新手更友好。
3. 核心实现:三段代码搞定商品、订单、图片
下面代码均来自真实仓库,只保留骨干逻辑,能直接抄。
3.1 商品发布:参数校验 + 事务入库
@RestController @RequiredArgsConstructor @RequestMapping("/api/item") public class ItemController { private final ItemService itemService; @PostMapping public IdResp publish(@Valid @RequestBody ItemPublishDTO dto, @Authentication Long userId) { // DTO 字段校验交给 @Valid,业务规则进 service return IdResp.of(itemService.publish(dto, userId)); } } @Service @RequiredArgsConstructor public class ItemService { private final ItemMapper itemMapper; private final RedisTemplate<String, Integer> redisTemplate; @Transactional(rollbackFor = Exception.class) public Long publish(ItemPublishDTO dto, Long userId) { Item item = Item.builder() .title(dto.getTitle()) .price(dto.getPrice()) .stock(dto.getStock()) .userId(userId) .build(); itemMapper.insert(item); // 缓存预热:后续读多写少 redisTemplate.opsForValue() .set("item:stock:" + item.getId(), dto.getStock()); return item.getId(); } }关键点
- 用
DTO接收参数,杜绝Map<String,Object>导致维护地狱。 - 数据库与缓存双写,但先写数据库,再写缓存,避免并发读脏数据。
3.2 订单创建:Redisson 分布式锁 + 幂等令牌
@RedissonLock(key = "order:lock:uid:#{userId}", waitTime = 0) @Transactional public Long createOrder(Long itemId, Integer quantity Long userId) { // 1. 幂等性校验:同一 item+user 30s 内禁止重复提交 String dedupKey = "order:dedup:uid:" + userId + ":item:" + itemId; Boolean absent = redisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", Duration.ofSeconds(30)); if (Boolean.FALSE.equals(absent)) throw new BizException("操作太频繁"); // 2. 缓存扣库存 String stockKey = "item:stock:" + itemId; Long remain = redisTemplate.opsForValue().decrement(stockKey, quantity); if (remain < 0) { redisTemplate.opsForValue().increment(stockKey, quantity); // 回滚缓存 throw new BizException("库存不足"); } // 3. 写订单 Order order = Order.builder() .itemId(itemId) .quantity(quantity) .userId(userId) .status(OrderStatus.PAY_PENDING) .build(); orderMapper.insert(order); return order.getId(); }注解说明
@RedissonLock为自定义 AOP,底层RLock.tryLock(...),保证 0 等待,失败立即抛异常,避免羊群效应。- 先扣缓存再写订单,即使 JVM crash,Redis 的库存也是原子回滚,不会超卖。
3.3 图片上传:直链 OSS,服务器只存 URL
@RestController @RequestMapping("/api/upload") public class UploadController { private final OSSTemplate ossTemplate; // 封装了 OSSClient @PostMapping("/image") public UrlResp uploadImg(@RequestParam MultipartFile file) { String key = "item/" + Instant.now().getEpochSecond() + "/" + UUID.randomUUID() + ".jpg"; String url = ossTemplate.upload(key, file); return UrlResp.of(url); // 只返回 URL,不落盘 } }前端拿到 URL 直接回显<img :src="url"/>,Git 仓库再也不会被 100M 的jpg撑爆。
4. 性能与安全:四个加固动作
4.1 JWT 令牌刷新
- AccessToken 有效期 15 min,RefreshToken 7 天,存 HttpOnly Cookie。
- 定义
JwtUtils.create(refreshClaims)与validate方法,刷新逻辑放在网关层,业务代码零侵入。
4.2 接口限流
使用 Bucket4j + Redis 存储令牌桶:
@Bean public RateLimiter rateLimiter() { Bandwidth limit = Bandwidth.classic(20, Refill.intervally(20, DurationMinutes.ONE)); return Bucket4j.extension(new RedisBucket4jConfiguration()) .builder() .addLimit(limit) .build(); }在拦截器里针对userId维度限流,20 次/分钟,超出返回 429,保护下游。
4.3 XSS 防护
Vue3 默认把v-html外的插值做 escape,但仍需:
- 后端富文本字段(商品描述)用 jsoup 白名单过滤。
- 开启 Spring Security 的
XContentTypeOptionsHeaderWriter防嗅探。
4.4 关键日志
使用 Logback + AsyncAppender,统一打印traceId,方便在 ELK 里跨服务追踪。慢 SQL 阈值 300 ms,超即报警。
5. 生产环境避坑清单
| 模块 | 默认配置 | 推荐值 | 说明 |
|---|---|---|---|
| 数据源 | HikariCP maximum-pool-size=10 | 20 | 4C8G 服务器可顶 200 并发 |
| 静态资源 | Spring Boot 自带 | Nginx 代理 | 减少 Tomcat 线程阻塞 |
| HTTPS | 无 | 免费证书 Let’s Encrypt | 微信小程序强制 https |
| 文件上传 | 1 MB | 10 MB | 相机原图常 5-6 MB |
Nginx 关键片段:
location /static/ { alias /data/campus-shop/static/; expires 7d; add_header Cache-Control "public, immutable"; }6. 留给你的思考题:没有运维,怎么做日志监控?
文章里所有加固手段,在单机 Docker 环境就能跑通,但上线后最怕“用户说卡,你却看不到日志”。下面给出两条可落地路线,欢迎挑一条动手改造:
- 轻量级:Docker 起 Loki + Promtail,把 stdout 直接收集到 Loki,Grafana 里配好 Alert Webhook 推送到飞书群。
- 零运维:接入 SLS 阿里云日志,免费额度 500 MB/天,足够毕设演示;再写个 Shell 脚本,按小时归档到 OSS,成本忽略不计。
把日志监控加进去,你的答辩 PPT 就能多一页“可观测性”大图,老师很难不给高分。
仓库地址(GitHub):
https://github.com/yourname/campus-shop
分支说明:main为单体版;micro为 Spring Cloud 拆分版,含网关、订单、账户三个服务,供学有余力的同学继续折腾。
结语
从“跑通”到“能扛”,校园二手交易平台浓缩了并发、缓存、存储、安全、部署所有必修课知识点。把文中三段代码拉下来,按 5 个避坑项改完配置,你就有了一份能写在简历上的“高可用”项目。下一步,不妨给订单服务加上 RocketMQ 异步消息,把日志监控再接入 Grafana,体验一把“全链路可观测”。动手吧,毕设不只是为了通过,更是给未来自己铺路。