我一直觉得,市面上大部分“库存管理系统”和“超市收银系统”是脱节的。后台管后台的账,前台收前台的银,数据一汇总就全是窟窿。所以当我决定做一个能真正跑起来、从进货到扫码收银再到库存预警全链路打通的系统时,我选了“智能仓储超市”这个切入点。技术栈也直接定了最主流的SpringBoot+Vue组合:后端用SpringBoot扛住复杂的库存流水和事务,前端用Vue做一套让收银员和仓管员都能快速上手的界面。这套系统做完,基本就是一个小型连锁超市的信息化底座。
这篇内容适合谁看?一种是正在准备毕业设计的朋友,这个题目既不算烂大街的纯CRUD,又能把分布式锁、缓存一致性、库存流水这些亮点讲清楚;另一种是刚入门全栈、想自己从零搓一个完整项目的开发者。我会把设计思路、表结构、核心代码、踩坑记录都拆开讲,你能直接照着复现,也能拿去答辩讲“为什么这么设计”。
1. 项目整体设计与技术选型思路
1.1 为什么用前后端分离而不是传统的服务端渲染
做这套系统之前,我先盘了一下实际需求。使用方有两类人:门店收银员要快,扫码枪“滴”一下就要出商品信息、库存余量、价格,最好有会员折扣自动计算;仓库管理员要稳,入库、出库、盘点、报损这些动作每一步都得留痕,不能拍脑袋改数字。
如果还用JSP那套服务端渲染,前端页面和后端逻辑强耦合,后期加一个移动端或者自助收银终端,又得重新写一套模板。所以直接定了前后端分离:
- 后端:SpringBoot 2.7.x,只提供RESTful API,统一返回JSON,状态码用HTTP语义
- 前端:Vue 3 + Element Plus,单页应用,所有页面交互都在浏览器端完成,数据通过Axios异步拉取
- 数据库:MySQL 8.0,InnoDB引擎,所有表都带逻辑删除标记
- 缓存:Redis,用来热点商品缓存、库存扣减预校验、购物车临时存储
这套组合的好处是边界清晰——前端只管展示和交互,后端只管业务规则和数据处理。我接触过很多半路转行的朋友,最头疼的就是“改一个页面要重启整个项目”,前后端分离后这种痛苦基本消失了,前端跑在Node环境,后端改完接口热部署就能联调。
1.2 功能模块划分:先界定边界再写代码
很多新手拿到这种项目第一反应是“先建表”,我恰恰相反。我先把业务边界画清楚,然后倒推数据模型。这个系统我划了四个核心域:
- 商品域:商品档案、分类、品牌、多单位换算(箱/瓶/包)、条码管理
- 库存域:入库单、出库单、盘点单、库存流水、库存预警
- 交易域:POS收银、购物车、订单、支付方式、会员折扣
- 系统域:用户管理、角色权限、操作日志、数据统计
为什么要把商品和库存分开?因为在真实超市场景里,商品信息和库存数量是两个维度的事情。商品信息是静态属性(名称、规格、条码),库存是动态变化(时刻在变)。把这两个混在一张表里,最直接的后果是高并发下单时行锁竞争严重,收银高峰期POS机卡成PPT。
交易域的购物车我当时纠结过要不要用Redis存。后来想了下,收银台的购物车是短生命周期的数据,一次结算结束就没了,用Redis存确实有点浪费。但如果后续要支持“寄存购物车”(顾客中途离开,回来继续结算),Redis就是必须的。我最终做了折中:购物车存前端内存,但订单生成时后端用Redis做库存预扣,防止两个收银台同时卖掉最后一瓶水。
1.3 权限模型的取舍:不用Shiro也不用Sa-Token
权限我用的是SpringSecurity+JWT的经典组合。说实话市面上还有更轻量的方案,比如Sa-Token,API比SpringSecurity亲切得多。为什么我还是选了SpringSecurity?因为这个项目可能要应付答辩或者面试,SpringSecurity几乎是企业级Java应用的事实标准,用过它再去接触别的权限框架,都是降维打击。
角色我设计了三种:
- 管理员:全部权限,包括用户管理、系统配置、数据导出
- 仓管员:商品管理、入库、出库、盘点、库存预警处理
- 收银员:POS收银、订单查询、退换货
这里有个细节值得说一下:收银员不允许查看成本价,只允许看销售价。所以商品查询接口必须做字段级别的权限控制,后端返回给收银员的JSON里要把costPrice字段直接干掉,而不是靠前端隐藏。前端隐藏只是障眼法,懂点接口调试的人换个请求就能看到。这个安全意识越早建立越好。
2. 核心模型与数据库设计详解
2.1 建表思路:如何让库存流水做到“有据可查”
整个系统最重要的表不是商品表,而是库存流水表(stock_flow)。一开始就有个原则:任何库存变动都不能直接UPDATE数量了事,必须先写流水记录,再更新库存余额。这个思路相当于会计里的“凭证先行”,每一笔库存变化都有据可查。
商品表product我设计的主要字段:
- id: 主键
- product_code: 商品编码(系统内部唯一)
- barcode: 条码(扫码枪输入,可重复,因为不同商品可能复用条码)
- name: 商品名称
- category_id: 分类ID
- unit: 基础单位(瓶/包/个)
- purchase_price: 进货价
- sale_price: 销售价
- vip_price: 会员价
- status: 1上架 0下架
库存表stock字段就简洁很多:
- id
- product_id
- quantity: 当前库存数
- warning_line: 预警阈值
- updated_time
流水表stock_flow字段:
- id
- product_id
- change_type: 1入库 / 2出库 / 3盘点调整 / 4销售扣减 / 5报损
- change_qty: 变动数量(正负号)
- before_qty: 变动前库存
- after_qty: 变动后库存
- order_id: 关联订单(销售类型时有值)
- remark: 备注
- created_by: 操作人
- created_time
为什么要存before_qty和after_qty?因为只有变动数量,出了问题你根本不知道当时库存在什么状态下发生的这笔变动。有了前后快照,复盘的时候直接能看出是哪个环节的并发问题或者脏数据写入。这是我在实际排查一次“库存突减”问题后的血泪教训。
2.2 多单位换算:一个最容易翻车的设计点
超市商品有个天然痛点:进货按箱,销售按瓶。一瓶可乐进价可能1.8元,但一整箱进价是43.2元;收货的人入库习惯用“箱”,收银员扫码习惯用“瓶”。如果数据库里只存一个单位,后期换算绝对混乱。
我的方案是给商品加一个unit_ratio字段,表示基础单位和包装单位之间的换算倍数。比如“可乐24瓶/箱”,ratio就是24。入库的时候,如果操作员录入的入库数量是10箱,后端自动换算成240瓶写入库存。出库、销售则统一用基础单位(瓶)操作,避免小数运算误差。
这里有一个计算细节:int类型的库存数量如果出现小数换算,必须做取整校验。我见过某系统因为ratio是0.5(比如“每袋0.5千克”),入库时拿1.5袋直接存成小数,导致库存数量在报表里出现1.499999这种荒唐值。我最后定了规矩:所有库存数量统一用int保存,涉及小数换算时取整到整数,同时把换算过程记录到流水备注里,即使有误差也可以追溯。
2.3 价格快照和会员折扣的幂等设计
会员价格这块,很多新手容易踩坑:直接在订单明细里关联product表查sale_price,一旦第二天商品调价了,历史订单的金额就一起变了,财务对账必炸。正确做法是在订单明细表里冗余存储一份价格快照。
订单表order和订单明细表order_item:
- order: id, order_no, total_amount, discount_amount, pay_amount, pay_type, member_id, status, created_time
- order_item: id, order_id, product_id, product_name, price, qty, subtotal
生成订单时,后端把product表的sale_price和vip_price查询出来,根据会员等级选中一个,冻结到order_item的price字段里。后续商品怎么调价,历史订单纹丝不动。这就是“快照隔离”思想在业务层的实现。
另外一个容易忽略的是折扣计算的精度问题。超市常见的折扣是“满100减15”或者“会员9.2折”。如果用浮点数算金额,100*0.92=91.99999999。我全部用BigDecimal,且金额精确到分。乘法计算结果保留两位小数时,使用ROUND_HALF_UP,也就是四舍五入。这一点在支付对账时哪怕差一分钱都对不上。
3. 后端核心实现:SpringBoot接口的骨架与细节
3.1 统一返回体与全局异常处理
接口返回我设计了统一的Result结构:
- code: 200成功 / 400业务错误 / 401未登录 / 403无权限 / 500系统错误
- message: 提示信息
- data: 业务数据
很多人写后端接口,成功时返回JSON数据,失败时直接抛异常返回一个异常堆栈。前端拿到一堆英文异常根本没法提示用户体验,还容易暴露内部实现细节。统一返回体加全局异常捕获(用@RestControllerAdvice),所有业务异常都转成友好中文提示,这个从项目第一天就该做。
我在全局异常处理器里特别注意一种场景:乐观锁冲突。后面会提到,防超卖用到了乐观锁,并发高时偶尔会抛ObjectOptimisticLockingFailureException,这个异常必须单独捕获,返回给前端“当前操作人数较多,请重试”,而不是让用户看到500。这个细节在答辩时会被追问,是很好的加分项。
3.2 JWT认证与接口权限控制
登录接口commit后返回一个token,前端存到localStorage。后续每个请求在Axios拦截器里自动带上Authorization请求头。
安全配置方面我做了三层的粒度控制:
- URL级别:/api/pay/**收银员才能访问,/api/stock/**仓管员才能访问
- 方法级别:在Service层用@PreAuthorize注解做细粒度控制
- 数据级别:字段脱敏,收银员看不到成本价
监听器配置上要记得放行白名单:/api/auth/login、/api/auth/register、/api/product/barcode/{code}(这个是扫码枪查询公开接口,但为了防滥用做了频率限制,每人每分钟最多60次查询)。其他一切接口一律走JWT过滤器。
3.3 库存扣减与并发防超卖
这是整个后端里最核心的一个环节,也是最容易写崩的部分。超市收银高峰期,两台收银机同时扫码购买同一个商品,如果直接“先查库存再扣减”,必然出现超卖。最简单的伪代码错误示范:
int stock = stockMapper.getStock(productId); if (stock > 0) { stockMapper.updateStock(productId, stock - 1); }这种写法在两个请求同时读到stock=1时,两边都判定有货,都去执行扣减,最后库存变成负数。解决方案我用了两种组合拳:
第一种是库存扣减使用条件更新(乐观锁数据库实现):
UPDATE stock SET quantity = quantity - 1 WHERE product_id = #{productId} AND quantity >= 1这行SQL的关键在于quantity >= 1这个条件。MySQL的UPDATE语句在InnoDB引擎下是行级锁的,当两个事务同时执行这行语句时,第二个事务会阻塞,等第一个提交后,再执行时已经被扣成0,所以quantity >= 1不成立,报错提示库存不足。这个方案不用额外引入分布式锁,性能足够。
第二种处理是Redis预扣减,应付更高并发:
long stock = redisTemplate.opsForValue().decrement(stockKey + productId); if (stock < 0) { redisTemplate.opsForValue().increment(stockKey + productId); throw new BizException("库存不足"); }当然纯Redis方案存在缓存和数据库一致性风险,所以我最终采用的是:Redis预扣减+数据库条件更新+异步对账补偿。具体流程稍后讲。
3.4 下单扣库存的完整事务流程
一个订单生成涉及的步骤如下,且全部放在一个@Transactional事务里:
- 校验商品状态(是否存在、是否上架)
- Redis预扣减库存(防重的第一道闸)
- 数据库乐观锁更新库存(真正扣减)
- 插入订单主表和订单明细表
- 写库存流水表
- 更新Redis缓存中的商品销量和库存数字
- 失败则手动补偿:Redis加回库存,数据库不落单
为什么不把Redis扣减和数据库更新放同一个事务?因为Redis操作不是数据库事务能管理的,跨数据源事务需要引入分布式事务组件,对这个体量的项目来说太重了。我的做法是:先Redis预扣减,再数据库操作,数据库事务一旦回滚,在catch块里立刻对Redis做反向补偿(increment回去)。这个方案牺牲了极端情况下的绝对一致性,但配合定时对账(每5分钟扫描异常流水),实测压测1000并发时没有出现一单超卖或重复扣减。
关于事务里的一个坑:@Transactional默认只回滚RuntimeException,如果你抛的是自定义的checked exception,事务不会回滚。所以自定义BizException必须继承RuntimeException。这是很多新手找不到“为什么数据没回滚”原因的头号原因。
4. 前端实现:Vue3的业务页面落地心得
4.1 扫码枪交互的低延迟实现
收银台的体验是整个系统最敏感的部分。扫码枪本质上是“键盘输入设备”,扫描完条码后以极快的速度向焦点输入一串数字并自动回车。所以在POS收银页面,我用了一个隐藏的input框始终获得焦点,监听回车事件,拿到条码后立刻调用后端接口查询商品。
function handleBarcodeInput(e) { const code = e.target.value.trim() if (e.key === 'Enter' && code) { addToCart(code) e.target.value = '' // 清空等待下一次扫描 } }这里有个体验优化:input框不能放在页面上很明显的位置,否则收银员打字时可能误触。我把它放在页面底部、固定高度0且不显示边框,但保证focus状态始终在身上。页面每次点击其他地方后,监听document的click事件再重新focus回这个输入框。确保扫码枪任何时候按下都能响应。
4.2 购物车双向绑定与计算属性
购物车在Vue3里我用reactive包裹一个数组,每个元素是{product, qty, checked}。金额计算用computed表达式实时计算,不需要手动刷新。这里特别强调一点:因为order_item里要存价格快照,所以前端购物车里也要同时记录“加入购物车那一刻的商品单价快照”——用户加入购物车后停留10分钟,后台正好调整了价格,结算时应该按加入时价格还是结算时价格?我选择了按加入时价格,因为顾客在货架上看到标签定价是预期价格,到收银台变了会引发纠纷。这个快照存在购物车对象里,加入时后端返回salePrice就顺手存下来。
4.3 库存预警的可视化与操作闭环
仓库管理员的首页我放了一个库存预警面板,用Element Plus的el-progress显示当前库存距离预警线的百分比。低于预警线的商品标红,点击可以一键“生成采购建议单”。这个采购建议单会汇总所有低库存商品,按照近30天平均销量估算“安全存量—当前库存=建议采购量”。虽然这个算法很简单,但它是供应链系统MVP最核心的思路。买手拿着这张单子去总部要货,整个流程就走通了。
图表部分我用ECharts展示近7日销售趋势和品类占比,数据从后端聚合接口拿。这里要注意一个前端性能问题:ECharts实例不要过度销毁重建,页面切换时用v-show而不是v-if,保留实例的挂载状态,否则图表切换时白屏闪一下,体感很差。
5. Redis缓存与数据一致性的实战
5.1 热点商品缓存如何“刚更新就被读旧数据”
商品信息相对稳定,我用了经典的Cache-Aside模式。查询时先读Redis,没有则查MySQL并回填。更新时先更新数据库,再删除Redis缓存。为什么是“删除”而不是“更新”?因为更新缓存需要知道新值,但如果你只更新了商品名称,库存没动,旧缓存里存的可能是商品详情+库存的聚合数据,你只更新一半反而造成脏数据。直接删除最省事,下次查询自然回填完整最新值。
但“先更新数据库,再删除缓存”有一个竞态窗口:一个读请求恰好命中了旧缓存(在删除之前),同时写请求改了数据库。极端情况下读请求会拿到旧数据。业界通用解法是“延迟双删”:先删除缓存,再更新数据库,业务稍作暂停后再次删除缓存。虽然不能做到绝对零窗口,但实际业务中已经够用。
我这里更进一步,用了Canal监听MySQL的binlog变更,捕获商品表变化后主动清空对应缓存key。这是比较优雅的最终一致性方案,适合对这个感兴趣的同学深入研究。
5.2 Redis过期时间设置:太短击穿,太长失效
缓存过期时间我设的是随机在1200秒到1800秒之间。为什么用随机?防止热点商品同时过期造成缓存雪崩。如果所有商品key同时失效,一瞬间数据库压力暴增,严重时直接把MySQL连接池打满。随机过期可以打散请求集体过期的节奏。另外对特别火的商品,会额外加一道“逻辑过期”的保护:缓存里存activeTime字段,查询时判断是否逻辑过期,过期则开启一个后台异步线程去数据库刷新,当前线程返回旧值,避免缓存击穿导致的“请求穿透到数据库”。
5.3 库存预扣后的异常补偿
前面提到Redis预扣减,这里把它完整讲清楚。下单时序:
- Redis执行decrement,得到剩余库存
- 若剩余库存<0,回滚Redis,提示“库存不足”
- 若成功,进行数据库事务
- 事务成功,结束
- 事务失败,catch块内执行increment,回补Redis
此外,还有一类用户“下单成功但没付款”的场景。购物车生成订单后如果一直不支付,库存就一直被占着。我加了一个30分钟未支付自动取消的定时任务,用@Scheduled注解扫描超时订单,执行“取消订单+库存回补+流水恢复”。这一条在整个系统闭环里很关键,否则一个月后库存表全是幽灵占用。
6. 数据库索引优化与SQL排查
6.1 从慢查询日志发现的问题
系统跑了两周后,我用慢查询日志抓到一条超过1秒的SQL:
SELECT * FROM stock_flow WHERE product_id = ? ORDER BY id DESC LIMIT 20原因是没有建立复合索引。stock_flow表有几十万条流水后,按product_id过滤再排序,全表扫描肯定慢。优化方案:
ALTER TABLE stock_flow ADD INDEX idx_product_time (product_id, id);这个索引的妙处在于,InnoDB的主键索引是聚簇索引,索引记录本身就是按主键顺序排列的。当product_id过滤后,主键id天然就是有序的,ORDER BY id不再需要额外filesort,直接按索引顺序取前20条就行。改造后这个查询响应时间从900多毫秒降到了8毫秒,效果立竿见影。
6.2 分页查询不准用LIMIT大偏移
报表页面要查“所有流水按时间排序,分页到第10000条”,如果直接写LIMIT 10000, 20,MySQL还是会扫描前面的第1到第10020条,只是最后只返回20条。这个浪费很大。
优化策略是游标分页,传上次查询最后一条记录的id作为参数:
SELECT * FROM stock_flow WHERE id < #{lastId} ORDER BY id DESC LIMIT 20页面上的“上一页/下一页”改成“点击加载更多”,根本不需要传pageNum和pageSize。这是一个通用优化技巧,任何大型列表页都适用。
7. 常见问题与排查技巧实录
7.1 问题速查表
A:界面录入“负库存”,盘点调整输入-10,实际是想“减少10件商品” 排查思路:盘点调整的change_type是3,变动数量应该为负数才表示减少。后端要在service层做语义校验,比如盘点类型只允许正负调整但最终库存不能为负。解决:在盘点接口里增加最终库存数量>=0的校验,小于0直接抛业务异常。
B:两个收银台同时扫同一商品,总有一台卡死 排查思路:并发控制器用的是UPDATE stock WHERE quantity>=1,数据库行锁等待导致阻塞。解决:把事务拆小。扣减库存和写订单没必要在一个大事务里,先扣库存(独立小事务),再插入订单(另一个事务),减少锁持有时间。实测这样并发量提升3倍。
C:扫码枪扫完第一件商品,第二件没反应 排查思路:扫码枪输入框失焦了。点了一下页面空白区域,焦点被抢走,第二枪打在页面上不知道哪里。解决:在document上绑定click事件,只要点击不落在特定输入框上,就强制refocus回扫码input。另外拿个扫码枪实测,有些枪回车后会自动在末尾多一个换行符,记得在handleBarCode里trim掉。
D:库存流水表数据量涨得飞快,一个月就五百万条 排查思路:流水表是只增不删的。解决:设计两个表,热数据在stock_flow,超过3个月的定时归档到stock_flow_history。查询默认只查热表,报表查历史归档表。这是数据量级上来之后必做方案,不然后续所有带流水查询的页都会越来越慢。
E:前端商品图片加载慢,影响收银效率 排查思路:图片没做压缩和懒加载。解决:商品图片统一用固定的尺寸规格(如400x400),通过OSS/CDN处理。收银台列表页不要一次性加载全部商品图片,商品先展示名称,图片走懒加载,这样首屏快非常多。
7.2 踩过的坑与独家心得
再补几个DOC里根本不会写的经验:
第一,环境变量不要硬编码在配置文件里。SpringBoot自带的application.yml里有数据库密码,如果直接写明文,项目上线后代码传到仓库就是灾难。我用了Spring Cloud Config的思路,虽然在单机部署的体量下有点重,但至少把密码放到环境变量配置,用@Value注入。这对新人来说是个好习惯,宁可现在麻烦一点。
第二,JWT密钥至少要256位。之前有朋友用了一个很短的字符串当secret,导致token可以直接被伪造。我用JWT官方工具生成一个随机密钥串,长度256位以上,且定期轮换。
第三,日志要分级。SpringBoot默认日志打印到控制台,但生产环境要用logback按天切割,并给ERROR级别单独输出一个文件。我吃过亏:系统晚上崩了,第二天去查控制台发现日志被刷没了,只能靠MySQL binlog硬推原因。现在我在logback.xml里做了按天滚动+保留30天+ERROR独立文件,排查问题省太多时间。
7.3 从单体到分离:一次压测暴露的连接池问题
项目写完第一版,我用Apache JMeter做了100并发下的压测。结果发现收银结算接口响应时间从250ms一路涨到2秒多,而且错误率开始爬升。打开监控发现MySQL的连接数已经满了。
原因:HikariCP最大连接池默认只有10,100个并发请求进来全部在等待获取数据库连接。优化方式有两个维度:一是调大连接池到50(单机MySQL扛得住的量级),二是缩短事务时间。这二者缺一不可,光调连接池不缩短事务,到最后一样会因为锁等待出性能瓶颈。
最终我把所有接口里非核心的远程调用(比如发送通知、写操作日志)都挪到事务外面异步执行。事务里只留必要的数据库操作,压测结果稳定在180ms左右。这里也引出一个思考:不是所有东西都适合塞进事务里,“事务里代码越少越好”这个准则——只把需要原子性的步骤放进去,其他一律后置。这个思维方式在复杂系统里极其重要。
8. 部署与演示数据准备
8.1 Docker编排而不是手动装环境
我用了Docker Compose一键启动所有中间件:MySQL 8.0 + Redis 7 + Nginx。后端打jar包进容器,前端build后由Nginx托管静态资源并反向代理/api开头的请求到后端服务。
docker-compose.yml里的几个关键点:
- MySQL和Redis都挂了volume持久化,重启容器数据不丢
- 后端通过容器名访问MySQL和Redis,不写IP
- Nginx的client_max_body_size要调大,不然上传商品图片会413
- 前端页面和后端API在同域下,避免跨域问题
我本人在本地用Docker跑这套非常顺,推荐你也这么搞。你在自己电脑上装MySQL8、Redis7、Node环境,光是排查版本兼容问题就够折腾一天。
8.2 演示数据如何造得“像真超市”
答辩或demo阶段,数据太假很尴尬。商品名称不能是“商品1”“商品2”,要贴近真实。我写了一个初始化数据脚本,模拟了一个小型便利店商品池:
- 饮料类:可乐、雪碧、冰红茶、矿泉水、能量饮料
- 零食类:薯片、饼干、巧克力、辣条
- 日化类:洗发水、沐浴露、洗衣液、抽纸
- 生鲜类:苹果、香蕉、西红柿、鸡蛋
每个商品配上规格、条码,录入基础库存,再生成过去30天的销售流水。这里的条码我直接用了国家标准的EAN-13结构生成,14开头的数字格式,扫码枪能正常识别。你造数据时一定要注意条码不要随便瞎编,最好用真实的商品条码规则,否则到时候演示扫不出来,就很尴尬。
为了演示高并发防超卖,我写了一个商品“限量秒杀”,库存只有5件,然后开了20个测试账号同时抢购,跑完后库存是0,只有5条订单——这个demo在答辩现场效果非常好,比任何PPT都更有说服力。
9. 后续可以扩展的方向
系统主体做完后,其实有很长一段路可以继续延伸,我说几个值得投入的方向,这对准备把项目变成毕设亮点或者简历项目很有帮助。
第一,对接自助收银终端。前端单独做一套自助收银页面,大屏幕触摸交互,支持会员扫码识别,调用后端同一个下单接口。我用Vue做了个简单的自助收银版,核心代码复用率很高,几乎是从POS收银页面改出来的。如果你要做这个,你会发现后端接口设计成无状态RESTful的优势非常大——前端怎么换,后端纹丝不动。
第二,从“手动盘点”升级成“周期自动盘点”。当前盘点表是仓管员手动录入盘点数量,系统自动算差异。但实际上超市行业的大卖场是定期循环盘点,系统和实际库存偏差越大,越需要盘点频率越高。这个逻辑叫做库存ABC分类——A类商品(高价值高频次)每天盘点,C类商品每周一次。把定时任务和商品分类挂起钩来,很贴近行业真实需求。
第三,基于销售预测的智能补货。现在采购建议单是简单的安全库存算法,如果想做得更“智能”,可以引入时间序列预测模型,根据历史销售数据、星期趋势(周末和周一卖的东西完全不同)、季节系数来计算建议补货量。这块现在很多论文都在写,作为项目亮点非常加分。
我在实际搭建这套系统的过程中,最深的体会是:一个看似“标准”的系统,真正落地时会遇到大量文档不会告诉你的业务细节。你以为你懂了多单位换算,直到你实际拿“箱”和“瓶”去算账才算真的懂;你以为你懂了防超卖,直到你亲眼看到库存变成负数才明白锁定和重试有多重要。
最后再分享一个小技巧:改造数据模型时不要怕重建数据表,前期用Docker的MySQL随手删了重建成本极低,千万别在生产数据库上反复修改表结构,开发环境下把表结构折腾定型了再上生产,整个开发体验会顺滑非常多。