机车零售后端的库存账本:领域拆分、操作审计与并发一致性
本文目录
本文根据一个个人机车零售与服务管理后端的代码结构,整理领域拆分和库存流水的工程思路。案例只描述可复用的技术形态,不包含仓库地址、真实账号、环境配置或业务数据。下面的代码是说明性示例,不代表案例项目的原始实现,也不等于已经通过生产验证。
技术结构:薄路由、业务控制器与模型
项目采用 TypeScript、Express、Sequelize 和 PostgreSQL。认证相关依赖包括 JWT 与 bcrypt;路由、控制器、模型、中间件、分页和响应工具各自分层。一个请求大致经过:
HTTP 请求
-> Express 路由
-> 身份验证与角色检查
-> 领域控制器
-> Sequelize 模型 / PostgreSQL
-> 统一响应
这样的组织方式适合中小型业务后台:路由负责声明资源与权限边界,控制器编排业务步骤,模型映射持久化数据,共用响应和分页逻辑避免接口各自发明格式。随着领域规则变多,库存校验、价格计算等逻辑应进一步沉淀到领域服务,避免控制器逐渐变成难以测试的大文件。
从模块划分可以看到机车商品、配件、用户、预约、报价、服务项目、店铺配置、数据字典和库存记录等业务对象。它比单纯的商品 CRUD 多了一层运营流程:库存变化要有操作者、原因和时间;低库存要能被发现;批量与扫码操作也需要走相同的数据规则。
库存流水为什么要记录前后值
一条可审计的库存流水通常至少包含:对象类型和 ID、操作类型、数量、操作前库存、操作后库存、操作者、业务原因、条码和创建时间。前后值让管理者可以回答“谁在什么时候把库存从多少改成多少”,比只保存一个最终库存数字更利于排错。
但流水表和库存字段不能各自独立更新。一次入库或出库应当把库存变更与流水写入放在同一个数据库事务里:两者一起提交,或者一起回滚。否则,库存已经变化但流水写入失败时,账面就无法解释;反过来,流水先写成功而库存更新失败,也会留下错误记录。
一个可靠的出库事务
先在接口边界把输入转成明确类型,再验证数量必须是正整数。对于并发写入,可以在事务中锁定目标库存行,读取最新库存、验证不超卖、写入新数量和流水:
async function stockOut(itemId: string, quantity: number, operator: string) {
if (!Number.isSafeInteger(quantity) || quantity <= 0) {
throw new Error('quantity must be a positive integer');
}
return sequelize.transaction(async (transaction) => {
const part = await Part.findByPk(itemId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
if (!part) throw new Error('part not found');
if (part.stock < quantity) throw new Error('insufficient stock');
const beforeStock = part.stock;
const afterStock = beforeStock - quantity;
await part.update({ stock: afterStock }, { transaction });
await StockLog.create({
itemType: 'part',
itemId: part.id,
itemName: part.name,
type: 'out',
quantity,
beforeStock,
afterStock,
operator,
}, { transaction });
return { beforeStock, afterStock };
});
}
这是模式示例,实际项目还应将业务错误映射为稳定的 HTTP 错误码,并覆盖数据库约束、死锁重试策略和并发测试。Sequelize 默认不会自动把普通模型调用包进事务;事务需要显式传给参与本次操作的查询与写入。PostgreSQL 的行级锁可以让同一条库存记录上的竞争更新串行化。
批量与扫码:接口不同,规则不应分叉
批量入库、批量出库和扫码入库看起来是不同入口,本质上都是库存变更命令。可以先把输入转换成统一的内部命令,再调用同一套校验和事务逻辑。这样条码识别不会绕过数量校验,批量操作也不会漏记操作者或原因。
批量接口需要明确选择一种语义:
- 整批原子:任一条失败,整批回滚。适合订单履约、盘点差异等要求全有或全无的业务。
- 逐条部分成功:每条独立提交,并在响应中返回成功项、失败项和可重试原因。适合后台导入,但必须让界面准确呈现部分结果。
如果调用方可能因网络超时重试,还应设计幂等键或业务操作号。否则,服务端已经完成出库但客户端没收到响应时,重复请求可能再次扣减库存。
历史记录要可解释,不要直接抹掉
库存流水是审计链的一部分。直接删除旧流水并反向修改当前库存,容易在后续流水已经发生时破坏前后值之间的关系。更稳妥的做法是追加一条冲正或调整流水,引用原操作并说明原因;历史记录尽量保持不可变,当前库存可通过受控的盘点调整重新对齐。
低库存阈值也应当作为业务数据管理:明确阈值为零时的含义,统一缺省阈值,更新阈值时验证整数范围,并记录是谁修改了它。告警列表和库存概览可以复用同一个阈值规则,避免页面上“显示预警”与实际筛选条件不一致。
权限边界放在服务端
管理端界面隐藏按钮只能改善体验,不能提供授权。库存写入、流水修改、阈值调整和批量操作都应在服务端验证身份及角色;权限检查应由路由中间件或共享的授权层完成,而不是只依赖前端传来的角色字段。对于会影响资金、库存或审计记录的操作,还要验证操作者身份并保留服务端生成的审计信息。
适合复用的工程经验
这类业务项目最值得保留的设计方向,是把库存视作一组可追溯的业务事件,而非一个随手可改的整数:
- 用统一领域模型表达商品、配件和库存流水。
- 让批量、扫码和手工操作共用校验与写入逻辑。
- 把库存更新与流水记录绑定在一个事务里。
- 用数据库锁或条件更新解决并发,不依赖“先读再写”碰运气。
- 用追加式冲正保留历史解释能力。
- 对重试、部分成功、权限和失败恢复设计明确的接口语义。
以上是基于代码结构的静态整理;本文没有连接该项目数据库、运行其服务或验证线上行为。将同类方案用于真实库存前,应补充事务与并发回归测试,并按目标数据库和部署环境验证。
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。