
简介这是一套面向零售与供应链领域开发者、技术团队及企业信息化选型人员的高度可定制供应链中台基础系统全部源码开放可用于商用目的。系统覆盖零售管理、电子商务、供应链、财务、车队、仓库、人员、产品、订单、会员、连锁与加盟等业务模块前端采用React与Ant Design后端基于Java Spring及自有开源框架通过REST接口实现前后端完全分离并兼容MySQL、PostgreSQL及国产数据库南大通用GBase 8s适合需要快速搭建或二次开发供应链平台的团队参考。资源包共6801个文件约28.63MB以Java后端源码、JavaScript与TypeScript前端脚本、Less与SCSS样式文件为主另含JSP页面、Markdown说明文档、SQL脚本及少量图片与配置文件目录结构完整便于按模块检索与二次开发。目前已有436人学习下载可作为供应链系统从架构设计到业务落地的完整参考。1. 全流程供应链系统到底在管什么从一张采购单到最终结算的链路拆解很多团队做供应链系统第一反应是“上个 ERP 就完了”结果上线三个月发现采购、仓储、销售三套数据对不上财务月底关账靠 Excel 手工拼。全流程供应链系统的核心不是某个模块而是把“需求→采购→入库→出库→结算”这条链上的状态变更串成一条可追溯的事件流。标题里“高度可定制”意味着业务对象、审批流、单据字段都能改而不是写死几张表“可用于商用目的”则要求权限隔离、数据审计、并发扣减这些工程底线必须过关。这套东西适合两类人一是中小型贸易或制造企业里被 Excel 和微信群折磨的 IT 负责人二是想拿一套可二次开发的供应链底座做行业定制的开发者。源码在手改的是业务规则不是从零造轮子。2. 先定领域模型再写代码供应链系统的四张核心表怎么设计2.1 为什么先画状态机而不是先建表供应链系统最容易翻车的地方是采购单状态和库存数量各写各的。我一般会先把单据状态机定死再倒推表结构。以采购单为例状态至少要有草稿、待审批、已审批、部分到货、全部到货、已结算、已关闭。每个状态迁移都要有触发条件和操作角色否则后面加审批流时你会发现代码里全是 if-else。状态机定完后表结构自然就出来了主表存单据头明细表存行项目状态变更单独一张流水表。这样查历史、做审计、回滚都有依据。很多开源供应链项目把状态直接塞在主表的一个字段里改一次状态覆盖一次出了问题连“谁在什么时候改的”都查不到这是血泪经验。2.2 四张核心表的字段与关系下面是我在多个模拟项目里反复用到的四张基础表用 SQL 表达。注意这里只给结构具体字段长度和索引按业务量调整。-- 采购单主表一张单一个头 CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 单号业务生成, supplier_id BIGINT NOT NULL COMMENT 供应商ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待审 2已审 3部分到货 4全部到货 5已结算 6关闭, total_amount DECIMAL(14,2) NOT NULL DEFAULT 0, created_by BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_supplier_status (supplier_id, status) ) COMMENT 采购单主表; -- 采购单明细一行一个SKU CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, qty DECIMAL(14,3) NOT NULL COMMENT 采购数量, received_qty DECIMAL(14,3) NOT NULL DEFAULT 0 COMMENT 已到货数量, price DECIMAL(14,4) NOT NULL COMMENT 单价, INDEX idx_order (order_id) ) COMMENT 采购单明细; -- 库存流水只追加不修改 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, change_qty DECIMAL(14,3) NOT NULL COMMENT 正数入库负数出库, biz_type TINYINT NOT NULL COMMENT 1采购入库 2销售出库 3调拨 4盘点, biz_no VARCHAR(32) NOT NULL COMMENT 关联单号, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_sku_warehouse (sku_id, warehouse_id), INDEX idx_biz (biz_type, biz_no) ) COMMENT 库存流水; -- 库存余额由流水汇总可定时重算 CREATE TABLE stock_balance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, qty DECIMAL(14,3) NOT NULL DEFAULT 0, UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id) ) COMMENT 库存余额;逻辑说明采购单主表和明细表是一对多状态只在主表上流转库存流水表只追加任何库存变动都写一条记录余额表是流水表的物化视图。参数上qty用 DECIMAL 而不是 INT是因为很多行业存在拆零和称重场景received_qty单独存是为了支持部分到货避免每次到货都去汇总明细。2.3 可定制字段的落地方式“高度可定制”不是让用户随便加列而是预留扩展字段表。常见做法是给主表加一个ext_json字段存非结构化扩展同时建一张field_define表描述哪些字段可配、类型是什么、是否必填。这样前端渲染表单时读配置后端校验也读配置不用改代码就能加一个“项目编号”字段。CREATE TABLE field_define ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL COMMENT 业务类型如purchase_order, field_key VARCHAR(64) NOT NULL, field_label VARCHAR(64) NOT NULL, field_type VARCHAR(16) NOT NULL COMMENT text/number/date/select, required TINYINT NOT NULL DEFAULT 0, options VARCHAR(512) DEFAULT NULL COMMENT select类型的选项JSON, UNIQUE KEY uk_biz_field (biz_type, field_key) ) COMMENT 可定制字段定义;参数说明biz_type用来区分是采购单还是销售单field_key是程序里用的键field_label是界面显示名。options只在 select 类型时用存 JSON 数组。这样一套配置可以同时服务多个单据类型改字段不用动表结构。3. 从采购到入库的完整链路用代码把状态流转跑通3.1 采购单创建与审批的接口实现先看创建采购单的接口。这里用 Python 伪代码表达重点在事务边界和状态校验。def create_purchase_order(supplier_id, items, operator_id): # 1. 校验供应商是否存在且启用 supplier supplier_repo.get(supplier_id) if not supplier or supplier.status ! 1: raise BizError(供应商不可用) # 2. 校验明细非空且数量为正 if not items: raise BizError(采购明细不能为空) for it in items: if it[qty] 0 or it[price] 0: raise BizError(数量或价格非法) # 3. 生成单号日期序列序列用Redis原子自增 order_no PO today() str(redis.incr(po_seq)) # 4. 事务内写主表和明细 with db.transaction(): order_id purchase_order_repo.insert({ order_no: order_no, supplier_id: supplier_id, status: 0, # 草稿 total_amount: sum(i[qty] * i[price] for i in items), created_by: operator_id }) for it in items: purchase_order_item_repo.insert({ order_id: order_id, sku_id: it[sku_id], qty: it[qty], price: it[price] }) return order_id逻辑说明单号生成用 Redis 自增而不是数据库自增是为了避免高并发下锁表事务只包住写库操作校验放在事务外减少锁持有时间。参数上status初始为 0 表示草稿草稿状态可以随意改提交审批后才进入 1。审批接口的核心是状态机校验# 允许的状态迁移当前状态 - 可迁移到的状态集合 TRANSITIONS { 0: [1], # 草稿 - 待审批 1: [2, 0], # 待审批 - 已审批 或 退回草稿 2: [3, 4, 6], # 已审批 - 部分到货/全部到货/关闭 3: [4, 6], # 部分到货 - 全部到货/关闭 4: [5], # 全部到货 - 已结算 } def approve_order(order_id, action, operator_id): order purchase_order_repo.get(order_id) if action approve and order.status 1: new_status 2 elif action reject and order.status 1: new_status 0 else: raise BizError(当前状态不允许该操作) if new_status not in TRANSITIONS.get(order.status, []): raise BizError(非法状态迁移) with db.transaction(): purchase_order_repo.update_status(order_id, new_status) order_log_repo.insert(order_id, order.status, new_status, operator_id)参数说明TRANSITIONS字典是状态机的核心任何新增状态都要在这里注册否则接口直接拒绝。order_log_repo记录每次变更方便审计。注意退回草稿时要把已审批的痕迹保留不能删日志。3.2 入库扣减库存的并发处理入库是整个链路里最容易出并发问题的地方。常见做法是“先写流水再更新余额”并且余额更新用条件更新防止超卖。def receive_goods(order_id, items, warehouse_id, operator_id): order purchase_order_repo.get(order_id) if order.status not in (2, 3): raise BizError(当前状态不可入库) with db.transaction(): for it in items: # 1. 写库存流水 stock_flow_repo.insert({ sku_id: it[sku_id], warehouse_id: warehouse_id, change_qty: it[qty], biz_type: 1, biz_no: order.order_no }) # 2. 更新余额用 INSERT ... ON DUPLICATE KEY UPDATE stock_balance_repo.upsert_add( it[sku_id], warehouse_id, it[qty] ) # 3. 更新采购明细已到货数量 purchase_order_item_repo.add_received( order_id, it[sku_id], it[qty] ) # 4. 判断是否全部到货更新主表状态 all_received purchase_order_item_repo.check_all_received(order_id) new_status 4 if all_received else 3 purchase_order_repo.update_status(order_id, new_status)逻辑说明upsert_add对应 SQL 的INSERT INTO stock_balance ... ON DUPLICATE KEY UPDATE qty qty VALUES(qty)这样即使两个请求同时到数据库行锁也能保证不丢更新。参数上biz_type1表示采购入库biz_no存采购单号方便反查。注意这里没有用“先查再改”的写法那种写法在并发下必然翻车。3.3 结算与对账的数据一致性校验结算前必须做一次对账核对采购单、入库流水、发票三方的数量金额。我一般会写一个校验任务按供应商和月份跑。-- 核对采购单已到货数量与入库流水汇总是否一致 SELECT poi.order_id, poi.sku_id, poi.received_qty AS order_received, COALESCE(SUM(sf.change_qty), 0) AS flow_received FROM purchase_order_item poi LEFT JOIN stock_flow sf ON sf.biz_no (SELECT order_no FROM purchase_order WHERE id poi.order_id) AND sf.sku_id poi.sku_id AND sf.biz_type 1 GROUP BY poi.order_id, poi.sku_id, poi.received_qty HAVING order_received flow_received;参数说明biz_type1限定采购入库避免把销售出库的负数流水算进来。HAVING过滤出不一致的行这些行就是需要人工介入的差异。这个查询建议每天跑一次差异超过阈值就告警不要等到月底关账才发现。4. 避坑与排查供应链系统上线后最容易踩的五个坑4.1 库存扣减出现负数现象销售出库时库存扣成了负数但系统没有拦截。原因扣减逻辑用的是“先查余额再更新”两个请求同时查到相同余额各自扣减后覆盖。解决改成条件更新UPDATE stock_balance SET qty qty - ? WHERE sku_id ? AND warehouse_id ? AND qty ?根据影响行数判断是否成功失败就抛异常回滚。4.2 单据状态跳变导致流程卡死现象采购单从“已审批”直接变成“已结算”跳过了到货环节。原因状态更新接口没有校验迁移合法性前端传什么状态就写什么状态。解决所有状态变更必须走状态机校验TRANSITIONS字典里没注册的迁移一律拒绝并且记录操作日志。4.3 可定制字段导致查询变慢现象加了十几个扩展字段后列表查询从 200ms 涨到 3 秒。原因扩展字段存在ext_json里查询时用JSON_EXTRACT过滤无法走索引。解决高频查询的扩展字段单独建列并加索引低频的才放 JSON或者建一张扩展字段索引表用触发器或应用层同步。4.4 审批流改动后历史单据无法查看现象改了审批流配置老单据打开报错。原因审批流版本没有和单据绑定新流程去解析老数据。解决每张单据创建时记录flow_version查看时按版本加载对应的流程定义流程定义表只追加不修改。4.5 并发入库时采购单状态更新丢失现象两批货同时到采购单状态只更新了一次另一批的到货数量没算进去。原因状态判断和更新不在同一个事务里或者用了乐观锁但没重试。解决把“更新明细到货数量”和“判断是否全部到货”放在同一个事务并且对主表加行锁SELECT ... FOR UPDATE确保串行化。5. 让系统真正可商用的三个进阶技巧5.1 用事件表替代直接调用解耦模块商用系统里采购入库后可能要触发通知、生成应付、更新报表。如果全写在入库接口里改一个需求就要动核心代码。我一般会加一张domain_event表入库成功后写一条事件其他模块订阅处理。CREATE TABLE domain_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_type VARCHAR(64) NOT NULL COMMENT 如purchase_received, biz_no VARCHAR(32) NOT NULL, payload JSON NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待处理 1已处理 2失败, retry_count INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_status_type (status, event_type) ) COMMENT 领域事件表;参数说明status控制消费进度retry_count限制重试次数超过阈值转人工。这样入库接口只写事件不关心谁消费新增报表模块时不用改入库代码。5.2 用快照表解决历史数据查询单据关联的商品名称、供应商名称会变如果直接 join 当前表历史单据显示的是最新名称。商用系统必须存快照。做法是在明细表里冗余sku_name、supplier_name等字段创建时写入之后不更新。这样查历史单据时不需要 join也避免了名称变更导致的对账歧义。5.3 权限控制到字段级别“高度可定制”往往意味着不同角色看到的字段不同。比如采购员看不到成本价财务看不到库存明细。实现方式是在角色权限表里加field_scope接口返回数据前按角色过滤字段。不要在前端隐藏前端隐藏只是障眼法后端必须过滤。def filter_fields(data, role): allowed role_field_repo.get_allowed_fields(role, data[biz_type]) if not allowed: return data return {k: v for k, v in data.items() if k in allowed}参数说明allowed为空表示不限制否则只返回允许的字段。这个过滤要放在序列化之前确保敏感字段根本不进入响应体。这套系统我前后改过几版最大的教训是不要一开始就追求大而全先把采购入库这条链路跑通状态机和库存流水写对后面加销售、加调拨都是复制模式。源码在手的好处是你可以按自己的业务节奏改但前提是核心表结构和状态机不能乱。希望帮到你。本文还有配套的精品资源点击获取