
1. 项目背景与需求定位1.1 这个项目到底解决什么问题做这个零售与仓储管理系统之前我最直观的感受是很多小型零售企业还在用Excel管库存甚至靠手写账本。仓库里进了什么货、卖了什么、还剩多少全依赖人工登记。一旦商品种类超过几百个或者门店和仓库不在同一个地方这种模式的混乱程度会直线上升。库存对不上账、补货不及时、月底盘点费时费力这些都是常态。所以我当时做这个项目的核心目标很明确用一套系统打通零售前端和仓储后端让进货、入库、出库、销售、盘点这几条核心链路在一个系统里运转起来。Spring Boot作为项目底座不仅仅是图它开发效率高更重要的是它的生态足够成熟后续扩展权限管理、对接支付、生成报表等能力都很顺手。1.2 适合谁参考这套设计如果你是刚学完Java基础想找一个完整的项目练手这个系统的复杂度恰到好处。它不像那种纯CRUD的图书管理系统那么简单也不像分布式电商那么复杂模块划分、权限控制、库存逻辑都有足够的深度可以挖掘。如果你是在小公司做内部管理系统这套方案的可复制度很高。零售加仓储的组合非常常见核心模型拎出来改改可以直接迁移到其他行业的管理系统上。我做这套系统时有一个很深的体会项目的价值不在于用了多新的技术而在于业务逻辑是否梳理得清晰数据是否准确可靠。这一点这个项目体现得很充分。2. 整体架构与技术选型思路2.1 为什么选Spring Boot而不是其他框架技术选型这一步我先说说自己的考量。之前用SSHStrutsSpringHibernate做过类似项目配置文件多到让人崩溃每加一个类都要去XML里注册。Spring Boot最大的优势就是自动配置和约定优于配置省掉了大量繁琐的装配工作。开发阶段用内嵌的Tomcat跑起来打包成可执行Jar丢到服务器上就能运行部署成本非常低。Java这个语言选得也比较稳。零售仓储系统涉及资金和货物稳定性和事务能力是第一位的。Java在这方面的积累很深JDBC、JPA、MyBatis这些数据访问层方案都非常成熟遇到复杂查询或者事务问题社区里能搜到大量现成的解决方案。2.2 核心依赖与分层结构我用的技术栈是Spring Boot 2.x MyBatis Plus MySQL Redis。Spring Boot负责整体框架和请求分发MyBatis Plus负责数据访问MySQL存业务数据Redis存登录会话和缓存热数据。分层结构上我保持了经典的四层架构Controller层接收前端请求Service层写核心业务逻辑Mapper层做数据库交互实体类映射表结构。这里要强调一点很多初学者容易把业务逻辑写在Controller里图省事。但一旦业务复杂起来Controller膨胀得飞快后期几乎没法维护。我的习惯是Controller只做参数接收和结果包装所有判断、计算、异常处理都下沉到Service层。2.3 为什么用RBAC模型做权限管理权限管理我采用了基于角色的访问控制模型RBAC就是给用户分配角色给角色分配权限。超级管理员、仓库管理员、销售员、店长这几种角色覆盖了大部分业务场景。每个用户登录后系统根据其角色返回可见菜单和可操作的接口。这个设计看起来简单实际操作时有两个很关键的点。第一个是权限粒度我控制到了按钮级别哪些角色能看到“删除商品”按钮哪些角色只能看“编辑”都在前端根据权限标识进行控制。第二个是后端接口必须再做一次校验不能光靠前端隐藏按钮。我曾见过一些项目前端把按钮藏得很严实结果有人直接调接口就绕过了限制这个问题务必避免。3. 数据库设计与核心功能模块切分3.1 数据表规划的核心逻辑数据库设计是整个系统最重要的一环。我总结一个原则宁可多拆几张表不要把所有字段塞进一张大表。合理的表结构能让后续的统计和扩展变得很顺畅。我设计的主要数据表包括用户表、角色表、权限表、商品分类表、商品信息表、供应商表、仓库表、库存表、入库单表、出库单表、销售订单表、盘点记录表。每个表的主键用自增ID所有金额字段统一用Decimal类型避免浮点数精度问题。以商品表为例字段不止包括商品名称、条码、规格、单位这些基础信息还专门加了预警阈值字段。这个预警阈值是库存管理的关键当库存数量低于这个值时系统自动在首页生成补货提醒。有些系统把预警写死在代码里不同商品用一个阈值这其实不太符合实际情况。比如矿泉水可能剩100瓶还很充裕但一台昂贵的设备剩1台就得补货。阈值放在商品表里按商品单独配置这个设计我建议直接抄走。3.2 库存字段到底放哪张表库存的设计上我踩过一个小坑。最开始我把库存数量直接放在商品表里查询是方便了但每次出入库都要更新商品表而且没法追溯历史库存变化。后来我改成了单独的库存表按仓库加商品维度存数量即每条记录对应“某商品在某仓库的库存量”。这样设计的好处有三个。一是多仓库场景下库存数据天然隔离每个仓库各算各的二是盘点时只需要锁定库存表不需要动商品主数据三是后续如果要按仓库维度统计库存金额直接查库存表关联商品单价即可。入库明细表和出库明细表则把每一次操作的数量、单价、经办人、时间都记录下来形成完整的溯源链路。3.3 功能模块的边界划分整个系统我拆成了六个核心模块商品管理、供应商管理、仓库管理、入库管理、出库管理、销售管理。外加系统管理模块负责用户和权限配置统计报表模块负责数据汇总。商品管理管的是商品档案包括新增商品、编辑信息、上下架、条码管理。供应商管理维护供货商档案和合作记录。仓库管理管的是仓库基本信息包括库位编码。入库管理覆盖采购入库、退货入库、其它入库这几类场景。出库管理覆盖销售出库、报损出库、调拨出库等。这里我特别想说的是销售管理和出库管理的关系。有些系统把销售订单和出库流程完全分开导致数据不一致。我的做法是销售订单创建后自动生成一条对应的销售出库单库存扣减以出库单为准确保销售数据和库存数据的联动。这一块逻辑看着简单实际编码时要处理并发问题后面我会详细讲。4. 核心业务流程与关键实现4.1 入库流程的状态流转入库流程的完整链路是创建入库单 → 审核入库单 → 更新库存 → 记录入库明细。第一步仓库人员根据采购单或到货通知创建入库单填写供应商、入库仓库、商品明细和数量。这里有一个容易忽略的地方入库单的编号一定要系统自动生成采用日期加流水号的格式。比如RK20250615001可读性强后期对账、客服查询都会方便很多。第二步是审核。我设置了一个原则创建人和审核人不能是同一人。入库单保存后处于待审核状态审核通过后才会真正影响库存。这个设计是为了防止操作失误被直接写入库存给复核留出空间。第三步和第四步是同步执行的。审核通过的事务里先按“仓库加商品”维度去库存表查是否存在记录。存在则增加数量不存在则先新增一条库存记录再累加。同时把每一件商品的入库明细写入入库明细表。4.2 库存扣减的并发控制方案库存超卖是仓储系统最敏感的问题。假设某商品库存只剩10件同时来了两个销售订单一单要8件一单要5件如果不做控制最终可能卖出去13件库存变成负数。我用的方案是数据库悲观锁配合事务隔离。执行库存扣减的SQL语句时用SELECT * FROM stock WHERE product_id ? AND warehouse_id ? FOR UPDATE先把这一行锁住然后再检查库存是否充足执行扣减。这样同一时刻只有一个事务能操作这条库存记录彻底避免超卖。这里分享一个性能层面的小经验。悲观锁在并发量不是极大的场景下完全够用零售仓储系统的并发量一般不会像秒杀那么夸张。如果后续量大了可以改用Redis的分布式锁或者乐观锁版本号机制。但双十一那种级别的并发这个架构本身就不合适需要引入消息队列做异步削峰那就是另一套设计了。4.3 销售与出库的联动实现用户在前台下单后系统会做这几步操作校验库存是否充足、生成销售订单、生成出库单、扣减库存。这四个步骤我放在同一个事务里。一旦扣减库存时发现异常整个事务回滚销售订单也不会生效。有一个实际问题需要防范用户下单后没有付款订单处于待付款状态这个时候就扣库存用户又不买了库存就被白白锁死。我需要区分普通电商和零售门店场景。如果是门店收银基本上是现场付款直接扣库存问题不大。但如果系统将来要支持线上预订建议把扣减时间延后到支付成功之后或者引入预占库存和释放库存的机制。4.4 盘点模块的盈亏处理盘点这块我觉得最能体现一个系统是否真正贴合业务场景。实际盘点时业务人员先录入账面数量再录入实盘数量。系统自动计算盈亏数量。如果实盘数大于账面数说明盘盈需要做盘盈入库。如果实盘数小于账面数说明盘亏需要做盘亏出库。这一进一出都要生成对应的出入库单据并且记录盈亏原因。我在系统里预设了几种常见原因扫码遗漏、货物损坏、供应商少发、录入错误。这样财务复核时能直接看到原因归类省去很多沟通成本。盘点单还有一个状态进行中、已提交、已审核。已经审核的盘点单不可修改确实是复盘后确认的数据。这个状态机的控制保证了盘点流程的严肃性。5. 实操记录与关键配置明细5.1 环境准备与项目初始化项目开发环境我用的JDK 1.8Spring Boot 2.5版本。虽然JDK 17和Spring Boot 3.0都已经很成熟但考虑到很多生产环境的服务器还在用JDK 8而且相关生态的兼容性问题少我最终选了相对保守的版本组合。用Spring Initializr初始化项目时我勾选了Web、MySQL Driver、MyBatis、Redis这几个依赖。数据库方面本地先用MySQL 5.7后续生产切换到MySQL 8.0连接驱动升级一下即可代码改动很小。字符集一定要设utf8mb4否则存emoji或者特殊符号容易报错。5.2 核心配置文件示例application.yml里的几个关键配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/retail_storage?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有几个细节值得展开说。map-underscore-to-camel-case设为true后数据库的create_time字段能自动映射到实体的createTime属性省去手动写映射规则的麻烦。逻辑删除配置也很有用我没用物理删除所有业务表都加了deleted字段。用户误删商品档案时实际上数据还在只是查询时被过滤掉了随时可以恢复。5.3 统一返回结果与全局异常处理这个部分强烈建议所有Spring Boot项目都做。我定义了一个通用的Result类包含code、message、data三个字段。所有Controller接口都返回这个类前端可以根据code判断请求是否成功。状态码我用的是20000表示成功50000表示业务异常40100表示未登录或登录过期。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(20000); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(50000); result.setMessage(message); return result; } }全局异常处理用RestControllerAdvice注解实现。业务异常、参数校验异常、数据库异常分别处理统一包装成Result对象返回。这样还有一个深层好处前端拿到统一格式的响应体后可以封装一套通用的请求拦截逻辑遇到40100自动跳登录页遇到50000统一弹出提示。不用每个页面各写一套错误处理逻辑。5.4 登录验证与JWT令牌用户登录后我用JWTJSON Web Token生成登录令牌存到Redis里同时设置过期时间。每次请求经过拦截器时从请求头取出令牌并校验。使用JWT加Redis的组合既保留了JWT无状态的特点又能通过Redis手动踢人下线。令牌的有效期设置要结合业务场景来考虑。零售门店的人员流动性大如果令牌一个月不过期员工离职后账号仍然有效会带来安全隐患。我设置的是8小时过期配合Redis里存的会话记录超时后自动重新登录。门店收银员一天登录一次体验上也没有问题。5.5 前端页面的技术选型前端部分用了Thymeleaf加Bootstrap没做前后端分离。为什么这么选因为这套系统的使用人群是门店店员和仓库管理员他们需要的质检场景是登录后用浏览器操作页面不追求炫酷效果只追求简单好用加载速度快。如果要做前后端分离我建议后端提供纯JSON接口配合Vue或React做管理后台。但这会引入跨域处理、Token存储、构建部署等一系列额外复杂度。对于这个项目规模服务端渲染的方案在开发效率和维护便利性上反而更有优势。6. 常见问题与排查技巧实录6.1 数据库连接池连接超时系统运行一段时间后偶尔会报Communications link failure错误。排查后发现是数据库连接的闲置时间超过了MySQL的wait_timeout。解决办法是在配置文件中设置连接池参数让连接池定时检查并回收失效连接。spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 max-lifetime: 1800000 connection-test-query: SELECT 1这里max-lifetime设置成30分钟要比MySQL的wait_timeout短一些确保连接被提前回收不至于被数据库服务端断开后继续使用。6.2 精度问题导致金额对不上有同事反馈某个订单的总金额差了一分钱。最后定位到是商品单价用了double类型计算产生浮点数误差。这个问题的教训很直接涉及金额的计算绝不能用double或float。后来我把所有金额字段统一改成Decimal代码里用BigDecimal计算再也没出现过这种问题。BigDecimal计算时要注意构造方法尽量用字符串参数比如new BigDecimal(19.90)不要直接用new BigDecimal(19.90)。后者由于二进制浮点数表示问题得到的值往往不是精确的19.9。初学的时候容易在这里踩坑。6.3 MyBatis Plus的字段填充失效使用MyBatis Plus的自动填充功能时我在实体类的createTime字段上加了TableField(fill FieldFill.INSERT)注解但新增数据时字段值还是null。排查发现是MetaObjectHandler的实现类没有交给Spring管理缺了Component注解。这个实现类里我重写了insertFill和updateFill两个方法分别填充创建时间和更新时间。这类问题通常在配置层面代码编译不报错运行时才暴露排查起来需要细心对照官方文档。6.4 并发测试中出现的库存负数系统上线前做压测并发开30个线程同时购买同一件商品日志里出现了库存负数的情况。这说明单纯在Service层判断库存是否充足是挡不住并发的。多个线程同时读到库存为10同时判断10大于购买数量全部通过然后一起扣减库存就变成负数了。我改用数据库行级锁之后同一商品同一仓库的库存行同一时刻只有一个事务能操作后面的线程必须等待前一个事务提交后才能读到最新库存。再加上扣减的SQL语句里带上条件stock quantity双重保障彻底解决了这个问题。6.5 高频查询的Redis缓存策略商品列表、分类信息这类数据变化频率低、读取频率高非常适合放到Redis里缓存。我的做法是添加或修改商品时主动删除相关缓存查询时先查缓存缓存不存在时再查数据库并回填缓存。这里要重点提醒一句缓存一定要设置过期时间不能是永久key。不设置过期时间的缓存一旦忘记主动清理数据不一致的问题会长期存在。时间设短了效果差设长了数据旧一般我会根据业务对实时性的要求调整商品列表设30分钟登录会话设8小时是比较常规的配置。6.6 常见问题速查表问题现象可能原因解决方案项目启动失败端口被占用8080端口被其它进程占用使用netstat命令查找进程并停止或修改配置端口点击登录无反应登录接口报500SQL语句有误查看后端日志定位具体报错行导入商品Excel乱码文件编码不是UTF-8读取时指定字符集前端上传时做编码处理库存列表查询慢商品表数据量大未加索引在商品条码、分类ID字段上建立索引报表数据与明细对不上统计SQL存在重复关联先用子查询去重再关联汇总表7. 部署上线与数据备份经验系统部署我用了最直接的方式Maven打包出Jar文件扔到服务器上运行。生产环境我用的是CentOS系统执行nohup java -jar retail-storage.jar app.log 21 日志输出到文件方便排查问题。关于日志级别我建议生产环境用info级别开发环境用debug级别。debug日志记录的信息量很大如果生产环境开启磁盘很容易被刷爆。同时要用logback的按天滚动策略保留最近30天的日志避免日志文件无限增长。数据库备份这件事我吃过亏。项目上线初期某天误操作批量修改了商品数据由于没有备份最后只能手工逐条恢复花了好几个小时。后来我写了一个shell脚本每天凌晨两点用mysqldump全量备份备份文件保留7天再配合Binlog做增量恢复。每隔两周手动演练一次恢复流程确保备份真的能恢复而不是备份了个寂寞。8. 项目复盘与个人实操心得整个系统从设计到开发再到上线加起来用了大约六周时间。现在回头复盘这个项目的价值主要有三层。第一层是业务建模。零售仓储虽然没有特别深奥的算法但业务流程的梳理很考验逻辑能力。入库、出库、盘点、销售之间环环相扣的状态流转一开始我想得简单画完时序图才发现各种边界情况都要处理比如审核驳回后库存如何处理、盘点期间能不能继续出入库等等。第二层是数据一致性的把握。库存、金额、订单这三类数据必须保证绝对准确任何一个环节出错后续的对账和财务都会很痛苦。事务边界、并发控制、唯一约束这些基本功在这个项目里体现得淋漓尽致。第三层是对用户体验的认识。这个系统的使用者年龄跨度大很多员工对计算机操作并不熟练。所以我做页面时尽量遵循了几个原则一个页面只做一件事操作按钮文字直白比如“新增商品”“确认出库”不要用“执行”这种模糊词汇。重要的操作有二次确认弹窗比如删除商品、盘点审核。每一步操作后有明确的成功或失败提示。这些看似琐碎的设计反而让系统在实际使用中推广得很顺利。最后再分享一个小技巧多岗位角色协同使用系统时操作日志的作用远比想象中重要。我在所有关键业务表上都加了创建人、创建时间、更新人、更新时间字段并且单独建了一张操作日志表记录谁在什么时间做了什么操作。上线后有一次盘点差异排查就是靠操作日志发现有人在非工作时段做过一次入库单修改才找到数据对不上的原因。这个设计不需要太高深的技术但对运维和审计非常有价值。如果你准备复刻这个项目强烈建议把这个细节加进去。