这题我见得多了。早几年带学生做毕设或者帮朋友公司搭内部系统十有七八会拿“二手交易平台”这个题材但你真把他们的代码拉下来看八CD是能注册能发商品、点“立即购买”就弹个“功能开发中”的半成品。为什么因为大家把精力都花在了写接口和堆页面却很少有人认真想过一件事二手数码交易平台真正的难点根本不是CRUD而是交易闭环。我写这篇东西就是拿一套基于SpringBootVue的二手数码产品交易平台做拆解对象从需求拆解、技术选型、表结构设计、核心交易流程到前端交互细节、开发踩坑、上线部署把整个开发链路里真正值得琢磨的环节过一遍。文章里所有涉及代码和配置的地方都是参考真实项目中能跑通的做法不是网上一搜一大把的demo。适合准备做毕设的学生、想练全栈项目的初级开发者以及打算接外包小项目但对业务设计没什么底气的朋友看。1. 先想明白“二手数码”和“交易平台”这两个词的分量1.1 为什么这个题目比“图书管理系统”难一个量级图书管理系统的本质是单机信息管理需求边界极其清晰图书的增删改查、借阅、归还、逾期罚款满打满算三张表。二手数码交易平台不一样它表面上也是商品管理加订单管理但“二手”和“交易”这四个字把系统的复杂程度直接拉高了。先说“二手”。一件二手数码产品不是一个简单的SKU它有成色、购买年限、是否有磕碰、配件是否齐全、发票还在不在这些非标信息直接影响商品的定价策略。而“交易”意味着买家、卖家、平台三方角色要同时在线一个订单生命周期里要处理支付、发货、确认收货、取消、超时、退款等一堆状态还要面临一个经典问题同一件二手商品只能卖给一个人你不能像卖T恤那样让库存随便减。如果把需求边界画清楚这个项目至少要覆盖以下模块用户体系注册登录、个人资料、收货地址、买家订单、卖家订单商品体系发布、审核、上架、下架、搜索、筛选、收藏、浏览记录订单体系下单、支付、发货、确认收货、取消订单、超时关闭后台管理用户管理、商品审核、分类管理、举报处理、数据统计所以想把这种项目做好重点不是代码量而是要提前把业务流程梳理清楚再动键盘。1.2 用户角色与权限矩阵别把“卖家”做成一套独立系统很多新手第一次建模时喜欢把“买家”和“卖家”拆成两种不同的用户表这是平台类项目一个绕弯路的典型错误。现实情况是在二手交易平台里所有注册用户既可以是买家也可以是卖家。一个人今天买个二手手机明天可能把自己手头的旧本子挂上去卖。所以系统里只有一个用户表通过角色字段区分“普通用户”和“管理员”通过业务上是否有在售商品来判断“这个用户是否是卖家”这就可以了。权限矩阵大概这样功能模块浏览者未登录普通用户买家/卖家管理员浏览商品列表/详情支持支持支持发布商品不支持支持支持购买/下单/支付不支持支持支持收货地址管理不支持支持支持商品管理审核/下架不支持不支持支持用户管理封禁/解禁不支持不支持支持举报投诉处理不支持支持支持做成平台还要考虑体验上的细节发布商品前是否需要手机号绑定实名认证要不要做这块在毕设或小项目里可以简化但至少要在数据库设计阶段把字段预留出来否则后期想加也加不进去。1.3 二手数码特有的业务规则需求文档里必须写清针对“数码产品”这个垂直品类系统里要加一些通用商城没有的字段。比如“成色”的概念全新、99新、95新、9成新不同成色对价格影响极大。每一件商品还可能要填序列号、保修截止日期、入手渠道、瑕疵描述。再比如交易方式二手数码的交易不只是线上邮寄还有大量“同城自提”的场景这就需要商品表里有“交易方式”字段订单流程也要区分“快递发货”和“自提核销”。这些规则不是说技术实现有多难而是产品层面的决策。如果你今天开始做这个项目第一步不是建SpringBoot工程而是把这些问题用文字定下来商品发布要填哪些字段什么样的商品不能上架买家下单后多久不支付自动关闭卖家发货期限几天确认收货后多久自动完成这些问题看着琐碎但它们就是平台的“玩法”代码只是把玩法落地。2. SpringBootVue之外还需要哪些技术配套2.1 技术栈选型不是越新越好匹配JDK和部署环境才是第一原则我现在带项目SpringBoot这边默认选2.7.x JDK8的组合除非用户明确要求用3.x JDK17。原因很现实SpringBoot 3.0之后最低要求JDK17而很多学校机房、公司服务器、老项目的生产环境用的还是JDK8依赖兼容性也天差地别。二级缓存、验证码、拦截器这些常规操作在2.7里资料比3.x多得多踩坑好查。前端这一侧Vue选Vue 3 Vite PiniaUI框架选Ant Design Vue或者Element Plus两个都可以。如果你已经有Vue 2 Element UI的经验转过来的学习曲线其实不长。这个技术栈在“最新网络热词”里出现频率很高但真要落地你会发现安装依赖、配置Vite代理、处理路由守卫哪一步都有新的坑。SpringBoot侧边的常见配套——整合Quartz定时任务处理超时未支付订单、整合JWT做无状态登录、整合Swagger做接口文档、配置FastJson或Jackson做参数序列化。我的习惯是尽量“少而精”能靠SpringBoot原生配置解决的不额外引依赖非要引的就选维护活跃、用的人多的。2.2 MySQL是主力Redis是加速器但别为用而用项目的基础存储一定是MySQL。用户、商品、订单这些核心数据必须落在本地表里保证事务。Redis在这个项目里属于加分项常见用途有三个存登录token和验证码保证无状态认证的时效性缓存商品详情减少数据库压力做接口幂等防止用户在超时未支付订单里“重复下单”如果你还没学Redis也可以不引入只是你要在商品详情接口里做一层本地缓存用Caffeine或者Guava都行。重点是别为了在简历上写“缓存”两个字就把系统复杂度陡然抬高。每个技术选型都应该对应一个具体的业务痛点否则就是过度设计。2.3 JWT鉴权配合过滤器拦截比Session更贴合前后端分离过去用Session做登录态需要后端保存会话对跨域和移动端都不算友好。现在做前后端分离的SpringBoot项目主流方案是JWT流程是用户登录成功后端生成一个签名Token返回前端拿到后存进localStorage每次请求在header里带上Authorization: Bearer Token后端过滤器统一解析校验。用JWT要避开几个坑密钥要放在配置文件里别硬编码、过期时间建议设置为2小时起步、登出时前端要主动清除token如果涉及封禁用户还需要在Redis里维护一个黑名单因为JWT是无状态的服务端没法主动把它“作废”。这些点看起来小却是很多面试官喜欢追问的地方。3. 后端从零到一核心模块和代码设计思路3.1 数据库七张核心表提前想好字段后面少改一百次接口先列个核心表的清单表名功能说明关键字段user用户id, username, password, nickname, avatar, phone, role, statusproduct商品id, seller_id, category_id, title, description, detail, price, original_price, condition, images, status, view_countproduct_image商品图片id, product_id, image_url, sortcategory分类id, name, parent_id, sortaddress收货地址id, user_id, receiver, phone, province, city, district, detail, is_defaultorders订单id, order_no, product_id, seller_id, buyer_id, total_amount, pay_amount, status, pay_time, deliver_time, confirm_time, address_snapshotmessage留言/举报id, user_id, order_id, content, type, status几个容易忽略的字段写出来product表里必须冗余seller_name列表页展示“卖家昵称”时少一次联表查询orders表里必须记录地址快照因为买家的收货地址可能在下单后被修改或删除历史订单不能跟着变订单金额一律用“分”存储用Integer或BigDecimal别用浮点型浮点计算在支付场景会出大事product.status的枚举值要提前定义好0待审核、1在售、2已售、3下架、4驳回。状态机是整个交易闭环的核心后面马上展开3.2 商品发布与状态机一次状态错乱整个平台就“感觉不对劲”先说发布商品的后端流程。用户从前端提交商品表单后端接收后要做几件事校验用户是否登录、校验分类是否存在、校验价格和成色的合法性然后插入product记录和product_image记录初始状态设为0待审核同时把分类id和seller_id建立索引。商品管理最大的坑在于状态机的切换。平台的商品状态流转是待审核(0) - 在售(1)管理员审核通过 待审核(0) - 驳回(4)管理员审核不通过 在售(1) - 已售(2)买家下单并支付成功 在售(1) - 下架(3)卖家手动下架 在售(1) - 下架(3)管理员强制下架违规/被举报 下架(3) - 在售(1)卖家重新上架状态转换代码里最忌讳的是直接对数据库字段执行“更新成某个状态”而不管之前是什么状态。比如买家下单时如果商品是“审核中”或“已成交”状态就不能让它继续下单。正确做法是写一个更新语句用update product set status #{newStatus} where id #{productId} and status #{expectStatus}这样并发场景下才会安全这个写法本质是乐观锁。3.3 订单交易闭环从下单到确认收货每一步都要防并发订单流程我建议做成这样下单买家点击购买后端先校验商品存在且处于“在售”状态再校验卖家id不能等于买家id二手交易平台一般不允许自买自卖。然后生成订单号插入orders表再把商品状态从在售改为“锁定”或直接置为“已售”。两种做法都可以但无论锁商品还是改状态要放在同一个事务里。支付真实项目可以接入支付宝/微信支付但单机毕设项目一般用模拟支付。下单后生成一个支付二维码或者一个“模拟支付”按钮点击调用支付接口后端校验订单存在且状态是“待支付”再更新订单状态为“已支付”同时更新商品状态为“已售”。发货与确认收货卖家操作发货更新发货时间和物流单号买家点击确认收货更新确认时间订单状态变为“已完成”。这套流程要特别注意两个并发隐患。第一个是重复下单两个买家同时点击购买同一件二手产品如果不做条件更新就会出现超卖。解决方法是下单前使用select ... for update锁住商品行或者使用上面提到的乐观锁方式更新商品状态只有更新成功才继续创建订单。第二个是支付幂等支付回调或模拟支付请求可能被前端连续触发后端要先判断当前订单状态是否为“待支付”不是就直接返回。订单状态机可以用一张表列出来状态触发动作下一步可流转状态待支付(0)买家下单已支付(1)、已取消(4)、超时关闭(5)已支付(1)买家模拟支付已发货(2)、已取消卖家关闭已发货(2)卖家发货已完成(3)、售后处理中(6)已完成(3)买家确认收货终态已取消(4)买/卖任意一方取消终态超时关闭(5)定时任务扫描终态3.4 超时未支付自动关闭Quartz定时任务与状态判断线上下单之后买家迟迟不付款系统得有个兜底机制。我采用的方案是集成Quartz每1分钟扫描一次订单表把“创建时间超过30分钟且状态为待支付”的订单批量置为“超时关闭”同时把商品状态改回“在售”。不少同学会在这一步踩坑比如定时任务把已支付的订单也关了或者扫描全表导致慢查询。正确做法是查询条件加上状态字段和创建时间范围生产环境再加一个分页扫描避免一次扫太多。另外定时任务执行时要加分布式锁避免多实例部署时重复执行单机部署就无所谓了。这些看似底层的东西做毕设时写成“如何解决订单超时自动关闭”的亮点答辩时非常加分。3.5 事务失效这个坑我在这个项目里也踩了很多同学以为事务是加了Transactional就完事了但在SpringBoot项目里至少有三种情况会让事务失效同类内部调用this.method()事务切面拦截不到不会开启新事务方法被try-catch吞掉了异常事务感知不到需要回滚方法不是public修饰的Spring的代理机制默认不拦截我实际写这个平台时在下单方法里为了统一返回结构加了一个try-catch结果事务没回滚商品状态变了但订单没生成。查了半天才发现是事务失效。这个问题在我平常带的项目反馈里出现率极高专门写出来提醒一下。解决方案是把事务边界放在最外层Controller调用的Service方法上异常不要吞掉统一抛出去由全局异常处理器处理。4. Vue前端开发中的关键交互不只有数据渲染4.1 工程化初始化装依赖、配代理、跑通前后端联调前端用Vite创建项目之后第一件正事就是配置开发环境的代理解决跨域。你可以在vite.config.js里写export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/user/login就会代理到后端http://localhost:8080/api/user/login开发环境不会有跨域问题。注意后端接口统一以/api开头这在前后端分离项目中是一个不成文的规范避免以后生产环境加Nginx反向代理时路径混淆。Vue项目的目录结构建议按功能模块划分views放页面components放复用组件api放封装好的接口请求router放路由表store放Pinia状态。Axios封装要统一配置baseURL、请求拦截器、响应拦截器响应结构如果是标准的{code, message, data}在拦截器里判断code 200再返回data否则统一弹错误提示页面代码会清爽很多。4.2 路由与权限控制守卫不是摆设token过期要有处理路由是Vue项目里非常容易出乱子的地方。我见过很多项目前端只配置了页面路径完全没有考虑“未登录能不能访问个人中心”这种问题。配合JWT登录态前端要做两件事第一路由表里通过meta标记哪些页面需要登录哪些是公开页面。比如首页是公开的个人中心、发布商品、订单管理需要登录。第二在router.beforeEach导航守卫里做判断const whiteList [/login, /register, /] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (token) { next() } else if (whiteList.includes(to.path)) { next() } else { next(/login?redirect to.fullPath) } })还有一点很容易漏后端返回401时说明token过期或用户被封禁前端Axios响应拦截器里要统一处理比如清除本地token跳回登录页并提示重新登录。这个逻辑如果只在某个页面里处理其他页面就会出现“看起来登录了但一操作就报错”的诡异状态。4.3 商品列表、搜索筛选与详情页到底该怎么设计交互商品列表页是平台的流量入口交互上至少要支持搜索关键词、选择分类、按成色筛选、价格区间筛选、按价格/发布时间排序、分页加载。接口设计成GET /api/product/list?keyword手机categoryId1condition99新minPrice1000maxPrice3000sortprice_ascpage1pageSize12我实际开发中有一个体会前端做筛选时不要每次点击都重新请求。比如用户输入了关键词一般要做300毫秒防抖用户切换分类或价格区间才立即请求。筛选条件用一个响应式对象保存统一拼成query避免散落一堆变量后面接入URL同步或分享功能也容易。商品详情页除了展示多图轮播和商品信息还有一个关键交互是“联系卖家”和“购买”。二手场景里很多买家习惯先和卖家聊几句再拍下但这个项目如果做成即时聊天后端要上WebSocket复杂度会飙升。我的建议是做成“留言”或“一键复制卖家联系方式”的简化方案既满足业务又不至于失控。4.4 发布商品页多图上传、预览、压缩一个都不能少发布商品页是二手平台里最考验细节的页面。用户要填标题、描述、成色、价格还要上传多张商品照片。图片上传我用的Ant Design Vue的a-upload配合后端multipart/form-data接口。要处理的坑至少有三个大小限制手机拍的图动不动两三兆后端配置再大也扛不住前端要在上传前做压缩可以用Canvas或compressorjs统一压到1MB以内再传上传进度a-upload自带进度条但跨域或接口异常时要有错误提示回显编辑商品时要能回显已上传的图片所以图片列表存的是URL前端回填时直接把URL数组切给上传组件后端接收图片要注意配置spring.servlet.multipart.max-file-size10MB和max-request-size50MB这是热词里“springboot如何上传下载大文件”最常出现的一个问题。同时要对上传后的图片做访问路径映射把上传目录映射成静态资源URL否则前端拿到的是本地磁盘路径根本没法访问。5. 我在实际开发中踩过的坑以及解决方案5.1 跨域三连坑CORS配置、代理、路径拼接跨域问题是我每次带项目都必然会遇到的一关。开发环境下你通过Vite代理请求后端一般没问题但还有三个场景容易出幺蛾子。第一个是前端直接请求后端IP而不是走代理比如微信小程序、H5或Swagger页面调试就会触发CORS。解决方案是在后端配置一个全局CORS过滤器Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }第二个是Nginx部署时路径拼接出错。比如前端打包后放在/dist目录后端接口走/apiNginx配置要正确处理location /和location /api/的位置否则页面白屏或接口404这问题在线上环境排查起来特别费劲。第三个是Vue路由使用history模式刷新页面时Nginx配置不写try_files $uri $uri/ /index.html;就会直接404。这个坑每个用history模式的人都会遇到提前防好。5.2 上传文件大小超限别只看SpringBoot的multipart配置上传图片时后端报MaxUploadSizeExceededException或者前端跑一半失败最开始大家都会想到改application.yml里的multipart配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB但如果后端用的是Tomcat还有一层maxSwallowSize和maxPostSize限制。尤其SpringBoot 2.x使用的Tomcat默认maxSwallowSize只有2MB即使你配置了multipart限制如果请求过大Tomcat本身也可能直接拒绝连接。解决方法是自定义TomcatServletWebServerFactory把maxSwallowSize调大。前端能压缩、能切片处理的话尽量在上传前解决而不是无脑改后端限制。5.3 时区问题、字符集问题和打包体积问题MySQL连接串如果不配置serverTimezoneAsia/Shanghai连数据库时会报时区错误。如果后端连字符集也没配中文商
