《看潮企业管理软件》做到第8步,终于轮到数据字典了。这个环节在项目开发里往往被低估,很多人觉得无非就是建表、列字段,实际动手才发现,数据字典定得清不清楚,直接决定后面业务逻辑写不写得顺手。我这次做的数据字典是分三部分推进的,这篇先讲第1部分:基础表结构、字段规范、值域约束怎么落地。不管是你在用若依这类快速开发框架,还是从零手写Spring Boot项目,这套思路都能直接抄作业。顺便说一句,“编程与数学”这个系列我在写的时候一直强调一件事:数据字典本质上就是一套数学上的映射关系——字段名对应业务含义,枚举值对应业务状态,表间主外键对应实体关系。把这个映射想明白,后续开发就能少走很多弯路。
1. 看潮项目里的数据字典到底在做什么
1.1 数据字典解决了什么实际问题
企业管理软件和普通个人项目最大的区别,就是实体多、状态多、字段杂。看潮这个项目涉及的模块包括客户管理、商品管理、采购订单、销售出库、库存台账、应收应付,光是把这些模块的表理顺,就已经是一份不小的工程量。如果没有一份统一的数据字典,开发到后期最常见的场面是:A开发在订单表里写了 orderType 表示订单类型,B开发在同一个表里写 order_status 表示状态,两个字段看着像又不一样,前端下拉框里的值跟后端枚举对不上,接口文档又没更新,最后联调的时候全乱套。
数据字典干的事情,就是把这些混乱消灭在设计阶段。它不只是一张表结构清单,而是一份包含字段名、字段类型、长度精度、是否必填、默认值、值域范围、业务含义、关联关系的完整元数据文档。在项目里,我通常把它拆成三个层级:第一层是表清单,描述系统有哪些业务表;第二层是字段清单,描述每张表里有哪些列;第三层是值域约束,描述状态字段、类型字段到底允许填哪些值。数据字典 3-1 这个标题,对应的就是这三层里的第一、二层,重点在建表和字段定义。
1.2 看潮项目为什么选用关系型数据库建模
看潮的定位是中小型企业的内部管理软件,核心特征是事务性强、数据一致性要求高、报表查询相对固定。这种场景天然适合关系型数据库,MySQL或者PostgreSQL都能很好地支撑。我也看到市面上有些项目为了求新,把核心业务数据扔进文档型数据库或者对象存储,前期确实爽,后面做多表关联查询、做审计追踪的时候就会痛苦。
所以我在设计看潮的数据模型时,坚持了几个原则:
- 每张业务表必须有明确的主键,优先使用自增ID或者雪花ID。
- 状态字段用小型整数或短字符串,值域统一维护在数据字典表里,不散落在代码里。
- 金额字段统一用 DECIMAL,精确到两位小数,绝不使用浮点数。
- 创建时间、更新时间、创建人、更新人作为标准审计字段,每张业务表都带上。
- 凡是出现了“一对多”或“多对多”的业务概念,单独建关联表,不通过逗号分隔存ID。
这些原则听起来像是教科书上的老生常谈,但实际项目里能严格执行的并不多。看潮这个项目从立项开始就把数据字典当成一等公民来对待,所以后面写 CRUD、写报表、写权限过滤的时候,几乎没有因为表结构设计不合理而返工过。
1.3 数据字典 3-1 的具体范围
在这个系列里,数据字典我拆成了三个部分。3-1 是第一篇,锚定的是基础数据结构和字段定义;3-2 会讲字典表如何与后端接口联动、如何做数据权限;3-3 会讲变更管理,也就是表结构迭代时数据字典怎么跟着演进。
如果你正在写一个项目,不用急着一次把数据字典做完美,先抓住最核心的两件事:一是把每张主表和核心流水表的字段都定义清楚,二是把枚举值的取值规则写明白。其他像字段索引、查询计划优化、冗余字段设计,可以在开发过程中逐步补充。这篇文章里,我会以看潮项目的用户、客户、商品、订单四张核心表为例,把数据字典从无到有搭一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字段设计之前,先把表间关系理清楚
2.1 用实体关系图给数据字典打底
我见过不少开发者,包括以前的我,都是直接上手建表,一边建一边想字段,建到一半发现表间关系对不上,然后再回头改。这种做法在小项目里能撑住,但到了看潮这种有十几个模块的中型项目,基本就是灾难。正确的顺序是:先梳理实体关系,再写物理表结构。
拿看潮项目来说,我在写数据字典之前,先用草稿画了一张实体关系图,核心实体大概有这些:
- 用户(User):登录后台系统的账号,归属某个部门,有角色。
- 角色(Role):权限的集合,一个用户可以有多个角色。
- 客户(Customer):销售模块的主体,包含联系人、电话、地址。
- 供应商(Supplier):采购模块的主体。
- 商品(Product):也叫物料,包含规格、单位、价格。
- 采购订单(PurchaseOrder):记录从供应商进货的单据。
- 采购订单明细(PurchaseOrderItem):一个订单包含多个商品,每条明细对应一个商品。
- 销售订单(SalesOrder):记录给客户出货的单据。
- 销售订单明细(SalesOrderItem):一个销售单包含多个商品。
画完关系图之后,很清楚就能看到哪些是主表,哪些是从表,哪些是字典表。用户和角色属于系统权限域;客户、供应商属于基础资料域;商品属于物料域;采购和销售订单属于业务流水域。每个域的内部表之间关系比较紧密,域与域之间的关联通过ID外键体现。数据字典不是把所有字段平铺在一张纸上,而是按域去组织,这样可读性和维护性都会好很多。
2.2 主外键关系的数学视角:为什么不能偷懒
第8步讲到数据字典,我想特别说一个数学概念:表的关联关系本质上是集合之间的映射。客户表的每一行是一个实体,订单表的每一行是另一个实体,订单表里的 customer_id 指向客户表,这就是从订单集合到客户集合的一个函数映射。既然它是函数映射,那就必须保证它总是有定义、不会指向不存在的元素,这在数据库里对应的就是外键约束或者应用层校验。
有些项目为了性能,习惯去掉物理外键,只用逻辑外键,也就是在订单表写 customer_id 但不加 FOREIGN KEY 约束,全靠开发人员自觉。实测下来,初期没有外键确实让插入和删除变快了,但随着数据量增加,孤儿数据满天飞。比如客户被删掉,订单还指着那个不存在的 ID,报表统计时多出来一堆脏数据。
看潮项目我采用了折中方案:主键和外键字段在数据字典中明确标注,数据库层面对核心关联表保留外键约束,对高频写入的流水表用索引加逻辑外键。这样既保证了数据一致性,又不会因为过度约束拖慢写入性能。需要说明的是:逻辑外键的可靠性完全依赖数据字典中“关联字段”这一列的正确填写,这也是为什么数据字典不能只写字段名,必须把 referenced_table 和 referenced_column 写出来。
2.3 范式与反范式的取舍
数据字典设计绕不开数据库范式。第三范式要求每个非主属性完全函数依赖于主键,不能有传递依赖。看潮项目在设计商品表时,就遇到一个典型问题:商品表里要不要直接放分类名称?如果只放 category_id,查商品列表的时候需要 JOIN 分类表,多一次关联;如果同时放 category_id 和 category_name,则存在数据冗余,一旦分类改名,商品表里的名称也得同步更新。
数学上,这就是一个函数依赖和同步一致性之间的权衡。我的选择是:基础资料表严格遵循第三范式,只存 category_id;但业务流水表允许适度冗余。比如销售订单明细里,除了存 product_id,我还会冗余存一份 product_name 和 product_price。这样做的好处是,订单一旦生成,商品后续改价、改名不会影响历史订单的展示。这是反范式的典型应用,前提是必须在数据字典中明确标注该字段是“冗余快照字段”,并在代码里规定只有创建订单时写入,不允许后续UPDATE。
3. 看潮核心表的数据字典手把手搭建
3.1 用户权限域:用户表和角色表
先看用户表。用户表是系统登录的基础,字段设计直接关系到登录认证、权限过滤、审计日志。看潮项目用户表 sys_user 我定义成下面这样:
| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 |
|---|---|---|---|---|---|
| user_id | BIGINT | 20 | 否 | 自增 | 主键 |
| username | VARCHAR | 50 | 否 | 无 | 登录名,唯一索引 |
| password | VARCHAR | 100 | 否 | 无 | BCrypt加密存储 |
| real_name | VARCHAR | 50 | 是 | 无 | 真实姓名 |
| dept_id | BIGINT | 20 | 是 | 无 | 关联部门表 |
| status | TINYINT | 4 | 否 | 0 | 0正常 1停用 |
| avatar | VARCHAR | 255 | 是 | 无 | 头像路径 |
| remark | VARCHAR | 500 | 是 | 无 | 备注 |
| create_by | VARCHAR | 50 | 否 | 无 | 创建人 |
| create_time | DATETIME | 无 | 否 | CURRENT_TIMESTAMP | 创建时间 |
| update_by | VARCHAR | 50 | 否 | 无 | 更新人 |
| update_time | DATETIME | 无 | 否 | CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
这里面有几个经验性的细节。密码字段长度设为100,而不是常见的50,是因为 BCrypt 编码后的字符串长度有60位,如果字段长度不够,注册新用户时会出现无法解释的插入失败。status 字段用 TINYINT 存储枚举值,这是数据字典值域约束的核心体现,我会在后面的字典表里维护“0代表正常、1代表停用”的映射关系。
角色表结构相对简单,role_id、role_name、role_key、status、remark 等字段,这里不再单独展开。关键的是用户和角色的关联关系。一个用户可以有多个角色,一个角色也可以分配给多个用户,典型的“多对多”关系,需要一张中间表 sys_user_role,包含 user_id 和 role_id 两个字段,联合主键。为什么不能直接在用户表里放 role_id 的逗号分隔串?第一,查询某角色下所有用户时没法走索引;第二,更新角色时会产生并发写冲突;第三,数据库层面无法保证关联完整性。从集合论的视角,多对多关系本来就该拆成“中间关系表”来承载,这是关系模型的基本要求。
3.2 物料域:客户、供应商、商品
客户表和供应商表结构上高度相似,都有名称、联系人、电话、地址、状态这些字段。看潮项目我没有做成一张“往来单位表”加一个类型字段,而是拆成两张独立表。原因是客户和供应商虽然字段相似,但后续挂接的业务模块不同,客户挂在销售订单上,供应商挂在采购订单上,各自的扩展属性也不同,硬合成一张表反而会让查询容易出现“类型判断”的脏代码。
商品表是物料域的核心。我在设计时给商品表加了很多关键约束:
product_code唯一约束,这是商品编码,业务上要求每个编码对应一个SKU。product_name普通索引,方便按名称模糊搜索。category_id关联分类表,分类表是树形结构,用parent_id表达层级。specification存储规格描述,比如“500ml/瓶”。unit存储计量单位编码,对应字典表。sale_price和purchase_price使用 DECIMAL(10,2),保证金额精度。stock_warning设置库存预警阈值,用于后续的库存监控。
这里特别说一下 DECIMAL 的精度选择。DECIMAL(10,2) 表示数值总位数10位,小数部分2位,整数部分最多8位,也就是最大 99999999.99,对于中小型企业的商品单价和订单金额完全够用。如果你做的是大型集团财务系统,金额可能会过亿甚至更高,这时候建议把整数位数留到10位甚至12位,也就是 DECIMAL(14,2) 或 DECIMAL(16,2)。数值字段的长度在数据字典里写清楚,就是为了避免后端实体类用 BigDecimal 时因为精度不一致导致四舍五入的差异。
3.3 业务流水域:采购订单和销售订单
业务流水表和基础资料表最大的不同是,流水表一旦生成基本不再修改,新增记录随时间增长非常快,而且经常要按日期范围查询。所以在设计订单相关表时,我在字段层面做了很多针对性的处理。
purchase_order 采购订单主表字段包括:order_id、order_no、supplier_id、order_date、total_amount、status、remark、审计字段。order_no 是业务单号,可以通过代码生成,比如“PO20231201001”,格式是“PO+年月日+三位流水号”。单号设计看起来简单,其实有讲究,它需要满足两个条件:可读性好,能从单号直接看出业务类型和下单日期;唯一性有保障,不能因为并发插入出现重复。我的做法是单号字段加唯一索引,生成逻辑放在后端,先用日期+随机序列生成,插入时遇到唯一冲突就重新生成。
purchase_order_item 采购订单明细表字段包括:item_id、order_id、product_id、product_name、quantity、price、amount、tax_rate、tax_amount。其中 product_name 是明显的数据冗余,但它保存的是下单时的商品名称快照,后续商品改名也不会影响历史单据的展示。对于这个冗余字段,数据字典里必须用注释写清楚“快照字段,仅在新增时填充”。
金额计算上,明细表的 amount 等于 quantity 乘以 price,主表的 total_amount 等于所有明细 amount 之和。这个计算逻辑在数据字典里看起来是简单的算术,但在代码实现里有一个常见的坑:前端传过来的 total_amount 如果直接入库,恶意用户完全可以伪造订单总金额。正确做法是后端从明细明细里重新累加,覆盖前端传的值。这就涉及到数学中的求和运算与数据校验的结合,也是数据字典注释里应该提醒“后端必须重算”的原因。
销售订单表的设计思路与采购订单一致,这里不再重复。与采购订单不同的是,销售订单出库后,要联动更新库存台账,这一部分会放到后面库存模块的篇章里详细写。
4. 把数据字典翻译成建表脚本和代码
4.1 从字典到 MySQL 建表语句
数据字典写得再漂亮,最终还是要落成 SQL。看潮项目使用 MySQL 8.0,字符集默认 utf8mb4,排序规则 utf8mb4_general_ci。我在生成建表脚本时,会严格按照数据字典的字段表来写,并且给每张表、每个字段都加上 COMMENT,这样后续开发只要打开数据库客户端就能看懂字段含义,不用再翻文档。
sql复制CREATE TABLE `sys_user` (
`user_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID,主键',
`username` VARCHAR(50) NOT NULL COMMENT '登录名,唯一',
`password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码',
`real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
`dept_id` BIGINT DEFAULT NULL COMMENT '部门ID,关联sys_dept.dept_id',
`status` TINYINT DEFAULT 0 COMMENT '状态:0正常,1停用',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像路径',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
`create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
在建表时有一个实用技巧:把数据字典里的“说明”列直接复制到 COMMENT 中。比如 status 字段,数据字典里写了“0正常 1停用”,我在 COMMENT 里也写了同样内容,这样当 Java 枚举类、前端下拉框、接口文档里的注释对不上时,数据库注释就是最后的仲裁依据。
有朋友可能会觉得用 Navicat 或者数据库迁移工具直接可视化建表更省事,我也同意。但至少要把最终的表结构以 SQL 脚本的形式保存到代码仓库里,否则新的开发人员拉代码后,本地没有表结构,只能找老同事拷数据库,效率非常低。
4.2 Spring Boot 实体类里的字段映射
看潮项目后端基于 Spring Boot 3 + MyBatis-Plus。数据字典落在实体类上时,有几个地方必须保持和字典严格一致:实体字段名、数据库字段名、类型、注释。MyBatis-Plus 默认开启驼峰映射,所以数据库的 real_name 会自动映射到 Java 的 realName,这一点我一般不在实体类上写多余的 @TableField,除非字段名无法按驼峰规则对应。
java复制@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long userId;
private String username;
private String password;
private String realName;
private Long deptId;
private Integer status;
private String avatar;
private String remark;
private String createBy;
private LocalDateTime createTime;
private String updateBy;
private LocalDateTime updateTime;
}
这里容易踩坑的是 create_time 和 update_time 这种 DATETIME 字段。Java 侧如果用 java.util.Date,在 JSON 序列化时会默认输出时间戳,前端还要做转换。我建议直接用 LocalDateTime,配合 Jackson 的 yyyy-MM-dd HH:mm:ss 格式化,接口输出更友好。
金额字段在实体类里对应 BigDecimal,绝对不能使用 double 或 float。原因很直白:浮点数在二进制环境下无法精确表示十进制小数,比如 0.1 在 IEEE 754 标准下是一个无限循环小数,累加多次后会产生肉眼可见的误差。在金额结算这种场景,0.1 的误差都不可接受。这是编程里最经典的“看起来是细节、其实是数学原理”的案例,也是我在这个系列里反复强调编程和数学关系的原因之一。
4.3 前端下拉框如何对接字典值
看潮项目前端使用 Vue 3 + Element Plus。页面里最常见的操作就是渲染下拉框,比如用户状态、订单状态、商品单位、客户类型。如果每个下拉框都在页面代码里写死一个数组,那数据字典就白做了。正确做法是维护一张字典表,前端通过后端接口一次性加载所有字典数据,然后按字典类型筛选。
后端可以提供一个简单的字典接口,伪代码如下:
java复制@GetMapping("/dict/data/{dictType}")
public Result<List<DictData>> getDictData(@PathVariable String dictType) {
return Result.ok(dictDataService.listByType(dictType));
}
前端在页面里调用这个接口,拿到 label 和 value,渲染下拉框。当业务上需要新增一个新的枚举值,比如给订单状态增加一个“已取消”,只需要在数据库字典表里插入一行,不用改后端代码,也不用发前端版本,刷新页面就生效了。
用数据字典驱动的下拉框还有一个好处,就是报表导出时可以直接把 status=2 翻译成“已取消”,不用在导出逻辑里写一堆 switch-case。这也是企业管理软件里常说的“代码与数据分离”,字段取值规则从代码里剥离,集中存到字典表,维护成本大幅降低。
5. 实战中踩过的坑和排查指南
5.1 字段长度不足导致插入失败
第一次给看潮写用户注册页面时,注册接口一直报数据库异常,排查了半天发现是密码字段长度只有 50。当时用 BCrypt 加密后密码长度是 60 个字符,插进 VARCHAR(50) 字段里直接被 MySQL 以严格模式拒绝。这个问题不只在密码字段容易出现,手机号、身份证号、银行卡号、描述文本等字段都容易踩。我的经验是:数据字典里每个字符串字段的长度,一定要根据实际业务场景去上限来定,不要随手写 50 或 255。VARCHAR 在 MySQL 里的长度是字符数,一个中文算一个字符,所以长度为 50 的字段可以存 50 个汉字,这和其他数据库不同,设计时要留心。
5.2 枚举值乱用导致统计数据失真
另一个常见问题是状态字段的取值不统一,有的地方用 0 和 1,有的地方用 1 和 2。如果数据字典里没有统一约定,统计报表就会出问题。比如有一个销售报表统计“有效订单”,开发A认为是 status != 3,开发B认为是 status IN (0, 1),两个口径统计出来的数字完全不同,业务方拿着两版报表一对账,直接炸锅。
解决办法是在数据字典中给所有状态字段定义完整值域,并且把这些值域汇总成一个枚举对照表,放到项目文档首页。同时,后端代码里不要散落魔法数字,而要用枚举类统一管理:
java复制public enum OrderStatus {
DRAFT(0, "草稿"),
CONFIRMED(1, "已确认"),
DELIVERED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELED(4, "已取消");
private final Integer value;
private final String desc;
OrderStatus(Integer value, String desc) {
this.value = value;
this.desc = desc;
}
}
这样写的好处是,编译期就能发现拼写错误,业务代码里也一眼能看出每个状态含义,不会出现 if (order.getStatus() == 2) 这种让人摸不着头脑的代码。
5.3 慢查询与缺索引
数据字典里除了字段定义,索引规划也是重要内容。看潮项目上线一段时间后发现销售订单查询变慢,排查后发现是按 order_date 和 supplier_id 查询时没有走索引。原来的表只有主键索引,其他字段都是裸查。后来我在数据字典的“索引建议”列里补充了 idx_order_date 和 idx_supplier_id,再通过 ALTER TABLE 添加索引,查询耗时从几秒降到几十毫秒。
这里也分享一个建索引的基本原则:如果某个字段经常出现在 WHERE 条件、JOIN 关联或 ORDER BY 里,就应该放索引。复合索引要遵循最左前缀原则,比如经常用 supplier_id + order_date 组合查询,那就建一个复合索引 (supplier_id, order_date),不要建两个单列索引,否则 MySQL 只能用到其中一个。索引不是越多越好,因为每次插入、更新都要同步维护索引,索引多了写性能会下降。看潮项目里的原则是,核心表的索引数量控制在5个以内,流水表最多再加1到2个针对查询条件的复合索引。
5.4 数据字典变更后如何同步
数据字典不是建完就一劳永逸的。项目开发过程中,字段会加、状态会变、长度会调,如果没有一套变更机制,字典很快就和实践脱节。我的做法是:每次修改表结构,都同步修改数据字典的对应字段,并在字典里增加“变更记录”表,记录变更时间、变更人、变更前后的定义、变更原因。
前面提过,这个系列数据字典的第三部分会重点讲变更管理,我这里先给一个简单模板:
| 变更编号 | 表名 | 变更内容 | 变更前 | 变更后 | 变更人 | 变更日期 |
|---|---|---|---|---|---|---|
| CD-001 | sys_user | password字段长度 | VARCHAR(50) | VARCHAR(100) | 张三 | 2025-11-20 |
这套记录看起来麻烦,但关键时刻能救命。比如线上出现问题,怀疑是某个字段被修改引起的,翻一下变更记录就能快速定位是谁在什么时候改了什么。对企业项目来说,这种可追溯性非常重要。
6. 关于数据字典 3-1 的收尾思考
这一篇内容比较多,从表间关系、字段设计、建表脚本写到实体类和前端字典对接,核心就是把数据字典从概念落到代码。我个人的一个强烈感受是:数据字典看起来是在“写文档”,实际上是在“做设计”,它逼着你把每一张表、每一个字段、每一个状态想清楚,一旦这一步没做好,后面写再多的代码都是在补窟窿。
这次做的数据字典 3-1 主要覆盖了看潮项目里最核心的用户、角色、客户、供应商、商品、采购订单和销售订单这些表。你在自己的项目里做数据字典时,不需要完全照搬我的表结构,但我建议一定保住这几个核心动作:用实体关系图打底、字段命名统一规范、定义清楚值域约束、数值字段用 DECIMAL、每张表带审计字段、数据库 COMMENT 写入完整说明。把这些动作做到位,数据字典的骨架就立住了。
后面我会继续更新数据字典 3-2 和 3-3,重点会放在字典表本身的设计、字典数据与接口权限的联动、以及表结构变更管理上。如果你在看潮项目或者其他企业管理软件项目里遇到了数据字典相关的问题,欢迎在留言区把你的场景发出来,我会挑有代表性的问题补进后面的篇幅里。
