1. 为什么校园二手交易平台值得自己动手写一套先说个背景。每年毕业季某高校的跳蚤群、闲置转让群都会炸开锅有人低价出教材、有人求购代步车也有同学因为信息分散吃了亏——要么发在群里半天没人理要么线下交易遇到扯皮。我当时接了这个需求设计并实现一个面向校园场景的二手交易系统。技术栈定为Spring Boot原因很直接生态成熟、上手快、Spring Boot 2.x MyBatis-Plus的组合做业务系统非常顺网上的资料也足够多遇到问题基本都能搜到答案。这个系统能做什么核心就三件事发布闲置、浏览检索、完成交易。围绕这三件事还要有用户注册登录、商品管理、订单管理、站内信通知等功能。适合谁来参考主要有两类人一是准备做毕业设计或课程设计的学生需要一套完整可落地的系统设计思路二是刚接触Spring Boot、想通过一个真实业务项目把前后端、数据库、缓存、文件存储串起来的初级开发者。刚开始我并没有急着写代码。做这种业务系统最忌讳拿到需求就建表、就写Controller。我花了大概一周时间做了一件事把整个系统的逻辑链路在白板上画出来。从用户发布一件商品开始商品会经历哪些状态、买家看到之后怎么联系、双方如何约定交易、订单如何生成、交易完成后怎么互评——每一步都对应后台的哪个模块、哪张表、哪个接口。这个梳理过程决定了后面两个月开发是否顺畅。有人可能会说二手交易不就是“发帖-聊天-线下交易”吗搞那么复杂干什么。但校园场景有它的特殊性信任基础薄弱、交易金额小、买卖双方都是学生、时间碎片化。如果不在系统层面设计好保障机制——比如实名认证、交易状态流转、违规举报——平台很快就会变成虚假信息集散地。所以这套系统表面上是“交易平台”本质上做的是信息撮合 信任保障。2. 技术选型与项目架构不追新只选最稳的组合2.1 后端框架与持久层选型分析技术选型我遵循一个原则团队熟悉度 新特性噱头 社区活跃度。这个项目最终选择了以下组合Spring Boot 2.7.x不用3.x是因为当时部分依赖和插件对3.x的支持还不太稳定2.7是2.x系列的最终版本维护周期长、资料全。MyBatis-Plus比起原生MyBatis它提供了通用的CRUD方法、分页插件、条件构造器能减少至少三分之一的基础代码量。很多同学排斥“ORM”担心SQL不可控但MyBatis-Plus本质还是MyBatis复杂查询一样可以写XML并没有失去控制力。MySQL 8.0免费、稳定、用的人多大学服务器上装起来也方便。Redis用来做验证码缓存、商品浏览量计数、热门商品排行后面会详细讲。JWT Spring Security做无状态登录认证。我见过不少项目为了“显示技术含量”硬上微服务、上消息队列最后把自己坑了——分布式事务、服务间调用、日志链路每一个都是新增的复杂度。这个项目的用户量预期就是几千人单体应用完全够用关键是把单体内的结构理清楚。2.2 前后端分离与项目分层项目采用前后端分离架构。前端用Vue 2 Element UI后端纯接口服务。很多学生项目会直接用Thymeleaf模板渲染页面开发速度确实快但后期维护体验很差——前端改个样式要重启后端接口和页面耦合在一起没法并行开发。后端按经典分层结构组织com.campus.trade ├── controller // 接口入口只做参数校验和结果封装 ├── service // 业务逻辑层事务控制在这里 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互的数据传输对象 ├── vo // 视图对象按需返回字段 ├── config // 配置类Redis、Security、MyBatis-Plus、跨域等 ├── utils // 工具类JWT工具、文件上传等 ├── common // 统一返回结果、异常处理、枚举 └── job // 定时任务这里有三点经验值得说明第一实体类Entity和视图对象VO一定不要混用。很多新手图省事直接把数据库实体返回给前端。比如User表里有密码字段如果不加JsonIgnore或转换为VO密码就会泄露到前端响应里。这个坑我在联调时踩过排查了半天才定位到。第二Controller里不要写业务逻辑。正确的做法是Controller只做“接收参数、调用Service、返回结果”三件事所有判断、计算、事务操作都在Service层。这样做的原因是方便复用和测试——如果多个接口需要同样的业务操作提取到Service后只需要调一个方法。第三统一返回结果结构。我定义了一个ResultT类包含code、message、data三个字段。前端通过code判断请求是否成功而不是依赖HTTP状态码。这样做的优点是业务异常和系统异常可以统一处理前端拦截器也只需要判断一种格式。2.3 为什么坚持用单体架构我也认真考虑过要不要把商品搜索、订单、支付拆成独立服务。反复衡量后放弃了。原因有三点这个项目的业务边界虽然清晰但数据关联非常紧密——订单要关联商品、用户、地址拆成微服务后要么做分布式事务要么接受数据不一致。部署难度直线上升。单体应用一个Jar包搞定微服务至少要多维护注册中心、配置中心、网关。团队成本不划算。就一个开发周期把精力花在业务逻辑上比花在基础设施上更有价值。如果你想把项目写进简历单体架构也不是减分项。面试官关心的是你对业务的理解深度而不是用了多少中间件。后面我在部署时用Docker容器化了MySQL和Redis应用本身保持单实例部署既简化运维又保留了一定的工程化实践。3. 核心业务模块设计从需求到接口的完整链路3.1 用户模块注册、登录与身份认证的设计细节用户模块看起来最简单——注册、登录、改密码但恰恰是这个模块最容易出安全问题。我总结了几点容易忽略的细节密码加密。数据库绝对不能存明文密码。我用的是BCrypt加密Spring Security自带的BCryptPasswordEncoder。每次校验时把用户输入的明文和数据库中的哈希值进行比对就算数据库泄露也无法反推出原始密码。注册时的验证码方案。最初我设计的是邮箱验证码后来发现校园场景下学生邮箱开通率不高就改成了短信验证码。但短信服务需要企业资质和个人开发者认证流程麻烦。最后的折中方案是图形验证码 手机号格式校验图形验证码存Redis5分钟过期。如果需要上线商用再接短信服务也不影响整体架构。登录状态管理。登录成功后服务端签发JWT前端把Token存到localStorage。每次请求在HTTP Header里带上Authorization: Bearer token。后端的Spring Security过滤器拦截请求解析Token并设置当前登录用户上下文。这里有个很关键的细节JWT的过期时间。设太短比如30分钟用户用着用着就被踢下线设太长比如7天又容易泄露。我的做法是双Token机制AccessToken有效期2小时RefreshToken有效期30天。AccessToken过期时前端用RefreshToken去换新Token用户无感。这个机制真正实现起来就是在登录接口里多返回一个refreshToken再加一个/auth/refresh接口。3.2 商品模块信息发布、审核与搜索商品发布是整个平台的核心入口。一个二手商品的信息质量直接决定了买家愿不愿意点进来。在设计发布表单时除了常规的标题、描述、价格、成色、原购买时间这些字段我还加了两个校园场景特有的字段交易地点如东区食堂门口、图书馆大厅和交易方式面交/校内自提/可邮寄。这两条信息看着不起眼实际对成交率影响很大。学生买家看重的是“方不方便拿”如果商品在大学城另一头很多人哪怕便宜也不会买。商品发布后不是直接上架而是先进入待审核状态。管理员后台有一个审核列表人工检查商品图片是否有违规内容、描述是否真实。审核通过后状态变为“在售”否则退回并附带原因。你可能觉得人工审核太重了但在平台初期人工审核是控制信息质量最有效的手段。后期可以做成“关键词过滤 用户信用分自动放行”。搜索模块我用了MySQL的LIKE %关键词%做模糊查询。商品量到万级时性能还可以接受配合索引和分页基本无压力。如果后期数据量涨到十万级以上再考虑引入Elasticsearch或MySQL全文索引。这里我不建议一上来就上ES维护成本和服务器成本都会上升。3.3 收藏、留言与站内通知用户在浏览商品时可以点击收藏方便后续在“我的收藏”里快速找到。这个功能底层很简单——一张用户-商品关联表。但我在设计时给这张表加了收藏时间字段商品列表按收藏时间倒序展示这样用户看到的是自己最近添加的而不是什么别的排序。留言功能相当于商品详情页下的评论区。买家可以留言询问“还在吗”“最低多少能出”卖家回复后买家能收到站内通知。这块用的是SSEServer-Sent Events做消息推送比WebSocket简单得多单向通知场景完全够用。主要逻辑是前端建立EventSource连接后端在留言回复或订单状态变化时向连接推送消息。4. 交易安全与订单状态机为什么信任机制是平台的生命线4.1 状态机设计从下单到完成的全流程二手交易和电商的标准流程不一样它更像是“双方线上达成一致线下履约”。但如果完全不管线下环节平台就无法处理纠纷。所以我把订单设计成一条有明确状态的流程待付款 → 待发货卖家确认 → 待收货买家确认 → 已完成 ↓ 退款/投诉 → 平台介入实际开发中状态机的实现有两种方式一是用if/else在Service里硬编码判断二是用状态模式把每个状态封装成独立的处理器。我选了后者因为状态流转不是线性的有些分支如取消、超时、退款会让流程回退。每个状态变更都要做的事情更新订单表中的status字段写入订单状态变更日志表关键操作留痕触发通知站内信或短信状态日志表是整个订单模块最容易忽略的表但它非常关键。当买卖双方产生纠纷时客服能根据日志还原整个过程——“是买家先点了取消还是卖家先点了发货”。没有这张表纠纷处理就完全靠双方各执一词。4.2 资金保障方案引入“平台保证金”机制校园二手交易违约率不低——买家拍下不付款、卖家收了钱不发货的情况时有发生。为了降低违约风险我在系统里设计了平台保证金机制类似“信用分”的惩罚式实现用户在首次发布商品前须缴纳10元保证金线上模拟支付当订单违约买家超时未付款、卖家超时未发货从保证金中划扣5元作为违约处罚保证金余额低于5元时用户不能再发布新商品或发起新订单余额可通过充值补充也可在毕业注销账号时申请退款这个机制模拟的是真实交易平台的保障逻辑。它不一定能完全杜绝违约但能显著提高违约成本把恶意用户的试错成本抬高。4.3 举报与信用评价体系交易完成后买卖双方可以互相评分1-5星并留下评价。用户的信用分由三部分组成基础分注册即默认100分交易加分每完成一笔订单2分好评1分违约减分订单超时/取消-5分被投诉成立-10分商品列表页会展示卖家信用分。分数低于70分的卖家发布商品时系统会强制增加一段风险提示文字。这样设计的目的很简单让守信的人更容易被看到让失信的人自然被市场淘汰。5. 数据库设计表结构、索引与缓存策略5.1 核心表结构与字段设计整个系统一共设计了15张表核心的几张如下用户表userid, username, password, nickname, avatar, phone, credit_score, status, create_time, update_time商品表productid, user_id, title, description, price, original_price, condition_level, category_id, trade_location, trade_method, images, status, view_count, create_time, update_time订单表ordersid, order_no, product_id, seller_id, buyer_id, amount, status, address, remark, create_time, pay_time, ship_time, complete_time订单状态日志表order_status_logid, order_id, from_status, to_status, operator_id, operator_role, remark, create_time在设计表时有几个细节值得说说第一金额字段用decimal(10,2)而不是float或double。浮点数在计算金额时会出现精度丢失——比如0.10.2可能等于0.30000000000000004。这在涉及钱的问题上绝对不可接受。第二商品图片不直接存在数据库。数据库里存的是图片URL数组用JSON格式真正的文件存在云存储或服务器本地。JSON字段虽然在这类设计里有些争议但对于“一个商品的图片数量不确定”这个场景它比单独建一张图片子表更简单直接。第三所有表都带create_time和update_time。MyBatis-Plus的自动填充功能可以在插入和更新时自动维护这两个字段省去手动设置。5.2 索引设计哪些字段必须加索引新手建索引最容易犯两个错误一是不建索引全表扫描二是盲目加索引导致写入变慢。我根据业务查询频率只对下面几个字段建立了索引表名索引字段理由userphone唯一索引登录时按手机号精确查询productuser_id查看“我发布的商品”productstatus, category_id商品列表页按状态和分类筛选productcreate_time按时间倒序排序ordersproduct_id, seller_id, buyer_id订单查询高频条件ordersorder_no唯一索引前端按订单号查询一个常见的陷阱是将LIKE %关键词%的查询也寄希望于索引。实际上前导通配符的模糊查询无法走正常索引只能全表扫描。这个在数据量达到百万级时才会明显但在设计阶段就要想清楚否则后期改起来很痛苦。5.3 Redis到底用来做什么这个项目里Redis主要承接三块职责验证码缓存注册和登录时生成图形验证码存Redis并设置5分钟过期。由于验证码有有效期用Redis的setex实现最合适。商品浏览计数用户每次打开商品详情product_id对应的浏览量加一。如果直接操作MySQL每次点击都是一次UPDATE高频打点会带来数据库压力。Redis的做法是在内存里累加再通过定时任务每5分钟同步一次到MySQL。热门商品排行榜用Redis的ZSet数据结构以商品被浏览、收藏、下单的次数加权计算热力值取前20名展示在首页“本周热门”区域。这个数据如果每次查询时临时计算性能不可接受用ZSet可以做到O(logN)的排序查询。需要特别提醒的是Redis不能作为强一致性的存储。比如订单状态这个关键数据绝对不能只存在Redis里。正确做法是数据库为准Redis仅作为缓存加速。一旦两者不一致以数据库版本为准并更新缓存。6. 权限控制与文件上传容易被忽略但决定体验的细节6.1 基于RBAC的权限模型系统里有三种角色普通用户、管理员、超级管理员。用户登录后后端通过PreAuthorize(hasRole(ADMIN))注解控制接口访问。普通用户只能操作自己的数据和浏览公开信息管理员可以审核商品、处理投诉超级管理员额外拥有用户封禁和系统设置权限。Spring Security的权限配置中有一个必须处理的点静态资源的放行。前后端分离模式下前端的静态资源放在Nginx上不需要经过后端。但后端的接口路径需要明确区分公开接口和需要认证的接口公开接口/api/auth/**登录注册、/api/product/list商品列表、/api/product/detail/**商品详情认证接口/api/order/**、/api/user/**、/api/favorite/**管理员接口/api/admin/**把所有接口的权限梳理清楚后在SecurityConfig里做配置。如果配错了最常见的表现是前端明明登录了但请求一直401。多数情况下是因为过滤器链的permitAll配置漏了某个前缀。6.2 文件上传本地存储 vs 云存储商品图片上传是另一个容易踩坑的地方。最开始我图省事直接把图片Base64编码后传给后端存数据库结果一张2MB的图片编码后接近2.7MB而且请求体过大导致Tomcat直接拒绝。后来改成MultipartFile流式上传。关于存储方式我做了一次对比本地存储把图片存到服务器指定目录Nginx映射出一个静态资源路径。优点是零成本、简单缺点是一台服务器的磁盘空间有限且如果应用部署在Docker容器里还需要挂载卷。云存储通过对象存储服务的SDK直接上传返回一个可访问的URL。优点是稳定、容量大、带宽快缺点是部分服务需要实名认证和个人开发者资质。考虑到这是校园项目最终采用的是本地存储 Nginx静态资源映射的方案。上传接口会把图片保存到/data/images/{yyyyMMdd}/目录下文件名用UUID避免冲突同时限制单个文件不超过5MB、只允许jpg/png/webp格式。上传接口我加了文件类型校验不能只依赖前端的accept属性。因为有人可以通过工具直接构造请求绕过前端限制。后端校验的方法很简单读取文件头部几个字节的魔数Magic Number判断实际文件类型而不是看扩展名。7. 部署实践与踩坑经验项目上线前必须知道的几件事7.1 从本地到服务器的部署全流程项目完成后在本地跑起来和部署到服务器上完全是两回事。这里分享一下我在部署过程中积累的完整步骤和一些实用经验。第一步云服务器环境准备。我用的是某云的2核4G配置装的是CentOS 7.9。Linux上的基础环境包括JDK 1.8、MySQL 8.0、Redis 6.x、Nginx 1.20。如果你第一次操作建议每个中间件的安装都用官方源不要下载第三方一键安装脚本。第二步数据库初始化和数据迁移。使用mysqldump导出本地数据库再通过source命令导入服务器。需要注意的是如果本地数据库版本高于服务器版本导出时可能需要在mysqldump加--set-gtid-purgedOFF参数否则导入会报错。第三步后端打包部署。在项目根目录执行mvn clean package -DskipTests打出一个可执行的Jar包。用java -jar启动前建议先设置JVM参数java -Xms256m -Xmx512m -jar campus-trade.jar --spring.profiles.activeprod第四步前端打包和Nginx配置。Vue项目执行npm run build后生成dist目录。将dist文件上传到服务器的/usr/share/nginx/html目录并配置Nginx反向代理后端接口server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }第五步启用HTTPS。项目上线前我给自己的应用配置了免费SSL证书这能避免浏览器提示“不安全”尤其涉及到登录密码的场景。配置完成后在Nginx里加443端口的监听块。7.2 部署过程中遇到的高频问题端口占用问题。有一次重启后端服务不管怎么启动都提示端口8080被占用。排查后发现是上次进程没有完全退出。可以用netstat -tlnp | grep 8080找到PID再kill -9杀掉。浏览器缓存问题。前端更新代码后用户可能看不到新页面。解决方案是在前端打包时给文件名加上hash。Vue CLI默认配置了[contenthash]如果没生效检查webpack配置。数据库连接被拒。部署到云服务器后应用连不上本地数据库这是最常见的坑。除了MySQL用户权限外还要检查云服务器安全组是否放行了3306端口。安全起见生产环境不建议把3306端口对外开放改为通过Spring Boot配置内网地址连接。跨域问题。前后端分离项目一定会遇到跨域。我的经验是生产环境用Nginx反向代理解决跨域开发环境用后端配置CorsFilter解决。不要在两边同时处理跨域否则浏览器会报几个Access-Control-Allow-Origin头冲突。7.3 日志与异常排查方法论上线后最重要的事是看日志。我把应用日志分成了两层一是Spring Boot框架日志logging.level.rootINFO二是业务操作日志用SLF4J记录关键操作。生产环境如果出问题第一件事就是打开日志文件排查。我的做法是用tail -f实时跟踪日志看报错堆栈用grep ERROR过滤异常记录如果问题无法立即定位复现一次操作后看日志输出有一个具体的排查案例用户反馈发布商品时偶尔会收到“系统繁忙”的提示。排查日志发现是图片上传时Nginx的client_max_body_size默认只有1MB请求超过大小限制直接被拒。修改为client_max_body_size 10m;后问题解决。这类问题如果不看日志光靠猜可能几天都找不到原因。8. 测试策略与性能优化别把“能跑”当成“能用”8.1 接口测试与单元测试的关键路径完整的系统不能只靠“启动后手动点一下”来验证。我的测试策略分三层第一层是单元测试用JUnit 5 Mockito。重点Mock掉数据库依赖只测试Service层的业务逻辑。比如交易状态机的流转、退款金额计算、信用分加减规则这些都是有明确输入输出的纯逻辑值得写单测。第二层是接口联调测试用Postman维护一套完整的接口测试集合。每个接口的请求参数、预期响应都固化下来前端改版后再跑一遍确保后端接口没被破坏。这套接口文档同时也能给前端同学参考。第三层是安全测试。至少要验证几个关键点未登录访问受保护接口返回401普通用户无法访问管理员接口商品编辑不能越权修改别人的商品输入框的XSS注入能否被过滤。8.2 性能优化页面速度从3秒到1秒以内的实践系统上线初期首页加载速度比较慢耗时接近3秒。我用了Chrome DevTools的Network面板分析后发现瓶颈主要有三个商品图片过大。用户上传的图片动辄2-3MB浏览器加载时非常耗时。解决方法是上传时用Java的Thumbnails库生成一张压缩后的缩略图宽度800px单独存储列表页和详情页默认加载缩略图点击才加载原图。数据库查询未加缓存。首页的商品列表每次请求都去查数据库即使数据没有变化。解决方法是把热门商品列表缓存到Rediskey为hot:products有效期10分钟后台商品审核通过时主动删除缓存实现缓存刷新。前端接口请求过多。首页一次要请求登录态、公告、热门商品、最新商品等多个接口串行请求导致耗时叠加。解决方法是后端新增一个聚合接口/api/home一次性返回首页需要的全部数据。这也就是BFFBackend For Frontend模式的雏形单体项目做起来很简单就是多写一个Controller。优化后首页加载时间降低到了800ms左右测试环境下600ms。这个优化过程教会我一个道理项目性能问题要先量化再定位最后针对性优化。不要一听“页面慢”就盲猜解决方案。9. 回顾与经验总结如果重新做一次我会改进什么整个项目从设计、开发到部署上线前后花了大概两个半月。做完之后回头看有些决策是正确的也有几个地方如果重来一次我会做得更好。做得对的地方首先是坚持了前后端分离让开发进度快了很多其次是设计了完整的订单状态机和状态日志后期排查问题非常方便再就是统一了接口返回格式和异常处理前后端联调几乎没有因为“响应格式不一致”吵架。如果重新做一次会改进的地方第一我会在上传图片时增加文件内容安全检测如鉴黄接口当时因资质问题跳过了。第二会在初期就引入接口文档管理工具如Swagger/Knife4j而不是让前端同学对着Postman找参数。第三会把管理员后台从Web端扩展到移动端适配因为审核员更习惯用手机审批。如果你也准备开发类似的系统我建议记住这三个原则先把业务流程画清楚再写代码关键操作一定要留痕不要追求技术栈的新奇稳定可靠才是第一位。最后再分享一个实际体会做这种综合性项目最大的收获不是学会了Spring Boot的某个API而是学会了怎么把一个大问题拆解成一个个小模块再逐个攻破。这份拆解能力是任何技术框架都给不了你的。