简介:本资源是一份面向数据库设计初学者与信息系统开发者的进销存管理系统数据库设计教学文档,适用于零售、批发及生产型企业信息化课程实践或毕业设计参考。文档以Word格式(.doc)完整呈现,共1个文件,大小2.54MB,内容结构严谨、覆盖数据库设计全流程核心环节。全文包含需求分析(系统目标、数据需求、组织结构图、功能模块图)、业务与数据流程图(顶层至第二层逐级展开)、详尽数据字典(数据项、数据流、存储、处理逻辑及外部实体定义),以及概念结构设计部分的局部E-R图(销售、采购、报损)与全局E-R图建模过程说明。已有32人学习下载,读者可直接获取规范化的数据库设计方法论、可复用的ER建模思路、标准化的数据字典模板及清晰的分层流程图绘制范例,为后续逻辑设计与系统开发奠定扎实基础。
1. 进销存管理系统数据库设计:不是画完E-R图就完事,而是让每张表在真实业务里扛得住“退货+调价+多仓库+跨月对账”四连击
你手头那份标着“进销存管理系统数据库设计.doc”的文档,大概率正躺在某个项目交接包里吃灰——它可能有漂亮的E-R图、规整的字段列表、甚至带了“符合第三范式”的批注。但真正上线跑三个月后,采购员反馈“同一商品在A仓入库、B仓出库,库存汇总总对不上”;财务说“上月28日做的销售单,月底结账时系统自动把成本算成下月采购价”;老板问“为什么查‘某客户近半年采购总额’要等8秒”。这些问题,90%不是代码慢,而是数据库设计在业务语义建模阶段就埋了雷。这份文档的核心价值,从来不是交差用的Word排版,而是作为一张可执行、可验证、可演进的业务逻辑契约:它必须能清晰表达“一笔采购入库如何触发库存变动+应付账款生成+供应商往来更新”,也必须支撑“销售退货时,原销售单、原出库单、新红字单、库存反向操作、毛利重算”这一整条链路。本文不讲范式理论,只带你用真实进销存场景倒推表结构——从用户信息表的主键陷阱,到价格变动如何不污染历史单据,再到为什么“库存快照表”比“实时sum(出入库)”更可靠。适合正在做ERP模块开发、接手老系统重构,或被业务方反复质疑“为什么这个查询这么慢”的一线工程师。
2. 从E-R图到落地表:先拆解业务动作,再决定字段要不要冗余、索引建在哪
2.1 为什么“用户信息表”不能只按第1关要求建?三个业务动作逼你加字段
很多文档把“用户信息表(user_info)”写成:id, username, password, real_name, phone, create_time。这在博客系统里够用,但在进销存里是灾难起点。我们看三个真实动作:
- 采购下单:采购员A登录系统,选供应商B下单。系统需记录“谁在什么时间、以什么身份(采购员/仓管员/财务)操作了哪笔单据”。仅靠
username无法区分同一人兼任多角色,也无法追溯操作上下文。 - 销售开票:销售员C给客户D开增值税专用发票,税务要求发票上必须显示“开票人”“复核人”“收款人”三类角色,且三者不能为同一人。
real_name字段无法承载角色绑定关系。 - 权限隔离:仓管员只能看到本仓库的库存,采购员只能看到自己负责的供应商列表。
phone字段无法支撑仓库/供应商维度的权限过滤。
所以,必须拆分角色与用户。常见做法是建三张表:
-- 用户主表(只存认证和基础信息) CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account VARCHAR(50) NOT NULL COMMENT '登录账号,唯一', password_hash VARCHAR(128) NOT NULL COMMENT 'bcrypt加密密码', status TINYINT DEFAULT 1 COMMENT '0禁用1启用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 角色定义表(预置采购员/仓管员/财务/销售等) CREATE TABLE sys_role ( id TINYINT PRIMARY KEY, code VARCHAR(20) NOT NULL COMMENT 'role_purchase/role_warehouse等', name VARCHAR(30) NOT NULL COMMENT '采购员/仓管员' ); -- 用户-角色关联表(支持一人多角色) CREATE TABLE user_role_rel ( user_id BIGINT NOT NULL, role_id TINYINT NOT NULL, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES sys_role(id) );提示:
sys_user表中绝不存real_name或phone。这些属于业务属性,应放在employee_profile(员工档案表)中,与sys_user通过user_id关联。原因:外包人员离职后账号禁用,但其历史单据仍需显示姓名;而employee_profile可记录入职/离职时间、所属部门、汇报线等HR域字段,与认证解耦。
2.2 商品表(product)的“价格”字段:为什么必须拆成三张表?
新手常建:product(id, name, unit, price, cost_price, last_update_time)。问题立刻暴露:
- 采购入库时,按本次采购价更新
cost_price→ 历史销售单的成本被篡改; - 销售开单时读取
price→ 促销调价后,已保存未审核的销售单价格错乱; - 财务要查“某商品2023年Q3平均销售单价” → 全表扫描
price字段,结果却是最新价。
正确解法:价格必须版本化、场景化、时效化。建三张表:
-- 商品主表(不含价格) CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(50) NOT NULL COMMENT '商品编码,唯一', name VARCHAR(100) NOT NULL, unit VARCHAR(20) COMMENT '基本单位,如"件"', category_id BIGINT COMMENT '品类ID', status TINYINT DEFAULT 1 COMMENT '0停用1启用' ); -- 采购价格表(按供应商+生效日期) CREATE TABLE purchase_price ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, supplier_id BIGINT NOT NULL, price DECIMAL(12,2) NOT NULL COMMENT '含税采购单价', valid_from DATE NOT NULL COMMENT '生效起始日', valid_to DATE COMMENT '生效截止日,NULL表示长期有效', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_prod_supp_date (product_id, supplier_id, valid_from) ); -- 销售价格表(按客户等级+生效日期) CREATE TABLE sale_price ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, customer_level_id TINYINT COMMENT '客户等级,如VIP/普通', price DECIMAL(12,2) NOT NULL COMMENT '销售单价', valid_from DATE NOT NULL, valid_to DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_prod_level_date (product_id, customer_level_id, valid_from) );逻辑说明:
purchase_price表中valid_from和valid_to构成时间区间,查询“某供应商对某商品当前采购价”时,用WHERE product_id=? AND supplier_id=? AND ? BETWEEN valid_from AND COALESCE(valid_to, '9999-12-31');sale_price支持不同客户等级不同定价,避免销售员手动改价;- 关键点:所有单据(采购单、销售单、入库单、出库单)在保存时,必须将当时有效的价格快照写入单据明细表(如
purchase_order_item.unit_price),而非关联价格表。这样历史单据价格永不变更。
2.3 库存表(inventory)的设计陷阱:为什么不用“实时sum(出入库)”?
常见错误设计:
-- ❌ 错误:用视图或每次查询sum(in_out_log)计算实时库存 CREATE VIEW inventory_current AS SELECT product_id, warehouse_id, SUM(CASE type WHEN 'in' THEN qty ELSE -qty END) AS qty FROM in_out_log GROUP BY product_id, warehouse_id;问题:
- 并发写入时,
in_out_log插入和视图聚合非原子操作,出现超卖; - 查询慢:百万级出入库记录,每次查库存都要全表扫描;
- 无法回溯:某天库存异常,你不知道是哪笔单据导致。
正确方案:库存主表 + 快照日志双驱动
-- ✅ 库存主表(核心状态,高频读写) CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, qty DECIMAL(12,4) NOT NULL DEFAULT 0 COMMENT '当前可用库存', frozen_qty DECIMAL(12,4) DEFAULT 0 COMMENT '冻结库存(如已分配未出库)', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_prod_ware (product_id, warehouse_id) ); -- ✅ 库存变动日志(审计与回滚) CREATE TABLE inventory_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(20) NOT NULL COMMENT 'purchase_in/sale_out/adjust/return', biz_id BIGINT NOT NULL COMMENT '关联单据ID,如purchase_order.id', product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, delta_qty DECIMAL(12,4) NOT NULL COMMENT '变动量,正为入,负为出', before_qty DECIMAL(12,4) NOT NULL COMMENT '变动前库存', after_qty DECIMAL(12,4) NOT NULL COMMENT '变动后库存', operator_id BIGINT NOT NULL COMMENT '操作人ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_biz (biz_type, biz_id), INDEX idx_prod_ware (product_id, warehouse_id) );落地逻辑:
- 所有业务单据(采购入库、销售出库、库存调整、销售退货)在事务内完成两件事:① 更新
inventory表的qty和frozen_qty;② 插入一条inventory_log记录; inventory表是业务系统库存查询的唯一数据源,inventory_log用于审计、对账、异常排查;- 每日凌晨跑一次校验脚本:
SELECT product_id, warehouse_id, SUM(delta_qty) FROM inventory_log GROUP BY product_id, warehouse_id与inventory表对比,不一致则告警——这是你的“后悔药”。
3. 数据流程图不是画给领导看的:它是单据流转的SQL执行路径说明书
3.1 一张采购入库单,触发多少次数据库写入?流程图必须标出事务边界
很多数据流程图(DFD)只画“采购员→采购单→仓库→入库单→库存”,却没标清每个箭头背后是几个SQL、是否跨库、事务是否包含下游。这直接导致线上故障时定位困难。以“采购入库”为例,标准流程图应明确标注:
| 流程节点 | 操作类型 | 数据库动作 | 事务边界 | 关键约束 |
|---|---|---|---|---|
| 采购单审核通过 | 状态更新 | UPDATE purchase_order SET status='approved' WHERE id=? | 单表事务 | status必须为draft才能改 |
| 生成入库单 | 插入+关联 | INSERT INTO stock_in_order (...);INSERT INTO stock_in_item (...) | 单事务 | stock_in_item.product_id必须存在于product表 |
| 执行入库 | 核心事务 | ①UPDATE inventory SET qty=qty+?, update_time=NOW() WHERE product_id=? AND warehouse_id=?;② INSERT INTO inventory_log (...);③ UPDATE purchase_order_item SET received_qty=received_qty+? WHERE id=? | 跨表强一致性事务 | 三步必须全部成功,否则回滚;inventory更新必须带WHERE qty+? >= 0防负库存 |
| 同步应付账款 | 异步消息 | 发送MQ消息触发财务模块建应付单 | 非事务 | 入库成功后发消息,财务模块失败可重试 |
注意:“执行入库”这一步必须用数据库事务保证库存、日志、采购单明细三者原子性。若用应用层事务(如Spring @Transactional),需确认所有表在同一数据库实例;若库存表在独立库,则必须用分布式事务(如Seata)或最终一致性(本地消息表+定时补偿)。
3.2 销售出库的数据流:为什么“可用库存”要单独计算?
销售开单时,界面需实时显示“该商品在所选仓库的可用库存”。很多人直接查inventory.qty - inventory.frozen_qty。但问题来了:
- 仓管员正在处理一笔出库单,已冻结库存但未提交;
- 另一销售员同时开单,查到的
frozen_qty是旧值,导致超卖。
正确数据流:
- 销售开单页面请求
/api/stock/available?product_id=1001&warehouse_id=201; - 后端执行:
-- 查当前库存(含已冻结) SELECT qty, frozen_qty FROM inventory WHERE product_id = 1001 AND warehouse_id = 201; -- 查**未完成**的出库单中已冻结但未出库的数量(即“待出库冻结量”) SELECT IFNULL(SUM(item.qty), 0) AS pending_freeze FROM stock_out_order ord JOIN stock_out_item item ON ord.id = item.order_id WHERE ord.status IN ('created', 'approved') AND item.product_id = 1001 AND ord.warehouse_id = 201; - 返回
available = qty - frozen_qty - pending_freeze。
这个pending_freeze必须从未完成的出库单中实时计算,而非存在inventory表中——因为冻结状态是动态的,随单据状态变化。
3.3 对账流程的数据流向:财务月结时,数据库如何避免锁表?
财务每月最后一天23:59要跑“库存月结”,生成期初/期末库存、当月出入库汇总。若直接SELECT SUM(...) FROM inventory_log WHERE create_time BETWEEN '2024-05-01' AND '2024-05-31',百万级日志表会锁表数分钟。
优化路径:
- 预计算汇总表:每日凌晨跑任务,将昨日数据按
product_id+warehouse_id+date聚合到inventory_daily_summary表; - 月结时只查汇总表:
SELECT * FROM inventory_daily_summary WHERE date >= '2024-05-01' AND date <= '2024-05-31'; - 汇总表结构:
CREATE TABLE inventory_daily_summary ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, date DATE NOT NULL, begin_qty DECIMAL(12,4) NOT NULL COMMENT '当日期初', in_qty DECIMAL(12,4) DEFAULT 0, out_qty DECIMAL(12,4) DEFAULT 0, end_qty DECIMAL(12,4) NOT NULL COMMENT '当日期末', UNIQUE KEY uk_prod_ware_date (product_id, warehouse_id, date) );
提示:
inventory_daily_summary的end_qty必须等于begin_qty + in_qty - out_qty,每日任务需校验此等式,不成立则告警——这是防止数据漂移的“安全阀”。
4. 数据字典不是字段说明书:它是开发、测试、运维三方对齐业务语义的宪法
4.1 字段命名必须带业务上下文,拒绝“status”“type”这类黑匣子
status字段在进销存里至少有5种含义:
purchase_order.status:draft/approved/received/closedstock_in_order.status:created/approved/done/canceledproduct.status:enabled/disabled/obsoleteinventory_log.biz_type:purchase_in/sale_out/adjust/returnsys_user.status:active/inactive/locked
若统一用TINYINT+字典表,开发查sys_dict要翻10页,测试写SQL要猜枚举值,运维看日志根本不知status=2代表什么。数据字典必须为每个字段定义独立枚举集:
| 表名 | 字段名 | 数据类型 | 取值范围 | 业务含义 | 示例值 | 是否可空 |
|---|---|---|---|---|---|---|
purchase_order | status | ENUM('draft','approved','received','closed') | 'draft':草稿;'approved':已审核;'received':已收货;'closed':已关闭 | 'approved' | 否 | |
inventory_log | biz_type | ENUM('purchase_in','sale_out','adjust','return') | 'purchase_in':采购入库;'sale_out':销售出库;'adjust':库存调整;'return':销售退货 | 'sale_out' | 否 | |
product | unit | VARCHAR(20) | '件','箱','千克','升','米' | 计量单位,影响换算 | '件' | 否 |
注意:MySQL 8.0+ 支持
ENUM,但为兼容性,生产环境常用VARCHAR+应用层校验。关键是数据字典文档必须明确列出所有合法值及含义,禁止“详见字典表”这种甩锅写法。
4.2 “金额”字段必须声明精度、币种、是否含税,否则财务系统直接拒收
DECIMAL(12,2)看似通用,但在进销存里是危险信号:
- 采购价含13%增值税,销售价含6%增值税,成本核算需分离税额;
- 外币结算(如USD采购),汇率变动影响损益;
- 促销满减、优惠券抵扣,需记录原始价、折后价、实付价。
正确字段设计:
-- 采购订单明细 CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, qty DECIMAL(12,4) NOT NULL, unit_price DECIMAL(12,4) NOT NULL COMMENT '不含税单价', tax_rate DECIMAL(5,4) DEFAULT 0.13 COMMENT '税率,如0.13', amount DECIMAL(12,4) NOT NULL COMMENT '不含税金额 = qty * unit_price', tax_amount DECIMAL(12,4) NOT NULL COMMENT '税额 = amount * tax_rate', total_amount DECIMAL(12,4) NOT NULL COMMENT '价税合计 = amount + tax_amount', currency CHAR(3) DEFAULT 'CNY' COMMENT '币种,ISO 4217代码' );数据字典对应条目:
| 字段名 | 类型 | 精度 | 币种 | 含税标识 | 业务规则 |
|---|---|---|---|---|---|
unit_price | DECIMAL(12,4) | 小数4位 | currency字段值 | 不含税 | 采购合同约定单价,不随汇率变动 |
tax_amount | DECIMAL(12,4) | 小数4位 | 同currency | 含税部分 | amount * tax_rate,四舍五入到小数4位 |
currency | CHAR(3) | — | — | — | 必须为ISO 4217标准码,如'CNY','USD','EUR' |
4.3 时间字段必须明确时区和业务意义,别让“create_time”变成玄学
create_time DATETIME DEFAULT CURRENT_TIMESTAMP是最大坑:
- 服务器时区为UTC,但业务要求所有时间按东八区显示;
create_time记录的是“系统创建时间”,但业务需要“业务发生时间”(如采购单的“预计到货时间”、销售单的“承诺发货时间”);- 审计要求记录“最后修改人”和“最后修改时间”,但
update_time默认ON UPDATE会覆盖人工修改痕迹。
数据字典强制规范:
| 字段名 | 类型 | 时区 | 业务含义 | 默认值 | 是否可空 |
|---|---|---|---|---|---|
create_time | DATETIME | 数据库服务器时区 | 记录插入数据库的物理时间 | CURRENT_TIMESTAMP | 否 |
biz_time | DATETIME | 业务约定时区(如Asia/Shanghai) | 业务动作发生时间,如采购单的“下单日期”、销售单的“开单日期” | 业务传入,不可为空 | 否 |
update_time | DATETIME | 同create_time | 最后一次人工修改时间(非自动更新) | NULL | 是 |
updated_by | BIGINT | — | 最后修改人ID | NULL | 是 |
落地代码(MyBatis-Plus):
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; // 自动填充 @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime bizTime; // 业务时间,插入/更新时由业务代码设置 @TableField(fill = FieldFill.UPDATE) private LocalDateTime updateTime; // 仅更新时填充 @TableField(fill = FieldFill.UPDATE) private Long updatedBy; // 仅更新时填充5. 避坑指南:那些让进销存数据库在上线后集体翻车的5个血泪经验
5.1 现象:销售出库时库存扣减正确,但财务对账发现成本结转金额不对
原因:成本核算采用“加权平均法”,但库存变动日志(inventory_log)中delta_qty和before_qty未同步记录对应的成本单价。财务模块从日志反向计算成本时,用的是当前库存均价,而非出库当时的实际成本。
解决:在inventory_log表中增加cost_unit_price DECIMAL(12,4)字段,每次出库时,将该商品在该仓库的当前加权平均成本写入此字段。财务对账时,出库成本 = delta_qty * cost_unit_price。
5.2 现象:多仓库调拨单提交后,源仓库库存减少,目标仓库库存未增加
原因:调拨单涉及两个仓库的库存更新,但事务中只对源仓库UPDATE inventory,目标仓库的更新被遗漏,或因warehouse_id条件写错(如用source_warehouse_id更新了目标表)。
解决:调拨单的库存更新必须在一个事务内完成两次UPDATE,且必须显式指定WHERE warehouse_id = ?,禁止用变量名模糊匹配。代码中写:
UPDATE inventory SET qty = qty - ? WHERE product_id = ? AND warehouse_id = ?; -- 源仓 UPDATE inventory SET qty = qty + ? WHERE product_id = ? AND warehouse_id = ?; -- 目标仓并在SQL执行后,用ROW_COUNT()检查是否都影响了1行,否则抛异常回滚。
5.3 现象:导出“某客户年度采购报表”时,MySQL内存溢出(OOM)
原因:报表SQL使用JOIN连接purchase_order、purchase_order_item、product、supplier四张表,且未加LIMIT,数据量达百万级。MySQL临时表超出tmp_table_size限制。
解决:
- 分页导出:前端分页,后端SQL加
LIMIT ? OFFSET ?; - 物化中间结果:先查出客户所有采购单ID到临时表
temp_customer_orders,再用IN (SELECT id FROM temp_customer_orders)关联明细; - 最有效:建立覆盖索引:
ALTER TABLE purchase_order_item ADD INDEX idx_cust_prod_time (order_id, product_id, create_time);,让JOIN走索引而不回表。
5.4 现象:凌晨库存月结任务运行时,销售开单接口超时
原因:月结任务执行SELECT ... FROM inventory_log WHERE date BETWEEN ...全表扫描,持有inventory_log表的共享锁,阻塞了销售出库时的INSERT INTO inventory_log。
解决:
- 月结任务改用
READ UNCOMMITTED隔离级别(因日志表无更新,脏读无风险); - 或更彻底:将
inventory_log按月分表(inventory_log_202405),月结只查当月表; - 必须做:在
inventory_log表的date字段上建索引:CREATE INDEX idx_date ON inventory_log(date);。
5.5 现象:供应商A的采购价更新后,历史采购单的“采购金额”显示为新价格
原因:采购单明细表(purchase_order_item)的unit_price字段未存储快照,而是通过JOIN purchase_price动态查询,导致价格表更新后,历史单据价格“漂移”。
解决:
purchase_order_item.unit_price必须为NOT NULL,且在采购单保存时,从purchase_price表查出当时有效的价格,硬编码写入;- 在
purchase_order_item表上加约束:CHECK (unit_price > 0),防止零价入库; - 增加校验脚本:定期扫描
purchase_order_item,比对unit_price与purchase_price历史价格,不一致则告警。
6. 进阶技巧:用数据字典自动生成校验代码,把业务规则焊死在数据库层
6.1 为什么手工写MyBatis校验逻辑注定失败?
你写过这样的代码吗?
if (order.getStatus().equals("approved") && item.getQty() > inventory.getQty()) { throw new BizException("库存不足"); }问题在于:
getStatus()返回字符串,IDE无法提示合法值;inventory.getQty()是实时查的,但并发时可能刚查完就被其他单据扣减;- 规则散落在Service层,新人看不懂哪里校验、哪里不校验。
真正的防线应该在数据库层——用CHECK约束、触发器、或应用层基于数据字典生成的强类型校验。
6.2 用数据字典生成Java枚举,让status不再裸奔
假设数据字典中purchase_order.status定义为:
取值范围: ['draft','approved','received','closed'] 业务含义: draft=草稿, approved=已审核, received=已收货, closed=已关闭用Python脚本(gen_enum.py)自动生成Java枚举:
# gen_enum.py import json # 从data_dict.json读取字段定义 with open('data_dict.json', 'r', encoding='utf-8') as f: data_dict = json.load(f) for table in data_dict['tables']: for field in table['fields']: if field['name'] == 'status' and table['name'] == 'purchase_order': enum_name = f"{table['name'].replace('_', '').title()}Status" values = [v.split('=')[0].strip() for v in field['values'].split(',')] meanings = [v.split('=')[1].strip() for v in field['values'].split(',')] with open(f'{enum_name}.java', 'w', encoding='utf-8') as f: f.write(f"public enum {enum_name} {{\n") for i, val in enumerate(values): f.write(f" {val.upper()}(\"{meanings[i]}\"),\n") f.write(" ;\n") f.write(" private final String desc;\n") f.write(" private " + enum_name + "(String desc) { this.desc = desc; }\n") f.write(" public String getDesc() { return desc; }\n") f.write("}\n")生成PurchaseOrderStatus.java:
public enum PurchaseOrderStatus { DRAFT("草稿"), APPROVED("已审核"), RECEIVED("已收货"), CLOSED("已关闭"), ; private final String desc; private PurchaseOrderStatus(String desc) { this.desc = desc; } public String getDesc() { return desc; } }然后在实体类中强制使用:
public class PurchaseOrder { private PurchaseOrderStatus status; // 不再是String! // getter/setter... }效果:编译期报错
order.setStatus("approved")(String不能赋值给枚举),必须写order.setStatus(PurchaseOrderStatus.APPROVED)。IDE自动提示所有合法值,测试用例覆盖所有枚举项,业务规则从此不会漏。
6.3 用数据字典驱动SQL模板,消灭手写JOIN的低级错误
数据字典中inventory_log表定义:
关联字段: biz_type → biz_type_dict.code, biz_id → purchase_order.id OR stock_out_order.id OR ...据此生成MyBatis SQL片段:
<!-- inventory_log_mapper.xml --> <sql id="join_biz_table"> <choose> <when test="bizType == 'purchase_in'"> LEFT JOIN purchase_order po ON il.biz_id = po.id AND il.biz_type = 'purchase_in' </when> <when test="bizType == 'sale_out'"> LEFT JOIN stock_out_order soo ON il.biz_id = soo.id AND il.biz_type = 'sale_out' </when> <otherwise> <!-- 兜底,但业务不应走到这里 --> </otherwise> </choose> </sql>调用时:
<select id="selectWithBizInfo" resultType="Map"> SELECT il.*, <include refid="join_biz_table"/> FROM inventory_log il <where> il.biz_type = #{bizType} </where> </select>这样,biz_id关联哪张表,完全由biz_type字段值决定,无需开发者记忆“purchase_in对应purchase_order”,也不会写错JOIN条件。
6.4 把数据字典变成测试用例生成器:覆盖所有状态流转
进销存核心是状态机。purchase_order的状态流转图是:draft → approved → received → closeddraft → canceledapproved → canceled
用数据字典中的状态定义,自动生成JUnit测试:
// PurchaseOrderStatusTest.java(自动生成) @Test void testStatusTransition() { PurchaseOrder order = new PurchaseOrder(); // 草稿可审核或作废 order.setStatus(PurchaseOrderStatus.DRAFT); order.approve(); // 合法 assertEquals(PurchaseOrderStatus.APPROVED, order.getStatus()); order.setStatus(PurchaseOrderStatus.DRAFT); order.cancel(); // 合法 assertEquals(PurchaseOrderStatus.CANCELED, order.getStatus()); // 已审核不可再作废(业务规则) order.setStatus(PurchaseOrderStatus.APPROVED); assertThrows(BizException.class, () -> order.cancel()); }我的习惯是:每次修改数据字典的状态定义,就运行一次生成脚本,把新测试用例注入CI流水线。上线前,所有状态流转必须100%覆盖。这比写文档靠谱一万倍——因为文档会过期,而测试用例跑不过,构建就失败。
希望帮到你。
本文还有配套的精品资源,点击获取