ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

企业信息管理系统落地:边界、主数据、权限与审批流

企业信息管理系统落地:边界、主数据、权限与审批流

接手"企业信息管理系统"这类项目,最怕的不是技术难,而是从一开始就没人说得清它到底要管什么。我见过太多团队,需求文档第一页写着"实现全公司人、事、物的信息化管理",第二页开始画架构图,等到第三个月上线,业务方点开首页问的第一句话是"我要的那张表在哪"。这中间差的不是代码量,是边界、口径和取舍。这篇东西不讲大道理,就把一个企业信息管理系统从零到跑起来、再到用三年不废的关键节点拆一遍:系统边界怎么划、需求评审桌上吵的三件事怎么定、组织树和人员主数据怎么建模、权限为什么总会失控、审批流该不该上引擎、接口对接的顺序、数据迁移怎么校验、上线后怎么推广、以及系统老了以后怎么治理。适合正在做类似系统的开发者、技术负责人,也适合被拉来做需求对接的业务侧同学,看完至少能少返工两轮。

1. 从"全公司信息都能查"这句话开始拆系统边界

1.1 那句"全公司信息都能查"埋了什么雷

"企业信息管理系统"这个名字本身就带着陷阱,它听起来像一个大一统的东西,实际上是一个筐。销售合同、员工档案、固定资产、供应商资质、项目进度、考勤记录,全都能往里装。真按这个思路做,最后一定做成一个首页有三十个入口、每个入口点进去都是半成品的怪物。

我的经验是,凡是需求方说出"所有信息都能查"这种话,先别急着记,反过来问三个问题:谁查、查了干什么、查完这个动作之后下一步是什么。第一问定权限,第二问定展示形态,第三问定这个功能到底属不属于本系统。很多时候问完会发现,对方真正想要的是一个可以按部门筛人的通讯录,而不是一个覆盖全业务的数据中台。

举个具体的例子。某公司行政侧提需求,说希望系统里能看到"所有员工的所有信息"。听起来很吓人,拆开一问,实际场景是:新员工入职第一天,前台要知道这人坐哪、找谁领电脑、门禁卡开哪几道门。这三个动作对应三份数据,分布在不同模块,需要一个"入职引导页"把它们聚合起来,权限范围仅限行政和该员工直属主管。这么一收敛,工作量从"建一个全量数据中心"降到"做一个聚合查询页",两周能出东西。

所以边界的第一个判断依据,不是数据多不多,而是有没有一个明确的、反复发生的业务动作在支撑这个功能。没有动作支撑的功能,做出来就是一次性演示品。

1.2 用"停摆测试"划出第一版范围

划范围有个很土但特别好用的办法,我叫它停摆测试:假设这个功能明天不存在,哪些业务会当场停下来?

停摆的,进第一版。会变麻烦但能绕过去的,进第二版。只是"有了更方便"的,先记在待办清单里,一年后再看还有没有人提。

按这个标准过一遍,第一版通常只剩下四块:

模块停摆判断是否进第一版
组织架构与人员档案新人入职无法建档、找人靠问是
账号与权限无法控制谁看什么是
核心审批(如入职、调岗、离职)流程靠口头和纸质单是
数据报表与大屏领导想看趋势,但有周报顶着否
移动端电脑上能办否
与第三方系统深度集成手工导一次表能撑住部分

这个表的价值在于,它把"要不要做"变成了"不做会不会死",讨论立刻从主观偏好变成可验证的判断。我在几个项目里推过这套,最大的收益是把第一版工期从半年压到十周左右,而且上线后没人觉得缺东西——因为真缺的东西本来就少。

提示:停摆测试一定要让业务方自己回答,不要技术人员替他们判断。技术视角下"这个功能很简单",业务视角下可能是"这个功能不做我们每天多花两小时",两边的权重完全不同。

1.3 主数据、流程、报表:三段论把系统切成能交付的块

范围定了之后,架构上我会强行把系统切成三层,这三层的生命周期、变更频率、负责人都完全不一样,混在一起做必然互相拖累。

主数据层管的是"谁是我们的员工、我们有哪些部门、我们有哪些岗位"。这一层的特点是变更频率低、准确性要求极高、一旦错了全系统跟着错。它必须有唯一数据源,别的地方只能引用不能自己维护。

流程层管的是"一件事从发起到结束经过谁"。它变更频率高,业务规则一年改三回是常态,所以这一层要设计成可配置的,改流程不改代码。

报表层管的是"把前两层的数据按不同维度拼出来看"。它最容易被低估,实际上工作量往往占整个项目三成以上,因为每个领导的看数习惯都不一样。

三层分开的好处很直接:主数据出问题不影响流程跑,流程改了不用动报表口径,报表加个维度也不用去改表结构。反过来,我见过把三者揉在一个大表单里的系统,加一个审批节点要改数据库字段,最后没人敢动。

2. 需求评审桌上最容易吵起来的三件事

2.1 "我要一个大屏"背后的真实诉求

需求会上出现频率最高的一句话是"给我做个大屏"。这句话几乎不能直接执行,因为大屏是个形式,不是需求。我一般会追问:这块屏放在哪、谁看、看多久、看完做什么。

放在领导办公室的,通常是看趋势和异常,数据要少而准,宁可三个指标做对,不要三十个指标全是错的。放在业务大厅的,是给来访者看的门面,重点在视觉和实时性。放在部门工位旁边的,是给一线用的,需要能下钻到明细,最好能点进去直接处理。

这三种诉求对应三套完全不同的实现。第一套重点在数据准确性校验和口径固化,第二套重点在刷新频率和渲染性能,第三套重点在查询下钻和权限过滤。如果不问清楚就开工,做出来的东西通常是谁都不满意。

还有一个隐藏问题:大屏的数据口径由谁背书。指标算错了,业务方第一反应是系统有问题,但往往错在口径定义。所以我会要求每个上屏指标都写清楚三件事:数据来源表、计算公式、更新时点。这三样东西写不出来,这个指标就不上屏。

2.2 字段口径之争:同名不同义与同义不同名

企业信息管理系统里最消耗时间的不是写代码,是给字段定口径。典型冲突有三种。

第一种是同名不同义。财务说的"在职人数"包含试用期,人力说的"在职人数"不包含,两边报表一对比差二十个人,开会吵一下午。解法是给每个指标加限定词,叫"在职人数(含试用)"和"在职人数(不含试用)",名字长一点,但从此不再吵。

第二种是同义不同名。"部门""科室""中心""事业部"在不同业务线指同一层级的东西。这种要在建模阶段就统一成一个概念,展示层再按业务线做别名映射,否则数据库里会长出一堆含义重叠的字段。

第三种是枚举值不统一。员工状态在A系统里是"在职/离职",在B系统里是"正式/试用/待离职/已离职/停薪留职"。合并的时候必须建立一个映射表,把所有旧值映到新值,并且保留原值备查。这一步偷懒,后面所有统计都会出错。

我的做法是在需求阶段就产出一份字段字典,至少包含:字段名、业务含义、数据类型、取值范围、是否必填、数据来源、责任人。这份字典看起来笨重,但它是后面所有争议的裁决依据。字典没定完就开工,等于把返工写进了排期。

2.3 历史数据:清洗、照搬、还是不迁

老系统里的数据怎么办,这是每个项目都躲不开的问题,而且往往在项目后期才被想起来,然后炸掉半个排期。

我一般给业务方三个选项,把代价摆明:

方案一,全量清洗后迁移。数据质量最好,代价是人力投入大,需要业务部门派人一条条核对,通常要占整个项目周期的四分之一以上。适合数据量在几万条以内、且历史数据后续还要长期使用的场景。

方案二,照搬迁移,不做清洗。快,但脏数据原封不动带进新系统,新系统上线第一天就继承了老系统的所有毛病。适合过渡期系统,也就是这个系统只打算用一两年。

方案三,只迁活数据,历史归档。在职人员、在库资产、未结流程迁进来,已离职、已结案的数据打包成只读归档库。这个方案在实践中最常见,因为它同时兼顾了成本和可用性。归档库可以做成一个独立查询入口,需要的时候能查,平时不占新系统资源。

不管选哪个,有一条必须坚持:迁移前先做一次数据体检,统计空值率、重复率、格式异常率。这三个数字直接决定迁移工作量,也是说服业务方派人参与核对的最有力材料。我见过一个项目,体检发现某关键字段空值率 38%,业务方当场同意延期两周做补录,如果等到上线后才发现,就是一场事故。

3. 数据模型:组织树、人员主数据、权限这三张底牌

3.1 组织树的四种存法及其代价

组织架构看起来就是一棵树,但怎么存进关系型数据库,选择不同,后面几年的查询体验天差地别。

邻接表,也就是每行存一个父节点 ID。写入简单,改一个部门的上级只改一个字段。缺点是查某节点下所有子孙要递归,层级深了性能会掉,而且不同数据库的递归写法还不一样。

路径枚举,每行存一个类似/1/5/12/的路径字符串。查子孙只需要一个前缀匹配,非常快。缺点是部门移动的时候要批量更新所有子孙的路径,移动一棵大子树会成为一次重操作。

闭包表,单独一张表存所有祖先-后代关系对。查询任意层级都是一次普通 JOIN,移动节点也只需要增删关系行。缺点是表会膨胀,节点数 N 的话关系行数量级是 N 乘以平均深度。

嵌套集,用左右值编码。查询子树极快,但插入和移动要重算一大片区间,写多读少的场景不适用。

实际项目里我的默认选择是:组织规模在几千个节点以内、查询以"取某部门及下级"为主,用路径枚举加索引,简单够用。如果组织经常调整、且层级很深,用闭包表。邻接表适合只做展示、不做复杂下钻的场景。

具体到建表,最小可用的一组字段大致是这样:

CREATE TABLE org_unit ( id BIGINT PRIMARY KEY, parent_id BIGINT NOT NULL DEFAULT 0, path VARCHAR(500) NOT NULL, -- 如 /1/5/12/ name VARCHAR(100) NOT NULL, unit_type VARCHAR(20) NOT NULL, -- 公司/部门/小组 sort_no INT DEFAULT 0, status TINYINT DEFAULT 1, -- 1 有效 0 停用 effective_from DATE NOT NULL, effective_to DATE DEFAULT NULL, UNIQUE KEY uk_path (path) );

注意这里有两个容易被忽略的字段:status和生效时间。组织是会撤并的,直接物理删除会让所有历史单据失去归属,所以只能停用。生效时间是给"下个月一号组织调整"这种场景准备的,能提前录入,到点自动生效。

3.2 人员主数据:一人多岗、兼职、离职的建模细节

人员表是整个系统被引用最多的一张表,设计得好不好,决定了后面所有模块的开发效率。

第一个必须解决的问题是人和岗位分离。一个人可能同时是两个部门的负责人,也可能在主部门之外兼一个项目角色。如果把部门 ID 直接放在人员表上,这种一人多岗就没法表达。正确做法是三张表:人员表存"这个人是谁",岗位表存"有哪些坑",任职关系表存"谁坐在哪个坑上,什么时候开始,什么时候结束"。

CREATE TABLE employee ( id BIGINT PRIMARY KEY, emp_no VARCHAR(32) NOT NULL, name VARCHAR(50) NOT NULL, id_card_hash VARCHAR(128), -- 证件号只存哈希与后四位 status VARCHAR(20) NOT NULL, -- 试用/正式/待离职/已离职 hire_date DATE NOT NULL, leave_date DATE DEFAULT NULL, UNIQUE KEY uk_emp_no (emp_no) ); CREATE TABLE position_assignment ( id BIGINT PRIMARY KEY, emp_id BIGINT NOT NULL, org_id BIGINT NOT NULL, position_id BIGINT NOT NULL, is_primary TINYINT DEFAULT 0, -- 是否主岗 start_date DATE NOT NULL, end_date DATE DEFAULT NULL, KEY idx_emp (emp_id, end_date) );

这个结构带来两个好处。查"某人现在在哪些部门",条件是end_date IS NULL OR end_date > CURDATE(),一次查询搞定。查"某部门现在有哪些人",同样简单。历史归属也不会丢,因为旧的任职记录只是加了结束日期。

第二个问题是离职处理。绝对不要物理删除人员记录。离职员工的历史审批单、历史资产领用记录都需要能追溯到人。正确姿势是把status改成已离职、填上leave_date、同时把账号禁用。同时要在设计上允许"离职人员不可被新流程选中",这个过滤条件放在查询层,不要写在数据里。

第三个问题是外包与实习。这类人员的劳动关系和正式员工不同,但业务上又必须能走流程、能进项目。我的处理方式是加一个"用工类型"字段,权限体系和流程节点根据这个字段做差异处理,而不是单独建一张表。单独建表会让所有关联查询都要 UNION,后期维护成本极高。

3.3 权限模型:功能权限、数据权限、字段权限要分开设计

权限是企业信息管理系统里最容易烂掉的部分,因为它会随组织变化不断膨胀,而且没人愿意主动回收。

我一般把权限拆成三层,分别用不同机制实现:

功能权限,也就是能不能进这个页面、能不能点这个按钮。这层用标准的用户-角色-权限模型就够。角色按岗位设,不要把角色设得太细,否则会出现几百个角色没人说得清谁是谁。实践中的经验值是角色数量控制在 30 个以内,超了就说明设计粒度有问题。

数据权限,也就是同一个页面里能看到哪些行。这层的实现方式是在查询上追加过滤条件,过滤条件的来源是"当前用户的管辖范围"。管辖范围的计算规则要单独做成一个服务,比如"本人""本部门""本部门及下级""全公司""指定几个部门",规则存成配置,查询时统一调用。千万不要在每个查询里手写 if-else,那样改一次组织规则要动几十个地方。

字段权限,也就是同一行数据里哪些列能被看到。典型场景是薪资、证件号、银行账号。这层在接口返回前做一次字段裁剪,不要指望前端隐藏,前端隐藏等于没隐藏。

// 数据权限过滤的示意:把范围规则翻译成查询条件 function buildScopeFilter(user, resource) { const scope = user.scopeOf(resource); // 由权限服务统一计算 switch (scope.type) { case 'SELF': return { field: 'owner_id', op: '=', value: user.id }; case 'DEPT': return { field: 'org_id', op: 'IN', value: scope.orgIds }; case 'DEPT_AND_CHILD': return { field: 'org_path', op: 'LIKE_PREFIX', value: scope.paths }; case 'ALL': return null; // 不加限制 default: return { field: 'id', op: '=', value: -1 }; // 兜底:查不到任何数据 } }

那个兜底分支很关键。权限服务返回了没见过的类型时,宁可让人看不到数据,也不能默认放开。我见过一个系统因为 switch 没有 default,权限配置出错时把全公司数据都放了出来。

3.4 编码规则与唯一标识:别让工号成为业务主键

工号、部门编码、资产编号这类业务编码,看起来天然适合当主键,实际上是个大坑。

原因很简单:业务编码会变。公司改名要换编码前缀,部门合并要重新编号,历史遗留的重复编码要清理。一旦业务编码被当成主键,并且被几十张表以外键引用,改一次编码就是一次全库手术。

正确的做法是双 ID:内部主键用无意义的自增 ID 或雪花 ID,业务编码单独一个字段,允许修改,但修改要走审批并且留痕。对外展示、导出、打印都用业务编码,系统内部关联全用内部主键。

编码规则本身也有讲究。定长比不定长好,纯数字比混合字符好,带校验位比不带好。带校验位的意思是最后一位由前面几位算出来,这样人工录入错了能当场发现,不用等到数据对不上才回头查。这个技巧在资产编号上特别有用,能省掉大量对账时间。

4. 审批流的轻重取舍:什么时候该上引擎

4.1 先判断哪些流程值得进系统

不是所有审批都值得做成系统流程。判断标准还是那三条:频次、金额或风险、参与人数。

高频低风险的,比如请假、加班、外出报备,值得进系统,因为量大,自动化收益明显。低频高风险的,比如重大采购、组织调整,值得进系统,因为要留痕、要可追溯。低频低风险的,比如领一支笔,进系统反而是负担,走个共享表格就行。

还有一个维度是参与人数。三个人以内能闭环的流程,走群聊可能比走系统快。超过五个人、跨两个以上部门的流程,不上系统就一定会出现"我以为他同意了"这种事。

我一般会跟业务方一起列一张流程清单,每条流程标上"月均单量""平均参与人数""是否有金额或合规要求",然后按这三列排序,前几名进第一版。这个排序过程本身就是一次很好的需求收敛,很多原本以为必须做的流程,标完数据发现一个月就三单,直接挪到二期。

4.2 轻量状态机与通用工作流引擎的对比

这是技术选型上分歧最大的一个点。有人主张直接上通用工作流引擎,有人主张用状态机手写。我的判断依据是流程的数量和变化频率。

对比项状态机手写通用工作流引擎
适用流程数5 个以内10 个以上
流程变更改代码、发版改配置、即时生效
学习成本低,团队都能接手高,需要专人维护
复杂路由(会签、条件分支)要自己实现,容易出 bug内置支持
排查问题看代码就知道走到哪要查引擎的流程实例表
性能可控有一定额外开销

实践中的甜点区是:核心流程(入职、离职、调岗、报销)用状态机手写,因为稳定、可控、出问题好查;长尾流程(各种申请、备案、临时审批)用引擎配置,因为变化快、数量多。

我参与过一个项目,一开始所有流程都上引擎,结果最核心的离职流程因为引擎版本升级出了兼容问题,卡了三天。后来把三条核心流程拆出来用状态机重写,剩下二十多条继续用引擎,问题就再没出现过。这个教训值得记:越是核心的流程,越应该用最朴素的方式实现。

状态机手写的最小结构其实不复杂,一张主表存单据,一张流水表存每一步的操作记录:

CREATE TABLE approval_instance ( id BIGINT PRIMARY KEY, biz_type VARCHAR(30) NOT NULL, -- 如 RESIGN biz_id BIGINT NOT NULL, current_node VARCHAR(30) NOT NULL, -- 当前节点 status VARCHAR(20) NOT NULL, -- RUNNING/APPROVED/REJECTED/CANCELED starter_id BIGINT NOT NULL, created_at DATETIME NOT NULL, KEY idx_biz (biz_type, biz_id) ); CREATE TABLE approval_task ( id BIGINT PRIMARY KEY, instance_id BIGINT NOT NULL, node VARCHAR(30) NOT NULL, assignee_id BIGINT NOT NULL, action VARCHAR(20), -- APPROVE/REJECT/TRANSFER/ADD_SIGN comment VARCHAR(500), handled_at DATETIME, KEY idx_assignee (assignee_id, handled_at) );

流水表只追加不修改,这样任何时刻都能还原出"谁在什么时候同意了哪一步",审计的时候特别有用。

4.3 会签、加签、转办、撤回:四个最容易实现错的动作

这四个动作看起来都是"多个人参与审批",实际语义完全不同,实现错了业务会直接卡住。

会签是一个节点多人审批,全部通过才往下走。这里有个细节要提前定清楚:是"所有人都必须表态"还是"有一票否决就结束"。前者需要等所有人处理完,后者可以提前终止。我倾向于后者,因为实际业务里没人愿意等一个永远不处理的人。但要在界面上明确提示"该流程已被否决",避免其他人白跑。

加签是当前审批人临时拉一个人进来。分前加签和后加签,前加签是先让新人审再回到自己,后加签是自己审完再给新人。实现上加签不要新增节点,而是在当前节点下挂子任务,用父子关系表达,否则流程走向图会乱。

转办是把整个任务交给别人,自己退出。和加签的区别是转办之后原审批人不再有这条任务。这里要记录转办链,出问题能查到是谁转给谁的。

撤回是发起人把已经发出去的单子收回来。关键约束是:只有还没人处理的节点才能撤回,一旦有人审批过,就只能走"作废"而不是"撤回"。这两个动作在数据上的区别是,撤回后单据回到草稿状态可以再编辑,作废之后单据终态不可改。很多系统把这两个混成一个按钮,结果就是审批记录对不上。

注意:这四个动作一定要在需求阶段就跟业务方确认清楚语义,尤其是"会签是否允许一票否决"和"撤回的边界在哪"。这两条写进需求文档,后面能省掉大量扯皮。

5. 选型与集成落地:接口、对账、性能

5.1 自研、低代码、成品采购的三方取舍

这个决定通常在项目最开始就要做,做错了后面全是补救。

成品采购适合标准化程度高的场景,比如考勤、薪酬核算。优点是上线快、功能全、有服务商兜底。缺点是定制困难,稍微特殊一点的需求就要二次开发,而二次开发的成本往往比自研还高,还得受制于对方的版本节奏。

低代码平台适合流程类、表单类需求,尤其是那些"每个部门都想要一个自己的小应用"的场景。优点是业务侧能自己搭,IT 只做底座和权限。缺点是复杂逻辑和数据量大了之后性能会掉,而且平台锁定风险要提前评估——数据能不能完整导出,导出后能不能在别处跑起来。

自研适合核心主数据和关键业务流程,也就是那些"跟别人不一样"的部分。优点是贴合业务、可控、可演进。缺点是要养团队,而且前期投入大。

实际项目里最常见的是混合模式:核心主数据和三条关键流程自研,考勤薪酬买成品,各种零散申请用低代码。这个组合的关键在于接口要通,主数据只有一份,其他系统都从这一份取。

我的建议是,无论选哪种,都要在合同或方案里明确一条:数据所有权归企业,任何时候可以完整导出,格式为通用结构化格式。这条不写清楚,三年后想换系统会非常被动。

5.2 接口对接的顺序与幂等设计

企业信息管理系统几乎不可能孤立运行,它一定要和考勤、财务、门禁、邮件这些系统打交道。对接顺序我一般按"谁是数据源头"来排。

人员和组织的主数据,源头通常在一处,其他系统只读。所以第一步是把主数据同步出去,方向是单向的:本系统推给考勤、推给门禁、推给邮件。单向同步的好处是不会有冲突,也不用处理双向覆盖的问题。

第二步才是反向拉取,比如从考勤拉每日出勤结果,从财务拉报销状态。这一步要注意时间窗口和补偿机制——对方系统挂了怎么办,数据漏了怎么办。

接口设计上,有三件事必须做:

幂等。同一个请求重复发多次,结果要一样。做法是给每个业务动作生成一个唯一业务号,接收方先查这个号处理过没有。这个机制在重试场景下是救命的,没有它,一次网络抖动就可能产生两条重复记录。

对账。每天跑一次比对,两边条数、关键字段是否一致,不一致的生成差异清单人工处理。对账比任何日志都管用,因为它直接告诉你"少了哪条"。

失败隔离。一个下游系统挂了,不能把主流程拖死。做法是把同步动作异步化,进队列,失败重试,重试多次仍失败就进死信表告警。绝不要在用户点保存的时候同步调三个外部接口,那样任何一个慢都会让用户以为系统卡死。

5.3 数据量涨上来之后的查询优化

企业信息管理系统的数据量增长往往是被低估的。人员几千条不算多,但操作日志、审批流水、变更历史这些表,三年下来轻松到千万级。上线时跑得飞快,第三年开始每个列表页都要转圈。

优化要提前埋,几个关键动作:

分页必须有稳定排序。只按创建时间排序,遇到同一秒的多条记录会翻页重复或丢失。正确做法是加一个自增 ID 作为第二排序键,保证顺序唯一。

大表按时间分区或归档。审批流水超过两年的数据,业务上很少查,可以迁到历史表。查询时先查热表,需要历史再单独入口。这个策略要在设计阶段就定好,事后加会牵动很多代码。

组织树查询加缓存。组织架构变更频率低,完全可以整体缓存在内存里,查询时直接算。缓存失效用版本号或变更事件触发,不要用固定过期时间,否则组织调整后要等缓存过期才生效,用户会以为没改成功。

列表页的关联字段做冗余。部门名称、岗位名称这类字段,如果每次列表都要 JOIN 三四张表,性能会很难看。可以在业务表里冗余存一份名称,主数据变更时异步更新。代价是要处理一致性,收益是查询快一个数量级。这个取舍在报表和下钻场景里通常值得。

5.4 审计日志:留痕不是记流水账

审计日志在很多系统里是应付检查的摆设,实际用起来什么都查不到。有用的审计日志要满足三条:能定位到人、能定位到字段、能看到改前改后的值。

只记"某某修改了员工信息"是没用的。要记成"某某在什么时间,把员工 A 的手机号从 X 改成 Y,来源 IP 是什么"。字段级变更记录看起来数据量大,但真正出问题时,这一条记录能顶几天排查。

实现上我一般用一张变更日志表,存业务类型、业务 ID、字段名、旧值、新值、操作人、操作时间、来源。写入用异步,不要阻塞主流程。敏感字段(如证件号、银行卡号)的新旧值要脱敏存储,只留后四位。

日志的保留策略也要定。合规要求通常是最少留几年,但全量存成本高。折中方案是:关键字段变更长期保留,非关键字段保留一年后归档。这个策略要写进设计文档,别等到存储告警了才临时清理。

6. 上线那两周:迁移、双轨、推广

6.1 数据迁移的四步校验法

数据迁移出问题是项目上线阶段最常见的事故源,而且往往在上线当天才暴露。我用一套四步校验,能挡掉大部分问题。

第一步,条数校验。源表多少条,目标表多少条,必须一致(除明确排除的记录外)。这一步最简单,但能抓到大部分漏迁。

第二步,关键字段抽样校验。随机抽 200 条,逐字段比对。抽查比例不高,但能发现格式转换、编码映射这类系统性问题。

第三步,聚合校验。按部门统计人数、按状态统计条数、按金额分段统计总额,和源系统对一遍。聚合对得上,说明整体没有系统性偏差。

第四步,业务验证。让业务方在新系统里跑十个真实场景,比如"查某个人现在在哪个部门""查某笔报销走到哪一步"。这一步最费时间,但最能发现"数据都对但业务不对"的问题。

四步都过了再切换,任何一步不过就回滚重来。回滚方案要提前写好并演练一次,不要临时想。

6.2 双轨运行与切换时机

双轨运行的意思是新旧系统同时可用,新系统为主,旧系统只读备用。这个阶段通常持续两到四周。

双轨期要盯三个指标:新系统日活、旧系统查询量、差异工单数。新系统日活稳定上升、旧系统查询量降到很低、差异工单趋零,就可以关旧系统。如果双轨一个月旧系统还有人天天查,说明新系统一定有问题,去问那些还在查的人,答案往往很具体。

切换时机也要选。避开月末、季末、年末这些业务高峰,避开组织调整期。最佳窗口通常是月中某个周三,出问题还有两天缓冲到周末。

6.3 推广的"最后一公里":谁用、怎么教会、卡在哪

系统上线和技术上线是两回事。我见过功能做得很好但没人用的系统,问题几乎都出在最后一公里。

推广的第一件事是找种子用户。每个部门找一两个愿意尝鲜的人,提前两周让他们用,收集问题,改完了再让他们在部门内做示范。这比任何培训都有效,因为同事说的话比 IT 说的话可信。

第二件事是把培训做成任务而非课程。不要开一小时的培训会讲功能清单,那没人记得住。改成"今天下班前用系统提交一次请假申请",做完就算过关。任务驱动的方式通过率高得多。

第三件事是处理卡点。推广初期最常见的卡点是登录和权限。密码忘了、账号没开通、权限不够,这三种占了绝大多数工单。所以上线第一个月必须有一个响应很快的支持通道,能当场解决问题。这一关过不去,用户会迅速退回旧习惯,而且很难再拉回来。

7. 系统用三年之后:数据治理与权限清理

7.1 数据质量治理:把校验放进录入环节

系统跑起来之后,数据质量会缓慢劣化。手机号少一位、部门选错、离职日期填成未来、同一个供应商录了两遍,这些问题单看都不大,积累起来会让报表完全失去参考价值。

治理的核心思路是前置校验,而不是事后清洗。事后清洗的成本是前置校验的十倍以上,而且清洗完还会继续脏。

具体做法:手机号用正则校验,证件号校验位算法校验,日期做范围校验,部门用树形选择器避免手输,供应商名称做重复检测(新增时提示"已存在相似名称,是否直接使用")。这些都是很小的开发量,但能挡掉八成脏数据。

对于已经存在的脏数据,定期跑体检报告。统计每个关键字段的空值率、重复率、格式异常率,按部门排名通报。这个动作要挂在某个固定周期上,比如每月一次。有了排名,各部门自己就会去改。

7.2 权限膨胀:账号回收比授权更难

系统用三年,权限一定膨胀。表现是:离职员工的账号还开着,调岗员工的旧权限没撤,临时授权的项目角色一直没删,某些账号的权限大到没人说得清为什么。

清理权限要做两件事。

一是定期跑权限审计。列出所有账号及其权限,标注"最近 90 天是否登录""权限是否超出岗位需要"。超过 90 天没登录的直接禁用,超范围的通知责任人确认。

二是把权限和任职关系绑定。人走了、岗变了,权限自动跟着变。这个机制要在设计阶段就做进去,用任职关系驱动权限计算,而不是人工手动配。人工配的权限,一定会忘记回收。

还有一个隐性问题是共享账号。有些部门为了方便,几个人共用一个账号。这在审计上是灾难,因为所有操作都记在同一个人头上。解决方式不是技术上封杀,而是提供替代方案——比如给临时人员开短期账号,到期自动失效。有了方便的正路,走偏门的人自然就少了。

7.3 二次开发与接口稳定:给后来的人留条活路

这类系统的生命周期通常是五到八年,中间一定会换人维护。所以设计时要考虑"别人接手能不能看懂"。

几个具体做法:接口一律版本化,/api/v1/和/api/v2/并存一段时间,不要直接改老接口;数据库变更全部走脚本,脚本按序号命名并入库,不要手动改生产库;关键业务规则写成文档放在代码库里,和代码一起版本管理;每个模块的负责人和交接说明写在 README 里。

还有一个容易被忽略的点:不要把业务规则硬编码在存储过程或触发器里。这两样东西隐蔽性强、调试困难、迁移麻烦,接手的人往往要找很久才能发现逻辑藏在数据库里。业务规则放在应用层,哪怕代码写得不那么优雅,至少是可读可改的。

最后分享一个我自己的习惯。每次做完一个模块,我会写一段"这个模块为什么这么设计"的说明,不超过一页,记下当时排除了哪些方案、为什么排除。三年后回头看,这些说明比任何技术文档都有价值,因为它记录的是决策过程,而不只是最终结果。接手的人看到这段,能少走很多弯路,也不容易一拍脑袋把有原因的设

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进