企业级OA数据字典设计解析:扩展字段、组织模型与考勤统计
简介:《致远OA V8.1 数据字典》面向致远OA二次开发、系统集成、数据库维护与数据迁移人员,用于快速查阅V8.1版本的表结构和字段定义,减少因库表命名不熟而反复试错的成本。整包为1个PDF文件,约2.2MB,采用表格化编排,便于按表名、字段名定位内容。文档围绕通讯录及外部联系人等核心表展开,逐项列出字段名称、数据类型、是否强制与注释,例如ADDRESSBOOK中的主键ID、人员MEMBER_ID、创建和修改时间,以及文本、数字、日期、枚举、选人、选部门、选岗位等多类EXT_ATTR扩展字段。读者可据此理解字段用途、约束关系和扩展字段的业务含义,为接口开发、报表取数、数据迁移和系统升级提供可靠参照。已有2616人学习下载,适合需要案头速查致远OA数据库结构的开发者与运维人员。
1. 数据字典:一张表为什么能同时装下流程、考勤和论坛
翻开任何一套跑了几年的企业协同系统,最让人头皮发麻的不是业务代码,而是数据库里那些名字像缩写的表。以某 OA 系统 V8.1 的数据字典为例,光是一个ADDRESSBOOK通讯录表,就并列排着 70 个EXT_ATTR_*扩展字段。这还只是开始,从AGENT代理设置、ATTENDANCE_DAY_STATISTICS考勤日统计,一路到BBS_ARTICLE_REPLY讨论回复,几十张表彼此独立,描述着完全不同的业务域。
这背后其实是一个挺反直觉的设计结论:企业级 OA 的数据字典,本质上是业务模型的快照,而不是数据库表结构的说明书。你读它读的是「这个系统如何理解组织、时间和流程」,字段类型和长度只是顺带给出的线索。搞懂这一层,不管是做二开、做数据迁移,还是排查一个莫名其妙的统计偏差,都能少走很多弯路。
这套字典的底层机制到底怎么设计、为什么能撑住这么多业务,下面一层层拆。
2. 扩展字段:为什么一张通讯录表要留 70 个坑位
扩展字段是这套数据字典里最容易被误解的部分。很多人第一次看到ADDRESSBOOK表里EXT_ATTR_1到EXT_ATTR_70这种整齐排列,第一反应是「偷懒」,其实它解决的是一个非常现实的问题:客户要加的字段永远比你发布版本快。
2.1 固定字段与扩展字段的分工边界
先看这张表的核心结构。通讯录的主干字段只有寥寥几个:ID、MEMBER_ID、CREATE_DATE、UPDATE_DATE,剩下的几乎全是扩展位。这个划分不是随意定的,主干字段承载的是系统级语义——这条通讯录记录属于哪个人、什么时候建的、什么时候改的;扩展字段承载的是租户级语义——某家公司想在通讯录里加「工位号」,另一家想加「司龄」。
如果把工位号做成正式字段,就意味着所有租户的表都要多一列,绝大多数业务用不上,却要为此付出存储和索引成本。而扩展字段是一组预分配的空位,谁需要谁自己往里写。
| 字段区间 | 类型 | 语义 | 典型用途 |
|---|---|---|---|
| EXT_ATTR_1 ~ 10 | VARCHAR(1024) | 文本 | 备注、标签、短文本属性 |
| EXT_ATTR_11 ~ 20 | DECIMAL(19,4) | 数值 | 金额、评分、数量 |
| EXT_ATTR_21 ~ 30 | DATETIME | 时间 | 入职日、纪念日 |
| EXT_ATTR_31 ~ 40 | VARCHAR(50) | 枚举 | 状态、类别 |
| EXT_ATTR_41 ~ 50 | VARCHAR(50) | 选人 | 关联人员 ID |
| EXT_ATTR_51 ~ 60 | VARCHAR(50) | 选部门 | 关联部门 ID |
| EXT_ATTR_61 ~ 70 | VARCHAR(50) | 选岗位 | 关联岗位 ID |
这张表的信息量比表面看起来大。它没有把 70 个扩展字段都做成VARCHAR,而是按数据类型分段。这个细节非常关键,我一般会这么理解:分段是为了让前端渲染层可以按段推断控件类型。拿到EXT_ATTR_15,不用查任何配置表,只看编号落在 11~20 区间,就知道该渲染一个数字输入框;EXT_ATTR_45落在 41~50,就该渲染人员选择器。
2.2 类型分段背后的契约约定
这种「按编号约定类型」的做法,本质上是一层非强制契约。数据库本身并不禁止你往EXT_ATTR_15里写字符串,但业务代码、前端渲染器、导出组件都默认遵守这个约定。这就引出一个必须讲清楚的机制:扩展字段的元数据其实不在这张表里。
我们试着写一个查询,把某人的通讯录记录和扩展字段一起捞出来,看看业务层是如何消费这些空位的:
-- 查询某个人员的通讯录记录及扩展字段 -- 注意:扩展字段的语义解析依赖外部的字段定义配置 SELECT a.ID, a.MEMBER_ID, a.CREATE_DATE, a.UPDATE_DATE, a.EXT_ATTR_1 AS ext_text_1, -- 业务侧按配置映射为「工位号」 a.EXT_ATTR_15 AS ext_number_1, -- 按编号落入 11~20,前端渲染数字控件 a.EXT_ATTR_45 AS ext_member_1 -- 落入 41~50,渲染人员选择器 FROM ADDRESSBOOK a WHERE a.MEMBER_ID = ? AND a.TYPE = ?; -- TYPE 用于区分通讯录条目的归属类型这段 SQL 本身没什么技巧,重点是注释里那句「语义解析依赖外部字段定义配置」。这个配置是扩展字段机制能 work 的关键:编号只是物理槽位,真正的名字、可见性、是否必填、选人时能选哪个范围的组织节点,全都存在另一组元数据里。表里存的是值,配置里存的是意义,两者解耦,租户才能各玩各的。
这种设计带来一个真实的副作用:数据迁移的时候,如果你只迁了主表、没迁字段定义配置,那 70 列数据会全部变成「有值但没人认识」的孤儿。血泪经验是迁移方案里必须把扩展字段的元数据当成一等公民处理,否则迁完系统能启动,但所有人打开通讯录都看到一片空白。
另一个值得注意的点是长度。文本类扩展字段给了 1024,这个数字不是随口定的。够放一段短描述,又不至于让人当成富文本存储往里塞 HTML;数值类给DECIMAL(19,4),19 位整数位加 4 位小数,能覆盖绝大多数财务口径的金额,同时保留 4 位精度用于汇率、比率。这些参数选择透露出设计者对「扩展字段会被怎么滥用」是有预期的。
2.3 扩展字段的代价与适用边界
说完好处,得说代价。扩展字段最大的问题不是存储,而是索引。70 个几乎全为空的列,如果业务方要求按某个扩展字段查询,就得建索引,而绝大多数行的该字段是 NULL,索引选择性极差。更麻烦的是,你无法预知哪个租户会重点用哪个扩展字段,索引策略很难通用。
所以这套机制有一个隐含的适用边界:扩展字段适合低频查询、高频展示的场景。展示是行级读取,代价可控;查询如果依赖扩展字段做过滤和排序,性能会随数据量增长迅速恶化。搞清楚这条边界,二开时才不会把核心检索逻辑压在扩展字段上。
3. 组织模型:代理、范围与变更记录怎么串起来
数据字典里第二块值得深挖的,是围绕组织模型的一整组表。AGENT、AGENT_DETAIL、AGENT_SCOPE、AI_ORG_CHANGE_RECORD、AI_ORG_MEMBER_CHANGE这五张表放在一起看,你会发现它们其实在描述同一件事:当一个组织里的人发生变动,系统如何保持业务的连续性。
3.1 AGENT 三表:代理关系的状态机
先看代理。AGENT表里有几个字段值得停下来看:AGENT_ID(代理人)、AGENT_TO_ID(被代理人)、START_DATE、END_DATE、CANCEL_DATE、CANCEL_FLAG、AGENT_TYPE。
这不是一张简单的映射表,它是一台状态机。一条代理记录从创建到失效,会经过未生效、生效中、已取消、已过期几种状态,而状态是由时间字段和标志位共同推导出来的。START_DATE和END_DATE划定生效窗口,CANCEL_FLAG表示人为提前终止,CANCEL_DATE记下终止时刻。这种「时间窗口 + 取消标记」的组合,是处理有时效关系的经典做法——它保留了历史,任何时刻回查都能还原当时的代理状态。
AGENT_TYPE这个字段尤其能说明设计意图,它的注释写着「1-代理设置 2-离职交接」。两种完全不同的业务场景复用同一张表,是因为它们在机制上是同构的:都是「A 的职责临时转移给 B」。离职交接不过是代理窗口无限长、由一个特定的管理操作触发而已。这种同构复用在数据字典里很常见,判断标准是业务的行为模式是否一致,而非名字是否相似。
AGENT_DETAIL和AGENT_SCOPE则是两个维度的细化。AGENT_DETAIL用APP和ENTITY_ID指定代理覆盖哪些应用下的哪些实体,回答「代理什么」;AGENT_SCOPE用ENTITY_TYPE(如ACCOUNT、DEPARTMENT)和ENTITY_ID存储发起人范围,回答「替谁代理」。
-- 查询某被代理人在当前时刻生效的所有代理关系 -- 状态需要同时满足:已开始、未结束、未取消 SELECT g.AGENT_ID, -- 代理人 g.AGENT_TO_ID, -- 被代理人 g.AGENT_TYPE, -- 1-代理设置 2-离职交接 g.START_DATE, g.END_DATE FROM AGENT g WHERE g.AGENT_TO_ID = ? AND g.CANCEL_FLAG = 0 AND g.START_DATE <= NOW() AND (g.END_DATE IS NULL OR g.END_DATE >= NOW());这段查询把状态机的判定逻辑显式写了出来。注意END_DATE IS NULL的判断——离职交接场景下结束时间是开放的,不能简单用>=比较。这种边界处理不到位,交接期满后代理关系依然生效,就是典型的翻车现场。
3.2 变更记录表:为什么要冗余存旧值
AI_ORG_CHANGE_RECORD和AI_ORG_MEMBER_CHANGE这两张表的设计思路很有意思。它们不存「当前状态」,只存「变化本身」,而且把旧值和新值都记下来:OLD_ID、OLD_VALUE、NEW_ID、NEW_VALUE,人员变更表里则把旧单位、新单位、旧部门、新部门、旧岗位、新岗位、旧职务级别、新职务级别全部平铺。
为什么不用「变更前查一次、变更后再查一次」的思路,非要冗余存旧值?因为组织数据是会变的历史。一个部门在变更之后被改名、被合并、被撤销,你事后去关联查询,拿到的已经不是当时那一刻的快照。把OLD_VALUE存成字符串,就是为了对抗这种「历史被改写」的失真。
变更记录表的时序语义(以人员调岗为例): 时刻 T1 人员 P 在部门 D1 ↓ 触发调岗操作 时刻 T2 写入 AI_ORG_MEMBER_CHANGE 一行 OLD_DEPT_ID = D1, NEW_DEPT_ID = D2 OLD_POST_ID = 旧岗位, NEW_POST_ID = 新岗位 同时写入 CREATE_TIME = T2 ↓ 部门 D1 被合并、改名 时刻 T3 D1 的部门记录已经不在了 ↓ 但查询 T2 的变更记录 OLD_DEPT_ID 仍可关联历史,OLD/NEW 值语义完整这段时序说明了一个关键取舍:变更记录表是为追溯而生的写多读少表,它的正确性不依赖其他表当前的状态。这也解释了为什么ORG_TYPE字段用 0/1/2 区分部门、岗位、职务级别——三类组织实体的变更共用一张表,靠类型字段分流,避免为每种实体各开一张结构雷同的表。
3.3 组织变更与流程的耦合点
真正让这组表有价值的,是它们和流程、考勤等业务表之间的耦合。AI_PROCESSING_CONDITION表存自动处理条件,AI_DEAL_CONDITION存操作类型(回退、撤销、终止、不同意、处理时长),这些表都挂着TEMPLATE_ID(模板 ID)。组织变更一旦发生,流程引擎需要知道「这个人换部门了,他手上挂着的流程要不要重算处理人」,而这个判断依据,正是变更记录表提供的旧值。
耦合点带来的直接后果是:组织变更记录不能随便清理。很多系统给它设个三个月归档策略,结果一到年度审计或者流程回溯就抓瞎。我见过更隐蔽的坑——变更记录表按CREATE_TIME分区,跨分区查询旧值做关联时漏了分区条件,数据对不上还查不出错。这类表在运维策略上必须单独标注保留周期,不能套用普通日志表的清理规则。
4. 考勤数据模型:签到一条记录为何要拆成四张表
考勤模块是整套字典里表数量最多的一块,ATTENDANCE_ARRANGE、ATTENDANCE_CLASS、ATTENDANCE_FIX_TIME、ATTENDANCE_DAY、ATTENDANCE_DAY_STATISTICS、ATTENDANCE_HISTORY、ATTENDANCE_INFO……数下来十几张。这套模型拆得这么细,不是过度设计,而是因为考勤本质上是一个规则与事实分离的问题。
4.1 规则层与事实层的分离
先给这堆表分个类。规则层的表包括ATTENDANCE_TEAM(考勤组)、ATTENDANCE_ARRANGE(排班)、ATTENDANCE_CLASS(班次)、ATTENDANCE_FIX_TIME(固定时间)、ATTENDANCE_DAY(工作日安排);事实层的表包括ATTENDANCE_HISTORY、ATTENDANCE_INFO(打卡记录)、ATTENDANCE_DAY_STATISTICS(日统计)。
这个分离的意义在于:规则是可配置、可回滚、可并存的,事实是不可变、追加写入的。ATTENDANCE_TEAM表里有个字段叫IS_NEW,标注「是否最新」,配合START_DATE和END_DATE,说明考勤组的配置本身是带版本的时间区间。这意味着同一个考勤组在不同月份可能对应不同的规则快照,统计时必须用「打卡那天生效的那版规则」,而不是「现在这版规则」。
这是考勤统计出错的头号原因。用当前规则去算历史数据,结果就是每次调整班次时间,历史统计全跟着变,员工看到上个月的迟到数莫名其妙多了一条,投诉就来了。
-- 取某员工某天的日统计,并与当天生效的班次规则关联 -- 关键:班次规则要按考勤日筛选生效版本,而不是取最新 SELECT s.MEMBER_ID, s.PUNCH_DATE, s.LATE_NUM, s.LEAVE_EARLY_NUM, s.MISSINGCLOCK_COUNT, c.ATTENDANCE_CLASS_NAME, c.CLASS_TYPE -- 0-一天一次 1-一天两次 2-一天三次 FROM ATTENDANCE_DAY_STATISTICS s LEFT JOIN ATTENDANCE_CLASS c ON c.ATTENDANCE_TEAM_ID = s.TEAM_ID AND c.ATTENDANCE_ARRANGE_ID = s.RULE_ID WHERE s.MEMBER_ID = ? AND s.PUNCH_DATE = ?;这里的RULE_ID是连接事实和规则的桥。日统计表把当时生效的规则 ID 固化下来,事后无论规则怎么改,都能靠这个 ID 找回当时的配置。这个字段的价值远高于它看起来的分量——它是统计结果可复现的锚点。
4.2 ATTENDANCE_HISTORY 与 ATTENDANCE_INFO 的冗余之谜
这两张表的字段高度重合,几乎一模一样:都有OWNER_ID、SIGN_TIME、LONGITUDE、LATITUDE、CONTINENT、COUNTRY、PROVINCE、CITY、TOWN、STREET、NEAR_ADDRESS、IMG_NUM、RECORD_NUM。为什么同一份打卡数据要存两遍?
区别藏在细节里。ATTENDANCE_INFO多了IS_WORK_DAY(是否工作日)、MODIFY_NUM(签到修改次数)、DEVICE_INFO(设备信息)、STATE(打卡状态:3-上班迟到、4-下班早退、5-正常上班、6-正常下班)、MAC_ADDRESS、PUNCH_TYPE。ATTENDANCE_HISTORY的SIGN字段长度是 500,ATTENDANCE_INFO是 2000。
我一般会这么理解这个设计:ATTENDANCE_INFO是业务态的当前视图,它承载了打卡结果判定(状态、是否工作日、设备、修改次数),字段更长是为了保存更完整的地址和设备描述;ATTENDANCE_HISTORY是流水态的原始记录,服务于查询和审计,字段收敛,SIGN只保留必要信息。两张表通过PUNCH_DATE和人员维度对齐,修改签到需要改业务态并留下修改次数,流水态保留原始痕迹。
这种冗余是有意为之的读写分离:高频的业务判定读INFO,历史追溯和报表读HISTORY。代价是两份数据可能不一致,所以MODIFY_NUM这个字段就变得很重要——它是发现不一致的线索。如果某条记录MODIFY_NUM > 0而HISTORY里没有对应的修改痕迹,就说明写入链路有缺口。
4.3 地理信息字段的定位取舍
再看地理字段的设计。CONTINENT、COUNTRY、PROVINCE、CITY、TOWN、STREET、NEAR_ADDRESS、ADDRESS_CODE、ADDRESS_TYPE(1-poi、2-街道、3-路、4-其他)——这套字段是反范式的典型:把地理层级拆成多列平铺,而不是存一个 JSON 或者只存经纬度事后解析。
理由在于查询模式。考勤场景的典型查询是「某个考勤组在某个城市范围内的打卡记录」,如果只存经纬度,就得用空间函数做范围计算,索引难用;平铺成PROVINCE、CITY,可以走普通 B-Tree 索引,统计口径明确。代价是地理信息存的是打卡那一刻的解析结果,行政区划调整之后,历史记录里的CITY不会跟着变,这在统计时反而是优点——它忠实反映了当时的位置判定。
ATTENDANCE_SETTING表里有RELATIVE_ID字段,注释写「保存第三方系统导入的围栏数据 id」,ATTEND_RANGE存范围。这说明围栏(地理围栏)的判定逻辑依赖外部数据源,打卡记录里存的是判定后的结果,而不是判定过程。这个边界划得很清楚,判定规则变更不影响历史数据。
5. 讨论区模型:树形回复为什么用 USE_REPLY_ID 而不是邻接表
BBS_ARTICLE、BBS_ARTICLE_REPLY、BBS_BOARD、BBS_ARTICLE_ISSUE_AREA这几张表构成讨论区。其中BBS_ARTICLE_REPLY的回复结构设计,是全篇最有嚼头的部分之一。
5.1 回复层级的三种 ID 语义
一条回复记录里同时出现了三个 ID 字段:V3X_BBS_ARTICLE_ID(所属主题)、USE_REPLY_ID(引用的回复帖 ID)、TO_REPLY_ID(直接上层回复的 ID)。外加一个USE_REPLY_FLAG,取值 4(顶级回复)、5(顶级回复的回复)、6(顶级回复的振荡回复)。
这套字段组合说明回复结构不是简单的无限嵌套树,而是有明确工程约束的三层结构。TO_REPLY_ID表达直接父子关系,是树的主干;USE_REPLY_ID表达引用关系,用户引用了哪条回复来回复,两者可以不同——你可以在别人的回复下面,引用另一条完全不相干的回复。
USE_REPLY_FLAG的取值 4/5/6 则是一层语义压缩,把「这条回复在讨论结构里的位置」编码成一个短整数。振荡回复(6)这个说法暗示了产品侧对「反复引用、层层下拉」的视觉表现有专门处理,直接用TO_REPLY_ID递归渲染可能会把页面拉成一条长线。
-- 取某主题下的顶级回复(第一层) SELECT r.ID, r.NAME, r.REPLY_USER_ID, r.REPLY_TIME, r.PRAISE_SUM FROM BBS_ARTICLE_REPLY r WHERE r.V3X_BBS_ARTICLE_ID = ? AND r.USE_REPLY_FLAG = 4 -- 顶级回复 AND r.STATE = 0 -- 0-正常,排除已删除 ORDER BY r.REPLY_TIME DESC;注意STATE字段。讨论区的删除几乎都是逻辑删除,BBS_ARTICLE里同样有STATE(0-正常、1-删除)。这意味着查询必须处处带STATE条件,漏一次就会把已删回复渲染出来。这类逻辑删除字段是数据字典里最容易被新人忽略的坑,表里没写「默认值」,业务代码得自己保证。
5.2 点赞字段用逗号拼接字符串的妥协
PRAISE字段类型TEXT,注释「点赞人员 ID 集合」,配合PRAISE_SUM(点赞总数),这是一个典型的历史妥协设计。正确的做法是单独一张点赞关联表,(ARTICLE_ID, USER_ID)唯一索引,防重复点赞靠数据库约束保证。
用逗号拼接字符串存 ID 集合,问题很实在:无法高效判断某人是否点过赞,只能LIKE '%123%'模糊匹配,索引完全用不上,还有1和12、123这种前缀误匹配的风险;并发点赞下更新字符串需要加锁,容易产生丢失更新;数据量大了之后字符串会膨胀,TEXT类型也无法建索引。
那为什么这么设计?很可能是早期版本的性能妥协:讨论区读多写少,点赞数相对整个帖子数据量小,用SUM字段存总数、TEXT存明细,换取一次查询就把总数和明细都拿回来,避免额外的 JOIN。这种取舍在小规模下完全成立,规模上来后就是债。
提示:如果做二开需要判断「当前用户是否点赞」,不要解析
PRAISE字段。可行的做法是写一个带缓存的辅助查询,或者干脆另建一张点赞关系表做增量同步,不要指望在这张表上做实时判定。
5.3 空间类型与置顶分级
BBS_ARTICLE里有两个字段值得单独看:SPACE_TYPE和置顶相关的TOP_NUMBER_GROUP、TOP_NUMBER_UNIT、TOP_SEQUENCE。
SPACE_TYPE取值 1-部门、2-单位、3-集团、4-自定义团队、17-自定义单位、18-自定义集团。编号不连续且跨越较大,这是典型的历史演进痕迹——1/2/3 是最早的三级组织空间,4 是后加的自定义团队,17/18 是组织模型扩展后的新类型,为了不破坏已有数据而跳号分配。理解这一点,二开时就不会误以为中间缺号是数据错误。
置顶字段分成了TOP_NUMBER_GROUP和TOP_NUMBER_UNIT,说明置顶是按空间层级独立控制的,集团置顶的序号和单位置顶的序号互不干扰。这种设计避免了「集团置顶一个帖子,单位里的排序全乱」的问题,但代价是前端渲染时必须先判断当前空间层级,再用对应的字段排序。
BBS_ARTICLE_ISSUE_AREA表则负责发布范围,MODULE_ID+MODULE_TYPE的组合指向组织模型实体,ISSUE_TIME记录发布时间。这是可见性从内容中剥离的典型做法:帖子本身不知道谁能看,范围表说了算。这种剥离让同一篇帖子可以被投放到多个范围,也让权限变更只影响范围表,不动内容。
6. 避坑与排查:读数据字典时最容易踩的五个坑
6.1 扩展字段有值但业务不显示
现象:迁移或二开后,扩展字段在数据库里明明有数据,页面上却是空白。
原因:扩展字段的语义依赖外部的字段定义配置,编号只是物理槽位。配置没同步,业务层不知道该列对应哪个业务字段,自然不渲染。
解决:把扩展字段的元数据配置纳入迁移清单,和主表数据一起校验;上线后用一条 SQL 抽查主表有值而配置表缺定义的列。
6.2 考勤统计每次刷新结果都在变
现象:同一份历史考勤数据,隔几天查一次,迟到数和缺卡数不一样。
原因:统计时关联了当前生效的班次规则,而不是打卡当天生效的规则。规则一旦调整,历史统计跟着漂移。
解决:日统计表必须固化RULE_ID和TEAM_ID,统计一律按这两个 ID 回溯当时的规则快照,禁止用「最新规则」关联历史事实。
6.3 代理关系过期后仍在生效
现象:代理设置已到期,但被代理人仍能收到本应给代理人的待办。
原因:状态判定只看了CANCEL_FLAG,漏了时间窗口判断;或者END_DATE为空时被当成「永久有效」,而它在离职交接场景下应当是「开放结束」而非「无限制」。
解决:把所有代理判定收敛到统一的状态计算逻辑,显式处理END_DATE IS NULL和CANCEL_FLAG的组合,禁止各业务模块自己写 WHERE 条件。
6.4 讨论区已删回复被渲染出来
现象:用户明确删除了自己的回复,但刷新页面还能看到。
原因:STATE是逻辑删除字段,查询时漏了STATE = 0条件;更隐蔽的是关联查询里只给主表加了条件,子表或子查询没加。
解决:把逻辑删除条件写进数据访问层的统一拦截,或者至少在字典文档里给这类字段打上显眼标记,代码评审时专门检查。
6.5 组织变更记录被归档清理导致追溯失败
现象:年度审计需要回溯半年前的部门调整,发现变更记录已经被归档清理,查不到当时的旧值。
原因:AI_ORG_CHANGE_RECORD、AI_ORG_MEMBER_CHANGE被当成普通日志表套用了通用清理策略。
解决:在运维层给变更记录表单独标注保留周期,清理前必须确认无审计和流程回溯需求;如果按时间分区,跨分区关联查询要显式带上分区条件。
7. 设计取舍:数据字典真正教会我们的事
把这几组表放在一起看,会发现这套数据字典的所有设计都在回答同一个问题:怎么让系统在业务不断变化的情况下,还能解释清楚「当时到底发生了什么」。
扩展字段的 70 个槽位,牺牲了索引效率换来租户自定义能力;OLD_VALUE/NEW_VALUE的冗余存储,牺牲了存储空间换来历史可追溯;考勤的规则层与事实层分离、日统计固化RULE_ID,牺牲了查询时的 JOIN 成本换来统计结果可复现;代理状态机用时间窗口加取消标记,牺牲了查询的简洁换来历史状态的完整保留。每一个取舍都不是为了好看,而是因为在企业协同系统里,历史不可篡改这条约束比查询性能更硬。
有一个判断方法我一直在用:拿到任何一套数据字典,先找「旧值」「快照」「版本」「状态标记」这几类字段。它们出现的地方,说明系统认为这里发生过或即将发生需要被追溯的变化。AGENT的CANCEL_DATE、ATTENDANCE_TEAM的IS_NEW与START_DATE、AI_ORG_MEMBER_CHANGE的OLD_*/NEW_*、BBS_ARTICLE_REPLY的USE_REPLY_FLAG,全都踩在这个规律上。反过来,如果一套字典里全是「当前状态」而没有半点历史痕迹,那它在应对审计、回溯和对账时大概率会翻车。
验证取舍是否合理,也不用等出事。可以试着回答三个问题:这套表能不能还原某个时点的业务状态?规则变更后,历史统计会不会变?逻辑删除的数据能不能从查询里可靠地排除?三个问题只要有一个答不上来,就说明字典里有字段的设计意图没被吃透。
最后说个具体的习惯。从那以后我每次接手一套陌生的数据字典,都会先挑三张表——一张有扩展字段的主表、一张带时间窗口的状态表、一张写多读少的流水表——把它们的字段注释逐条读完,再写几条查询验证自己理解的语义。这三张表读完,整套字典的设计口味基本就摸清了,后面看别的表会快很多。希望这套读法帮到你。
这份 V8.1 的数据字典覆盖了通讯录、代理、考勤、讨论区等核心业务域的真实表结构与字段语义,拿去做二开设计、迁移核对或排查统计偏差,都比凭空猜字段含义靠谱得多。需要的话可以下载下来放在手边,对着真实的表定义逐张过一遍,比我这里举例的那几张收获更全。
本文还有配套的精品资源,点击获取