1. 项目定位:一套零食进销存系统到底要管多少事
先说结论:这是一个基于SpringBoot构建的休闲食品线上进销存平台,核心要解决的是零食批发零售业务里“进货、库存、销售、订单、会员”五大环节的数据打通问题。很多人一听“零食销售管理系统”,第一反应是“做个商品列表加购物车,不就是个简易电商站吗”,实际拆开来看远不止这些。
你可以把这类系统理解成一个专门为零食生意做数字化改造的运营支撑底座。前台消费者看到的是商品展示、下单、支付、查询物流;后台老板和仓管关注的是采购入库、库存预警、批次保质期、成本利润核算;运营人员还要处理优惠券、积分、限时促销。任何一个环节的数据对不上账,整个平台就会陷入“系统显示有货、仓库却找不到货,或者客户下单了、仓库发不出货”的混乱局面。
这个项目适合两类人。一类是计算机相关专业、正在准备毕业设计的学生,想找一个既覆盖主流技术栈、又具备完整业务闭环的选题;另一类是已经入行但想快速理解电商进销存业务逻辑的开发者,想看看SpringBoot是怎么把这类业务落地的。我下面讲的思路、表设计和代码细节,都是按照“可以直接动手复现”的标准来写的,不是停留在概念层面的空谈。
2. 技术选型与架构取舍:为什么这套组合够用
2.1 SpringBoot的定位:单体应用的工程化红利
先明确一点,这套系统不需要分布式微服务。原因不复杂:业务规模决定了架构复杂度。一个零食销售平台,哪怕日均订单量冲到几千单,单体应用配合合理的缓存和数据库索引完全扛得住,更别说毕业设计场景下,演示和答辩才是第一诉求。
SpringBoot对这类项目最大的价值就是“约定优于配置”。以前用Spring MVC搭建项目,光配置文件就能写好几页,各种XML bean定义、数据源配置、事务管理器配置、视图解析器配置,新手光跑通一个Hello World就要折腾半天。SpringBoot直接把内嵌Tomcat、自动配置、起步依赖这些机制打包好,一个启动类整个项目就能跑起来。开发效率的提升是肉眼可见的,这对有交付周期的项目来说是决定性优势。
我在实际开发里的体会是:SpringBoot真正省时间的不是启动那一下,而是“生态贴合度”。Spring Security做鉴权、Spring Data Redis做缓存、Spring AOP做日志切面、Spring Validation做参数校验——这些东西全是同一个编程模型,学习成本低,出了问题排查路径也统一,不像在多个框架之间来回切换,光上下文就要反复横跳。
2.2 配套技术栈清单与选择理由
既然是SpringBoot项目,我建议的配套组合是这样的:
| 组件 | 选型 | 选择理由 |
|---|---|---|
| ORM框架 | MyBatis-Plus | 单表CRUD不用手写SQL,复杂多表查询仍然走XML,可控性强 |
| 数据库 | MySQL 8.0 | 稳定可靠,InnoDB支持事务,网上资料最多,踩坑容易查 |
| 缓存 | Redis | 存登录token、热点商品缓存、秒杀场景的库存预扣 |
| 前端框架 | Vue 3 + Element Plus | 前后端分离开发,Element Plus自带表格和表单组件,后台管理页面搭建快 |
| 鉴权方案 | Spring Security + JWT | 无状态鉴权,适合前后端分离架构,比Session方案更适合REST接口 |
| 接口文档 | Knife4j | 自动生成Swagger文档,联调和答辩演示都方便 |
这套组合最大的优势是:覆盖面广但不偏门,任何一个环节遇到问题,都能在网上搜到成熟的解决方案。对毕业设计来说,这是很关键的隐形价值——你不需要在冷门框架上花时间试错,把省下来的精力投入到业务逻辑和项目亮点上,性价比最高。
2.3 别为了炫技而过度设计
我见过不少同学把技术栈堆得异常华丽:注册中心上Nacos、分布式事务上Seata、消息队列上Kafka,一个零食销售系统搞得像大厂核心交易平台。这里我劝你冷静一下,除非你有明确的业务场景支撑(比如用户量确实大到需要异步削峰),否则不要为了简历好看而引入分布式组件。
理由有两条。第一,答辩时老师更关注你对基础原理的理解深度,而不是组件清单的长度。你能把一个乐观锁扣库存讲清楚,比“我用过Seata做分布式事务但说不清AT模式原理”有价值得多。第二,分布式会让事务问题变成灾难——一个零食订单的创建和库存扣减,在单体应用里就是一个本地事务的事,拆成微服务后就得引入补偿机制,节点一多连演示都容易翻车。说到底,架构是为业务服务的,不是用来展示的。
3. 核心业务模块设计:从进货到出货的完整闭环
3.1 进销存三件套是怎么串起来的
进销存,字面上就是进货、销售、库存,但这三件事不是三个孤立的模块,而是环环相扣的数据流。
进货环节对应采购模块:运营人员创建采购单,选择供应商,录入采购的零食品类、数量、进价;入库审核通过后,系统需要保存供应商信息、采购明细,并同步增加对应SKU的库存量,同时写一条库存流水。销售环节对应订单模块:用户下单、支付成功后,系统扣减库存,生成出库记录。库存模块则像一个中央账本,每一次进出都要留痕。
这套逻辑里最容易出错的地方是“中间状态”。比如采购单已经创建但还没审核,库存该不该变?用户下单但还没支付,库存能不能扣?我的建议是:采购单在审核通过前不改变库存,订单在支付成功前不扣减库存,所有库存变动必须对应一条带有业务单号和时间戳的库存流水记录。这样一来,任何时间点都能回答“当前库存是多少、为什么是这个数”这两个问题,账面不一致的时候照流水反查就行。
3.2 商品管理的SPU/SKU设计
零食这个品类做商品管理有个天然难点:同一个商品有多种规格和口味。比如一款薯片,有原味和香辣味,有40克小包和105克大包,价格和库存都不同。如果你在商品表里每条记录混着存“名称+规格+口味+价格+库存”,后面做订单、做统计都会非常痛苦。
正确的做法是拆成SPU和SKU两层。SPU(Standard Product Unit)是商品聚合层,一张表存商品名称、主图、品牌、分类、描述这些不变的信息;SKU(Stock Keeping Unit)是具体售卖单元,一张表存规格、口味、单价、库存、预警阈值这些会变化的信息。用户在商品详情页看到的是SPU,选中某个规格后加入购物车、下单时使用的则是SKU。
这个设计有几个实际好处:售货架上展示商品列表时不会重复;做多规格选择器时前端逻辑清晰;统计销售额时可以按SKU粒度分析哪些口味卖得好。更关键的是,库存永远挂在SKU上,而不是SPU上,这是后续避免发错货、算错账的基础。
3.3 订单与支付状态的流转设计
订单是整个系统的核心聚合根。一个订单里至少包含:订单基本信息(订单号、用户ID、总金额、收货地址、订单状态)、订单明细(SKU、数量、单价、小计)、支付信息(支付方式、支付单号、支付时间)。
订单状态我建议用枚举常量管理,避免在业务代码里写死数字:
public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"), REFUNDING(5, "退款中"), REFUNDED(6, "已退款"); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code = code; this.desc = desc; } public Integer getCode() { return code; } public String getDesc() { return desc; } }这里有一个非常重要的实战经验:状态流转必须走“状态机校验”,而不是直接setStatus。比如一个“已发货”的订单不允许被取消,一个“已完成”的订单不能再进入“退款中”。我在项目里是写了一个状态校验工具类,在每次修改订单状态前先判断当前状态是否允许跳转到目标状态,不允许就抛出业务异常。这样虽然多写几行代码,但能挡住大量脏数据。
3.4 会员积分与营销活动的落地
零食属于高频低价消费品,复购是生意的核心,所以会员和营销模块不能只用“有”来敷衍。积分业务相对简单:用户消费金额按比例累计积分,积分可以在结算时抵现金。优惠券则需要处理创建、领取、使用、过期四个环节,使用时间在SQL层面做校验,防止前端绕过限制。
我做这个项目时在营销功能上加了两个有展示价值的点:一是“满减活动”按订单金额自动匹配最优活动,比如满99减10、满199减30,系统自动取优惠力度最大的方案;二是“限时秒杀”用Redis做库存预扣,把热点SKU的库存提前加载到缓存里,秒杀下单时先扣Redis再异步落库。这两个点既贴合业务实际,又能体现你对高并发场景的思考,答辩时是很好的加分项。不过要提醒一句:秒杀功能做演示的时候,记得先在后台准备好活动商品,别等到现场才发现Redis里的库存没有加载。
4. 数据库设计:核心表结构与字段解析
4.1 表设计总览
数据库设计是进销存系统的地基,我建议按业务域拆分成以下几组表:
| 业务域 | 表名 | 说明 |
|---|---|---|
| 用户体系 | t_user、t_role、t_user_role | 用户、角色、关联表,支持多角色 |
| 商品体系 | t_category、t_product、t_product_sku | 分类表、SPU表、SKU表 |
| 采购体系 | t_supplier、t_purchase_order、t_purchase_order_item | 供应商、采购单、采购明细 |
| 库存体系 | t_stock_record | 库存流水表,记录每次出入库 |
| 销售体系 | t_order、t_order_item | 订单主表和明细表 |
| 营销体系 | t_coupon、t_user_coupon、t_integral_record | 优惠券、用户优惠券、积分流水 |
| 系统管理 | t_admin、t_menu、t_role_menu | 后台账号、菜单、权限 |
这里我特别想强调t_stock_record这张流水表。很多人做进销存容易忽略它,觉得只要在SKU表里update一个stock字段就完事了,结果一旦出现数据对不上,连排查的入口都没有。库存流水表每一条记录都包含:商品SKU ID、变动类型(采购入库、销售出库、盘点调整、报损出库)、变动数量、变动前库存、变动后库存、关联业务单号、操作人、操作时间。有了这张表,账实不符时你可以顺着流水一步步倒推,这是商用系统里非常关键的一张表。
4.2 关键表字段设计举例
以SKU表和订单表为例,我把核心字段列出来供参考。
t_product_sku:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| product_id | bigint | 所属SPU ID |
| sku_name | varchar(100) | 规格名称,如“105g香辣味” |
| price | decimal(10,2) | 销售单价 |
| cost_price | decimal(10,2) | 进货成本价 |
| stock | int | 当前库存 |
| warn_stock | int | 库存预警阈值 |
| version | int | 乐观锁版本号 |
| status | tinyint | 上架/下架状态 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
t_order:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 业务订单号,唯一 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 商品总金额 |
| discount_amount | decimal(10,2) | 优惠金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| status | tinyint | 订单状态编码 |
| receiver_name | varchar(50) | 收货人 |
| receiver_phone | varchar(20) | 收货人电话 |
| receiver_address | varchar(200) | 收货地址 |
| pay_time | datetime | 支付时间 |
| ship_time | datetime | 发货时间 |
| create_time | datetime | 下单时间 |
两个设计细节值得注意。第一,金额字段全部用decimal(10,2),绝对不能用double或float——二进制浮点数在计算金额时会出现0.1+0.2不等于0.3的经典问题,这在财务相关场景是不可接受的。Java代码里对应使用BigDecimal,所有金额计算都走这个类型。第二,订单号要有唯一索引,生成方式推荐用雪花算法或“时间戳+随机数”的组合,避免用数据库自增ID直接当订单号暴露给用户,容易被别人估算订单量。
4.3 库存扣减的并发安全:一条SQL解决超卖
库存扣减是整个系统里最容易出并发问题的地方。假设库存只有5件,两个用户同时下单,如果代码逻辑是“先查库存,大于0就UPDATE减一”,在高并发下两个请求可能同时读到库存为5,然后都执行UPDATE,结果就会出现卖了6件只剩4件库存的脏数据。
解决办法很多,最直观的是在数据库层面加乐观锁。给SKU表加一个version字段,扣减时带上版本号条件:
UPDATE t_product_sku SET stock = stock - #{qty}, version = version + 1 WHERE id = #{skuId} AND stock >= #{qty} AND version = #{version}如果UPDATE返回的影响行数为0,说明库存不够或者版本已变化,此时抛出业务异常并提示用户“库存不足”或“请重试”。这里的关键点是stock >= #{qty}这个条件,它保证了扣减操作在数据库层面就是原子且安全的,哪怕并发请求同时到达,InnoDB的行锁机制也会让它们串行执行,第二个请求自然因为库存不足而更新失败。
对于秒杀这种瞬时高并发的场景,还可以配合Redis做预扣。先把库存加载到Redis,扣减时用DECR命令:
Long remain = redisTemplate.opsForValue().decrement("seckill:stock:" + skuId); if (remain < 0) { redisTemplate.opsForValue().increment("seckill:stock:" + skuId); throw new BusinessException("手慢了,商品已抢光"); }Redis的DECR是原子操作,性能远高于数据库行锁。用户拿到“预扣成功”的资格后才去创建订单、支付,真正落库时再扣减数据库库存。这套“Redis预扣+数据库确认”的双层方案,是我在真实项目里验证过的思路,演示时效果也很好。
5. 实操过程:核心链路代码级拆解
5.1 环境准备与项目初始化
开始动手之前,先把环境准备到位。JDK用8或11都行,SpringBoot推荐用2.7.x这个版本,稳定且跟Spring Security、MyBatis-Plus的兼容性好。MySQL建好数据库,Redis本地起一个默认端口实例。这三样齐了,项目就能跑起来。
项目初始化直接用Spring Initializr生成骨架,依赖选上Spring Web、Spring Security、MyBatis-Plus、MySQL Driver、Spring Data Redis、Lombok。然后配置application.yml:
spring: datasource: url: jdbc:mysql://localhost:3306/snack_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8080这里有个细节:map-underscore-to-camel-case一定要设置为true,否则数据库里的create_time字段映射不到Java实体里的createTime属性,你就会看到一堆字段查出来全是null,还死活找不到原因。另外,开发环境下把SQL日志打开,联调时能看到每次请求执行的SQL,排查问题效率翻倍。
5.2 商品上架与库存初始化的完整流程
商品上架看起来是个简单CRUD,实际要把控好几个环节。我在项目里的实现流程是这样的:
第一步,创建商品基本信息(SPU),包括名称、品牌、分类、主图、详情描述。第二步,添加SKU信息,逐个录入规格、口味、销售价、成本价、初始库存、预警阈值。第三步,保存后生成一条库存初始化的流水记录,表示“期初入库”。第四步,商品状态默认设为下架,审核无误后再手动上架,避免数据没填完就被用户看到。
这里有个很容易被忽略的点:库存初始化也要走库存流水。有的同学直接在SKU表里把stock设为初始值就算完事,但这样一来,盘点时无法区分“期初库存”和“采购入库”,账务逻辑就断了。正确做法是stock字段存当前值,同时插入一条STOCK_INIT类型的流水记录。这个习惯从第一天就养好,后面所有出入库都统一走流水。
5.3 下单到发货的状态机实现
下单链路是系统的核心,我把完整流程拆成几个关键动作:
用户点击结算时,后端接收到购物车中的SKU列表和数量,第一步校验商品是否上架、库存是否充足;第二步计算订单金额,应用优惠券和满减活动,生成实付金额;第三步锁定库存——这一步是预占,不直接减库存,而是通过订单状态来体现,订单创建成功但未支付;第四步创建订单主表和明细表,返回订单号;用户支付成功后,回调里再执行真正的库存扣减和出库流水写入。
写代码时有一个重要的教训:整个下单过程必须在同一个事务里,尤其是“创建订单”和“扣减库存”这两个动作。如果你把它们拆到两个方法里,中间一旦出异常,订单建了但库存没扣,或者库存扣了但订单没建,都会留下脏数据。事务的边界就是业务一致性的边界,这个道理在这个项目里体现得格外明显。
发货环节相对简单:后台管理员对“已支付”状态的订单执行发货,录入物流单号,订单状态改成“已发货”。这里状态机的校验逻辑就发挥作用了——一个“待支付”的订单不应该能直接发货,必须等支付确认后才能操作。
5.4 前端联调中的几个关键配置
前后端分离开发,前端用Vue 3加Element Plus,联调时最容易遇到三个问题。
第一个是跨域。前端运行在9527端口,后端在8080端口,浏览器默认会拦截跨域请求。解决办法是在后端写一个CORS配置类,允许前端域名访问;或者使用HTTP代理转发,前端把请求统一转发到8080。我在项目里两种方式都测试过,CORS配置类更直观,代码也就几行:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }第二个是鉴权。后端用了Spring Security + JWT,前端每次请求需要在请求头里带上Authorization: Bearer token。在前端的Axios拦截器里统一添加请求头,再对返回的401状态码做统一跳转登录处理,就不用每个页面都写一遍鉴权逻辑。
第三个是接口文档。启动项目后访问Knife4j的地址,就能看到所有接口的定义,前端直接参照文档联调,传参和返回结构一目了然。这也是我强烈建议加Knife4j的原因——省去大量“你接口到底要什么参数”的沟通成本。
6. 常见问题与排查技巧实录
6.1 库存超卖问题
这个几乎是进销存项目的必考题目。症状表现为:并发下单后,系统里卖出数量大于实际库存。排查方向按优先级来。
先检查数据库扣减SQL是否包含stock >= #{qty}这个条件。很多超卖问题的根源就是查出来库存够、执行UPDATE时没带条件,导致两个请求都更新成功。其次检查事务有没有真正生效,Service类上是否有@Transactional注解,事务管理器是否正确配置。最后检查代码里是否有独立的“查库存”动作——如果每次都先查再减,即使SQL带条件,也容易因为两个操作之间存在时间窗口而出问题。最稳妥的做法是:把“查询+扣减”合成一个原子操作,也就是前面提到的那条UPDATE语句。
6.2 订单状态错乱与事务边界
我调试过程中遇到过一个问题:用户点击“取消订单”后,支付回调仍然把订单状态改成了“已支付”,导致用户退货时系统里状态对不上。排查后发现,支付回调的方法没有做状态校验,直接把状态从“待支付”改成“已支付”,而取消操作和回调是并发的,最后一步写入的状态覆盖了对方。
这个问题的解决办法就是状态机校验。在支付回调处理逻辑里,先判断当前订单状态是否为“待支付”,只有这个状态才能进入“已支付”,否则直接丢弃本次回调。同理,取消订单也要加状态校验。这里我更推荐画一张状态流转图,把允许的跳转关系列出来,然后在代码里用一个Map或枚举定义合法流转,这样既能防错,也好向答辩老师解释。
6.3 MyBatis-Plus分页查询的性能问题
后台管理页面大量使用分页列表,MyBatis-Plus自带的分页插件在单表查询时表现没问题,但一遇到多表关联就经常翻车。常见现象是:分页返回的总条数不对,或者关联查询后数据被截断。
原因在于MyBatis-Plus的分页插件处理多表查询时,自动生成的COUNT语句可能漏掉关联条件或去重条件。我的处理办法是:分页列表的查询,主表负责查询条件,明细数据用循环查询或者单独的列表接口,不强行一条SQL把多个表都JOIN进来。如果确实需要关联查询,就在XML里手写分页SQL,同时手写配套的COUNT查询,明确指定统计逻辑。另外,多表联查时一定要给WHERE条件涉及的字段建索引,尤其是SKU表的product_id、订单表的user_id、order_no,这几个字段是查询的高频入口。
6.4 登录状态下重复提交与金额精度
还有一个现实中很常见的问题:用户连续点击“提交订单”按钮,导致后台创建了多个相同的订单。解决办法有两个层面:前端在提交后把按钮置灰,防止重复点击;后端在创建订单前做幂等处理——最简单的做法是前端生成一个请求唯一标识(比如UUID),后端Redis中设置一个以该UUID为key的短时缓存,重复请求直接拦截。后端的幂等机制是必须的,因为前端防重复可以绕过。
金额精度的问题在前面已经提过,这里补充一个实战技巧:所有涉及金额的字段,在前后端交互时都用字符串或整数(以“分”为单位)传输,避免JavaScript浮点数计算误差。比如后端返回的金额是12.30,前端拿到后直接用字符串展示,不做浮点运算,结算页面的金额计算交给后端完成,前端只负责展示。这个习惯能帮你省掉大量“为什么金额差了几分钱”的排查时间。
6.5 常见问题速查表
| 问题现象 | 常见原因 | 排查/解决思路 |
|---|---|---|
| 商品列表图片不显示 | 图片路径是绝对路径或跨域 | 使用相对路径+统一静态资源映射,或配置可访问的图片服务器域名 |
| 登录后接口返回401 | JWT过期或请求头没传token | 检查前端Axios拦截器是否统一添加Authorization头,检查token有效期配置 |
| 数据库字段映射为null | 驼峰映射未开启 | 检查map-underscore-to-camel-case配置,或SQL中为字段起别名 |
| 删除分类报外键约束错误 | 分类下已有商品 | 删除前先检查关联数据,逻辑删除或禁止删除有商品的分类 |
| Redis缓存和数据库不一致 | 更新数据库后未同步删除缓存 | 采用“先更新数据库,再删除缓存”的策略,必要时设置过期时间兜底 |
| 采购单审核后库存没变化 | 审核逻辑漏了入库流水 | 审核通过方法中必须调用库存入账服务,并写入流水记录 |
7. 毕业设计答辩与交付建议
7.1 演示流程怎么设计才不踩坑
一个认真的毕业设计,答辩表现和项目本身同样重要。我建议演示流程按业务链路来设计,而不是按功能菜单来罗列。先登录后台,展示商品管理:创建一个零食分类,上架一个新商品,带多个SKU;然后创建采购单,审核入库,展示库存从0变成有值的过程,同时展示库存流水;切到前台,用测试账号下单,购物车、确认订单、模拟支付成功;回到后台处理发货,展示库存扣减和流水记录;最后展示几张统计图表,比如销量排行、库存预警列表。这条链路走完,系统五个核心模块全部覆盖,而且过程自然流畅。
演示前一定要做的事情是重置数据。把测试数据清理干净,库存和订单状态都归位,不要出现“上一个订单的支付回调还没处理”这种现场翻车的情况。提前演练至少两遍,特别是秒杀功能,确保Redis库存已加载、时间配置正确。
7.2 论文写作的侧重点
论文是答辩的另一半分数。我的经验是:论文不要写成一堆功能清单,而要突出设计权衡和问题解决过程。比如“为什么库存用乐观锁而不是悲观锁”,从性能角度分析;比如“为什么订单和库存扣减要在同一事务”,从一致性角度分析;比如“为什么秒杀场景要引入Redis预扣”,从并发瓶颈角度分析。
项目里遇到的实际问题及解决方案,这类内容是评委最喜欢看到的,也最能体现你的独立思考能力。比如你调试过程中遇到了超卖问题,把它写进论文的“系统测试与问题分析”章节,说明问题现象、排查思路、最终方案和验证结果,这比洋洋洒洒写几十页原理更有说服力。
7.3 这个项目还能往哪些方向扩展
这套系统做完之后,如果时间充裕,有几个自然扩展的方向。一是接入第三方支付的真实对接(演示时模拟即可),商家收款流程会更完整;二是增加数据可视化看板,用ECharts展示销量走势、热销单品、库存周转率,让老板每天打开后台就能看到经营状况;三是加入文件导入导出功能,批量导入商品Excel、导出订单报表,实用性非常强;四是做移动端适配,把前台商城改成响应式页面,或者单独做一个H5版本。
我个人在实际开发中最大的收获是:做这类进销存系统,真正值得花时间研究的不是框架API,而是业务数据流怎么设计才不打架。库存什么时候该动、订单状态什么时候该变、流水怎么留痕,这些想清楚了,代码只是水到渠成的事情。你把这个系统的数据流吃透,以后做点单系统、仓储系统、批发管理系统,迁移起来都很快,因为底层那套“进销存账实一致”的思维是通用的。