去年帮导师带一个本科毕设,题目就是“基于Spring Boot的校园闲置物品租售系统设计与实践”。我当时跟学弟说得很直白:这个题目看上去简单,但想做到“能用”而不仅仅是“能答辩”,坑比想象中多得多。校园二手交易场景特别真实——毕业季的教材、考研资料、电动车、小冰箱,全是刚需;可微信群里的闲置交易,信息沉底、价格混乱、交易也没有保障。做一套租售系统,本质上是解决三件事:信息撮合、交易履约、安全信任。这篇博文不是功能演示,而是我把整个系统从需求拆解、数据库设计、核心接口、安全防护到部署上线的完整复盘,适合正在做类似系统,或者想用 Spring Boot 独立拿下一个小型全栈项目的人参考。
1. 为什么是 Spring Boot?校园项目技术选型背后的判断
1.1 校园闲置系统到底在解决什么问题
很多人在动手前容易犯一个错:把“校园闲置物品租售系统”理解成一个增删改查的 CRUD 管理后台,于是功能表列得飞快,但业务逻辑一深想就卡住了。实际砍完需求后,系统要支撑的核心业务有下面几条:
- 用户侧:注册登录、学号认证、个人资料、收货地址、收藏夹。
- 商品侧:发布闲置、编辑下架、图片上传、分类检索、关键字搜索、按价格/发布时间排序。
- 交易侧:下单购买、下单租赁、押金处理、订单状态流转、交易完成、取消退款。
- 交互侧:站内通知、留言咨询、买家与卖家的沟通记录。
- 管理侧:商品审核、违规举报处理、用户封禁、数据统计。
这个需求清单看起来不复杂,但有几处很容易被低估:租售双模式的状态流转比单纯二手交易复杂;图片上传和访问涉及存储方案;下单存在并发问题;权限边界如果设计不好,任何一个学生都能改掉别人的订单。Spring Boot 恰好把这些复杂度控制在一个单体能解决的范围内,同时生态又足够成熟,不需要从零造轮子。
1.2 单体应用还是微服务:先看清项目规模
我在技术选型时,第一件事就是拦住学弟用微服务的冲动。他当时看到网上不少项目都在讲“微服务实践”,也想把用户、商品、订单拆成三个服务,再加上网关和注册中心。我说你得想清楚一个问题:你的部署资源有多少?一个 2 核 4G 的云服务器能不能跑起完整的一套微服务?如果项目本身只有几个模块、几个开发人员,微服务带来的网络开销、分布式事务、链路追踪,全是负资产。
Spring Boot 单体架构在校园项目里是最合适的选择。它不代表代码就乱,核心是用模块化的包结构把边界划清楚,比如 controller、service、mapper、domain 分层,内部按用户、商品、订单、消息做模块分包。这样既保留了单体部署简单、调试方便的优势,又在代码层面为未来拆服务埋好了伏笔。
1.3 配套技术栈的选型逻辑
配套技术栈我最终用了下面这套,每一组选型都有具体理由:
| 组件 | 选型 | 理由 |
|---|---|---|
| 核心框架 | Spring Boot 2.7.x | 稳定、资料多、兼容性好,不建议一上来追最新的 3.x,部分第三方库可能还没跟上 |
| ORM | MyBatis Plus | 单表 CRUD 可以少写大量样板代码,分页插件、逻辑删除、字段填充都很成熟 |
| 数据库 | MySQL 8.0 | 事务支持好,校园项目数据量远没到分库分表级别,InnoDB 足够 |
| 缓存 | Redis | 验证码、接口限流、热门商品缓存、WebSocket 在线状态存储 |
| 认证 | JWT | 无状态,前端小程序和 Web 端都好用;配合 Redis 做注销失效 |
| 前端 | Vue 3 + Element Plus / 微信小程序 | 校园场景手机访问比例很高,H5 或者小程序都比纯 PC 后台实用 |
| 实时通知 | WebSocket | 下单成功、订单状态变化、收到留言时,主动推送消息给用户 |
这里特别想提醒一下 ORM 的选择。JPA 虽然用起来也很舒服,但对大多数学生的 SQL 功底来说,JPA 的懒加载、N+1 查询、级联保存等问题容易踩坑而且很难查。MyBatis Plus 更“所见即所得”,SQL 可控,排查问题容易,配合代码生成器,建表后几分钟就能生成一套基础 Mapper 和 Service,非常适合这种业务不算复杂的系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与订单状态机:所有功能的地基
数据库设计是这个系统里最不能跳过的一环。我见过太多人一开始就把表建出来,结果写接口时发现字段不够、状态混乱、业务逻辑和数据库对不上,只能反复改表结构。提前把状态机和关系想清楚,后面所有功能都会顺畅很多。
2.1 核心表结构和字段设计
项目最终的核心表有 8 张左右,下面挑几张最关键的给一个参考结构。
用户表 t_user:
sql复制CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`student_no` varchar(32) NOT NULL COMMENT '学号',
`password` varchar(128) NOT NULL COMMENT 'BCrypt加密后的密码',
`real_name` varchar(32) DEFAULT NULL COMMENT '真实姓名',
`nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(255) DEFAULT NULL COMMENT '头像地址',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号(脱敏展示)',
`role` tinyint NOT NULL DEFAULT '1' COMMENT '角色:0管理员,1普通用户',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1正常,0封禁',
`credit_score` int DEFAULT '100' COMMENT '信用分',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_no` (`student_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
商品表 t_goods 是最核心的一张表,设计时要把“卖”和“租”两种业务同时装进去:
sql复制CREATE TABLE `t_goods` (
`id` bigint NOT NULL AUTO_INCREMENT,
`seller_id` bigint NOT NULL COMMENT '发布者用户ID',
`title` varchar(128) NOT NULL,
`description` text COMMENT '商品描述',
`category_id` bigint NOT NULL COMMENT '分类ID,如教材、数码、生活用品',
`type` tinyint NOT NULL COMMENT '交易类型:1出售,2出租',
`price` decimal(10,2) NOT NULL COMMENT '出售价,出售时必填',
`rent_price` decimal(10,2) DEFAULT NULL COMMENT '租赁单价',
`rent_unit` tinyint DEFAULT NULL COMMENT '租赁计价单位:1按天,2按周,3按月',
`deposit` decimal(10,2) DEFAULT NULL COMMENT '押金,出租场景必填',
`stock` int NOT NULL DEFAULT '1' COMMENT '库存数量,一般闲置商品为1',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面图',
`status` tinyint NOT NULL COMMENT '状态:0待审核,1上架中,2已下架,3已售出/出租中,4违规下架',
`view_count` int DEFAULT '0' COMMENT '浏览量',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`deleted` tinyint DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_seller_id` (`seller_id`),
KEY `idx_category_status` (`category_id`, `status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲置物品表';
订单表 t_order 和订单状态流水表 t_order_log 也要一并设计好。状态流水表每次状态变化都插一条记录,这样出现纠纷时能看完整轨迹,而不是只看到最终状态。
2.2 “租”和“售”两种业务怎么放进同一套模型
刚开始学弟问我,要不要把出售和出租分成两张商品表,我否了。原因是两种业务共享的商品属性太多:标题、描述、图片、分类、卖家。拆表会导致查询和展示都要做合并,麻烦且没有收益。正确做法是用一个 type 字段区分,出租额外带 rent_price、rent_unit、deposit 这几个字段。
这个设计模式上的思路叫“策略模式”,在接口层也延续了同样的处理。创建订单时,根据 type 走不同的计价逻辑:出售订单直接按 price 结算;出租订单要计算 rent_price * 天数 + deposit,天数又由 rent_unit 决定。把这两套逻辑封装成独立的计价策略类,后续加“以物换物”之类的业务时,不影响原有代码。
2.3 订单状态机:最容易改崩的地方
这个系统的订单状态机我建议一开始就画清楚,否则写 controller 的时候会非常痛苦。出售订单的状态流转如下:
| 状态码 | 状态含义 | 可流转到的状态 |
|---|---|---|
| 0 | 待付款 | 1 已付款 / 5 已取消 |
| 1 | 已付款(待自提/发货) | 2 已完成 / 5 已取消 |
| 2 | 已完成 | 无 |
| 3 | 退款中 | 4 已退款 / 1 已付款 |
| 4 | 已退款 | 无 |
| 5 | 已取消 | 无 |
出租订单因为涉及到“归还”动作,状态要多两步:
| 状态码 | 状态含义 | 可流转到的状态 |
|---|---|---|
| 0 | 待付款 | 1 已付款 / 6 已取消 |
| 1 | 已付款(等待取件) | 2 租用中 / 6 已取消 |
| 2 | 租用中 | 3 待归还确认 |
| 3 | 待归还确认 | 4 已完成 / 5 纠纷处理中 |
| 4 | 已完成 | 无 |
| 5 | 纠纷处理中 | 4 已完成 |
| 6 | 已取消 | 无 |
实现状态机时不要在每个 service 方法里随手改 order.status,那样后期维护堪称灾难。我建议用状态模式或者至少一个集中式检查工具,比如写一个 OrderStatusTransition 类,里面维护一张 Map<Integer, Set<Integer>> 的合法流转表,每次更新前统一校验:
java复制public class OrderStatusTransition {
private static final Map<Integer, Set<Integer>> SALE_TRANSITIONS = new HashMap<>();
static {
SALE_TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 5)));
SALE_TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 5)));
SALE_TRANSITIONS.put(3, new HashSet<>(Arrays.asList(4, 1)));
}
public static boolean canTransition(Integer category, Integer from, Integer to) {
Set<Integer> next = SALE_TRANSITIONS.get(from);
return next != null && next.contains(to);
}
}
这样每次状态变更都调用 canTransition 校验,非法跳转直接抛业务异常,比散落在各路代码里的 if 判断可靠得多。
2.4 索引和慢查询:提前考虑还是后面补?
索引设计是 mysql 数据库实践里最值得反思的一环。校园项目的表数据量不算大,但也别走极端——完全不建索引,或者无脑给每个字段都加索引。我最终按查询路径建索引:
- 商品列表页:按分类 + 状态筛选,所以建了联合索引
(category_id, status)。 - 我的发布/我的订单:全部按当前用户查,
t_goods.seller_id、t_order.buyer_id / seller_id必须建索引。 - 首页按时间排序:
create_time建索引。 - 搜索场景:如果只用
LIKE '%keyword%',任何索引都帮不上忙,初期数据量小可以接受,后续量大再引入全文索引或者 Elasticsearch。
这里额外提一个经验:在开发阶段打开 MyBatis Plus 的 SQL 日志,把执行超过 500ms 的 SQL 都捞出来看。真正出问题的往往不是单表查询,而是关联查询的嵌套循环,用 EXPLAIN 看执行计划才是排查正道。
3. 从注册登录到订单闭环:核心接口的设计与实现
数据库和状态机定下来,接下去就是接口实现。这一章我不打算把所有接口都列一遍,而是挑几个“写错就会炸”的地方重点展开。
3.1 注册登录:Session 还是 JWT?
我最终选了 JWT。原因很简单:这个项目最好要配一个微信小程序端或者 H5 端,小程序没有 Cookie 的概念,用 Session 做登录要么自己封装,要么就得放弃跨端复用。JWT 的 token 由服务端签发,前端每次请求放到 Authorization 头里,后端用一个拦截器统一解析校验,逻辑非常干净。
注册时有一个校园项目特有的点:学号认证。设计上有两条路,一条是真实对接学校的统一身份认证接口,这个现实中不一定有权限;另一条是模拟认证——用户填学号和姓名,后台拿这两项和数据库中的学生信息表比对,比对了就认证通过。对毕设来说,第二条路更现实,但要注意提醒用户,真实投入运营时必须有学校官方接口授权。
密码存储必须用 BCrypt,不要用 MD5,更不要明文:
java复制// 注册时加密
String encodedPwd = BCrypt.hashpw(rawPassword, BCrypt.gensalt());
// 登录校验
boolean match = BCrypt.checkpw(rawPassword, user.getPassword());
后面所有接口都通过拦截器解析 token,把当前登录用户放进 ThreadLocal 或者一个 UserContext 对象,这样业务代码里随时能拿到 userId,避免每次手写 token 解析。
3.2 商品发布与列表:图片上传是第一个大坑
商品发布的接口难度不高,但图片上传这块特别容易想简单。校园项目常见的做法是把图片存到服务器本地某个目录,然后返回一个 URL。这里有一个关键点,Spring Boot 接收 MultipartFile 后,如果直接把文件名拼进路径,会有两个问题:一是中文文件名乱码,二是恶意文件名可能覆盖已有文件。处理方式是重命名文件,统一用 UUID:
java复制String originalFilename = file.getOriginalFilename();
String ext = StringUtils.getFilenameExtension(originalFilename);
String newFileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
String datePath = LocalDate.now().toString();
File dest = new File(uploadDir + datePath + "/" + newFileName);
文件大小限制写在配置文件里:
yaml复制spring:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 20MB
商品列表接口除了查数据库,还得处理排序和缓存。我选择用 Redis 缓存分类维度的商品 ID 列表,比如 goods:list:category:{categoryId},过期时间设置 10 分钟。这样首页和分类页的 QPS 压力小很多。等用户发布商品时再删除对应分类的缓存,下次重新加载,这个“缓存穿透 - 击穿 - 雪崩”的坎基本就避开了。
3.3 下单并发与幂等性:不能靠运气
这是整个订单接口中最核心、也最容易被疏忽的问题。用户在商品详情页连点两次“立即购买”,如果后端不设防,会生成两笔一模一样的订单,这就是典型的幂等性问题。我用了两种手段一起兜底:
第一层是幂等 Token。前端打开下单页面时先请求后端一个“幂等凭证”,后端把它存到 Redis,设置例如 5 分钟过期。真正提交订单时,前端必须带上这个 token,后端处理逻辑用 SET NX EX 的原子操作消费掉 token。一个 token 只能成功使用一次,第二次请求过来直接提示“请勿重复提交”。
java复制String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("order:token:" + userId + ":" + token, "1", Duration.ofMinutes(5));
// 提交订单时校验
Boolean consumed = redisTemplate.delete("order:token:" + userId + ":" + submitToken);
if (!Boolean.TRUE.equals(consumed)) {
throw new BusinessException("请勿重复提交订单");
}
第二层是库存扣减。我在商品表加了一个 stock 字段,用乐观锁方案控制并发,而不是先查库存再 update。SQL 长这样:
sql复制UPDATE t_goods
SET stock = stock - 1
WHERE id = #{goodsId} AND stock > 0
如果执行后影响行数为 0,说明库存不够,直接提示“手慢了,商品已被抢走”。注意这里要用数据库的行锁来保护并发,不能在内存里先判断再更新,那在并发下一定会超卖。整个扣库存和创建订单要放在同一个事务里,任何一个失败都要整体回滚。
出租订单的金额计算用策略模式实现,这里简单展示一个结构:
java复制public interface RentCalculator {
BigDecimal calculate(BigDecimal rentPrice, Integer rentUnit, int days);
}
public class DailyRentCalculator implements RentCalculator {
@Override
public BigDecimal calculate(BigDecimal rentPrice, Integer rentUnit, int days) {
return rentPrice.multiply(BigDecimal.valueOf(days));
}
}
3.4 站内通知:用 WebSocket 把事件推给用户
一开始我没打算加实时通知,后来发现不行。用户下单之后,卖家如果等半天才看到订单,体验非常差。我用 spring boot 集成 websocket 做了一套站内通知,客户端的 WebSocket 连接服务端之后,服务端以用户 ID 作为标识保存连接会话:
java复制@Component
public class WebSocketServer {
private static final Map<Long, Session> SESSION_MAP = new ConcurrentHashMap<>();
public void sendMessage(Long userId, String message) {
Session session = SESSION_MAP.get(userId);
if (session != null && session.isOpen()) {
session.getBasicRemote().sendText(message);
}
}
}
下单成功、订单状态变化、收到新留言时,业务层调用 webSocketServer.sendMessage(userId, content),前端通过 onmessage 接收并弹提示。这个功能看起来不大,但对项目整体完整度的提升非常明显。
4. 安全设计比功能更重要:防刷、防越权、防注入
很多校园项目把安全当成可有可无的摆设,答辩老师问一句“你的系统怎么保证用户之间不能越权操作”,就答不上来。安全这部分我做了一次系统性自查,对照数据安全评估的几个维度逐个补齐,下面讲几个投入不大但收益最高的点。
4.1 登录防刷与接口限流
登录接口、注册接口一定不能裸奔,不然会被脚本刷到瘫痪。我用 Redis 做了两层:
一是验证码。登录注册都要图形验证码,我用 hutool 的验证码工具生成,把验证码文本存到 Redis,key 是 captcha:uuid,5 分钟过期。用户提交时校验,校验通过就删掉 key,防止重放。
二是接口限流。登录接口对 IP + 用户账号做双重限制,同一账号 1 分钟内错误次数超过 5 次就锁定 15 分钟;同一 IP 的并发请求用 RateLimiter 做简单限流。实现不复杂,关键是意识要到位。
4.2 越权检查:不能让用户改别人的订单
越权是这个系统最危险的漏洞。比如“取消订单”接口,如果只接收一个 orderId,然后执行 UPDATE t_order SET status = 5 WHERE id = orderId,那任何登录用户传有别人的订单号都能取消。正确做法是操作前必须有归属校验:
java复制// 取消订单
Order order = orderMapper.selectById(orderId);
if (order == null || !order.getBuyerId().equals(currentUserId)) {
throw new BusinessException("订单不存在");
}
// 再执行状态机校验和更新
这个思路要贯穿所有涉及资源操作的接口:修改商品、删除商品、取消订单、确认收货、评价。凡是操作了“别人数据”的,都要先判断归属。我这边统一封装了一个 checkOwner 的基础方法,所有 Service 继承之后直接用。
4.3 SQL 注入、XSS 与上传安全
SQL 注入因为用了 MyBatis Plus 的 #{} 参数绑定,风险基本被框架兜住了,但要注意一点:写自定义 SQL 时不要用 ${} 拼接,那会重新引入注入风险。
XSS 的核心防御点在用户输入的标题、描述、昵称。我在全局加了一个 XssFilter,对请求参数做过滤转义,把 <script>、onerror 这类危险内容清洗掉。同时前端展示时也要转义,双端配合才稳妥。
上传安全这一段同样值得强调。文件上传除了限制大小,还要校验 Content-Type 和扩展名白名单,不能只信前端传的文件名。我这边把扩展名白名单限定为 jpg、jpeg、png、gif、webp,并用 ImageIO.read() 测试能否正常解析,解析失败直接拒绝,这种方式能挡住大部分伪装成图片的恶意文件。
4.4 敏感信息保护与隐私合规
校园系统里最敏感的是学号、姓名、手机号,偶尔还有学生证照片。我的处理方式是:手机号在数据库和接口返回中都做脱敏,只展示前三位和后四位;学生证照片除非管理员审核需要,否则不提供公开展示;密码统一 BCrypt,任何人包括管理员都看不到明文。
同时给整个系统梳理过一轮数据安全自查,内容包括:访问控制是否闭环、日志是否记录操作人、敏感字段是否加密、备份策略是否可靠。这些内容不是流程堆砌,而是项目上线前最基础的体检项。至少要做到给用户加删除账号的入口,并说明数据保留策略,而不是让用户数据永久躺在数据库里。
5. 部署上线踩坑实录:从开发环境到外网可访问
代码写完之后,部署环节才是真正磨人的地方。这一章全是实际操作中踩过的坑,建议直接保存。
5.1 开发环境的三个隐藏地雷
第一个坑是 MySQL 连接串时区。如果 URL 里没有加 serverTimezone=Asia/Shanghai,就会报 The server time zone value 错误。第二个坑是字符集,连接串必须带 characterEncoding=utf8mb4,否则中文在读写时可能乱码。第三个坑是跨域,前后端分离部署时,前端页面和后端 API 不在同一个端口,必须配置 CORS:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
三个坑都不难解决,但每一个都至少耗费掉半天时间,提前配好能省很多事。
5.2 本地存储图片的方案和坑
本地存储图片适合部署在单台云服务器上的情况。启动类里要实现 WebMvcConfigurer,把本地目录映射成虚拟路径,这样访问 /images/** 时自动映射到服务器磁盘:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceHandler("file:" + uploadDir + "/");
}
这里有个大坑:Spring Boot 打包成 jar 后,项目内相对路径不可靠,图片上传目录一定要用绝对路径,写在配置文件中,例如 /opt/goods-images/。我用 application.yml 配了一个 upload.dir 属性,代码里统一读取它,而不是 getResource 获取运行时路径。
如果服务器磁盘紧张,或者后续想接入小程序,更推荐对象存储。对象存储的优势是容量弹性、上传走客户端直传、访问走 CDN,配置不复杂但比本地方案麻烦一点。对毕设级项目,本地存储完全够用,重点是路径规划清楚。
5.3 Docker Compose 编排一套环境
部署阶段我选择了 Docker Compose,把 MySQL、Redis、应用本身三个容器编排在一起,服务器上只要装了 Docker 就能一键拉起。docker-compose.yml 的简化版本看起来像这样:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
container_name: goods-mysql
environment:
- MYSQL_ROOT_PASSWORD=your_root_pwd
- MYSQL_DATABASE=campus_goods
- TZ=Asia/Shanghai
volumes:
- /opt/mysql-data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7.0
container_name: goods-redis
ports:
- "6379:6379"
app:
build: .
container_name: goods-app
ports:
- "8080:8080"
depends_on:
- mysql
- redis
注意两点:持久化目录必须挂载到宿主机,否则容器重建数据全丢;TZ=Asia/Shanghai 必须设置,容器默认时区是 UTC,会导致数据库时间戳差 8 个小时。
5.4 日志与监控:别等出了问题再翻日志
最后上线前,我给系统加了一套简单的日志与监控。业务日志用 AOP 统一记录,哪些接口被谁调了、参数是什么、耗时多久,全部打进日志文件。关键业务操作(下单、支付、取消订单、审核商品)额外插入一条操作流水到 t_operation_log 表。MySQL 慢查询日志打开,long_query_time = 1,超过 1 秒的 SQL 全部记录下来。
这一套做完之后,线上出现问题时定位效率会高非常多。售后同学跟我说“用户订单状态不对”,我能直接从订单流水表看到每一步动作,而不是瞎猜。
6. 如果重写一次,我会在哪些地方做出改变
6.1 为什么不同时上微服务
这个问题在选型时聊过,但部署完之后我又有新的体会:如果当初真的拆了微服务,这套系统在 2 核 4G 的服务器上根本跑不动完整一整套。现在单体应用配合 Docker Compose,资源占用低、日志集中、排查链路短,上线第一天几乎没出运维问题。这印证了我的一个判断:技术选型不是越“高级”越好,而是越匹配团队和场景越好。等系统真的到了需要独立扩容、多人协作且业务边界清晰的阶段,再拆分也完全不迟。
6.2 真正值得扩展的三个方向
如果时间充裕,我会优先做这几件事:
- 信用评价体系。交易完成后双方互评,评价影响信用分。信用分高的用户发布商品时权重更高,搜索排序靠前。
- 个性化推荐。用户浏览和收藏行为的数据先落到一张浏览日志表,再按分类聚合,推荐同分类的热门商品。数据量小,不一定要上推荐算法框架,用 SQL 算个排序分就行。
- 在线即时聊天。WebSocket 通知已经有了雏形,可以进一步做成买卖双方的在线聊天页,而不是只靠留言板。这个功能对成交转化率提升很明显。
6.3 给后来者的实际操作体会
讲点我第二次做这种项目会调整的细节。首先是按订单状态先建一个小型状态机测试用例,把所有合法和非法流转都覆盖一遍,而不是等到联调时才手动点流程。其次是在一开始就定好统一响应体 Result<T> 和全局异常处理器,否则后面几百个接口的返回风格很难统一。再有一个很重要的点:数据库的字段注释和接口文档要写清楚,项目到中期以后,代码自己能读明白,但业务规则只有文档记得住。
如果让我重新开这个项目,我会在最开始就用一天时间把租售模式的状态流转图、权限矩阵图和页面原型画清楚,而不是急着让代码跑起来。这个项目真正的难度从来不是 Spring Boot 的 API 怎么调,而是业务规则在并发、状态、安全条件下怎么保持正确。把业务想清楚,再写代码,速度反而会快很多。
