简介面向致远OA V8.1开发、实施与运维人员的数据字典PDF重点解决表结构查阅、字段含义确认及二次开发取数时缺少权威参考的问题。压缩包内仅含1个PDF文件体积约2.2MB便于随查随用文档以表为单位整理字段名称、数据类型、是否必填与注释并覆盖ADDRESSBOOK、ADDRESSBOOK_MEMBER等通讯录相关表细分文本、数字、日期、枚举、选人、选部门、选岗位等扩展字段能帮助读者快速理解主键、人员ID、创建与修改时间等设计约定。已有2616人学习下载。其价值在于将零散数据库信息汇总为可检索的结构说明可用于数据完整性核对、系统升级迁移前的结构梳理、报表取数与接口开发中的字段定位也可作为开发维护过程中的文档参考减少因字段语义不清造成的沟通与排错成本。1. 老系统交接时最值钱的不是源码是一份能查的字段表接手一个跑了五六年的协同办公平台二开任务源码能拿到接口文档基本没有数据库里几十张表连注释都被清过一遍。那天要写一个通讯录同步接口卡在一个字段上ADDRESSBOOK表里从EXT_ATTR_1一直排到EXT_ATTR_70光看名字完全看不出哪个是“手机号备份”、哪个是“人员引用”。翻遍仓库最后救场的是一份 V8.1 数据字典——每张表的字段名、数据类型、是否强制、中文注释都列得清清楚楚。这份字典覆盖协同办公平台里 32 张核心表通讯录、考勤、代理交接、讨论区、流程智能处理。适合三类人要写取数 SQL 做报表的、要做二开加字段的、要从老库迁数据到新库的。它不含业务代码但它决定了你能不能读懂这套系统的数据模型。2. 表结构全景32 张表按前缀切成五个业务域拿到一堆表名先别急着看字段先分组。这套系统的表命名有很强的前缀规律前缀本身就是业务域标记分组分对了后面查表能省一半时间。2.1 五个业务域与它们的边界前缀业务域表数代表表是否高频改动ADDRESSBOOK通讯录、外部联系人6ADDRESSBOOK、ADDRESSBOOK_MEMBER、ADDRESSBOOK_SET高AGENT代理设置与离职交接3AGENT、AGENT_DETAIL、AGENT_SCOPE中AI_流程自动处理、催办、组织变更留痕6AI_DEAL_CONDITION、AI_REMIND_RECORD、AI_ORG_MEMBER_CHANGE低ATTENDANCE_考勤13ATTENDANCE_TEAM、ATTENDANCE_CLASS、ATTENDANCE_HISTORY高BBS_讨论区4BBS_ARTICLE、BBS_ARTICLE_REPLY、BBS_BOARD低AI_这一组名字容易误导。它跟现在说的大模型没关系是“自动处理”的缩写AI_DEAL_CONDITION存的是流程节点上配置的自动动作回退、撤销、终止、不同意、处理时长AI_PROCESSING_CONDITION存自动处理的触发条件AI_REMIND_RECORD和AI_REMIND_COUNT是一对催办记录表AI_ORG_CHANGE_RECORD和AI_ORG_MEMBER_CHANGE则是组织调整时的旧值新值快照——离职交接、部门合并之后要追溯“这个人原来在哪个部门”靠的就是这两张表。分组的实际价值在于你改通讯录永远只动前 6 张表和人员主表你改考勤才需要动那 13 张。跨域引用只有一种情况——所有业务表都通过MEMBER_ID、OWNER_ID、CREATOR_ID指向人员通过ACCOUNT_ID指向单位。2.2 命名规约ID、XXX_ID、ACCOUNT_ID 各管什么字典里字段名看着杂其实就五类认全了基本能盲读ID每张表的主键统一BIGINT且强制非空。取值不是自增就是雪花字典本身不写生成策略这个要去源码里找。XXX_ID外键引用比如MEMBER_ID、ATTENDANCE_TEAM_ID、AGENT_TO_ID。注意AGENT_ID和AGENT_TO_ID是一对前者是代理人后者是被代理人。ACCOUNT_ID/ORG_ACCOUNT_ID单位租户隔离字段。多单位部署时任何统计 SQL 漏掉这个条件数据就会串到别的单位去。XXX_TYPE/ENTITY_TYPE/MODULE_TYPE多态引用一个字段可以指向人员、部门、岗位中的任意一种具体是哪一种由旁边的类型字段决定。AGENT_SCOPE.ENTITY_TYPE就是典型注释写的是“如 ACCOUNT、DEPARTMENT”。IS_XXX/XXX_FLAGSMALLINT的开关或状态绝大多数是 0/1但不是全部——第 5 章会专门讲这个坑。时间字段也要分清。CREATE_DATE、UPDATE_DATE、START_DATE、END_DATE、SIGN_TIME、PUNCH_DATE在字典里全是DATETIME命名上DATE后缀并不代表只存日期实际存的是完整时刻。写查询的时候用 2024-06-01 AND 2024-07-01这种左闭右开区间别用DATE_FORMAT包字段包了索引就废掉。2.3 用元数据表核对字典与生产库是否一致字典是文档库是事实两者会漂移。老项目最常见的翻车场景就是照着字典写完代码上线报Unknown column。上线前先跑一遍核对五分钟的事。-- MySQL拉出字典里列过的表逐列核对是否真实存在、类型是否一致 -- oa_prod 换成你的库名表名列表从字典里抄 SELECT c.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, -- 实际类型和字典的 VARCHAR/DATETIME 对齐看 c.CHARACTER_MAXIMUM_LENGTH, -- 变长字段长度重点看 50 / 1024 有没有被改过 c.IS_NULLABLE, -- 对应字典的“强制”列 c.COLUMN_COMMENT -- 线上库可能已经把注释清空 FROM information_schema.COLUMNS c WHERE c.TABLE_SCHEMA oa_prod AND c.TABLE_NAME IN ( ADDRESSBOOK, ADDRESSBOOK_MEMBER, ADDRESSBOOK_SET, ATTENDANCE_HISTORY, ATTENDANCE_INFO, AGENT ) ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION;这段查的是information_schema.COLUMNSMySQL 5.7 和 8.0 都能用结果按表名加列顺序排肉眼扫一遍就能发现字典里有、库里没有的列。如果现场是 Oracle 部署换成ALL_TAB_COLUMNS字段名对应关系是COLUMN_NAME、DATA_TYPE、DATA_LENGTH、NULLABLE如果是 SQL Server查INFORMATION_SCHEMA.COLUMNS写法几乎一样。参数上唯一要改的是TABLE_SCHEMA把它换成本地开发库、测试库、生产库分别跑一次三份结果 diff 一下环境之间的结构差异就全暴露了。我一般把这个查询存成一个.sql文件放仓库里跟版本走。提示COLUMN_COMMENT在很多现场是空的别指望它替代字典。真正的中文语义只有字典里有这也是这份资料不能丢的原因。3. 通讯录六张表拆解ADDRESSBOOK 的 70 个扩展字段怎么定位通讯录是二开需求最密集的模块因为几乎所有“人员附加信息”的需求最后都落在这里。六张表各管一段理清分工之后加一个字段就知道该加在哪。3.1 主表、人员表、设置表的分工ADDRESSBOOK通讯录条目主表。核心只有ID、MEMBER_ID、CREATE_DATE、UPDATE_DATE、TYPE剩下 70 个全是EXT_ATTR_*扩展字段。MEMBER_ID指向人员字典里没标强制意味着可以存在“没挂人”的条目。ADDRESSBOOK_MEMBER外部联系人表字段是实体业务字段——NAME、COMPANY_NAME、COMPANY_DEPT、COMPANY_POST、COMPANY_PHONE、MOBILEPHONE、EMAIL、WEBSITE、MSN、QQ、CREATOR_ID、CATEGORY_ID、MEMO。这张是“结构化的联系人”字段固定不像主表那样靠扩展。ADDRESSBOOK_SET通讯录的可见性配置。VIEW_SCOPE控制查看范围KEY_INFO和KEY_INFO_TYPES1 手机号加职务级别、2 手机号、3 职务级别控制哪些信息算关键信息EXPORT_PRINT控制导出打印权限DISPLAY_COLUMN存显示列头。另外两个字段值得单独记M3_SET/M3_DISPLAY_COLUMN和ZX_SET/ZX_DISPLAY_COLUMN是移动端和另一个客户端各自的字段显示配置跟DISPLAY_COLUMN是并列关系不是覆盖关系。ADDRESSBOOK_SET_SCOPE设置的作用范围。ADDRESSBOOK_SET_TYPE区分这条范围记录属于哪种设置1 查看范围、2 关键信息、3 导出与打印权限USER_ID加USER_TYPE是多态引用指向组织模型里的实体。ADDRESSBOOK_TEAM个人组或个人类别。TYPE是 0 系统组、1 个人组、2 私人通讯录这三者的可见性完全不同做权限判断必须先看这个字段。ADDRESSBOOK_TEAM_MEMBERS组和人员的关联表只有TEAM_ID和MEMBER_ID典型的多对多中间表。ADDRESSBOOK_SET里的DEPARTMENT_INFO_ORDER是个容易被忽略的字段它控制部门信息和人员信息的展示先后取值是SMALLINT。做前端渲染时如果自己写死了“先部门后人员”跟后台配置就冲突了。3.2 EXT_ATTR 的七段式布局70 个扩展字段不是随便排的是按数据类型分段的。这是个很聪明的设计——字段名统一但类型按段划分前端渲染表单时只要知道段号就知道该给什么控件。区间类型语义对应控件1–10VARCHAR(1024)文本单行或多行文本框11–20DECIMAL(19,4)数字数值输入保留 4 位小数21–30DATETIME日期时间日期选择器31–40VARCHAR(50)枚举下拉框41–50VARCHAR(50)选人人员选择器51–60VARCHAR(50)选部门部门选择器61–70VARCHAR(50)选岗位岗位选择器选人字段那个VARCHAR(50)长度是有信息量的。一个BIGINT的 ID 最长 19 位50 位长度装一个 ID 绰绰有余装两个拼接就紧张了。所以基本可以判定这 30 个引用类字段存的是单值 ID需要多选的场景不会塞在这一列里会另走关联表。这个推断在做数据迁移时很关键——你不能按“逗号分隔字符串”去解析它。DECIMAL(19,4)也是同理。19 位精度加 4 位小数能装下千亿级的金额说明这套扩展字段从一开始就考虑过财务类扩展。3.3 一条查询把扩展字段翻译成中文字典能告诉你EXT_ATTR_41是“选人类型数据字段”但不会告诉你这个位置当前配的是不是“项目经理”。配置存在另一处表单模板或扩展定义表不在字典的 32 张表里所以实际开发得先确认占用情况再写查询。-- 通讯录主表带扩展字段的取数示例 -- 假设 EXT_ATTR_1 存的是“工号”、EXT_ATTR_11 存的是“年假余额”、EXT_ATTR_41 存的是“直属主管” SELECT a.ID AS 通讯录ID, a.MEMBER_ID AS 人员ID, m.NAME AS 外部联系人姓名, a.EXT_ATTR_1 AS 工号, a.EXT_ATTR_11 AS 年假余额, a.EXT_ATTR_41 AS 直属主管ID, a.CREATE_DATE AS 创建日期, a.UPDATE_DATE AS 最后修改日期 FROM ADDRESSBOOK a LEFT JOIN ADDRESSBOOK_MEMBER m ON m.ID a.MEMBER_ID -- 字典里 MEMBER_ID 非强制必须用 LEFT JOIN WHERE a.TYPE 1 -- TYPE 用于区分通讯录类别取值以现场配置为准 AND a.UPDATE_DATE 2024-06-01 AND a.UPDATE_DATE 2024-07-01;三个地方要留意。第一LEFT JOIN不是可选项MEMBER_ID在字典里标的是FALSE允许空用INNER JOIN会静默丢掉一批记录。第二EXT_ATTR_41拿到的是 ID要显示姓名还得再关联人员主表——而人员主表不在这份字典里它在组织架构模块字段名常见的是ORG_MEMBER或带版本前缀的V3X_开头具体得去库里SHOW TABLES LIKE %MEMBER%找。第三UPDATE_DATE上如果要频繁按时间增量同步记得让 DBA 补索引这套表默认的索引通常只覆盖主键和外键。代码里读扩展字段建议封一层映射而不是到处写getExtAttr(41)# 扩展字段区间映射把段号翻译成业务语义和控件类型 EXT_SEGMENTS [ (1, 10, text, VARCHAR(1024), 文本), (11, 20, number, DECIMAL(19,4), 数字), (21, 30, datetime, DATETIME, 日期), (31, 40, enum, VARCHAR(50), 枚举), (41, 50, member, VARCHAR(50), 选人), (51, 60, dept, VARCHAR(50), 选部门), (61, 70, post, VARCHAR(50), 选岗位), ] def resolve_ext(index: int): 传入 1-70返回该字段所在区间与控件类型越界直接抛错避免脏配置上线 if not 1 index 70: raise ValueError(f扩展字段序号越界: {index}) for start, end, kind, sql_type, label in EXT_SEGMENTS: if start index end: return {index: index, kind: kind, sql_type: sql_type, label: label} raise ValueError(f未匹配区间: {index}) print(resolve_ext(41)) # {index: 41, kind: member, sql_type: VARCHAR(50), label: 选人}这段的作用是把“字段名”这件事从代码里彻底抽掉。以后业务说“第 42 个扩展位改成存部门”你只需要改配置表不用全局搜EXT_ATTR_42。resolve_ext的入参就是一个整数返回值里带上 SQL 类型可以直接用来做校验——比如kind number时往DECIMAL(19,4)写之前先做精度截断避免 MySQL 的Data truncated警告。4. 考勤与代理13 张考勤表和 3 张代理表的状态流转考勤是这套系统里表最多、关系最深的一块。13 张表看着吓人其实是一条链加几个旁支。代理那 3 张表反倒简单但判定逻辑有坑。4.1 从考勤组到打卡记录的完整链路按配置到数据的顺序排一遍ATTENDANCE_TEAM考勤组本身。ATTENDANCE_TEAM_NAME、ATTENDANCE_MANAGER、ORG_ACCOUNT_ID还有IS_NEW/START_DATE/END_DATE三个字段。这三件套说明考勤组是带版本的——改一次组设置就生成一条新记录IS_NEW1的是当前生效版本旧版本靠END_DATE封存。ATTENDANCE_AUTH_ENTITY组里包含谁。ENTITY_TYPE明确写了 3 人员、1 部门、0 单位ENTITY_ID就是对应的组织实体 ID。ATTENDANCE_ARRANGE排班。ARRANGE_TYPE区分 0 固定排班、1 周期排班。固定排班所有人一样周期排班要往下看ATTENDANCE_DAY。ATTENDANCE_CLASS班次。CLASS_TYPE是 0 一天 1 次、1 一天 2 次、2 一天 3 次FLEX_TIME、LATE_SERIOUS、ABSENT_WORK、LEAVE_FIX_TIME、ATTEND_FIX_TIME这几个字段存的是弹性时间、严重迟到阈值、旷工阈值、晚走时间、晚到时间类型都是VARCHAR(50)而不是数值——说明存的是配置串取出来要自己解析。ATTENDANCE_FIX_TIME具体时间点。WORK_TIME、END_TIME、EARLY_WORK_TIME最早签到、LAST_END_TIME最晚签退还有ATTEND_REMIND/LEAVE_REMIND两个提醒开关。ATTENDANCE_DAY周期排班里哪天上班。WEEK_DAY加IS_WORK0 不是、1 是。ATTENDANCE_HISTORY和ATTENDANCE_INFO打卡流水两张表。ATTENDANCE_DAY_STATISTICS按人按天汇总迟到次数、早退次数、缺卡数全在这一张。ATTENDANCE_SETTING地理围栏。LONGITUDE、LATITUDE、ATTEND_RANGE、FILTRATE_ENTITYRELATIVE_ID存第三方系统导入的围栏 ID。ATTENDANCE_REMIND、ATTENDANCE_NO_REMIND_MEMBER、ATTENDANCE_AT提醒相关的三张旁支表。链路清楚了取数就有方向要明细查HISTORY或INFO要统计查DAY_STATISTICS要配置查TEAM那一串。别拿流水表去COUNT出勤天数那是最常见的性能自杀写法。4.2 两张打卡表的差异与状态码口径ATTENDANCE_HISTORY和ATTENDANCE_INFO字段高度重合都有OWNER_ID、DEPT_ID、SIGN_TIME、SIGN、TYPE1 签到、2 签退、3 外勤、经纬度、洲省市县、IMG_NUM、RECORD_NUM、NEAR_ADDRESS、ADDRESS_TYPE。差别在后者ATTENDANCE_INFO多了IS_WORK_DAY是否工作日、STATE打卡状态3 上班迟到、4 下班早退、5 正常上班、6 正常下班、MODIFY_NUM签到修改次数、DEVICE_INFO设备 ID、PUNCH_TYPE排班类型、MAC_ADDRESSWiFi Mac 地址。ATTENDANCE_HISTORY没有状态判定字段。常见做法是一次打卡同时写两张表HISTORY存轻量原始流水供列表展示INFO存带判定结果的明细供考勤计算和申诉修改。所以查“某人某天到底打了几次卡”查HISTORY就够查“这次打卡算不算迟到”必须查INFO.STATE。字典里INFO.STATE那行注释写得很直白但很多人只看SIGN_TIME就自己算迟到结果和系统的DAY_STATISTICS.LATE_NUM对不上——因为弹性时间、严重迟到这些规则是ATTENDANCE_CLASS里配的自算很难对齐。-- 月度考勤汇总直接读日统计表别去流水表里 COUNT SELECT s.MEMBER_ID, SUM(s.NORMAL_DAY) AS 正常出勤天数, SUM(s.LATE_NUM) AS 迟到次数, SUM(s.LEAVE_EARLY_NUM) AS 早退次数, SUM(s.MISSINGCLOCK_COUNT) AS 缺卡总数, SUM(s.OUTSIDE_NUM) AS 外勤次数 FROM ATTENDANCE_DAY_STATISTICS s WHERE s.ACCOUNT_ID 1001 -- 单位隔离不能省 AND s.PUNCH_DATE 2024-06-01 AND s.PUNCH_DATE 2024-07-01 GROUP BY s.MEMBER_ID ORDER BY s.LATE_NUM DESC;PUNCH_DATE是“考勤日”不是打卡时刻用它做区间过滤才能保证跨零点夜班算在正确的日期上。MISSINGCLOCK_COUNT是总缺卡数WORKING_MISSINGCLOCK和KNOCKOFF_MISSINGCLOCK分别拆上班缺卡和下班缺卡做扣款规则时用后者更准。PUNCH_CLASS记录应打卡次数1 一天一次、2 一天两次算缺卡率的时候拿MISSINGCLOCK_COUNT / (PUNCH_CLASS * 天数)才对直接除以天数会算爆。4.3 代理三表一条代理到底生不生效AGENT是主表AGENT_DETAIL限定应用范围AGENT_SCOPE限定发起人范围。判定一条代理生效要同时满足四个条件少一个都会出问题-- 判定某人在某时刻的有效代理未取消 在期限内 类型是代理设置 应用范围匹配 SELECT a.AGENT_TO_ID AS 被代理人, a.AGENT_ID AS 代理人, a.START_DATE, a.END_DATE FROM AGENT a WHERE a.CANCEL_FLAG 0 -- 0 未取消、1 已取消 AND a.AGENT_TYPE 1 -- 1 代理设置、2 离职交接两者语义不同 AND a.AGENT_ACCOUNT_ID 1001 -- 代理人所属单位 AND NOW() BETWEEN a.START_DATE AND a.END_DATE AND EXISTS ( SELECT 1 FROM AGENT_DETAIL d WHERE d.AGENT_ID a.ID AND d.APP 88 -- 具体应用 ID按现场配置表取 );几个参数值得展开。AGENT_TYPE必须卡死类型 2 是离职交接交接产生的代理记录在业务上是“历史遗留”拿它去判断当前代理人会把离职员工的待办算到别人头上。AGENT_ACCOUNT_ID和AGENT_TO_ACCOUNT_ID是两个不同单位跨单位代理集团管理员代单位员工场景下二者不一致只过滤一个会漏数据。AGENT_SCOPE是最容易漏的一层它通过ENTITY_TYPEACCOUNT、DEPARTMENT等加ENTITY_ID限定“只代理这些发起人的流程”。如果没有AGENT_SCOPE记录通常意味着不限范围如果有就必须 join 上去比对发起人。- 这一点字典的注释写得很简短我是踩过一次“代理范围不生效”才记住的。另外AGENT.MATTER_TYPE是事项条件设置类型0 按处理人设置、1 按发起人设置默认值 1。做代理规则校验时取值不同走的分支不一样别默认都是 0。5. 避坑清单五个看着对、跑起来错的字段陷阱字典给的是字段定义不给的是历史包袱。下面五条都是上线后才暴露的问题按“现象 → 原因 → 解决”记。5.1 语义反转类0 和 1 不一定是一个意思现象讨论区匿名回复上线后选了匿名的帖子把真名显示出来了没选匿名的反而隐藏了。代码逻辑看着没问题就是读ANONYMOUS_FLAG判断真假。原因BBS_ARTICLE的注释是“匿名标志0-正常1-匿名”BBS_ARTICLE_REPLY的注释是“回复是否匿名发起0-匿名1-否”。两张表字段名一样语义正好相反。同一个模块里出现相反口径光看字段名必然翻车。解决所有状态字段在代码里包一层常量禁止裸写数字。// 讨论主表与回复表的匿名字段口径相反必须分开定义常量禁止共用 public final class BbsFlags { // BBS_ARTICLE.ANONYMOUS_FLAG: 0-正常 1-匿名 public static boolean isArticleAnonymous(Integer flag) { return flag ! null flag 1; } // BBS_ARTICLE_REPLY.ANONYMOUS_FLAG: 0-匿名 1-否 public static boolean isReplyAnonymous(Integer flag) { return flag ! null flag 0; } }两个方法入参一样、返回值含义相反命名必须体现出来。判空也要处理flag为null时按非匿名走避免历史数据把内容藏了。顺带一个同类坑ATTENDANCE_REMIND.ATTEND_REMIND的取值是 1 签到不提醒、2 签到提醒不是 0/1。写if (remind 1)判断“要提醒”的话逻辑正好是反的。5.2 多租户类漏一个 ACCOUNT_ID 就串数据现象某单位管理员导出的通讯录里混进了另一个单位的联系人。原因ADDRESSBOOK_MEMBER本身没有ACCOUNT_ID租户隔离要靠CREATOR_ID关联人员再取单位或者通过ADDRESSBOOK_SET.ACCOUNT_ID过滤。写查询时觉得“这张表没单位字段就不用过滤”直接全表扫。解决凡是带ACCOUNT_ID、ORG_ACCOUNT_ID、AGENT_ACCOUNT_ID的表一律先加条件再写别的不带单位字段的表确认它的隔离路径后再决定怎么 join。给所有手写 SQL 加一条自查规则WHERE里没出现单位字段的要么是纯字典表要么就是漏了。5.3 扩展字段类位置被占了才知道现象新加的“合同到期日”写进EXT_ATTR_21上线一周后发现原有的“入职日期”全被覆盖了。原因EXT_ATTR_21早已被表单模板占用字典只告诉你这个位置是“日期类型扩展字段 1”不告诉你它当前装的是什么。扩展字段是共享资源字典不是台账。解决建一份自己的扩展字段台账字段名、段号、业务语义、占用模块、负责人用一张表管起来。上线前对目标段号做一次全表COUNT(EXT_ATTR_N) 0的检查只要有人用过就必须确认是不是废弃数据才能复用。这套流程听着笨但比事后写数据修复脚本便宜太多。5.4 软删除与归档类统计口径莫名偏小现象讨论区发帖量统计比运营后台看到的少一截。原因BBS_ARTICLE.STATE是软删标记0 正常、1 删除ATTENDANCE_NO_REMIND_MEMBER.STATE也是0 正常、1 废弃。统计 SQL 没加STATE 0但更大的问题是已经物理归档的数据不在主表里历史月份跨年统计会凭空少几天。解决所有业务统计固定带STATE 0跨年报表先确认归档策略ATTENDANCE_HISTORY这类流水表通常会按年清理需要历史数据得去归档库或者用ATTENDANCE_DAY_STATISTICS的汇总值兜底。别假设主表里什么都有。5.5 时间类DATETIME 的边界与空值现象月末最后一天的考勤数据统计不到。原因PUNCH_DATE、SIGN_TIME都是DATETIME写 2024-06-30等价于 2024-06-30 00:00:00当天 23:59 的记录全被切掉。解决统一用左闭右开 起始日 AND 次日。另外注意START_DATE、END_DATE、CANCEL_DATE这些字段都允许为空BETWEEN遇到NULL直接返回UNKNOWN整行被过滤掉。代理期限判断要用COALESCE兜底把空END_DATE当成“长期有效”。-- 正确左闭右开 空值兜底 SELECT COUNT(*) AS 打卡条数 FROM ATTENDANCE_HISTORY h WHERE h.ACCOUNT_ID 1001 AND h.SIGN_TIME 2024-06-30 AND h.SIGN_TIME 2024-07-01 AND COALESCE(h.TYPE, 0) IN (1, 2, 3); -- TYPE 为空的历史数据单独处理别混进统计6. 二次开发把数据字典做成每天跑的字段校验脚本字典最怕的不是不准是没人看。放在共享盘里的 Excel三个月后就跟库对不上了。我的习惯是把它变成一个每天自动跑的脚本解析字典、连库核对、输出差异报告出问题直接推给负责人。这个投入大概半天之后每次上线前省掉的是半小时的手工比对和一整晚的排障。第一步把字典整理成结构化格式。字段名、类型、是否强制、注释四列就够了表名单独一列存成 CSV 或者直接写进 YAML。第二步拿上一章那段information_schema查询拉实际结构。第三步逐表 diff。import csv def load_dict(path: str) - dict: 读取字典 CSVTABLE_NAME,FIELD_NAME,DATA_TYPE,REQUIRED,COMMENT tables {} with open(path, encodingutf-8) as f: for row in csv.DictReader(f): t row[TABLE_NAME].strip().upper() tables.setdefault(t, {})[row[FIELD_NAME].strip().upper()] { # 字典里的类型可能带长度比如 VARCHAR(1024)拆出基础类型比对 type: row[DATA_TYPE].split(()[0].strip().upper(), required: row[REQUIRED].strip().upper() TRUE, comment: row[COMMENT].strip(), } return tables def diff(expect: dict, actual: dict) - list: expect 来自字典actual 来自 information_schema返回差异清单 issues [] for table, cols in expect.items(): real actual.get(table) if real is None: issues.append(f[缺表] {table}) continue for name, meta in cols.items(): if name not in real: issues.append(f[缺列] {table}.{name} 字典有、库里无) continue if real[name][type] ! meta[type]: issues.append( f[类型不符] {table}.{name} 字典{meta[type]} 实际{real[name][type]} ) return issuesload_dict里做了一次类型降维把VARCHAR(1024)拆成VARCHAR因为长度变化在业务上通常不致命类型变了才是大事。diff只查“字典有、库里没有”和“类型不符”两类不反向查库里多出来的列——扩展表和临时表太多反向查噪音会淹没真正的问题。如果要更严可以加一个白名单文件把已知的额外列排除掉。跑法很简单挂个定时任务结果写进日志有issues就退出码非 0监控直接告警# 每天 07:00 跑一次差异写日志退出码非 0 触发告警 0 7 * * * cd /opt/oa-check \ python3 check_dict.py --dict dict.csv --db oa_prod /var/log/oa-dict.log 21 || \ echo 数据字典与库结构不一致请查看 /var/log/oa-dict.log常用改造场景对应到具体动作整理成表更直观改造需求动哪张表注意点通讯录加一个“合同到期日”ADDRESSBOOK.EXT_ATTR_21~30找空位复用前先COUNT检查占用新增考勤组ATTENDANCE_TEAM插新记录IS_NEW1旧记录补END_DATE不能直接UPDATE老记录加一个外勤打卡类型ATTENDANCE_INFO.TYPE已有 3 外勤不要新增码值先确认前端是否已支持讨论区加匿名开关BBS_ARTICLE.ANONYMOUS_FLAG与BBS_BOARD.AFFILIATEROOM_FLAG板块级和主题级配置要一起看最后提醒一句验证方法任何改完的 SQL先在测试库用“单位 ID 加时间区间”这种最窄的条件跑一次确认行数再放宽条件跑一次看行数是不是成比例放大。如果放宽后行数不增反降十有八九是某个LEFT JOIN被写成了INNER JOIN或者是NULL把WHERE整行干掉了。这套流程我吃了两次亏才固定下来——第一次是扩展字段被覆盖第二次是考勤统计跨月少一天。从那以后我每次拿到一份新的数据字典第一件事不是看字段而是先把它变成能跑的校验脚本跑通再说。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?