简介:这份《OA办公系统大数据库设计.doc》是一份面向OA办公自动化管理系统设计与开发人员的完整数据库设计说明文档,核心目标是将业务数据分析结果整理为可供开发的计算机模型,从而指导物理数据库的构建。文档选用MSSQL SERVER 2008 R2作为数据库平台,库名为OASYSDB/OA系统数据库,内容涵盖引言、数据字典设计、数据库设计三大部分,其中数据字典对人员编号、姓名、性别、出生年月、部门、岗位、联系电话、工资等核心数据项的类型、位置、描述均有细致定义;数据库设计则涉及系统物理结构、表设计、表间关联、存储过程、触发器及Job设计,为系统开发、验收、评审和测试人员提供统一的数据规范与实现参考。资源为1个doc文档,大小约211KB,目前已吸引144人学习,适合OA系统研发人员、数据库设计人员及相关项目评审人员查阅借鉴。
1. OA办公系统大数据库设计:先搞清楚这套库在解决什么
很多从业者第一次接触 OA 办公系统数据库设计,是从一份《OA办公系统大数据库设计.doc》这样的文档名开始的。这类文档名里的“大”,说的是这套库的表多、关系深、生命周期长——组织、权限、流程、表单、文件、消息都要装进同一套模型里,一次设计就要应对好几年的数据增长。它不是大数据组件,不是 Hadoop、数仓那一套,而是典型的业务关系型数据库设计。好的设计能让 OA 在后续加表单、加审批流、加报表时不动主干;不好的设计会在需求一到就频繁改表,加班补数据。下文按“容量估算 → 组织权限 → 流程表单 → 避坑 → 上线自检”展开,适合正在设计 OA 库、或准备二次开发老 OA 系统的开发者和团队直接参照。
2. 容量与选型:先算账,再谈建表
2.1 为什么这套库是“大数据库”,而不是“大数据”
OA 系统的用户量通常只有几百到几千,日活并发也不会太高,正常企业的 OA 根本不需要互联网级的吞吐设计。真正“大”的是表的数量和关系复杂度:组织架构、权限、流程实例、审批任务、动态表单、文件索引、消息待办,这些模块加起来轻松超过 100 张表。我习惯把这张库的表拆成四类来看:基础数据(用户、部门、岗位、菜单)、权限数据(角色、角色菜单关联、用户角色关联)、业务数据(流程实例、任务、表单、审批记录)、系统数据(文件索引、消息、操作日志)。设计文档的骨架,本质上就是把这四类数据的模型定清楚。
这里有一个常见误区:看到标题里的“大数据”就往分布式上靠。OA 的业务特征是 OLTP 为主、少量报表统计,数据量到千万级已经算很大的部署。在单实例 MySQL 上做合理的表结构、索引和归档策略,远比引入分库分表、消息队列更可靠,成本也更低。我一般会先按单实例方案设计,把容量估算写在最前面,只有当估算结果明确超出单机承受范围时,才考虑按模块拆库。
2.2 容量估算公式:一张表算出库要多大
动手建表前先回答一个问题:这台库预计要装多少数据?不建议对每张表做精确估算,只算最核心的两类:流程实例/任务流水,以及文件索引。流程与表单流水可以按下面的参数估算。
| 参数 | 取值参考 | 说明 |
|---|---|---|
| 在职用户数 | 500 | 按峰值取,包含离职后需要保留的账号 |
| 日活比例 | 60% | OA 的日活比互联网产品高,取 0.5~0.7 比较稳 |
| 人均日交互次数 | 20 | 发起流程、审批、查看待办、传附件等合计 |
| 平均行大小 | 1 KB | 流程实例和任务记录的平均大小 |
| 每年工作日 | 250 | 内部系统按每年 250 个工作日估算 |
| 保留年限 | 5 年 | 企业档案要求的常见保留周期 |
计算公式:容量 ≈ 用户数 × 日活比例 × 人均日交互 × 平均行大小 × 250 × 保留年限。代入上面的取值:500 × 0.6 × 20 × 1 KB × 250 × 5,结果是 7.5 GB 左右。这个量级对任何单实例数据库都没有压力,但它的价值在于让你心里有底:核心业务表五年内也就是千万行以内,建索引、做分页查询时完全不需要考虑分区表。
文件要单独估算。500 个用户,平均每人每月传 2 个 2 MB 附件,一年就是 500 × 2 × 12 × 2 MB,约 24 GB,远超业务表本身。所以文件本体一定不能塞进数据库,库表只保存文件索引信息,这一点在后面的存储设计里会展开。
2.3 存储选型与基础参数:MySQL 8.x 的常规配置
容量算完,接下来是选型。自建 OA 最常见的底座是 MySQL 8.x 单实例,老项目用 SQL Server 的也不少,但设计思路通用。如果从零开始,我一般会固定几项基础参数:引擎统一 InnoDB,事务隔离级别用 READ-COMMITTED,字符集 utf8mb4,排序规则先用 utf8mb4_0900_ai_ci——这是 MySQL 8.0 的默认值,对中英文混合排序表现正常;如果项目对中文排序有特殊要求,不要想当然,造几十条数据实测排序结果再定。时区统一成东八区,连接层指定 time_zone,避免半夜跑批时时间错乱。
内存参数里最重要的是 innodb_buffer_pool_size,按物理内存的 60%~70% 设置即可。8 GB 内存的机器给 5 GB,16 GB 给 10 GB,这能让热点数据和索引尽量留在内存。max_connections 设置为 200 一般够用,OA 是内部系统,连接数冲高往往不是并发高,而是慢查询把连接占住了。sort_buffer_size 保持 2 MB 默认值,不要调大,否则高并发下内存会被排序缓冲区吃光。这些参数在配置文件的 [mysqld] 段修改,改完重启生效,属于上线前就固定好的基础项。
3. 组织与权限模型:六张表管住人、部门和菜单
3.1 用户、部门、角色的核心建表 SQL
组织与权限是 OA 的地基,几乎所有业务表都要引用用户表和部门表。常见做法是六张表:用户、部门、角色、菜单、用户角色关联、角色菜单关联。下面给出最核心的四张表,菜单表结构简单,后面按相同套路补即可。
-- 部门表 CREATE TABLE sys_dept ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '部门ID', name VARCHAR(64) NOT NULL COMMENT '部门名称', parent_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '上级部门ID,0为根', ancestors VARCHAR(500) NOT NULL DEFAULT '/' COMMENT '祖先链,格式如 /1/12/', sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), KEY idx_parent_id (parent_id), KEY idx_ancestors (ancestors(191)) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='部门表'; -- 用户表 CREATE TABLE sys_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', dept_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属部门ID', username VARCHAR(64) NOT NULL COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '密码哈希,不要用MD5加密原文', nickname VARCHAR(64) NOT NULL DEFAULT '' COMMENT '姓名', mobile VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号', email VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱', avatar VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像地址', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除,0正常 1已删', create_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人ID', update_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人ID', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_dept_id (dept_id), KEY idx_status (status) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='用户表'; -- 角色表 CREATE TABLE sys_role ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '角色ID', role_name VARCHAR(64) NOT NULL COMMENT '角色名称', role_key VARCHAR(64) NOT NULL COMMENT '角色标识,如 admin/manager/employee', sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_role_key (role_key) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='角色表'; -- 用户角色关联表 CREATE TABLE sys_user_role ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', role_id BIGINT UNSIGNED NOT NULL COMMENT '角色ID', PRIMARY KEY (id), UNIQUE KEY uk_user_role (user_id, role_id), KEY idx_role_id (role_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='用户角色关联表';这段 SQL 里有几个值得说明的细节。用户表的 dept_id 不建物理外键,而是保留数字引用,这是为了让部门调整时不必顾虑外键约束阻塞;删除用户时也不会被外键拦下,逻辑删除就够。ancestors 字段是部门树的冗余路径,索引上只取前 191 个字符,部门树深度在 10 层以内时不会有问题。密码字段用 VARCHAR(100),因为 bcrypt 哈希结果是 60 个字符左右,MD5 的 32 位长度根本装不下,这个字段长度直接决定你将来能不能顺利升级认证方式。
3.2 部门数据权限:ancestors 字段怎么设计、怎么查
菜单权限解决“能看到哪些功能”,数据权限解决“能看到哪些人的数据”。OA 里最常见的数据权限是部门隔离:部门经理只能看本部门及子部门的单据。这个需求靠部门表的 ancestors 字段就能实现。ancestors 存的是从根到父部门的完整路径链,例如部门 12 的祖先链是 /1/12/,它的子部门 123 的祖先链是 /1/12/123/。插入新部门时,先查父部门的 ancestors,再拼接父部门 id。
查询“部门 123 及其所有子部门下的用户”,SQL 是这样的:
SELECT u.id, u.username, u.nickname FROM sys_user u INNER JOIN sys_dept d ON u.dept_id = d.id WHERE d.id = 123 OR d.ancestors LIKE '/1/12/123/%';这里d.id = 123覆盖本部门,ancestors LIKE '/1/12/123/%'覆盖所有子孙部门。按前缀匹配能用上 idx_ancestors 索引,部门数据量不大时性能没有问题。维护时要留意:如果部门从 A 节点移动到 B 节点,需要批量更新该部门及所有子部门的 ancestors,常见做法是在事务里先查出子树,再逐个拼接新路径,这属于低频操作,正确性优先于性能。
3.3 账号字段的三个细节:密码、逻辑删除、审计
账号表最容易被低估的是三个细节。第一,登录账号的唯一索引不能只建在 username 上。用户离职后做逻辑删除,username 还占着,新员工入职想用同一个账号就会报唯一键冲突。常见解法是把唯一索引改成(username, deleted),逻辑删除时把 deleted 置为不同的数字(比如正常 0,删除后填主键 id),这样既保留了删除记录,又允许账号被重新使用。第二,审计字段 create_by、update_by、create_time、update_time 必须齐套,否则出问题要追溯“谁在什么时候改了什么”时完全无能为力。第三,status 字段的默认值要写 1,新账号默认启用;批量导入用户时,导入程序要显式校验账号、手机号格式,避免脏数据直接进表。这几点在写建表语句时顺手就做了,后面改起来成本高得多。
4. 流程审批与动态表单:OA 数据库最难的建模点
4.1 动态表单的三种建模方式,以及为什么选扩展 JSON
流程审批是 OA 和普通管理系统的分水岭。请假单、报销单、用印申请,每种单子的字段都不一样,而且业务部门随时会要求加一个字段。表单建模通常有三种方案,对比后选择取决于项目实际。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 硬编码宽表 | 查询直观,报表好写,字段有强约束 | 加字段就要 ALTER TABLE,表单类型一多就失控 | 表单固定、数量很少的演示项目 |
| EAV 属性表 | 加字段不用改表,模型万能 | 查询要把行转列,SQL 复杂,数据量一大性能明显下降 | 属性极少、没有高频列表查询的场景 |
| 主表 + JSON 扩展 | 兼顾灵活性和查询性能,公共字段拉平 | 依赖数据库 JSON 能力,需要理解生成列和函数索引 | 大多数 OA 动态表单场景 |
我一般直接选第三种:公共字段建列,动态字段放 JSON。流程实例表存流程本身的公共属性,表单数据表存每种单据的 JSON 内容。这样加字段时应用层改一个配置,数据库完全不动。JSON 字段不是黑匣子,需要参与查询的字段用生成列抽出来建索引,后面的小节会具体演示。
4.2 流程实例与任务节点:两张表落地完整状态机
流程引擎的核心是实例和任务。实例表示“这一单走到哪了”,任务表示“当前这一步该谁处理”。状态机中最少要有草稿、审批中、通过、驳回、撤回、终止六种状态,落到表里就是这个样子:
-- 流程实例表 CREATE TABLE oa_process_instance ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '实例ID', process_key VARCHAR(64) NOT NULL COMMENT '流程定义Key,如 leave_approve', instance_no VARCHAR(32) NOT NULL COMMENT '流程实例编号,如 PR20250617001', title VARCHAR(255) NOT NULL COMMENT '单据标题,列表页直接展示', form_key VARCHAR(64) NOT NULL COMMENT '表单类型Key', status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1审批中 2通过 3驳回 4撤回 5终止', current_node VARCHAR(64) NOT NULL DEFAULT '' COMMENT '当前节点Key', initiator_id BIGINT UNSIGNED NOT NULL COMMENT '发起人ID', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_instance_no (instance_no), KEY idx_initiator_status (initiator_id, status) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='流程实例表'; -- 流程任务节点表 CREATE TABLE oa_process_task ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '任务ID', instance_id BIGINT UNSIGNED NOT NULL COMMENT '流程实例ID', node_key VARCHAR(64) NOT NULL COMMENT '节点Key,如 manager_approve', node_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '节点名称,给待办列表展示用', assignee_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前办理人ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待办 1通过 2驳回 3已转办 4已撤回', comment VARCHAR(500) NOT NULL DEFAULT '' COMMENT '审批意见', version INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), KEY idx_assignee_status (assignee_id, status), KEY idx_instance_node (instance_id, node_key) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='流程任务节点表';状态值用 TINYINT 而不是字符串,是为了索引更小、查询更快;状态含义在应用层用枚举映射。任务表必须保留 node_name 这个冗余字段,待办列表页要频繁展示节点名称,每次去流程定义表解析 JSON 效率太低。version 字段是乐观锁,处理会签并发时的作用在避坑章节单独讲。
审批记录建议单独建一张 oa_process_record,存 instance_id、task_id、operator_id、action、comment、create_time,这张表只增不改,是审计和追溯的依据。查询某单的完整流转历史时,按 instance_id 和 create_time 排序即可。
4.3 表单数据的 JSON 扩展字段:建表、索引与查询
流程实例表只关心流程状态,具体填了什么内容要落到表单数据表。这张表的核心是 JSON 列:
CREATE TABLE oa_form_data ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '表单数据ID', instance_id BIGINT UNSIGNED NOT NULL COMMENT '流程实例ID', form_key VARCHAR(64) NOT NULL COMMENT '表单Key,对应每种单据类型', form_data JSON NOT NULL COMMENT '动态表单内容,如 {"days":3,"reason":"年假"}', version INT NOT NULL DEFAULT 1 COMMENT '表单版本号,防止覆盖提交', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_instance_form (instance_id, form_key), KEY idx_form_key (form_key) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='动态表单数据表';使用上有个容易踩的坑:不要在 WHERE 里直接写JSON_EXTRACT(form_data, '$.days') = 3,这样做全表扫描,数据一多查询就慢。规定一个原则:只有高频过滤的字段才抽出来建索引。比如请假单的“请假天数”经常要按大于等于 3 天筛选,就用生成列把它变成普通列:
ALTER TABLE oa_form_data ADD COLUMN leave_days INT GENERATED ALWAYS AS (CAST(JSON_UNQUOTE(JSON_EXTRACT(form_data, '$.days')) AS UNSIGNED)) STORED, ADD INDEX idx_leave_days (leave_days);生成列是 STORED,数据写入时就会物化,查询条件直接写成leave_days >= 3就能走索引。注意只对真正高频筛选的字段做这件事,为每个 JSON 字段都建生成列会让表变得臃肿,得不偿失。
4.4 附件与消息的存储:数据库只管索引,不管文件本体
附件是 OA 里最容易被设计错的模块。正确的做法是:文件本体放文件服务器或对象存储,数据库只登记一份文件索引,记录它在哪、是谁传的、属于哪个业务单据。文件索引表的结构很简单:
CREATE TABLE oa_file_store ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文件ID', file_name VARCHAR(255) NOT NULL COMMENT '原始文件名', file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径或对象Key', file_size BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件字节数', file_md5 CHAR(32) NOT NULL DEFAULT '' COMMENT '文件MD5,用于秒传和去重', biz_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型,如 expense_attachment', biz_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '业务主键ID', uploader_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '上传人ID', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), KEY idx_biz (biz_type, biz_id), KEY idx_uploader_time (uploader_id, create_time) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='文件索引表';文件表不存文件内容,这是铁律。把附件当 BLOB 存进数据库,短期内看不出问题,等到库文件膨胀到几十 GB,备份和恢复都会变成噩梦。文件索引表还有一层用处:定期扫描biz_type + biz_id查不到对应单据的记录,这些就是用户上传后没有提交的废文件,可以在存储端定时清理。消息待办表同理,只记录接收人、标题、内容摘要和业务关联 ID,正文超过一定长度就存 TEXT,列表查询只需要按receiver_id + is_read + create_time索引取前 50 条。
5. 避坑指南:OA 数据库设计中 5 个高频翻车点
5.1 硬编码宽表:一加字段就加班
现象:每种审批单都设计成独立宽表,字段全部拉平,上线后发现业务部门每个月都要调整表单字段,每次加字段都要 ALTER TABLE,发布窗口经常被阻塞到凌晨。
原因:建模时图查询方便,忽略了 OA 表单字段天然高频变动的特点。宽表在字段固定、数量少的场景是好方案,但 OA 的单据类型多,硬编码等于把需求变动直接引到数据库结构上。
解决:公共字段拉平,动态字段进 JSON,按前面 4.3 节的方案改造。改造时给旧表加一个 JSON 列,把新增字段都放进去,存量数据不动,老查询照常走原来的列,新查询走 JSON,这是成本最低的平滑迁移路径。
5.2 事务里夹带文件上传,锁等待堆到报警
现象:提交报销单的接口偶尔超时,数据库监控里出现大量锁等待,连接数飙高,业务高峰期整个 OA 变卡。
原因:代码里把文件上传写在业务事务里,上传走网络 IO,一次上传耗时几百毫秒甚至几秒,数据库连接被事务攥在手里不释放,其他事务只能排队等锁。
解决:调整事务边界。文件先传到临时存储,拿到 file_id 后,再把业务记录和文件索引的插入放在同一个短事务里完成。如果文件服务不稳定,宁可让用户重新上传,也不要让数据库事务替网络 IO 背锅。
5.3 逻辑删除的幽灵数据
现象:用户列表偶尔出现已经离职的人,部门统计人数偏大,或者新导入用户时提示账号已存在但界面上看不到这个账号。
原因:查询 SQL 漏写了deleted = 0条件,或者唯一索引建在 username 单列上,删除后的账号仍然占着唯一键。
解决:统一封装数据访问层,所有单表查询自动拼接逻辑删除条件,不允许业务代码手写裸 SQL;唯一索引改成(username, deleted),删除时把 deleted 字段置为主键 id,保证同一账号删除后可以重新注册。老代码里散落的select *要专项排查,这类问题隐蔽,数据量越大越难清理。
5.4 会签节点并发重复审批
现象:一个会签节点有多个审批人,两个人几乎同时点“通过”,结果同一条任务被更新了两次,审批状态和下一步流转逻辑错乱。
原因:两个请求都先 SELECT 到 status='pending' 的任务,再各自执行 UPDATE,没有做状态校验,后更新的覆盖了先更新的。
解决:用乐观锁。更新语句写成:
UPDATE oa_process_task SET status = 1, comment = '同意', version = version + 1 WHERE id = 123 AND status = 0 AND version = 1;执行后检查影响行数,为 0 说明任务已经被别人处理,直接提示用户“该待办已处理”。version 从上一节建表 SQL 里带出来,这是流程任务表必须加这个字段的根本原因。
5.5 日期时间字段存成了 varchar
现象:按时间范围查审批记录时,9 月的数据排在 8 月前面;统计月度报表时漏数据;不同办公室的同事看到的时间相差 8 小时。
原因:为了 Excel 导入方便,当初把时间存成了'2024-06-17 09:30'这种字符串,查询时按字符串排序和比较,结果完全不符合业务直觉。
解决:新表一律用DATETIME(3),带毫秒精度,建表默认值直接用CURRENT_TIMESTAMP(3);应用层统一用同一时区写入。存量数据用STR_TO_DATE转换,转换前先抽样校验格式,转完后把原字段改成时间类型并重建索引。这个坑看似低级,在真实 OA 项目里出现频率相当高。
6. 上线前自检:跑一条 SQL 验证你的 OA 库设计
6.1 数据量与索引体检 SQL
表结构全部建完后,不要急着写业务代码,先跑一条自检 SQL,看每张表的体量预期是否和容量估算对得上:
SELECT table_name, table_rows, ROUND(data_length / 1024 / 1024, 2) AS data_mb, ROUND(index_length / 1024 / 1024, 2) AS idx_mb FROM information_schema.tables WHERE table_schema = 'oa' ORDER BY (data_length + index_length) DESC LIMIT 15;table_rows是优化器的估算值,不是精确行数,但能反映量级趋势。重点看两类异常:一是某张业务表的数据量远超估算,说明可能漏了归档策略;二是索引占用接近甚至超过数据占用,说明有冗余索引,需要逐个核对。我习惯把每张表的索引导出来人工过一遍,重点查是否同时存在uk_xxx和idx_xxx指向同一列的情况,以及联合索引里有没有左前缀被浪费的字段。
6.2 进阶版本:把表单注册表加进设计
如果想让这套设计走得更远,可以增加一张表单注册表,把每种表单的字段配置也存进数据库:
CREATE TABLE oa_form_define ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, form_key VARCHAR(64) NOT NULL COMMENT '表单Key', form_name VARCHAR(64) NOT NULL COMMENT '表单名称', field_config JSON NOT NULL COMMENT '字段定义,如 [{"name":"days","type":"int"}]', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_form_key (form_key) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT ='表单注册表';有了这张表,新增一种审批单时只需要插一条配置记录,前端根据 field_config 动态渲染表单,后端校验逻辑也读这份配置,业务表和流程表一行都不用改。这是把 OA 从“改代码上线”推向“配置上线”的关键一步。
我自己踩过最深的坑,就是早年把审批单全做成宽表,后来每次加字段都像做手术一样提心吊胆。现在的习惯是先做容量估算,把 JSON 扩展字段用起来,再拿上面的自检 SQL 过一遍表结构和索引,确认没问题才交给业务方试用。这套流程帮你少走弯路,希望帮到你。
本文还有配套的精品资源,点击获取