大半年前我接手了一个“游泳用品专卖店系统”的活儿需求不算复杂但五脏俱全——有商品展示、购物车、下单流程还要有后台管理、库存扣减、订单状态流转这些基本功。技术栈很明确Spring Boot做后端。我一开始以为这也就是个普通的CRUD项目真正动手才发现泳具类目本身的差异化需求比如尺码、近视度数、款式分类加上库存和订单的联动才是最容易翻车的地方。这篇文章我打算把整个设计和实现过程完整拆开来讲包括我踩过的坑和最后采用的方案给同样在做商城类Spring Boot项目的朋友一个参考。1. 需求边界定下来之后我才开始动数据库这个项目的核心业务就是“游泳用品专卖店”听起来很小但实际上商品种类并不算少泳衣泳裤按尺码和性别分、泳镜按近视度数和成人/儿童分、泳帽按材质和松紧程度分、脚蹼和浮板又按规格型号分。如果一开始就把表结构设计成“一张商品表一堆字段堆上去”后面做筛选、做库存都会非常痛苦。我拿到需求后在纸上先把角色画了出来C端用户注册会员、浏览商品、管理购物车、下单、查看订单、后台管理员商品管理、类目管理、订单处理、会员信息查看。明确好这两个角色之后功能边界就很清楚了系统不需要做分销、不需要做评论审核、不需要做优惠券引擎那在数据库设计阶段就可以砍掉这些表。1.1 商品类目与商品详情的拆分思路游泳用品这块我最后是这么拆的分类表一级分类泳装、泳镜、泳帽、配件二级分类比如泳装下面再分男士泳裤、女士泳衣、儿童泳衣。商品表公共属性比如名称、主图、简介、所属二级分类、上下架状态。商品参数表专门存差异化的规格参数比如泳镜有“度数可选”泳帽有“头围范围”。这里用灵活键值对而不是每个参数建一个字段。商品SKU表真正决定价格和库存的层级一条SKU对应一个具体的规格组合例如“男士泳裤-黑色-2XL”。这样拆的好处非常直接商品表保持干净分类下新增品类时不需要改表结构。如果做泳镜这种有度数属性的商品也只需要在参数表里加一条而不是动数据库字段。1.2 订单与库存表的设计防线订单相关我设计了四张核心表订单主表、订单明细表、库存流水表、购物车表。这里特别想说一下库存流水表很多小项目根本没有这张表结果订单状态一乱库存对不上账查都没法查。我在设计订单和库存的时候定了一条硬性规则任何库存变化都必须落一条流水记录包括下单占用、支付确认扣减、取消释放、后台手动调整。这样一旦出现“系统显示有货但就是发不了货”的诡异问题可以直接查流水定位而不是靠猜。购物车表我关联了用户ID和SKU ID数量字段做了默认值1和上限校验。这个表看起来简单实际上最容易被忽略的是“加入购物车时是否需要锁定价格”我的选择是不锁定购物车只存SKU引用真正下单时再读一遍当前SKU价格并写入订单明细表快照。这个考量是基于电商常识购物车阶段价格变化是常态订单阶段才必须固化价格。2. 核心模块的后端实现不只做CRUD要想清楚状态流转Spring Boot给这类MVC项目提供了一套很成熟的架子——Controller层接收请求、Service层写业务、Mapper层操作数据库。但真正写起来业务复杂不在增删改查而在流程状态的管理。2.1 注册登录模块与密码处理用户注册登录我用的是Spring Security结合JWT。很多人一听到Spring Security就觉得重实际上对于单体项目来说配一次就够了不会多出多少复杂度。密码存储方面现在谁还明文存密码那就是给黑客送人头。我用BCrypt加密注册的时候加密入库登录的时候匹配。Spring Security的BCryptPasswordEncoder直接就能用不需要自己实现加密算法。注册的时候我还加了两个校验一是手机号格式合法性二是邮箱格式合法性两者至少填一个。这个设计是因为游泳用品的消费者里有一部分是企业团购或俱乐部批量采购他们不一定愿意绑定手机号邮箱渠道有时候也是刚需。JWT这块我用一个拦截器统一解析请求头里的Token把用户ID注入到上下文。登录接口和公开接口做一个白名单配置其他接口一律要求登录。这个拦截器比在每个Controller里加判断要省事得多代码也干净。2.2 商品列表与筛选接口的坑商品列表接口是我觉得最像“实际项目”的地方因为它不只是一个简单的SELECT * WHERE status 1。用户在C端看到的应该是已上架商品而且支持按二级分类筛选后台管理员看到的则是全部商品包括已下架和库存为零的。关键点在于C端和B端的商品查询最好分开写两个接口而不是一个接口加个参数硬切换。原因很简单两个场景的字段权限不一样C端不需要看到成本价、库存预警值这些敏感字段如果硬塞进同一个VO里返回一坨null字段非常膈应。列表接口还需要支持分页。我用的是MyBatis-Plus的Page对象传入pageNum和pageSize返回总条数和当前页数据。筛选条件我用了一个QueryDTO来接收里面包含分类ID、关键字、价格区间、排序字段。排序我固定做白名单只允许price_asc、price_desc、sales_desc、create_time_desc这几种不然容易被SQL注入或者搞出不存在的排序字段。2.3 购物车与下单业务串联购物车的接口比较简单增删改查但有一件事必须做在加入购物车时检查SKU是否存在、是否上架、库存是否大于0。这不仅仅是体验问题更是防止脏数据把订单流程带崩。下单是整个项目中业务逻辑最密集的操作我用一个事务方法包起来步骤是读取购物车中勾选的条目校验每一个SKU的状态和库存按当前价格生成订单主表和订单明细表记录占用库存——这里我把SKU表的库存字段扣减掉同时插入一条“占用”类型的库存流水清空已下单的购物车条目。这一步我特意留意了事务边界。用户在创建订单之后、支付确认之前库存其实是被预占的。如果什么都不做库存会一直挂着所以需要一个超时释放机制。我这个系统用了最简单的方案数据库定时任务每隔10分钟扫描一次超过30分钟未支付的订单把SKU库存加回去插入“超时释放”流水并把订单状态改为已关闭。2.4 订单状态的几个关键流转订单状态我设计成五个待支付、待发货、已发货、已完成、已取消。流转规则我用状态机的方式固化下来待支付 - 支付成功 - 待发货待支付 - 30分钟超时 - 已取消待发货 - 管理员发货 - 已发货已发货 - 用户确认收货 - 已完成待支付 - 用户主动取消 - 已取消。为什么强调状态机因为如果直接在前端随便改状态后台一塌糊涂。我这边的做法是在Service层写一个changeOrderStatus方法里面用一个Map或者条件判断来校验当前状态和目标状态是否合法非法流转直接抛业务异常。3. 表结构设计的关键字段与取舍说明写代码之前建表的细节值得多说一嘴因为很多新手在项目验收的时候被数据库设计卡住。下面是我实际建表的核心字段清单和理由。3.1 数据库字段规范我统一用create_time和update_time作为公共时间字段同时所有关键表都加了deleted逻辑删除字段默认0。这里有个小建议逻辑删除是做安全备份的手段但不能用它代替唯一索引。比如商品SKU的sku_code唯一索引是硬性约束谁也不能重复和逻辑删除无关。3.2 游泳用品特有属性的存储方案我刚才提到用参数的键值对存泳镜度数、泳帽头围这些属性。具体实现是这样我有一张product_param表结构是id, product_id, param_name, param_value。比如某款泳镜产品ID是1001那它下面会有三行记录度数-2.00、度数-2.50、度数-3.00等等。页面渲染的时候按照param_name分组展示非常灵活。对应的SKU表结构是id, product_id, sku_name, sku_code, price, stock, status, spec_json。其中spec_json存具体规格的ID组合比如泳镜产品下某条SKU的spec_json存“近视-2.00, 黑色, 标准款”。这个概念直接用JSON字符串存储省去规范化的复杂关联查询时解析效率也够用。3.3 表关系带来的启发整体表关系并不复杂分类表1对多商品表、商品表1对多SKU表、商品表1对多参数表、订单主表1对多订单明细表、用户表1对多订单表、SKU表1对多库存流水表。梳理下来最忌讳的是在业务表里无序增加冗余字段。比如订单明细表必须冗余商品名、商品主图、SKU规格文本这些下单时快照数据但绝不能在订单明细表里去关联商品表实时取商品名。4. Controller层与API响应格式的约定写接口多了之后我养成了一套固定习惯所有接口统一返回一个ResultT对象包含code、message、data三个字段。这样前端解析逻辑可以统一不管成功还是失败HTTP状态码一律200业务态用code区分。我这里简要定义一下public class ResultT { private Integer code; // 200成功400参数错误401未登录500系统异常 private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT fail(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }全局异常处理用RestControllerAdvice包一层业务异常类BizException向下继承RuntimeException。这样代码里任何地方直接throw new BizException(库存不足)最终响应都是{code:400,message:库存不足}非常干净。4.1 API路径设计与命名习惯我按照REST风格拆了几组接口路径商品GET /api/product/pageGET /api/product/{id}POST /api/admin/productPUT /api/admin/product/{id}购物车GET /api/cart/listPOST /api/cart/addPOST /api/cart/removePOST /api/cart/update订单POST /api/order/createGET /api/order/listGET /api/order/{id}POST /api/order/pay/{id}后台管理POST /api/admin/order/shipPUT /api/admin/sku/stock。实际上手时你会发现REST风格并不是死的。比如购物车的移除用DELETE /api/cart/{id}更符合语义但前端同学如果用习惯了POST /api/cart/remove也问题不大。关键是整个团队约定统一别一个项目里两种风格混着来。4.2 参数校验与防刷每个Controller的请求参数我都用javax.validation的注解校验NotNull、Min、Max、Size在DTO字段上直接标。加一个Valid就搞定了。防刷这件事在这个项目里不太重毕竟不是高并发秒杀系统。但为了防止有人写脚本扫接口我在几个写操作接口上加了一个简单的IP频率限制内存里存一个ConcurrentHashMapString, LocalDateTime即可。这种方式只能对付最低级的脚本真要高强度防护还得上Redis加分布式限流这里就不展开了。5. 从单体应用角度总结部署上线的几个注意事项Spring Boot项目最终是要打成JAR包部署的。这里有一些我试过的部署配置和遇到的问题值得单独说一段。5.1 多环境配置我建了application-dev.yml、application-prod.yml和主配置文件application.yml。主配置里只放通用内容环境差异全部按不同文件区分。启动命令用java -jar app.jar --spring.profiles.activeprod来切环境。生产环境数据库连接串、账号密码绝不写死在配置文件里用环境变量注入。比如在启动脚本里先export DB_HOSTxxx配置里写jdbc:mysql://${DB_HOST}:3306/swim_shop。这样即使代码泄露数据库也不是裸奔状态。5.2 静态资源和文件上传商品图片上传我用了本地磁盘路径存储然后Nginx做静态资源映射数据库里只存图片的相对路径。这个方案适合中小项目。如果你图省事把图片直接存数据库的BLOB字段查询列表时性能会很差而且备份数据库体积会爆炸。图片上传接口还要做大小和类型校验图片最大5MB、只允许jpg/png/webp不然用户传个恶意脚本伪装成图片文件虽然不一定直接造成危害但总归是个风险点。5.3 日志的重要性系统运行中日志我认为是必须写好的。我在生产环境配置了Logback按天生成日志文件保留30天。关键业务节点下单、支付、发货、取消必须打印业务日志包含订单号和操作人。订单一出问题看日志是最快的排查路径。如果日志里看不出完整链路真出了问题就只靠猜了这对运维来说是不可接受的。6. 复盘这个项目里我学到的几件“书上不写的事”项目收尾之后我回头看了看代码和设计记录有一件事体会很深Spring Boot本身只是一套快速脚手架真正决定项目成败的是业务建模和边界划分。具体到游泳用品专卖店这个场景如果一开始没想清楚SKU和商品参数的关系后面做泳镜度数筛选、泳帽头围筛选的时候就会各种别扭如果没设计好库存流水表上线第一个月就会遇到“账实不符”的扯皮问题如果订单状态机不固定前后端各写各的最后倒霉的还是用户。6.1 关于事务和并发的一个实用补充库存扣减我用了数据库行锁来避免超卖核心SQL类似于UPDATE product_sku SET stock stock - #{count} WHERE id #{skuId} AND stock #{count}这条语句靠stock #{count}条件保证不会扣成负数加行锁保证并发安全。如果更新影响行数为0直接抛“库存不足”异常。这种方案在高并发下依然有性能瓶颈但对专卖店这种量级完全够用。6.2 给后来者的几个建议第一先画状态流转图再写代码哪怕用纸画都行。第二所有时间字段尽量用数据库的CURRENT_TIMESTAMP维护少在代码里手工new Date()。第三接口返回数据用Map或者VO封装不要直接返回Entity免得把敏感字段泄露出去。我在做这个项目的时候踩过一个印象深刻的坑刚开始偷懒直接返回了User实体结果序列化后把密码的Hash也带出去了虽然前端没显示但抓包能看到就是个安全隐患。后来统一加了JsonIgnore敏感字段又改用VO层返回才彻底解决这个问题。这套系统最后顺利上线跑了两个多月没有出现严重线上事故。如果让我概括这个项目的关键词大概就是“稳”和“清晰”。Spring Boot提供的生态确实让单体应用的开发效率提升了一大截但作为开发者脑子里始终要有一根弦框架只是工具业务逻辑和数据结构设计才是真正需要反复推敲的地方。最后说一个小细节这个项目的启动类继承Spring Boot标配的main方法用IDEA的插件可以直接打包成Docker镜像配合一键部署脚本整个发布过程也就两三分钟。很多人忽略的其实是这一层——从能跑的代码到能稳定交付的代码中间还隔着不少运维功夫。希望这篇实战复盘能帮到你。不管是毕设还是小公司的实际需求游泳用品专卖店这个系统类型都是练手的好题材你把它做完做扎实电商类项目的核心思路基本就摸透了。