ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

致远OA V8.1数据字典:通讯录、代理与考勤表结构解析

致远OA V8.1数据字典:通讯录、代理与考勤表结构解析

简介:《致远OA V8.1 数据字典》面向致远OA二次开发、系统实施、数据库维护与数据治理人员,用于快速核对业务表结构、字段类型、约束条件和字段注释,减少理解数据库架构与排查数据异常时的试错成本。压缩包内共1个PDF文件,约2.2MB,以数据字典文档形式集中整理表名、字段名、数据类型、是否必填及中文注释,并覆盖通讯录等业务表,以及文本、数字、日期、枚举、选人、选部门、选岗位等扩展字段定义,可帮助读者对照业务场景判断字段用途。已有2616人学习下载。读者可据此梳理表间关系、确认主键与扩展字段规则,为二次开发、数据迁移、接口联调、报表取数和日常运维提供可查依据,也适合开发与实施团队作为共享的数据库参考手册。

1. 拿到一份表结构文档,能省掉三次 DESC 的功夫

接手一套跑了多年的协同办公系统,最怕的不是代码看不懂,而是库里那几百张表没人说得清。前阵子某公司的实施同事找过来,需求两条:通讯录要加三个业务字段,考勤要出月度汇总。他手里只有数据库连接,没有表结构文档,只能一条条DESC过去猜字段含义,光ADDRESSBOOK里那七十个EXT_ATTR就猜了一下午。致远OA 数据字典 V8.1 这类资源对应的正是这个场景——把表名、字段名、数据类型、是否必填、中文注释一次性摆出来,覆盖通讯录、代理、考勤、讨论区几个核心模块。做二次开发、报表取数、系统对接、迁移评估的人都能用得上:新手照着注释能写出第一条 SQL,熟手能从中读出这套系统的建模习惯和扩展边界。

2. 通讯录七张表:70 个 EXT_ATTR 的定长扩展怎么读

通讯录是这份数据字典里最能体现设计取向的一块。它没有用一张大表包打天下,而是拆成主表、外部联系人表、个人组表、关联表、设置表和设置范围表,一共七张,各管一段。读之前先把「谁扩展谁、谁配置谁」这层关系理顺,后面写 SQL 才不会连错。

2.1 为什么是预留列而不是属性表

ADDRESSBOOK的结构很直白:ID是必填主键,MEMBER_ID指向人员,CREATE_DATE和UPDATE_DATE记录时间,剩下的七十个列全部是扩展位。按类型分成七段,每段十个:

字段区间类型用途
EXT_ATTR_1 ~ 10VARCHAR(1024)文本类型扩展字段
EXT_ATTR_11 ~ 20DECIMAL(19,4)数字类型扩展字段
EXT_ATTR_21 ~ 30DATETIME日期类型扩展字段
EXT_ATTR_31 ~ 40VARCHAR(50)枚举类型数据字段
EXT_ATTR_41 ~ 50VARCHAR(50)选人类型数据字段
EXT_ATTR_51 ~ 60VARCHAR(50)选部门类型数据字段
EXT_ATTR_61 ~ 70VARCHAR(50)选岗位类型数据字段

这套「定长预留列」的做法在产品型系统里很常见。客户要加一个「员工生日」字段,如果每次都改表结构,升级脚本会和客户自定义字段打架,版本一多就成灾难;预留七十个槽位,配合ADDRESSBOOK_SET.DISPLAY_COLUMN存显示列头配置,就能做到加字段不改表。代价也很明显:类型语义全靠文档约定,数据库层面加不上 CHECK 约束,谁把日期写进EXT_ATTR_5也没人拦得住。

对比 EAV(属性-值-实体)方案,预留列的取舍其实很清楚。EAV 灵活,理论上无限扩展,但每次查询都要多次 join 属性表,做排序分页、做报表聚合时 SQL 会难看到没法维护。预留列查询是平铺的,WHERE EXT_ATTR_1 = 'x'直接走索引,报表好写。选它,本质是用「扩展上限」换「查询简单」,七十个位子用完就得动结构,这是硬边界。

实际排查时,先看某个人身上挂了哪些扩展值:

-- 按人员查通讯录扩展字段,七个类型各取第一个代表列 SELECT ID, MEMBER_ID, TYPE, EXT_ATTR_1 AS 文本扩展1, EXT_ATTR_11 AS 数字扩展1, EXT_ATTR_21 AS 日期扩展1, EXT_ATTR_31 AS 枚举扩展1, EXT_ATTR_41 AS 选人扩展1, EXT_ATTR_51 AS 选部门扩展1, EXT_ATTR_61 AS 选岗位扩展1 FROM ADDRESSBOOK WHERE MEMBER_ID = 10086;

MEMBER_ID是人员主键,这条 SQL 返回 0 行不代表人员不存在,只代表他没有被扩展过——ADDRESSBOOK是扩展属性表,不是人员主表。这一点在对接时最容易搞混,很多报表需求要的「人员基础信息 + 扩展信息」,基础信息还得回组织模型表取,两段拼起来才是完整视图。

TYPE字段是VARCHAR(50),用来区分联系人类型。从字段命名和上下文看,它区分的是内部联系人与外部联系人这两条链路,外部联系人的明细落在ADDRESSBOOK_MEMBER,TYPE起分流作用。写查询时不要假设它一定是数字,字符串比较更稳妥。

2.2 四张业务表的关联路径

真正承载业务数据的是四张表,关系是这样铺开的:

ADDRESSBOOK_MEMBER存外部联系人明细,NAME、COMPANY_NAME、COMPANY_DEPT、COMPANY_POST这些是单位信息,MOBILEPHONE、FAMILY_PHONE、FAX、EMAIL、ADDRESS、POSTCODE是联系方式。字段类型上有个细节值得注意,COMPANY_PHONE、FAMILY_PHONE、MOBILEPHONE、FAX全是VARCHAR(20),而不是数值型——电话号码带区号、带分机、带+86,存字符串是对的,千万别为了「规范」改成BIGINT,前导零会丢。

这张表通过CATEGORY_ID挂到ADDRESSBOOK_TEAM。ADDRESSBOOK_TEAM的TYPE有三个取值:0 系统组、1 个人组、2 私人通讯录。所以「某个人建了哪些私有通讯录」这条查询,条件是TYPE = 2 AND CREATOR_ID = ?。

ADDRESSBOOK_TEAM_MEMBERS是组和成员的关联表,只有TEAM_ID、MEMBER_ID、ID三个字段,典型的多对多中间表。查某个组里有哪些人,走它。

一个常见的取数需求是「列出某人私人通讯录里的全部外部联系人」:

-- 个人组 -> 关联表 -> 外部联系人明细,三段式关联 SELECT t.ID AS 组ID, t.NAME AS 组名, m.NAME AS 联系人, m.COMPANY_NAME AS 单位, m.MOBILEPHONE AS 手机 FROM ADDRESSBOOK_TEAM t JOIN ADDRESSBOOK_TEAM_MEMBERS tm ON tm.TEAM_ID = t.ID JOIN ADDRESSBOOK_MEMBER m ON m.ID = tm.MEMBER_ID WHERE t.TYPE = 2 -- 2 表示私人通讯录 AND t.CREATOR_ID = 10086 -- 只取某人创建的组 ORDER BY t.ID, m.NAME;

CREATOR_ID和CREATOR_NAME是冗余设计,CREATOR_NAME只有VARCHAR(20),人员改名后这个冗余字段不会自动跟着变,做展示可以,做统计口径不要用它。MEMO是TEXT类型,存长备注,别拿它做等值查询。

外部联系人表还有个时间字段命名的坑:ADDRESSBOOK用CREATE_DATE/UPDATE_DATE,ADDRESSBOOK_MEMBER用CREATED_TIME/MODIFIED_TIME。同一套库里两套命名,写跨表 SQL 时要一个个对,不能凭手感复制。

2.3 权限配置表的三个开关

ADDRESSBOOK_SET是通讯录的显示与权限总开关,一行对应一个单位的配置。字段不多但每个都影响可见性:

VIEW_SCOPE(SMALLINT)控制查看范围,KEY_INFO和KEY_INFO_TYPES控制关键信息展示,后者取值 1 表示手机号加职务级别、2 表示手机号、3 表示职务级别。EXPORT_PRINT管导出打印权限。EXTERNAL_SCOPE是编外人员权限,1 表示按工作范围显示、2 表示全部隐藏。M3_SET和M3_DISPLAY_COLUMN是移动端的独立显示配置,ZX_SET和ZX_DISPLAY_COLUMN是另一端的独立配置,都是「0 不单独设置、1 单独设置」的开关加列配置字符串。DEPARTMENT_INFO_ORDER控制部门下部门和人员的先后顺序。DISPLAY_COLUMN是VARCHAR(1024)的列头配置,多个列名怎么拼、用什么分隔,取决于前端解析约定,改之前先把现有值拿出来看格式。

范围表ADDRESSBOOK_SET_SCOPE才是权限落地的位置。它用ADDRESSBOOK_SET_TYPE区分三类配置:1 查看范围、2 关键信息、3 导出与打印权限。USER_ID配合USER_TYPE指向组织模型实体,也就是说这个范围既可以挂人,也可以挂部门、岗位。

查「某条设置对哪些实体生效」,标准写法是先定设置再拉范围:

-- 查某个单位通讯录设置下的全部授权实体 SELECT s.ID AS 设置ID, sc.ADDRESSBOOK_SET_TYPE AS 配置类型, -- 1 查看 2 关键信息 3 导出打印 sc.USER_ID AS 实体ID, sc.USER_TYPE AS 实体类型 FROM ADDRESSBOOK_SET s JOIN ADDRESSBOOK_SET_SCOPE sc ON sc.ADDRESSBOOK_SET_ID = s.ID WHERE s.ACCOUNT_ID = 20001 ORDER BY sc.ADDRESSBOOK_SET_TYPE, sc.USER_ID;

ADDRESSBOOK_SET_TYPE不加过滤就会把三类配置混在一起返回,做权限校验时必须带条件。另外ACCOUNT_ID在设置表里是单位 ID,在多组织架构下它是隔离键,单组织环境经常是固定值,容易被忽略,一旦上多组织就会漏数据。

3. 代理与智能处理:从 AGENT 到 AI_REMIND_COUNT 的链路拆解

代理这块是二次开发里出错率最高的模块之一,因为它不是单表能说清的,一条委托关系要跨三张表才能完整描述。后面的AI_*系列表又叠了一层自动化条件与催办,两者合起来构成「人不在,事照转」的底座。

3.1 AGENT 一行记录就是一段委托

AGENT表的核心是两个 ID:AGENT_ID是代理人,AGENT_TO_ID是被代理人。方向别搞反,AGENT_TO_ID才是那个「本来该我处理、现在交给你」的人。很多查询写反了,结果查出来的代理关系正好镜像。

时间维度有四个字段:START_DATE代理期限开始、END_DATE代理期限结束、CANCEL_DATE取消时间、CREATE_DATE创建时间。判定「当前是否生效」不能只看END_DATE > NOW(),还得加上CANCEL_FLAG = 0。CANCEL_FLAG取值 0 未取消、1 已取消,手工取消的代理END_DATE可能还在未来,光看结束时间会把已取消的也算进来。

AGENT_TYPE区分业务性质:1 是代理设置,2 是离职交接。两者数据结构一样,业务含义完全不同。做代理统计报表时如果不区分,离职交接会被算成正常代理,数字对不上。

AGENT_OPERATION记录操作来源:0 被代理人自己、1 集团管理员、2 单位管理员。这个字段在审计场景里很有用,能回答「这条代理是谁替他设的」。

MATTER_TYPE是事项条件设置类型,0 按处理人设置、1 按发起人设置,默认值 1。它决定AGENT_SCOPE里那些范围数据是作用在处理人维度还是发起人维度,读代理规则时必须一起看。

HAS_AWOKE字段注释明确写了「未使用」,AGENT_REMIND和AGENT_TO_REMIND是两个提醒开关,0 没有提醒、1 有提醒。历史遗留字段不要拿来写业务逻辑。

查某个被代理人当前生效的代理关系:

-- 生效判定:未取消 + 已开始 + 未结束 SELECT a.AGENT_ID AS 代理人, a.AGENT_TO_ID AS 被代理人, a.AGENT_TYPE AS 代理类型, -- 1 代理设置 2 离职交接 a.START_DATE, a.END_DATE, a.AGENT_OPERATION AS 操作来源 FROM AGENT a WHERE a.AGENT_TO_ID = 10086 AND a.CANCEL_FLAG = 0 AND a.START_DATE <= NOW() AND a.END_DATE >= NOW();

这条 SQL 的四个条件缺一不可。少CANCEL_FLAG会多算,少START_DATE会把未来才生效的提前算进来。索引层面,AGENT_TO_ID加CANCEL_FLAG的组合是高频过滤条件,如果做性能优化,优先在这两列上考虑。

两个ACCOUNT_ID字段也值得留意:AGENT_ACCOUNT_ID是代理人所属单位,AGENT_TO_ACCOUNT_ID是被代理人所属单位。跨单位代理在集团环境下很常见,两个值不相等就说明是跨单位委托,权限判断要格外小心。

3.2 AGENT_DETAIL 与 AGENT_SCOPE 补全两个维度

AGENT只说「谁代理谁」,不说「代理哪些应用」和「哪些发起人的事能代理」。这两个维度分别落在AGENT_DETAIL和AGENT_SCOPE。

AGENT_DETAIL的字段很少:AGENT_ID、APP、ENTITY_ID。APP是BIGINT且必填,代表应用标识,也就是说一条代理明细必须绑定一个具体应用。ENTITY_ID是这个应用里的实体标识。想查「某人代理了哪几个应用」,按AGENT_ID分组数APP就行。

AGENT_SCOPE存的是发起人范围,AGENT_ID关联主表,ENTITY_TYPE是字符串,注释里明确举例ACCOUNT、DEPARTMENT,ENTITY_ID是对应实体主键。这意味着范围可以按人配,也可以按部门配,部门配一条等于覆盖整个部门。

把两张表拼起来看一条完整的代理规则:

-- 一条代理关系 + 应用范围 + 发起人范围 SELECT a.ID AS 代理记录ID, a.AGENT_ID AS 代理人, a.AGENT_TO_ID AS 被代理人, d.APP AS 应用标识, s.ENTITY_TYPE AS 范围实体类型, -- ACCOUNT / DEPARTMENT s.ENTITY_ID AS 范围实体ID FROM AGENT a LEFT JOIN AGENT_DETAIL d ON d.AGENT_ID = a.ID LEFT JOIN AGENT_SCOPE s ON s.AGENT_ID = a.ID WHERE a.CANCEL_FLAG = 0 AND a.AGENT_TYPE = 1; -- 只取普通代理,排除离职交接

这里用LEFT JOIN而不是INNER JOIN,是因为有些代理只配了应用没配范围,或者反过来。用内连接会把这些不完整的规则直接过滤掉,排查「代理为什么不生效」时反而看不见问题记录。这种一对多的关联会产生笛卡尔放大,一条AGENT记录可能返回多行,做计数时记得COUNT(DISTINCT a.ID)。

APP的具体取值字典在数据字典里没有展开,它依赖应用注册表。对接时不要硬编码数字,从应用配置里读,否则版本升级后应用 ID 变化会静默失效。

3.3 AI_* 六张表:条件、留痕与催办

AI_前缀这组表名容易让人误以为是人工智能模型相关,实际上从字段看,它们对应的是自动化处理规则、组织变更留痕和催办计数,属于流程引擎的增强能力。

AI_DEAL_CONDITION存自动处理条件,字段包括TEMPLATE_ID模板、SENDER_ID发起人、USER_ID处理人、DEAL_TYPE操作类型、SIGNLE_VIEW_PERIOD处理时长(默认 0)、CREATE_TIME。DEAL_TYPE的枚举值得背下来:0 回退、1 指定回退、2 撤销、3 终止、4 不同意、5 处理时长。注意这里是「操作类型」而不是「条件类型」,它描述的是满足条件后执行什么动作。

AI_PROCESSING_CONDITION只有TEMPLATE_ID、CONDITION_VALUE(TEXT)、CREATE_TIME、UPDATE_TIME四个字段。CONDITION_VALUE是长文本,里面存的通常是序列化后的条件表达式,格式由应用层决定,数据库层面只能当字符串处理,别指望用 SQL 解析它。要看条件内容,导出来用代码反序列化。

AI_ORG_CHANGE_RECORD和AI_ORG_MEMBER_CHANGE是两张留痕表。前者记录组织对象变更,OLD_ID、OLD_VALUE、NEW_ID、NEW_VALUE加ORG_ACCOUNT_ID、ORG_TYPE(0 部门、1 岗位、2 职务级别)。后者记录人员归属变更,MEMBER_ID加上旧新两套ACCOUNT_ID、DEPT_ID、POST_ID、LEVEL_ID。这两张表是排查「为什么流程走到了错误的人那里」的第一现场——人员调岗后审批人没变,多半就是这里的变更记录没被消费。

催办这块是AI_REMIND_RECORD和AI_REMIND_COUNT两张表配合。AI_REMIND_RECORD记催办动作,REMIND_USER_ID催办人、REMIND_AFFAIR_ID被查看的已办或已发事项、COUNT查看次数、IS_SEND_MSG是否已发送提醒(0 未发、1 已发)、UPDATE_TIME。AI_REMIND_COUNT记被催办方的接收情况,AI_REMIND_ID关联记录表、AFFAIR_ID被催办事项、AFFAIR_RECEIVE_TIME事项接收时间、COUNT收到催办消息的次数。

统计「某个事项被催了几次」的标准查询:

-- 按事项聚合催办次数,只统计已发送成功的 SELECT r.REMIND_AFFAIR_ID AS 事项ID, COUNT(1) AS 催办记录数, SUM(c.COUNT) AS 累计接收次数, MAX(r.UPDATE_TIME) AS 最后一次催办时间 FROM AI_REMIND_RECORD r LEFT JOIN AI_REMIND_COUNT c ON c.AI_REMIND_ID = r.ID WHERE r.IS_SEND_MSG = 1 GROUP BY r.REMIND_AFFAIR_ID ORDER BY 累计接收次数 DESC;

SUM(c.COUNT)在LEFT JOIN下可能为NULL,报表展示时用COALESCE兜一下。AI_REMIND_RECORD.COUNT和AI_REMIND_COUNT.COUNT语义不同,前者是查看次数,后者是接收次数,名字一样但别混用。

4. 考勤二十余张表:排班、流水、日统计的取数路径

考勤是这份字典里表最多的一块,从ATTENDANCE_ARRANGE到ATTENDANCE_TEAM,二十多张表按「配置—排班—流水—统计」四层堆起来。不按层次读,很容易在ATTENDANCE_DAY和ATTENDANCE_DAY_STATISTICS之间迷路。

4.1 考勤组、排班、班次的三层结构

最上层是考勤组ATTENDANCE_TEAM,字段有ATTENDANCE_TEAM_NAME、CREATE_MEMBER、ATTENDANCE_MANAGER、ORG_ACCOUNT_ID、IS_NEW、START_DATE、END_DATE。IS_NEW取值 0 不是最新、1 是最新,配合START_DATE、END_DATE实现历史版本管理——考勤组改名或调整规则时不是原地改,而是插一条新记录并把旧记录IS_NEW置 0。查「当前生效的考勤组」必须带上IS_NEW = 1,否则会把历史版本一起拉出来,这是最典型的翻车点。

中间层是排班ATTENDANCE_ARRANGE,ARRANGE_TYPE区分 0 固定排班、1 周期排班,ATTENDANCE_TEAM_ID挂到考勤组。再往下是班次ATTENDANCE_CLASS,CLASS_TYPE有三个取值:0 一天一次、1 一天两次、2 一天三次,直接决定这个人一天要打几次卡。班次上还有一堆弹性规则字段:FLEX_TIME弹性时间、LATE_SERIOUS严重迟到阈值、ABSENT_WORK旷工阈值、LEAVE_FIX_TIME晚走多少时间、ATTEND_FIX_TIME晚到多少时间,全是VARCHAR(50)。这些字段存的是规则参数,字符串形式取决于前端配置格式,可能是HH:mm,也可能是分钟数,读之前先抽样看实际值。

班次的具体时间点落在ATTENDANCE_FIX_TIME:WORK_TIME上班时间、END_TIME下班时间、EARLY_WORK_TIME最早签到时间、LAST_END_TIME最晚签退时间,再加ATTEND_REMIND、LEAVE_REMIND两个提醒开关。它同时挂了ATTENDANCE_CLASS_ID、ATTENDANCE_ARRANGE_ID、ATTENDANCE_TEAM_ID三个外键,三路都能关联,查询时选最短路径。

ATTENDANCE_DAY是排班到具体日期的展开表,WEEK_DAY星期几、ATTENDANCE_ARRANGE_ID、ATTENDANCE_CLASS_ID、IS_WORK(0 不是工作日、1 是)、ATTENDANCE_TEAM_ID。它是「哪天该上哪个班」的答案表。

查某个人所在考勤组当前生效的班次配置:

-- 人员 -> 授权实体 -> 考勤组 -> 排班 -> 班次 SELECT t.ATTENDANCE_TEAM_NAME AS 考勤组, a.ARRANGE_TYPE AS 排班类型, -- 0 固定 1 周期 c.ATTENDANCE_CLASS_NAME AS 班次, c.CLASS_TYPE AS 班次类型, -- 0/1/2 对应一天 1/2/3 次 f.WORK_TIME AS 上班时间, f.END_TIME AS 下班时间 FROM ATTENDANCE_AUTH_ENTITY e JOIN ATTENDANCE_TEAM t ON t.ID = e.ATTENDANCE_TEAM_ID AND t.IS_NEW = 1 JOIN ATTENDANCE_ARRANGE a ON a.ATTENDANCE_TEAM_ID = t.ID JOIN ATTENDANCE_CLASS c ON c.ATTENDANCE_ARRANGE_ID = a.ID LEFT JOIN ATTENDANCE_FIX_TIME f ON f.ATTENDANCE_CLASS_ID = c.ID WHERE e.ENTITY_ID = 10086 AND e.ENTITY_TYPE = 3; -- 3 人员 1 部门 0 单位

ATTENDANCE_AUTH_ENTITY.ENTITY_TYPE的三个取值别弄反:3 人员、1 部门、0 单位。授权是按实体挂的,一个人可能既被直接授权、又通过部门被间接授权,两路都命中时会返回多行,业务上谁优先得看应用的合并规则,SQL 层面只能把两种来源都列出来。

ATTENDANCE_AT是另一张辅助表,AT_ORG_ID是被 at 的人,ATTENDANCE_ID对应考勤记录,作用范围很窄,主流程查询基本用不上。

4.2 ATTENDANCE_HISTORY 与 ATTENDANCE_INFO 的分工

打卡流水有两张字段高度相似的表,ATTENDANCE_HISTORY和ATTENDANCE_INFO。从字段差异能推断出分工:INFO多了IS_WORK_DAY、MODIFY_NUM(签到修改次数)、DEVICE_INFO(设备信息)、PUNCH_TYPE、STATE(3 上班迟到、4 下班早退、5 正常上班、6 正常下班)、MAC_ADDRESS(存储 WiFi Mac 地址),这些是判定结果和审计信息;HISTORY没有这些,更接近原始流水。

对比项ATTENDANCE_HISTORYATTENDANCE_INFO
SIGN 字段长度VARCHAR(500)VARCHAR(2000)
图片/语音信息IMG_INFO / RECORD_INFOIMG_INFO / RECORD_INFO(JSON 格式)
状态判定无 STATE有 STATE,含迟到早退正常
修改痕迹无有 MODIFY_NUM
设备信息无有 DEVICE_INFO / MAC_ADDRESS

两张表共有的字段也值得逐个过一遍。OWNER_ID打卡人员、DEPT_ID部门、SIGN_TIME打卡时间、ACCOUNT_ID单位、SIGN打卡地址或 IP、TYPE签到来源(1 签到、2 签退、3 外勤)、LONGITUDE/LATITUDE经纬度、FIX_TIME规定上下班时间。地理信息那一段特别长:CONTINENT洲、COUNTRY国家、PROVINCE省、CITY城市、TOWN区县、STREET街道、NEAR_ADDRESS附近地址、ADDRESS_CODE区号、ADDRESS_TYPE(1 表示 poi、2 表示街道、3 表示路、4 其他)。YEAR、MONTH、DAY三个冗余字段是为了报表分区查询方便,不用再对SIGN_TIME做函数运算。

取一个人某月的全部打卡流水,两张表都要扫:

-- 流水合并:以 INFO 为主,HISTORY 补充归档数据 SELECT OWNER_ID, SIGN_TIME, TYPE, SIGN, STATE, 'INFO' AS 来源 FROM ATTENDANCE_INFO WHERE OWNER_ID = 10086 AND YEAR = 2025 AND MONTH = 3 UNION ALL SELECT OWNER_ID, SIGN_TIME, TYPE, SIGN, NULL AS STATE, 'HISTORY' AS 来源 FROM ATTENDANCE_HISTORY WHERE OWNER_ID = 10086 AND YEAR = 2025 AND MONTH = 3 ORDER BY SIGN_TIME;

YEAR和MONTH是INT类型,用它们做过滤比SIGN_TIME BETWEEN更稳,也不会让索引失效。UNION ALL不要写成UNION,两张表理论上不该有重复记录,用UNION会多做一次去重排序,数据量大时很慢。

IMG_INFO和RECORD_INFO都是VARCHAR(500),注释说存的是附件表里没有的相关信息,INFO表注明是 JSON 格式。500 字符装几张图的元信息很容易顶格,解析前先判长度,别直接丢给 JSON 解析器,截断的 JSON 会抛异常。

4.3 用日统计表拼月度报表

ATTENDANCE_DAY_STATISTICS是给报表准备的聚合表,一个人一天一行。维度字段有ACCOUNT_ID、DEPT_ID、TEAM_ID、RULE_ID、MEMBER_ID、PUNCH_DATE考勤日、WEEK_DAY工作日星期。时间字段分上午下午四段:AM_START_TIME、AM_END_TIME、PM_START_TIME、PM_END_TIME,对应的结果字段是AM_START_STATE、AM_END_STATE、PM_START_STATE、PM_END_STATE。

聚合指标有:NORMAL_DAYS出勤正常天数、AB_NORMAL_DAY异常天数、NONWORKDAYS_DAY非工作打卡天数、LATE_NUM迟到次数、LEAVE_EARLY_NUM早退次数、OUTSIDE_NUM外勤次数、MISSINGCLOCK_COUNT缺卡总数、WORKING_MISSINGCLOCK上班缺卡数、KNOCKOFF_MISSINGCLOCK下班缺卡数、PUNCH_CLASS考勤班次应打卡次数(1 一天一次、2 一天两次)。

月度汇总直接在这张表上做:

-- 按人汇总某月考勤,口径以日统计表为准 SELECT d.MEMBER_ID AS 人员ID, d.DEPT_ID AS 部门ID, SUM(d.NORMAL_DAYS) AS 正常出勤天数, SUM(d.AB_NORMAL_DAY) AS 异常天数, SUM(d.LATE_NUM) AS 迟到次数, SUM(d.LEAVE_EARLY_NUM) AS 早退次数, SUM(d.OUTSIDE_NUM) AS 外勤次数, SUM(d.MISSINGCLOCK_COUNT) AS 缺卡总数, SUM(d.PUNCH_CLASS) AS 应打卡次数 FROM ATTENDANCE_DAY_STATISTICS d WHERE d.ACCOUNT_ID = 20001 AND d.PUNCH_DATE >= '2025-03-01' AND d.PUNCH_DATE < '2025-04-01' GROUP BY d.MEMBER_ID, d.DEPT_ID ORDER BY 异常天数 DESC;

日期区间用左闭右开,避免BETWEEN在月末最后一天带时分秒时漏数据。PUNCH_DATE是考勤日不是打卡时间,晚班跨零点的情况下它和SIGN_TIME的日期会对不上,报表口径要提前和业务确认清楚。

ATTENDANCE_DAY_STATISTICS是跑批生成的,如果统计任务没跑或者跑失败,这张表就是空的,月度报表会显示全员零出勤。排查时先查这张表最新PUNCH_DATE,再回去看流水表有没有数据,顺序别反。

ATTENDANCE_SETTING是地理围栏配置,LONGITUDE、LATITUDE、ATTEND_RANGE范围、AVAILABLE是否启用、RELATIVE_ID存第三方系统导入的围栏 ID。ATTENDANCE_REMIND是提醒配置,ATTEND_REMIND取值 1 签到不提醒、2 签到提醒,ATTEND_REMIND_TIME是提前分钟数。ATTENDANCE_NO_REMIND_MEMBER单独存不需要提醒的人,STATE0 正常、1 废弃,查的时候带上STATE = 0。

5. 避坑记录:五个字段级问题与排查顺序

这一节全是踩过的坑,按「现象—原因—解决」写,遇到同类问题可以直接对号入座。

5.1 扩展字段长度不够存多选

现象:通讯录的选人扩展字段配置成多选后,保存三个人只回来一个,或者后端直接抛数据截断异常。

原因:EXT_ATTR_41到EXT_ATTR_50是VARCHAR(50),一个BIGINT主键最长 19 位,加一个逗号分隔符,50 个字符最多放两个完整 ID。第三个 ID 写进去就被截断了。字段名叫「选人类型」,很容易被理解成「可以选多个人」,实际容量只够单选。

解决:单选用这几个字段,多选另建关联表(业务主键 + MEMBER_ID两列),不要硬塞逗号串。已经有历史数据的,用FIND_IN_SET查询能跑通,但要清楚这个函数用不上索引:

-- 查选了某人的通讯录记录,仅适用于历史逗号串数据 SELECT ID, MEMBER_ID FROM ADDRESSBOOK WHERE FIND_IN_SET('10086', EXT_ATTR_41) > 0;

数据量大时这条 SQL 会全表扫。真要长期用,把逗号串迁到关联表,查询改回普通 join。

5.2 同名 ANONYMOUS_FLAG 取值相反

现象:讨论区的匿名帖在后台导出成实名,或者明明选了实名发出去却显示匿名。

原因:BBS_ARTICLE的ANONYMOUS_FLAG是 0 正常、1 匿名;而BBS_BOARD和BBS_ARTICLE_REPLY的ANONYMOUS_FLAG是 0 匿名、1 否。同一个字段名,三张表两套语义,而且方向正好相反。从注释看,主题表是「是不是匿名」,板块和回复表是「是否允许匿名」,问句不一样,取值自然反着来。

解决:不要写通用映射函数,按表分别处理。稳妥做法是建两个视图,把语义统一成IS_ANONYMOUS(1 匿名、0 实名),应用层只查视图。历史 SQL 排查时,先看表名再决定比较值,别靠字段名猜。

5.3 代理有效期漏判取消标记

现象:代理早就手工取消了,流程还是往代理人那里转。

原因:只判断了END_DATE >= NOW(),没判断CANCEL_FLAG。手工取消时系统写的是CANCEL_FLAG = 1和CANCEL_DATE,END_DATE保持原值不变。

解决:生效条件固定写成四段:CANCEL_FLAG = 0 AND START_DATE <= NOW() AND END_DATE >= NOW(),再加上AGENT_TYPE过滤掉离职交接。把这个条件封装成视图V_AGENT_ACTIVE,全项目只引用视图,避免各处手写漏条件。另外CANCEL_DATE有值但CANCEL_FLAG还是 0 的脏数据要单独查一遍,这种记录说明取消流程中途失败了。

5.4 多次班次的缺卡口径

现象:一天两次班次的员工只打了一次卡,月度报表的缺卡总数显示 0,人事不认。

原因:ATTENDANCE_DAY_STATISTICS里MISSINGCLOCK_COUNT、WORKING_MISSINGCLOCK、KNOCKOFF_MISSINGCLOCK三个字段的口径依赖PUNCH_CLASS。当PUNCH_CLASS = 2时,应打卡次数是 2,但某些版本的统计任务只按上班卡判断缺卡,下班缺卡没累加进总数。字段注释只写了「缺卡总数」,没写清楚算法,光看文档会以为是全量。

解决:不要直接信MISSINGCLOCK_COUNT,用明细重算:应打卡数 = PUNCH_CLASS,实打卡数 = 当天 ATTENDANCE_INFO 中 TYPE IN (1,2) 的记录数,差值就是缺卡数。重算逻辑写进报表 SQL,和统计表并存一段时间做交叉验证,等口径确认后再切回统计表。

5.5 双流水表归档截断

现象:历史打卡记录的地理位置显示不全,地址到「街道」就断了,附近地址为空;偶尔图片信息解析报 JSON 格式错误。

原因:ATTENDANCE_HISTORY.SIGN是VARCHAR(500),ATTENDANCE_INFO.SIGN是VARCHAR(2000)。归档任务从INFO往HISTORY搬数据时,超过 500 字符的地址被数据库静默截断(非严格模式下不报错)。IMG_INFO和RECORD_INFO两张表都是 500,图片多了同样截断。

解决:归档前先SELECT MAX(CHAR_LENGTH(SIGN))看实际长度分布,超出就扩HISTORY.SIGN到 2000,或者归档时只保留结构化地址字段(省市区街道分开存),把长文本和图片信息落附件表。图片信息在入库前做长度校验,超长直接拒绝而不是截断,截断的 JSON 比没有更麻烦。

6. 把字典变成可校验的元数据:一个解析脚本和一条比对习惯

数据字典是文档,文档会过期。版本升级后字段可能改名、加长、新增,光靠人眼比对几十页字段表不现实。我一般会先把文档解析成结构化元数据,再拿它和生产的information_schema对一遍,差异一目了然。

6.1 解析成结构化元数据

原始文本里有

本文还有配套的精品资源,点击获取

返回列表