ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue3二手手机商城系统:从表设计到库存并发实战

SpringBoot+Vue3二手手机商城系统:从表设计到库存并发实战

1. 项目整体设计与技术选型

1.1 这个项目要解决什么问题

做过二手手机生意或者研究过电子数码产品回收的同学应该深有体会,二手手机行业跟普通电商不太一样。一台手机从用户手里回收进来,要经历质检、定级、翻新、入库、上架、出单、售后这一长串流程,而且每台机器都有唯一的串号(IMEI)、成色等级、维修记录。传统的一张文book或者Excel管理方式根本撑不住,尤其是当库存量级到了几百上千台的时候,经常出现“线下已经卖了,线上平台还挂着货”这种严重超卖的情况。

这个项目就是典型的电商交易系统,但业务重心跟普通卖衣服、卖鞋的商城有本质区别:它把“二手手机”当作核心业务对象,围绕商品的在库状态、上下架状态、订单履约状态做精细化管理。用SpringBoot+Vue3+MyBatis这一套在当前国内Java就业市场最主流的技术栈,配合MySQL做数据持久化,实现了从商品录入、库存管理、用户下单到后台订单处理的全链路闭环。

以“华强北商城”这个场景命名,其实很有代表性——华强北是国内二手数码产品最大的集散地,那边的档口老板们对系统的核心诉求就是:库存要绝对准确、订单履约要快、商品的复用到各个销售渠道要方便。所以这个项目虽然是教学性质的源码工程,但它的模块划分和业务逻辑是往真实产业场景靠的,不是那种糊弄人的CRUD Demo。

1.2 技术栈选型的理由

先看后端,SpringBoot是目前Java后端绝对的主流框架,自带内嵌Tomcat,不用打WAR包扔到外置容器里,本地开发调试极其方便。我在实际做这类系统的时候,会优先用SpringBoot 2.7.x或者3.x版本,配合JDK 8或17,稳定性和生态环境都是经过大量生产环境验证过的。MyBatis作为持久层框架,它的优势在于SQL由开发者完全掌控,二手手机这类业务有各种复杂的多表关联查询,比如同一款iPhone在不同商家手里的报价差异、某台机器的维修记录,用MyBatis的动态SQL可以写得很灵活,而且调优的时候直接改SQL就行,不需要像JPA那样去理解它生成的隐式查询。

前端选择Vue3+Element Plus,这个组合现在几乎是中小型管理系统的标配。Vue3的组合式API(Composition API)让代码组织比Vue2的Options API清晰太多,尤其是像订单列表这种需要同时处理筛选条件、分页、批量操作、弹窗详情多个逻辑的场景,用setup函数组织业务逻辑比用data/methods堆叠要舒服得多。Element Plus的表格、表单、弹窗、消息提示组件足够成熟,做后台管理界面效率非常高。

再说前后端分离这个架构。分离的意义不在于“显得高级”,而在于职责边界清晰:后端只需要提供一组RESTful API处理数据逻辑,前端负责页面渲染和交互。这样带来的直接好处是,如果哪一天想加一个微信小程序端,后端API几乎不用动,小程序直接复用同一套接口;如果未来需要把订单处理功能拆出来给仓库同事用,只需要定制一个独立前端页面即可,后端依然稳定服务。在真实的工作场景里,这个灵活性节省的时间成本是非常可观的。

1.3 适合谁学习和参考

这个项目的定位非常精准:它是为正在做毕业设计或者找Java后端实习/初级岗位的人准备的。通过阅读这套源码,你可以把SpringBoot的MVC分层架构、MyBatis的Mapper编写、Vue3组件通信、Axios请求封装、JWT权限认证这些知识点串成一条完整的业务链路。哪怕是零基础,只要能看懂Java基础语法和简单的SQL,跟着这套源码走一遍,也能大概明白一个真实电商系统是怎么从零到一搭建起来的。

对于已经有一定工作经验、想转行做二手数码交易方向的人,这套系统的表设计和业务逻辑也非常有参考价值,尤其是商品表、订单表的字段设计,值得深入琢磨。

2. 数据库表设计:二手手机业务的核心地基

2.1 核心表结构与设计思路

先来看数据库设计,这是整个系统的地基。表设计得合不合理,直接决定了后续业务的扩展空间和查询性能。这套系统围绕二手手机交易场景,核心表大致以下几个:用户表、商品表、订单表、订单详情表、分类表、轮播图表、购物车表、收货地址表这些常规电商表,外加库存操作记录表。

重点说商品表(我用product来代称)。二手手机的SKU(库存量单位)属性跟普通商品有差异,它天然带一些固定的成色字段。一张合理的手机商品表至少要有:唯一编号、商品名称、品牌、型号、成色等级(99新/95新/9成新等)、电池健康度、存储容量、颜色、入网型号、IMEI串号、质检报告编号、采购价、销售价、库存状态、是否上架、对应的分类ID。其中IMEI串号必须做唯一索引,这是二手手机区别于普通商品的关键字段。

订单表的设计也有讲究。订单主表记录订单编号、用户ID、订单状态、总金额、实付金额、支付方式、创建时间等通用字段。订单明细表关联具体商品ID、下单时的商品价格、数量、当时的成色描述快照。我见过很多新手在设计订单表时会犯一个错误:直接只存商品ID,等到查看历史订单时发现商品早已下架或被修改,导致订单详情页显示不出来原始购买时的商品信息。所以下单时的商品关键信息一定要冗余一份到订单明细表里,这是电商系统的通用实践。

2.2 库存状态机:二手系统里最不能含糊的一环

二手手机商城跟普通电商还不太一样,单独说到库存状态。新手机的库存管理一般是“有多少货就上多少”,但二手手机存在“在库、待质检、已售出、退货中、异常下架”多种状态。如果表设计上只用一个整数表示库存数量,那是完全不能用的。

推荐的做法是设计一张库存流转记录表,类似于财务流水账。每次发生商品入库、出库、锁定、解锁、售出都插入一条记录,记录操作前数量、操作后数量、操作类型、操作人、时间戳、关联订单号。这样当出现售后纠纷,比如客户说收到的手机成色跟描述不符时,你才能追溯这台机器每一手流转的记录,能拿出来自查:这台机器是从哪个供应商手里收的,当时质检的结论是什么,是谁审的,什么时候卖出去的。

在具体实现时,商品主表可以保留一个current_quantity字段作为当前剩余量,但不要在主表上直接做加减操作,而是利用UPDATE语句的原子性。比如用户下单时执行UPDATE product SET current_quantity = current_quantity - 1 WHERE id = ? AND current_quantity > 0,然后判断受影响行数。这个操作配合库存流水表,可以做到在高并发下比较好的防超卖效果。关于并发细节,后面在常见问题章节我会展开讲。

2.3 索引设计与SQL优化方向

索引这块,虽然项目规模不至于撞到性能瓶颈,但设计习惯要正确。首先,所有表的主键统一用bigint自增ID,不要用UUID当主键。为什么?因为MySQL的InnoDB存储引擎是聚簇索引结构,主键是物理排序的依据,UUID是随机的,插入时会导致页分裂,性能会很差,而且二级索引占用的存储空间也更大。对于这套系统,登录用户表(user表)可以对手机号或用户名做唯一索引,商品表对品牌、型号、成色组合索引,订单表对用户ID和订单状态建组合索引,这是查询频率最高的几个路径。

查询优化上的一个核心技巧是关于分页的。当商品数量很多、后台管理员翻页翻到很后面时,传统的LIMIT 10000, 20性能会急剧下降,因为MySQL要扫描前面10000条再丢弃。我在这类系统的Mapper里通常会优化为WHERE id > #{lastId} ORDER BY id LIMIT 20的方式,或者利用延迟关联把主表的分页扫描成本降低。这套源码里如果用的是普通LIMIT,你实操时也可以自己试着优化,那是很好的练手点。

3. 核心功能拆解与接口设计

3.1 用户端功能模块

从用户视角看,系统需要提供一套完整的购物流程:注册登录、浏览商品、搜索筛选、查看详情、加入购物车、提交订单、在线支付(模拟)、查看订单状态。开发这个模块时,首先要梳理接口清单,把用户可能执行的操作切成一个个细粒度的API。比如商品模块,至少要有:获取商品分类列表、按分类获取商品列表、按关键词搜索商品、获取商品详情。订单模块,至少要有:创建订单、获取当前用户订单列表、获取订单详情、取消订单、确认收货。

登录认证是用户端的重点。项目采用JWT(JSON Web Token)的方式做无状态认证——用户登录成功后,后端签发一个包含用户ID和过期时间的Token,前端把Token存到浏览器的localStorage里,之后每个请求在请求头(通常是Authorization: Bearer <token>)中带上它。后端用拦截器或Spring Security配置对需要登录的路径做校验,校验通过才能访问受保护的资源。我在真实项目中经常调整的一个细节是Token过期时间,课堂作业里设个24小时无所谓,真实环境建议短一点(比如2小时),配合一个刷新Token机制,这样即使Token失窃,影响的也只是一个短期会话。

3.2 管理端功能模块

管理端是这套系统中业务价值最高的部分。管理员登录后,可以管理商品分类、上下架商品、编辑商品信息、处理订单发货、查看销售统计。就实际开发流程来说,管理端的开发顺序一般是:先做商品分类管理,再做商品管理,然后才是订单处理。

商品管理列表相对复杂,它需要支持多条件筛选:按照品牌筛选、成色筛选、价格区间筛选、库存状态筛选、上下架状态筛选,同时支持分页展示。在Vue3前端实现时,筛选条件会绑定到一个查询参数对象上,每次点击查询或重置都要重新向后端发起请求;为了更好的体验,还可以增加“防抖搜索”,即用户在搜索框输入关键词停顿500毫秒后自动发起请求,避免每敲一个字就请求一次接口带来压力。

后台的订单处理要特别注意业务规则。比如发货操作,点击发货后,订单状态从“待发货”变成“已发货”,同时要更新商品的库存状态,并且生成一条物流记录。系统里我用一张order_shipping表记录物流公司和快递单号。前端呢,因为Element Plus的表格自带操作列,每一行数据后渲染“发货”“查看详情”“退款处理”等功能按钮,不同状态显示不同操作,这很考验Vue的响应式处理能力。

3.3 角色权限设计

权限模型不需要做得特别复杂。这套系统简单分为两个角色:普通用户和管理员。在用户表里用一个role字段区分。用户端接口和管理端接口分别有独立的Controller,在Controller类或方法上标注权限校验。

真实的权限框架方案是Spring Security + JWT,自定义OncePerRequestFilter,在过滤器里解析Token,然后加载用户的角色信息并设定到SecurityContext中。遇到一个细节:前端Vue侧需要用路由守卫做页面权限拦截。后台管理界面的路由放在一组meta: { requiresAdmin: true }的配置里,全局前置守卫中检查用户角色不是管理员就跳转到登录页或者403页面。这样做是必要的,因为前端路由隐藏了不等于后端接口安全,后端拦截才是真正讲安全的地方。

3.4 购物车与订单流程实现

购物车应该说是用户端体验比较细的模块了。购物车项表cart_item关联用户ID和商品ID,冗余了商品的售价快照、图片、名称。用户在购物车页面能改数量、单选/多选、删除和清空。实际开发中的一条经验是,加入购物车时如果对应用户已经有同款商品的购物车记录,可以直接把数量加一,避免出现重复行,否则页面上会出现两行一模一样的商品,体验很差。

生成订单这步颇有讲究。前端点击提交订单后,先把勾选的购物车项ID列表传到后端,后端做两件事:一是校验所有商品都是在架可售状态,二是计算订单总金额,注意使用数据库中的价格而不是信任前端传来的价格。完成订单主表插入和订单明细表批量插入后,再批量删除这批购物车项。整个操作需要放在一个数据库事务里,任何一步失败都要回滚,否则就会出现“订单下成功了但购物车项没删”或者“扣了库存但订单没创建”的数据不一致问题。

4. 前后端分离联调:从接口到页面的协作细节

4.1 后端接口规范与统一响应结构

前后端分离项目最常见的坑,就是接口约定不一致导致返工。这种项目连调的过程中,一上来就要约定统一的接口返回格式。我在这个系统里用的是最通用的Rest风格响应结构:包含状态码code、消息提示message、业务数据data三个字段。成功时code=200,失败时code=500或者业务异常码。不要小看这个约定,一旦接口各写各的,前端需要写一堆兼容代码去判断不同结构,调试起来非常痛苦。

后端还有一处全局处理值得参考:使用@RestControllerAdvice做全局异常拦截。Controller里只管正常业务逻辑,异常处理交给统一类处理,分别处理业务异常、参数校验异常、未知异常,这样前端收到的错误消息永远是清晰的中文描述,而不会抛出一大段堆栈信息。

以统一响应的方式配合前端封装的Axios实例使用,前端代码里放上一个响应拦截器,检查响应状态码如果不是200,直接弹出错误提示并中断后续处理逻辑。就我的经验,这一步值得花时间设计,系统做出来的使用体验,会跟那些没做统一封装的项目拉开明显差距。

4.2 Vue3前端架构与API层封装

前端项目基于Vite构建,目录结构大概是这样的:src/api放所有请求接口的封装模块、src/views放页面组件、src/router放路由配置、src/store用Pinia管理用户信息之类等全局状态。

其中一个对初学者来说容易卡壳的点是Axios的请求封装。推荐的做法是,在src/utils/request.js里创建一个Axios实例,设置baseURL和超时时间,然后在请求拦截器里从某个模块取出Token放进请求头,在响应拦截器里统一处理401未登录的情况。之后每个业务模块的API文件只需要导出具体的方法函数,比如export function getProductList(params){ return request.get('/product/list', { params }) }。以后页面里调用这个方法,语义清晰,也不用每写一次请求就写一遍缓存Token、处理错误状态的重复代码。

Vue3的组件通信方面,这套系统也会用到。管理端的商品编辑弹窗,父组件要把当前行的商品数据传到子组件弹窗里,子组件内部再加载可选的分类列表。Props、Emit这两种基础通信手段要熟练掌握。稍微复杂的跨组件状态就用Pinia作为全局Store,比如把用户信息放在里面,这样不同页面、路由跳转了之后信息都不丢。

4.3 跨域与代理配置

前后端分离绕不开跨域问题。开发环境下最简单的方案是使用Vite的Proxy代理:前端页面跑在5173端口,后端接口跑在8080端口,浏览器地址是5173,请求接口却指向8080,跨域。在vite.config.js里配置server.proxy,把以/api开头的请求转发到http://localhost:8080,这样就绕过了浏览器的同源策略限制。

生产环境下,通常是直接把后端打成的JAR包部署在一个Nginx或者服务器上,或者用Nginx做反向代理:一个location /api/把请求转发到后端服务,一个location /指向前端打包后的静态文件目录。配置的时候注意路径重写,不要把/api前缀传给后端,如果后端Controller实际映射是/product/list,那代理配置里需要加proxy_set_header和rewrite。我会在部署文档里专门写清楚这条,因为很多人第一次部署Java+Vue项目就是挂在这里。

5. 踩坑实录与问题排查

5.1 MyBatis多表关联映射与数据库字段映射问题

第一个高频坑是实体类属性和数据库字段驼峰映射问题。数据库里我们习惯用下划线命名,比如product_name,Java代码里习惯用驼峰productName。如果没有在MyBatis配置文件里开启mapUnderscoreToCamelCase=true,你查出来会发现所有字段都是null。这是非常经典的低级错误,排查方式其实很简单:看控制台打印的SQL语句,然后再看返回结果,就能定位。

第二个坑是1+N查询问题。比如查询订单列表,每条订单都要查出对应的用户昵称和商品快照。很多初学的人顺手就写了循环里一条条去查,列表20条订单就是21条SQL,性能非常差。遇到这种情况,我用MyBatis的collection标签配置一对多映射,或者直接在SQL里使用JOIN语句查出扁平结果,再用分组逻辑组装。要学会看控制台SQL日志,并养成看SQL执行数量的习惯。

5.2 验证JWT登录态与前端路由守卫配合

前端路由守卫和后端JWT认证,是新手特别容易绕晕的一对东西。这里把清晰的职责梳理出来:后端过滤器负责“检查Token是否合法、用户是否存在、是否过期”;前端路由守卫负责“页面级别的跳转控制”,比如未登录用户访问个人信息页直接重定向到登录页,普通用户访问后台管理页直接重定向到首页。这两个是不同层面的保护,而且前端守卫只是改善体验,不能指望它保护数据安全,数据安全要由后端保障。

在实际开发时,我还建议在路由配置里给需要登录的页面加meta: { requiresAuth: true },给管理员页加requiresAdmin: true,然后再写全局守卫判断。以后再加新的页面时,只需要配置meta属性,会比在守卫里写一大堆页面路径清单要清晰得多。

5.3 高并发场景下的库存超卖问题

二手手机存量往往没有新款那么多,但“秒杀”、“特价捡漏”活动也是常见促销套路,如果同一时刻大量用户下单同一台低价手机,数据库层的超卖问题必须考虑。最直接的方案,也是这套系统采用的思路:使用数据库的行级锁配合条件更新。先执行UPDATE product SET stock = stock - 1 WHERE id = #{productId} AND stock > 0,返回受影响行数为1时,说明本次扣减成功;为0时说明库存不足,终止下单。这个操作本身是数据库原子的,不需要额外加分布式锁。

但是仅仅加上这个还不够。下单动作还关联了创建订单明细、生成物流单、清购物车等多步操作,因此这里必须使用@Transactional事务切面把它包起来。只要任何一步抛出运行时异常,整个事务回滚,之前的扣减也会撤销。注意这里要确认使用的是支持事务的存储引擎(InnoDB),并且@Transactional默认只对RuntimeException回滚,如果抛的是受检异常需要额外配置。我在实现时会把这个细节直接写进代码注释里面,因为遇到线上问题的时候,回滚这条命往往就是最后的救命稻草。

5.4 前端页面数据更新问题

Element Plus表格有时候明明改了数据,页面却没刷新,也是常见情况。本质问题多半在于响应式数据的引用关系,Vue3的reactive和ref有它自己的响应式转换规则。如果直接把后端返回的数组整体赋值,或者对数组里的某个对象直接改属性但没用解构、没用reactive包裹,就会出现页面不更新的现象。排查思路是,先在浏览器开发者工具Vue插件里确认数据有没有变——如果数据变了UI没变,那就是响应式丢的问题;如果数据本身就还是旧的,就检查代码里是不是赋值时机不对。实际操作中,遇到表格数据列表修改后,直接把新的响应式数组整体赋值给它,一次全量替换,通常立刻就能刷新,简单粗暴但有效。

我写这套系统源码时,最初的目标其实是尽量压缩“从懂技术到懂业务”的这个距离。跟着代码走一遍,你会发现你学到的不仅仅是Java和Vue的API用法,更重要的是电商系统里面各种状态怎么流转、数据怎么保证一致、前后端怎么约定协作。这些恰恰是工作中最值钱的部分。

以我个人的经验,最后再分享一个小技巧:拿到这套源码之后,第一步不要急着去跑起来,先把product表和order表的结构用工具画出来,在纸上推演一遍用户从注册下单到管理员发货的整个数据流转过程。你把这张图画清楚了,后面读代码的速度会快一倍,而且面试的时候,你能讲清楚业务逻辑和表设计背后的考虑,这比强调自己会多少框架要更能让人相信你的工程能力。希望这套系统能成为你进入Java实战的一个不错的起点。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进