# 用户主档与薪酬考勤改造记录 > **文档说明**:本文档由原《用户主档与薪酬考勤改造记录》与《员工门店归属变更问题分析与解决方案》**合并**而成,统一描述用户主档(`BASE_USER`)、**门店主档(`lq_mdxx`)与门店归属、薪酬、考勤相关的问题分析、方案、改造项**,以及**主档变更对计算与历史查看的影响**。 > **更新原则**:每确认一项需求或方案结论,在「改造项清单」追加或更新状态。 > **文档版本**:v2.4(R-003 门店主档版本化) > **创建/合并日期**:2026-01-09(门店归属分析) / 2026-04-03(R-001/R-002) / 2026-04-03(R-003 门店版本) --- ## 目录 1. [关联文档](#关联文档) 2. [改造项清单](#改造项清单) 3. [R-001 用户档案字段级变更履历(审计)](#r-001-用户档案字段级变更履历审计) 4. [R-001 详细设计(完整)](#r-001-详细设计完整) 5. [用户信息修改后对数据计算的影响(闭环梳理)](#用户信息修改后对数据计算的影响闭环梳理) 6. [员工门店归属变更:问题描述与现状](#员工门店归属变更问题描述与现状) 7. [员工门店归属:解决方案与对比](#员工门店归属解决方案与对比) 8. [员工门店归属:技术实现要点与注意事项](#员工门店归属技术实现要点与注意事项) 9. [员工门店归属:总结](#员工门店归属总结) 10. [R-003 门店主档版本化(完整设计)](#r-003-门店主档版本化完整设计) 11. [附录:正式环境设计审视与评审结论](#附录正式环境设计审视与评审结论) 12. [数据库变更与 SQL 脚本](#数据库变更与-sql-脚本) --- ## 关联文档 | 主题 | 文档 | | ----------------------- | -------------------- | | 门店归属快照表字段、索引、业务规则(详细设计) | `员工门店归属快照表方案详细设计.md` | | 数据库表与字段说明(维护时同步) | `数据库说明.md` | --- ## 改造项清单 | 序号 | 状态 | 提出/更新日期 | 主题 | 摘要 | | ----- | ----------- | ---------- | ------------------- | ------------------------------------------------------------------------------------------ | | R-001 | 详细设计已完成·待开发 | 2026-04-03 | **用户档案字段级变更履历(审计)** | 见下文 [R-001 详细设计(完整)](#r-001-详细设计完整);表 `lq_user_profile_audit`,与 `UsersService` 等写入点同事务落库。 | | R-002 | 待实施 | 2026-01-09 | **员工门店归属快照表(时间维度)** | 见本文第六节起;详细表结构见 `员工门店归属快照表方案详细设计.md`。 | | R-003 | 详细设计已完成·待开发 | 2026-04-03 | **门店主档时点版本** | 见 [R-003 门店主档版本化(完整设计)](#r-003-门店主档版本化完整设计);表 `lq_store_profile_version`;解决考勤回看与算薪取历史门店属性。 | --- ## R-001 用户档案字段级变更履历(审计) ### 业务目标 - 管理员在用户详情/编辑相关界面可查看该用户的**历史修改记录**。 - 每条记录至少包含:**操作时间**、**操作人**(用户 ID 或可解析姓名)、**字段标识/中文名**、**变更前值**、**变更后值**(敏感字段如密码仅记「已修改」或脱敏策略另定)。 ### 现状(结论) - `UsersService` 更新用户时仅维护 `LastModifyTime`、`LastModifyUserId`,**无**字段级历史表或审计中间件。 - `BASE_SYSLOG` 等系统日志**不保证**按字段拆解旧值/新值,且与用户详情未做产品级串联。 - 通用表 `lq_business_operation_log` 当前**未**接入用户编辑流程。 ### 概要方案 采用 **独立表 `lq_user_profile_audit`(方案 A,推荐)**:一行(或多行)记录一次业务动作中的**单个字段**差异;`F_BatchId` 聚合同一次保存。完整表结构、枚举、接口、触发点见下一节 **「R-001 详细设计(完整)」**。可选 **方案 B** 复用 `lq_business_operation_log`,见附录与 R-001 SQL 文件注释。 ### 闭环验收 - 修改门店/组织/岗位/考勤组后,历史中可见对应字段旧→新。 - 多次连续修改可追溯时间线。 - 与算薪/考勤排查可对照(不要求自动反算工资,仅要求**可查因**)。 --- ## R-001 详细设计(完整) ### 1. 目标与范围 | 项 | 说明 | | ------- | ---------------------------------------------------------------------------------------- | | **目标** | 对 `BASE_USER` 的变更保留**可查询**的字段级履历:时间、操作者、字段、旧值、新值(敏感信息按规则处理)。 | | **范围** | 所有经后端**持久化到 `BASE_USER`** 的写入路径(含管理员改人、本人改资料、岗位/角色批量维护、密码重置等);**不含**仅内存或缓存变更。 | | **非目标** | 不自动反算工资;不替代 `BASE_SYSLOG` 请求日志;不记录用户关系表 `BASE_USER_RELATION` 明细(若需可二期加 `F_Remark` 或扩展表)。 | ### 2. 方案选型 | 方案 | 做法 | 优点 | 缺点 | | --------- | -------------------------------------------------------------------------------------- | ------------------ | --------------------- | | **A(推荐)** | 新建 `lq_user_profile_audit` | 列表查询简单、索引清晰、字段语义固定 | 多一张表 | | **B** | `lq_business_operation_log`,`F_BizType=base_user`,`F_BizId=用户Id`,diff 放 `F_DetailJson` | 无新表 | 列表需解析 JSON;与考勤等业务日志混表 | **生产建议**:采用 **方案 A**;方案 B 仅作资源极度受限时的退路。 ### 3. 数据模型(与 SQL 一致) **脚本**:`项目文档相关/sql/2026-4-3/R-001_用户主档变更审计表_lq_user_profile_audit.sql` | 字段 | 类型 | 说明 | | ----------------------------------------- | -------------- | ------------------------------------------------------ | | `F_Id` | varchar(50) PK | `YitIdHelper` | | `F_TargetUserId` | varchar(50) | 被变更用户 | | `F_OperatorUserId` / `F_OperatorUserName` | varchar | 操作人;本人改资料时二者均为本人 | | `F_OperateTime` | datetime | 默认 `CURRENT_TIMESTAMP`,应用可显式传入与事务一致 | | `F_Action` | varchar(64) | 见 §4,区分业务场景 | | `F_BatchId` | varchar(50) | 单次请求内 `Guid`;无多字段可空 | | `F_FieldKey` | varchar(64) | 与 `UserEntity` 属性名一致(PascalCase),如 `Mdid`、`OrganizeId` | | `F_FieldLabel` | varchar(64) | 中文名,便于 UI | | `F_OldValue` / `F_NewValue` | text | 统一 **字符串化**(日期 `yyyy-MM-dd`、数字直接 `ToString`);敏感见 §6 | | `F_Source` | varchar(32) | `API` / `RELATION_SERVICE` / `OAUTH` / `PORTAL` 等 | | `F_ClientIp` / `F_UserAgent` | varchar | 可选,从 `HttpContext` 取 | | `F_Remark` | varchar(500) | 可选 | **索引**:`(F_TargetUserId, F_OperateTime)` 主查询;`F_BatchId`;`(F_OperatorUserId, F_OperateTime)`;`(F_Action, F_OperateTime)` 运维分析。 ### 4. `F_Action` 枚举(建议代码内用常量类) | 值 | 含义 | 典型入口 | | ------------------------------------------------------- | -------------------------- | ------------------------------------ | | `PROFILE_CREATE` | 新建用户 | `UsersService.CreateSimple` | | `PROFILE_UPDATE` | 管理员全量资料更新 | `UsersService.Update` | | `PROFILE_DELETE_SOFT` | 软删除 | `UsersService.Delete` | | `ENABLE_TOGGLE` | 启用/停用切换 | `UsersService.UpdateState` | | `ADMIN_RESET_PASSWORD` | 管理员重置密码 | `UsersService.ResetPassword` | | `CURRENT_MODIFY_PASSWORD` | 本人改密 | `UsersCurrentService.ModifyPassword` | | `CURRENT_BASE_INFO` | 本人改资料 | `UsersCurrentService.UpdateBaseInfo` | | `CURRENT_THEME` / `CURRENT_LANGUAGE` / `CURRENT_AVATAR` | 主题/语言/头像 | `UsersCurrentService` 对应 Put | | `RELATION_BATCH_POSITION` | 岗位关系批量增删导致 `PositionId` 变化 | `UserRelationService` | | `RELATION_BATCH_ROLE` | 角色关系批量增删导致 `RoleId` 变化 | `UserRelationService` | | `OAUTH_UPDATE` | 第三方登录回写用户 | `OAuthService`(若写 `BASE_USER`) | | `PORTAL_UPDATE` | 门户改用户 | `PortalService`(若写 `BASE_USER`) | 密码类动作:除 `FieldKey=Password` 一行外,`F_OldValue`/`F_NewValue` 均 **不写明文**,填占位如 `[REDACTED]` 或空 + 仅靠 `F_Action` 表达。 ### 5. 字段比对与写入策略 1. **时机**:在 `Updateable`/`Insertable` **执行前**读取库中**旧实体**(更新/删除);新增无旧实体则只写 `F_NewValue`(`F_OldValue` 用 `null` 或 `(空)`)。 2. **参与比对的列**:仅业务会**真正更新**的列(与 `UsersService.Update` 的 `UpdateColumns` 列表对齐,避免把未提交字段算成变更)。 3. **相等判定**:`null` 与 `""` 在展示层可按业务约定视为相同(可选归一化后再比);日期比较建议统一到日或 UTC。 4. **输出**:每个变化字段插入 **一行** `lq_user_profile_audit`,共享同一 `F_BatchId`。 5. **无字段变化**:可不插审计行;若需审计「空保存」,可插一行 `F_FieldKey=__META__`,`F_Remark=NoFieldChange`(可选)。 6. **软删除**:至少记 `DeleteMark`(若实体上有)或记 `F_Action=PROFILE_DELETE_SOFT` + `F_FieldKey=__META__` + `F_Remark`。 ### 6. 敏感字段与脱敏 | 类型 | `F_FieldKey` | 存储规则 | | ---- | -------------------- | ------------------------------ | | 密码 | `Password` | 不存哈希;`F_NewValue`=`[已重置]` 或空 | | 手机号 | `MobilePhone` | 建议 `138****8000`(保留前3后4) | | 证件号 | `CertificatesNumber` | 仅后4位或 `[已变更]` | | 邮箱 | `Email` | 可脱敏 `a***@domain.com` 或全量(按合规) | | 银行卡等 | — | 若未来有字段,同证件策略 | 查询接口应对 **无额外权限** 的用户再次脱敏(双保险)。 ### 7. `F_FieldKey` / 中文标签与审计级别(建议配置表或字典) **高优先级(算薪/考勤/权限强相关,必记)** | F_FieldKey | 建议 F_FieldLabel | DB 列 | | ----------------- | --------------- | --------------------- | | Mdid | 门店 | `F_MDID` | | OrganizeId | 组织 | `F_ORGANIZEID` | | Gw | 岗位 | `F_GW` | | PositionId | 职位ID | `F_POSITIONID` | | RoleId | 角色 | `F_ROLEID` | | AttendanceGroupId | 考勤分组 | `F_AttendanceGroupId` | | EntryDate | 入职日期 | `F_ENTRYDATE` | | IsOnJob | 是否在职 | `F_IsOnJob` | | EnabledMark | 启用标记 | `F_ENABLEDMARK` | | Account | 账号 | `F_ACCOUNT` | | RealName | 姓名 | `F_REALNAME` | **中优先级(人事资料,建议记)**:`Gender`、`Birthday`、`MobilePhone`、`Email`、`CertificatesType`、`CertificatesNumber`、`Nation`、`NativePlace`、`Education`、`UrgentContacts`、`UrgentTelePhone`、`PostalAddress`、`TelePhone`、`Landline`、`ManagerId`、`SortCode`、`Description`、`HeadIcon`、`Zw`、`Fyft`、`Gwfl`、`OpenId` 等。 **低优先级 / 默认忽略(降噪)**:`QuickQuery`(随姓名自动生成可不计或合并到 `RealName`)、`LastModifyTime`、`LastModifyUserId`、`Password`、`Secretkey`、各类 **登录统计字段**(`FirstLogTime`、`LastLogTime`、`LogSuccessCount` 等)、`ChangePasswordDate`(若已单独记密码动作可忽略)、`PropertyJson`/`Theme`/`Language`/`CommonMenu` 等按产品要求:**主题语言头像若需审计已包含在 `CURRENT_*` 动作中**。 > 实现建议:维护 `HashSet IgnoredPropertyNames` + `Dictionary FieldLabels`,与 `UserEntity` 反射名一致。 ### 8. 写入触发点(须全部接入同一审计服务) | # | 类/方法 | 行为 | F_Action | | --- | ------------------------------------------------------ | ------------------------ | ------------------------------------ | | 1 | `UsersService.CreateSimple` | Insert | `PROFILE_CREATE` | | 2 | `UsersService.Update` | UpdateColumns 多字段 | `PROFILE_UPDATE` | | 3 | `UsersService.Delete` | 软删 | `PROFILE_DELETE_SOFT` | | 4 | `UsersService.UpdateState` | `EnabledMark` 切换 | `ENABLE_TOGGLE` | | 5 | `UsersService.ResetPassword` | 密码 | `ADMIN_RESET_PASSWORD` | | 6 | `UsersCurrentService.ModifyPassword` | 密码 | `CURRENT_MODIFY_PASSWORD` | | 7 | `UsersCurrentService.UpdateBaseInfo` | 个人资料 | `CURRENT_BASE_INFO` | | 8 | `UsersCurrentService` 主题/语言/头像 | 对应更新 | `CURRENT_*` | | 9 | `UserRelationService` 批量增删岗位/角色 | 更新 `PositionId`/`RoleId` | `RELATION_BATCH_*` | | 10 | `OAuthService` | 若更新用户表 | `OAUTH_UPDATE` | | 11 | `PortalService` | 若更新用户表 | `PORTAL_UPDATE` | | 12 | `LqReimbursementWorkflowConfigService` 等 **Insert 用户** | 若存在 | `PROFILE_CREATE` + `F_Source=SYSTEM` | **要求**:新增任何 `Updateable` 必须 Code Review 是否接审计;禁止脚本直连改 `BASE_USER` 而不留痕。 ### 9. 事务与一致性 - 审计插入与 **用户表同一数据库事务**:审计失败则 **整单回滚**(推荐)。 - 批量 `UserRelationService` 多用户更新:每个用户独立事务块内写审计,或整批一个大事务(与现网一致)。 ### 10. 后端接口设计(建议挂在权限模块) | 项 | 约定 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **路由** | `GET api/permission/Users/{targetUserId}/profile-audit-logs`(挂在 `Users`/`UsersService`,与现有用户模块同权限体系) | | **查询参数** | `current`(页码)、`pageSize`、`fieldKey`(可选)、`action`(可选)、`startTime`、`endTime`(可选) | | **权限** | 与 **编辑该用户** 相同数据范围:`user.dataScope` 含目标用户 `organizeId` 的 `Edit`,或管理员;可增设 `user:audit:view` | | **响应行** | `id`、`operateTime`、`operatorUserId`、`operatorUserName`、`action`、`fieldKey`、`fieldLabel`、`oldValue`、`newValue`、`batchId`、`source`、`clientIp`(可选);可选 `oldDisplay`/`newDisplay`(门店/组织/职位等解析名,服务端组装) | **传参约定**:若管理端 axios 对 GET 统一走 **data** 而服务端仅绑 Query,需二选一:**(1)** 前端将该接口改为 query string;**(2)** 增加 `POST .../profile-audit-logs/query`,Body 承载分页与筛选(与 `PageInputBase` 一致)。实现前与现网 `permission/user` API 风格对齐。 ### 11. 前端设计(管理端 `antis-ncc-admin`) | 项 | 说明 | | ------- | ------------------------------------ | | **入口** | 用户管理—编辑用户弹窗/详情增加 **Tab「变更记录」** 或独立抽屉 | | **表格列** | 操作时间、操作人、动作、字段中文名、变更前、变更后 | | **筛选** | 时间范围、字段、动作(二期) | | **空态** | 「暂无变更记录」 | | **性能** | 分页 `pageSize` 默认 20,最大 100 | ### 12. 非功能 | 项 | 建议 | | ------ | ------------------------------------------- | | **性能** | 单次保存字段 diff 通常 <30 行插入,同步写入;若压测瓶颈再考虑 Outbox | | **容量** | 按员工数 × 年均变更 × 行数估算;超期归档冷表(策略同附录评审) | | **安全** | 接口防纵向越权(禁止通过遍历 id 猜用户);审计接口同样鉴权 | ### 13. 测试用例(最小集) | 场景 | 预期 | | -------------- | --------------------------------------- | | 改门店+岗位一次保存 | 2 行审计,同 `F_BatchId` | | 管理员重置密码 | `ADMIN_RESET_PASSWORD` + `Password` 无明文 | | 本人改手机 | 脱敏后旧/新 | | 无权限用户访问他人审计接口 | 403 / 业务错误码 | | 岗位批量调整 | 多用户多条 `RELATION_BATCH_POSITION` | | 事务回滚(模拟审计插入失败) | 用户表未更新 | --- ## 用户信息修改后对数据计算的影响(闭环梳理) 以下基于当前后端实现口径归纳:**用户主档变更后,哪些模块会按「当前库里的用户表」参与计算或统计**。历史月份**重算工资**时,若仍读 `BASE_USER` 而非当月快照,会出现与「当时实际情况」不一致的风险(与第五节门店归属问题同源)。 ### 1. 按主档字段分项说明 | 修改字段 | 主要影响(计算/统计/规则) | 说明与风险 | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | | **门店 `F_Mdid`** | **考勤**:打卡/补卡/列表默认门店、月汇总与人关联的门店展示(如 `LqAttendanceRecordService` 取用户门店)。**工资**:店长、主任、店助、科技老师等按用户 `Mdid` 做门店维度聚合;**健康师工资**在业绩/消耗明细无门店时**回退到 `User.Mdid`**。**看板**:如事业部驾驶舱按「店长/健康师」且 `Mdid` 落在门店集合内筛选人员。 | 调店后重算历史月:分组与兜底门店可能变成**新门店**;与第五节「时间维度 vs 当前归属」矛盾。 | | **组织 `F_OrganizeId`** | **权限**:`@organizeId`、组织及子组织数据范围(`AuthorizeService`)。**工资**:科技部总经理、大项目部主管按 **组织 ID 是否在科技部/大项目部组织列表** 且岗位匹配进池;与 `lq_md_target` 当月科技部/大项目部字段联动门店范围。**大项目部老师工资**:用 `OrganizeId` 映射部门名,统计行上 `StoreId` 曾用部门维度。**科技部相关报表入口**:如总监驾驶舱按用户组织判断是否科技部并按 target 筛门店。 | 调部门后:能否进某类工资池、科技部/大项维度统计范围会变;**不等于门店**,勿与 `Mdid` 混用。 | | **岗位 `F_Gw`(及展示用 `F_PositionId`)** | **工资入池**:`Gw` 字符串决定是否进入对应薪酬服务(如健康师、店长、主任、店助/店助主任、科技老师、科技部总经理/总经理、大项目部主管等)。**健康师**等还会用 `PositionId` → 职位表填展示岗位名。 | `Gw` 与 `PositionId` 不一致时,可能出现「入了算薪池但展示名不同」或反之;改岗后**重算**会改变是否计入某类工资。 | | **考勤分组 `F_AttendanceGroupId`** | **考勤规则**:迟到早退、补卡、批量生成/重算等,记录上可带分组快照,缺省回**用户当前分组**(`LqAttendanceRecordService`)。**设置**:分组下绑定人数统计。 | 改分组主要影响**之后**生成/重算的记录;已落库且带快照的记录行为以记录为准。 | | **在职 `F_IsOnJob`(及启用等)** | **工资**:多处服务过滤 `DeleteMark`、`EnabledMark`;部分统计用在职标识(如事业总工资里 `IsTerminated` 来自用户在职状态)。**列表与选人**:一般仅在职/未删用户。 | 标离职后可能不再进入算薪名单;若需「离职当月仍计薪」需单独业务规则(本文不展开)。 | | **入职时间 `F_EntryDate`** | **假期/工龄等业务**(用户编辑已强调必填):与请假额度、工龄类规则相关;若其它模块按入职日算系数需单独核对。 | 改入职日会改变**按入职推算**的统计;与门店/岗位无关但影响合规类计算。 | | **角色 `F_RoleId`** | **权限与菜单**,不直接参与薪酬公式;间接影响「谁能操作算薪/导入」而非个人工资数额。 | 一般不计入个人业绩/工资算法。 | ### 2. 按业务模块汇总(便于测试回归) | 模块 | 与用户主档的关系 | | ------------------------------------------------ | ------------------------------------------------------------------------------------- | | 健康师工资 `LqSalaryService` | `Gw` 过滤入池;门店优先明细,否则 `Mdid`。 | | 健康师额外计算 `LqSalaryExtraCalculationService` | 新店等场景下 `Mdid` + `Gw`(健康师)。 | | 店长 / 主任 / 店助工资 | `Gw` 入池;按 `Mdid` 关联门店与业绩。 | | 科技老师工资 `LqTechTeacherSalaryService` | `Gw`;按 `Mdid` 等。 | | 科技部总经理工资 `LqTechGeneralManagerSalaryService` | `Gw` + `OrganizeId`;`lq_md_target`。 | | 大项目部主管工资 `LqMajorProjectDirectorSalaryService` | `Gw`(主管)+ `OrganizeId`;`lq_md_target`。 | | 大项目部老师工资 `LqMajorProjectTeacherSalaryService` | 名单来自 `lq_md_major_project_teacher_assignment`;用户表补姓名、`OrganizeId`、在职;**不依赖 `Gw` 入池**。 | | 事业部总经理/经理工资 `LqBusinessUnitManagerSalaryService` | 名单来自 `lq_md_general_manager_lifeline`;用户表主要补姓名、账号、在职。 | | 考勤记录/汇总 | `Mdid`、`AttendanceGroupId`;汇总表按 `UserId` 关联。 | | 事业部驾驶舱等 | `Gw` + `Mdid` 组合筛选「店长/健康师」等。 | ### 3. 闭环建议(与改造项对应) 1. **门店**:实施 **R-002 归属快照** 后,算薪与补录优先按**发生月/日**查归属,减少对当前 `F_Mdid` 的依赖。 2. **审计**:实施 **R-001** 后,主档变更可追溯,便于解释「为何重算结果变化」。 3. **组织/岗位**:科技部、大项主管等若需历史一致,需明确是否引入**按月任职/组织快照**或在工资结果表中固化当时维度(与快照表同类思路)。 --- ## 员工门店归属变更:问题描述与现状 ### 业务场景 员工每个月的归属门店可能会发生变化。当发生改变后,在以下场景中会遇到门店归属问题: 1. **工资计算场景**:员工 A 在 1 月在门店 A 工作,2 月调到门店 B(`BASE_USER.F_Mdid` 更新为门店 B),计算 1 月工资时如何确定该员工 1 月的归属门店? 2. **数据补录场景**:员工 A 在 1 月在门店 A 产生开单/消耗数据,2 月调到门店 B;若 1 月数据在 2 月补录,应归属哪个门店? ### 核心矛盾 - **时间维度**:数据应按实际发生时间归属门店。 - **当前归属**:`BASE_USER.F_Mdid` 只记录当前门店,无法追溯历史。 - **数据完整性**:补录历史数据需要正确的门店归属。 ### 当前数据存储情况 1. **员工门店归属**:`BASE_USER.F_Mdid`,仅当前门店 ID。 2. **开单/消耗数据**:`lq_kd_jksyj` / `lq_xh_jksyj` 有 `F_StoreId`;开单主表 `lq_kd_kdjlb` 有 `djmd`。 3. **工资计算逻辑**(以健康师工资为例):优先业绩 `StoreId` → 其次消耗 `StoreId` → 再回退 `BASE_USER.F_Mdid`。 ### 存在的问题 1. 业务数据无门店时回退到**当前** `F_Mdid`,历史月可能错档。 2. 补录历史数据时门店易被错归。 3. 历史月工资、门店统计、新店判断等可能偏差。 --- ## 员工门店归属:解决方案与对比 ### 方案一:时间维度优先 + 门店归属快照表(推荐) **核心思路**:以时间为标准,用门店归属快照表记录员工每月(可精确到日)的归属。 **实现要点**: 1. 创建门店归属表(表名建议 `lq_employee_store_assignment`;字段与索引见 `员工门店归属快照表方案详细设计.md`)。 2. 调店时自动维护记录;支持月中调店(同一月多条记录);支持历史导入。 3. **工资计算**:按统计月(及日期范围)查快照 → 无记录再回退现有逻辑。 4. **数据补录**:按单据/业绩发生日期查当月归属,写入 `StoreId`。 **优点**:时间清晰、可追溯、支持月中调店、对现有表侵入小。 **缺点**:需维护额外表与历史迁移。 ### 方案二:业务数据中强制记录门店 工资只信业务数据门店;无门店则报错或跳过;不再用 `F_Mdid` 回退。 **缺点**:历史补录、缺失门店时难以闭环。 ### 方案三:在 BASE_USER 上存历史(JSON 或附属表) **缺点**:改核心用户表或 JSON 查询性能;附属表与方案一类似。 ### 方案四:仅依赖工资统计表已有 StoreId **缺点**:首次计算与补录仍缺「当时归属」依据。 ### 方案对比表 | 方案 | 实施难度 | 数据完整性 | 可追溯性 | 扩展性 | 推荐度 | | ----------- | ---- | ----- | ---- | --- | ----- | | 方案一:快照表 | 中等 | 高 | 高 | 高 | ★★★★★ | | 方案二:业务强制 | 低 | 中 | 中高 | 中 | ★★★ | | 方案三:USER 扩展 | 高 | 中高 | 中高 | 中高 | ★★★ | | 方案四:工资表 | 低 | 低 | 中 | 低 | ★★ | ### 推荐与实施阶段(摘要) **推荐方案一**。建议阶段:数据准备(建表、历史整理)→ 功能开发(维护入口、算薪与补录改读快照)→ 迁移与校验 → 性能与监控。 --- ## 员工门店归属:技术实现要点与注意事项 ### 1. 建表示例(**勿直接用于生产**) > **正式 DDL 以脚本为准**:`项目文档相关/sql/2026-4-3/R-002_员工门店归属快照表_lq_employee_store_assignment.sql`(已与详细设计字段、索引对齐)。下文块仅为阅读摘要;迁移脚本**不可**在 MySQL 中调用 `YitIdHelper`,ID 须在应用层生成。 ```sql CREATE TABLE lq_employee_store_assignment ( F_Id VARCHAR(50) PRIMARY KEY, F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID', F_StoreId VARCHAR(50) NOT NULL COMMENT '门店ID', F_StoreName VARCHAR(200) COMMENT '门店名称', F_Year INT NOT NULL COMMENT '年份', F_Month INT NOT NULL COMMENT '月份', F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份YYYYMM', F_StartDate DATE COMMENT '归属开始日期', F_EndDate DATE COMMENT '归属结束日期', F_CreateTime DATETIME, F_UpdateTime DATETIME, F_CreateUser VARCHAR(50), INDEX idx_employee_month (F_EmployeeId, F_StatisticsMonth), INDEX idx_store_month (F_StoreId, F_StatisticsMonth) ) COMMENT '员工门店归属表'; ``` ### 2. 工资计算逻辑调整要点(伪代码) ```csharp // 伪代码示例 public async Task CalculateSalary(int year, int month) { var monthStr = $"{year}{month:D2}"; var storeAssignments = await _db.Queryable() .Where(x => x.StatisticsMonth == monthStr) .ToListAsync(); if (!storeAssignments.Any()) { // 回退:现有逻辑(业务数据或 BASE_USER) } foreach (var assignment in storeAssignments) { // 按归属查询业绩并计算 } } ``` ### 3. 数据补录逻辑调整要点(伪代码) ```csharp public async Task ImportBillingData(DateTime billingDate, string employeeId) { var monthStr = $"{billingDate.Year}{billingDate.Month:D2}"; var assignment = await _db.Queryable() .Where(x => x.EmployeeId == employeeId && x.StatisticsMonth == monthStr) .Where(x => billingDate >= x.StartDate && (x.EndDate == null || billingDate <= x.EndDate)) .FirstAsync(); billingRecord.StoreId = assignment?.StoreId ?? GetStoreIdByCurrentLogic(); } ``` ### 4. 注意事项 - 快照数据必须准确,定期校验。 - 索引与批量查询,避免 N+1。 - 无快照时必须有回退策略。 - 月中调店:一月多条记录,算薪按日期拆分。 - 历史迁移需人工抽检。 --- ## 员工门店归属:总结 核心矛盾是**时间维度**与 `**BASE_USER` 仅保存当前门店**之间的矛盾。推荐 **方案一(门店归属快照表)**,与 **R-002** 对应;详细字段与业务规则以 `员工门店归属快照表方案详细设计.md` 为准。 实施重点:数据准确性、性能、回退方案、历史迁移。 --- ## R-003 门店主档版本化(完整设计) ### 1. 问题与目标 | 问题 | 说明 | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **考勤回看** | `lq_attendance_record` 仅存 `F_StoreName` 等少量快照;**围栏 / Wi-Fi / 经纬度 / 营业时间**等打卡规则来自**当前** `lq_mdxx`。门店一改,历史日期的「当时按何规则判定正常/外勤」无法严格还原。 | | **算薪与费用** | 店长/主任等工资服务从 `lq_mdxx` 取 `**F_StoreType`、`F_StoreCategory`** 参与提成与分类逻辑;`**zxzt`(最新状态)**影响日报等统计是否纳入「开店」门店;**`dhhm`(热线)**在部分业务检索中使用。若只读当前主档,**重算历史月**会与当时口径不一致。 | **目标**:门店主档每次**影响考勤判定或算薪口径**的变更,落一条**不可变版本行**;历史查询与重算按**业务日期/时点**解析到对应版本。 ### 2. 与 R-002 的边界 | 维度 | R-002 员工门店归属 | R-003 门店主档版本 | | ----- | ------------ | --------------- | | 回答的问题 | 员工**在哪一家店** | 该店**当时属性是什么** | | 主键维度 | 员工 + 时间段 | 门店 + 版本号 + 有效时间 | 二者配合:算薪可先定「人→店」(R-002),再定「店→类型/类别/状态」(R-003)。 ### 3. 数据模型 **表名**:`lq_store_profile_version`(脚本:`项目文档相关/sql/2026-4-3/R-003_门店主档版本快照表_lq_store_profile_version.sql`) | 字段 | 说明 | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `F_StoreId` | `lq_mdxx.F_Id` | | `F_VersionNo` | 单店内从 1 递增 | | `F_ValidFrom` | 本版本生效时刻(建议用**保存成功**的服务器时间) | | `F_ValidTo` | 下一版本写入前,将上一行 `F_ValidTo` 置为**新版本**的 `F_ValidFrom`(左闭右开区间) | | `F_OperatorUserId` / `F_OperatorUserName` | 谁改的 | | `F_ChangeTrigger` | `STORE_UPDATE` / `TARGETS_UPDATE` / `IMPORT` 等 | | **算薪常用列(冗余,便于 SQL)** | `F_Dm`、`F_StoreType`、`F_StoreCategory`、`F_Zxzt`、`F_Dhhm`、`F_Status` | | `F_AttendanceSnapshotJson` | 与 `LqAttendanceRecordService.BuildStoreFenceSnapshot` **同结构**并含扩展:围栏、WiFi 成对、经纬度、地址、`AttendanceCheckFence`/`Wifi`、`BusinessHours`、`TrafficTips` 等 | | `F_FullExtensionJson` | 其余字段可选打包(图片、标签、目标等),按需填充 | **查询某日 `d` 当天有效版本**: ```text WHERE F_StoreId = @sid AND F_ValidFrom <= @dEndOfDay AND (F_ValidTo IS NULL OR F_ValidTo > @dStartOfDay) ``` (若一天内多次变更,应用层应保证**同一自然日**仅一条有效或约定「按最后一次」;推荐 **ValidFrom 精确到秒**,同日多条时取 `MAX(F_ValidFrom)` 仍落在当日的行。) ### 4. 纳入版本化的字段集合(首期) **必须进入 `F_AttendanceSnapshotJson`(或等价列)**:`longitude`、`latitude`、`fence_polygons`、`F_AttendanceCheckFence`、`F_AttendanceCheckWifi`、`F_AttendanceWifiPairs`、`F_AttendanceWifiVerifyPair`、`dm`、`dz`、`F_BusinessHours`、`F_TrafficTips`。 **必须进入冗余列(算薪/统计)**:`F_StoreType`、`F_StoreCategory`、`zxzt → F_Zxzt`、`dhhm → F_Dhhm`、`status → F_Status`、`dm → F_Dm`。 **可选进 `F_FullExtensionJson`**:事业部/教育部/科技部/大项目部归属、目标字段、门店介绍等——**仅当**后续算法依赖时再纳入,避免版本行过大。 ### 5. 写入触发点(与 `lq_mdxx` 写库对齐) | 入口 | 方法(约) | `F_ChangeTrigger` | | ------- | ------------------------------------------------ | -------------------- | | 门店资料保存 | `LqMdxxService.Update` | `STORE_UPDATE` | | 门店目标等 | `LqMdxxService.UpdateTargets`、同文件其它 `Updateable` | `TARGETS_UPDATE` 或细分 | | 批量导入/脚本 | 凡写 `lq_mdxx` 的通道 | `IMPORT` / `SYSTEM` | **策略**:`Update` **成功提交前**在**同一事务**内:(1) 读当前门店实体旧值;(2) 与即将写入的字段做 diff,若**版本集合内任一字段变化**则:关闭上一版本 `F_ValidTo = now`;`F_VersionNo = max+1`;插入新行并填充快照 JSON;(3) 再执行 `lq_mdxx` 的 `Update`。若**仅非版本字段**变更(如纯展示图),可不产生新版本(需在实现里列白名单)。 ### 6. 考勤侧改造要点 | 项 | 说明 | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **记录落库** | 在打卡/生成记录/重算考勤写入 `RuleSnapshotJson` 时,将 `**F_AttendanceSnapshotJson`** 嵌入 `F_AllRuleSnapshotJson` 的节点 `storeFenceSnapshot`(或新增列 `F_StorePunchSnapshotJson` 二选一);推荐**先扩 JSON** 减少 `ALTER` 表次数。 | | **详情接口** | 考勤记录详情展示「当时规则」时:**优先**用记录内快照;**legacy 无快照**则按 `AttendanceDate` + `StoreId` **回查** `lq_store_profile_version`。 | | **CurrentPunchConfig** | 仍读当前 `lq_mdxx`;无需版本表。 | ### 7. 算薪侧改造要点 | 项 | 说明 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **取门店属性** | 各 `*SalaryService` 在 `Queryable` 取 `StoreType/StoreCategory` 处,改为:对统计月取 **时点** `T`(如月末最后一秒或业务规定的「计薪锁定日」),`LEFT JOIN` 或子查询 `lq_store_profile_version` 解析该日有效版本;**无版本行**则回退当前 `lq_mdxx`(兼容冷启动)。 | | **已落库工资表** | 历史结果行若已写 `StoreType/StoreCategory`,以结果表为准做展示;**重算**时才用 R-003 纠正口径。 | | `**zxzt` 过滤** | 如 `LqDailyReportService` 等 SQL 中 `store.zxzt = '开店'`:历史报表若需严格一致,应改为「事实日 + 版本表 `F_Zxzt`」或接受「按当前主档」的产品口径并在文档声明。 | ### 8. 冷启动与回填 1. 建表后对每个门店插入 **V1**:`F_ValidFrom = 门店创建时间或系统上线日`,`F_ValidTo = NULL`,内容从**当前** `lq_mdxx` 拷贝。 2. 之后每次变更按 §5 追加版本。 3. **不保证**回填前历史与真实完全一致,需在发布说明中声明;争议月可人工插版本行修正。 ### 9. 可选增强 - `**lq_mdxx.F_ProfileVersionNo`**:指向当前最新 `F_VersionNo`,便于与版本表对账(脚本内注释)。 - **字段级审计**:门店维度可复用 `lq_business_operation_log` 或另建 `lq_store_field_audit`(与 R-001 同思路);**R-003 解决时点属性**,审计解决「谁改了哪一列」。 ### 10. 验收要点 - 修改围栏/Wi-Fi 后,旧考勤记录在详情中仍显示**修改前**规则(或能从版本表解析一致)。 - 修改门店类别/类型后,重算**过去某月**工资时使用该月时点版本(与产品约定 T 取日)。 - 修改 `zxzt`/`dhhm` 后,依赖字段的报表或算薪可按日期追溯(若产品要求)。 --- ## 数据库变更与 SQL 脚本 > **执行注意**:主键一律在应用层用 `YitIdHelper.NextId().ToString()` 生成;不要在 MySQL 存储过程/脚本里调用 C#。生产执行前在测试库验证;`utf8mb4` / 存储引擎与现网一致。 ### 变更总览 | 改造项 | 数据库对象 | 变更类型 | 脚本文件 | | -------------- | ------------------------------ | --------------------------- | ---------------------------------------------------------------------- | | R-001(方案 A,推荐) | `lq_user_profile_audit` | **新建表** | `项目文档相关/sql/2026-4-3/R-001_用户主档变更审计表_lq_user_profile_audit.sql` | | R-001(方案 B,可选) | `lq_business_operation_log` | 仅**可选追加索引** | 见 R-001 脚本内注释 | | R-002 | `lq_employee_store_assignment` | **新建表** | `项目文档相关/sql/2026-4-3/R-002_员工门店归属快照表_lq_employee_store_assignment.sql` | | R-003 | `lq_store_profile_version` | **新建表** | `项目文档相关/sql/2026-4-3/R-003_门店主档版本快照表_lq_store_profile_version.sql` | | `lq_mdxx` | 可选 | **可选** `F_ProfileVersionNo` | 见 R-003 脚本注释 | | `BASE_USER` | — | **首期不要求 ALTER** | 与 R-002 双写 `F_Mdid` 等,不强制加列 | ### R-001 表结构说明(`lq_user_profile_audit`,与 R-001 详细设计一致) | 字段 | 说明 | | ----------------------------------------------------------- | ------------------------------------------------ | | `F_Action` | 操作类型枚举,如 `PROFILE_UPDATE`、`ADMIN_RESET_PASSWORD` | | `F_TargetUserId` | 被改用户 | | `F_OperatorUserId` / `F_OperatorUserName` / `F_OperateTime` | 操作者与时间 | | `F_BatchId` | 同一次保存多字段共用 | | `F_FieldKey` / `F_FieldLabel` | 属性名与中文名 | | `F_OldValue` / `F_NewValue` | 脱敏后前后值 | | `F_Source` | `API` / `RELATION_SERVICE` 等 | | `F_ClientIp` / `F_UserAgent` | 可选 | | `F_Remark` | 可选 | **索引**:`(F_TargetUserId, F_OperateTime)`、`F_BatchId`、`(F_OperatorUserId, F_OperateTime)`、`(F_Action, F_OperateTime)`。 ### R-002 表结构说明 与 `员工门店归属快照表方案详细设计.md` 及 `R-002_...sql` 一致(含 `F_IsEffective`、`F_Remark`、`F_UpdateUser` 等)。 ### R-003 表结构说明(`lq_store_profile_version`) 见 [R-003 门店主档版本化(完整设计)](#r-003-门店主档版本化完整设计):版本区间、`F_AttendanceSnapshotJson`、算薪冗余列 `F_StoreType` / `F_StoreCategory` / `F_Zxzt` / `F_Dhhm` 等。 ### 建议执行顺序 1. R-002 建表 → 维护与双写(可先不改算薪读路径)。 2. R-003 建表 → 门店保存接版本写入 → 考勤快照嵌入 / 算薪 JOIN 版本(可分阶段开关)。 3. R-001 建表 → 各用户写入点接审计。 4. 方案 B 时按需 `ALTER` 业务操作日志索引。 ### 对账类 SQL(参考) 见 R-002 脚本末尾注释(有效段重叠检测)。 --- **文档性质**:问题分析、改造记录与影响梳理;具体代码变更以评审与迭代为准。 --- ## 附录:正式环境设计审视与评审结论 > 本节由任务协调视角对 v2.0 正文做**生产已上线**前提下的需求/设计细化,便于技术评审、排期与发版;实施时仍以代码与详细设计为准。 ### A. 文档问题清单(已处理项 + 持续关注) | 类型 | 说明 | | ------------ | ---------------------------------------------------------------------------------------------------------- | | **DDL 一致性** | 主文档第七节建表仅为摘要;**生产建表必须以** `员工门店归属快照表方案详细设计.md` **全字段与索引为准**(含 `F_UpdateUser`、`F_IsEffective`、`F_Remark` 等)。 | | **语义边界** | 「历史不可改」指**不直接改历史行语义**;允许对**当前开放归属段**做 **EndDate 截断 + 新段插入**(切段)。是否与**已锁定工资月**冲突见下文开放问题。 | | **伪代码** | 工资示例中按 `StatisticsMonth` 拉全表assignment 易误导性能;实现时应 **按参与算薪的员工 ID 集合批量查询**,与详细设计一致。 | | **迁移脚本** | 不得在 MySQL 中调用 C# `YitIdHelper`;ID 在应用层生成或按项目 DB 规范执行。 | | **现网基线** | 当前生产算薪/补录仍以 `BASE_USER.F_Mdid` 与业务明细门店为主;R-002 上线前需在发布说明中写明 **能力差距与回退开关**。 | | **多租户** | 若 R-001 复用或对齐 `lq_business_operation_log`,须遵守租户隔离(实体上 `[Tenant]`),查询必带租户条件。 | | **Markdown** | 影响矩阵表内错误嵌套已在 v2.1 修正。 | ### B. R-001 用户档案审计(生产级要点) 1. **触发范围**:凡最终 `Updateable` / 写 `BASE_USER` 的路径均需纳入(含批量);须做**静态清单 + Code Review**;禁止旁路 SQL 改用户表或统一走带审计门面。 2. **字段策略**:白名单至少含主文档所列薪酬/考勤敏感字段 + `RealName`/`Account`(排查串改);密码等**永不记原值**;手机等脱敏;可忽略 `LastModifyTime` 等纯元数据噪声。 3. **事务**:默认 **审计与主档更新同事务**,失败则回滚;若主档优先,须书面接受审计缺失并配补偿/告警。 4. **权限**:查询与用户编辑数据范围一致;敏感列可拆子权限;导出需额外约束。 5. **性能与留存**:高负载再评估异步(Outbox,禁止裸 fire-and-forget);索引 `(UserId, OperateTime)` 或 `(BizType, BizId, OperateTime)`;保留期与归档策略由人事/法务定。 ### C. R-002 门店归属快照(生产级要点) 1. **双写真值**:建议 `**F_Mdid` = 当前归属**;**历史月算薪/补录以快照段为准**。列表/选人是否展示「当前」仍以 `F_Mdid` 为主,除非产品另定。 2. **调店切段(事务内)**:截断当前有效开放段 `EndDate = 调店日-1`(**当日归属旧店或新店须业务拍板**)→ 插入新段 `StartDate = 调店日` → 更新 `F_Mdid`。 3. **回退优先级(建议定稿)**:P1 快照覆盖业务日 → P2 当月有快照但日无覆盖须**可观测**(告警/人工,勿静默乱兜底)→ P3 多月多段时 **主门店合并规则** 全服务统一 → P4 无快照则现有逻辑(明细门店 → `F_Mdid`)→ P5 仍无则业务定报错或待补队列。 4. **历史回填**:优先从**工资结果**反推;注意一月多店时单行工资会丢中段;再用业务众数等补洞;当前 `F_Mdid` 仅冷启动兜底;全量后跑对账报表。 5. **发布阶段**:Phase0 建表+维护+只写快照不算薪 → Phase1 调店双写+对账 → Phase2 算薪/补录双读与开关 → Phase3 全量新逻辑+fallback 监控 → Phase4 收敛。建议**按模块**灰度(如先补录、再某一类工资)。 6. **对账思路**:段重叠、与在职区间空洞、`F_Mdid` vs 最新开放段、锁定月跳过重算 diff 等。 ### D. R-001 与 R-002 发布顺序 - **无强依赖**,可并行开发。 - **建议**:M1 先 R-002 表 + 回填 + 维护 + 双写(**不改变算薪结果**)→ M2 读快照 + 双读校验 + 开关 → M3 R-001(或合规优先时 R-001 提前)。切段操作宜与审计联动(归属变更可追溯)。 ### E. 测试与验收清单(勾选用) **用户主档与权限**:改各敏感字段后展示与数据范围正确;(R-001)有审计且敏感不脱敏明文。 **R-002**:月初/月中/月末调店无重叠、无空洞(在约定定义下);软删/更正流程正确;批量导入失败策略。 **工资**:健康师(有/无业绩店、有/无快照、月中两店);店长/主任/店助调店月;科技部/大项与 `lq_md_target` 组合;**锁定月策略**符合拍板结论。 **考勤**:打卡/补卡与快照日一致;已落库分组快照不受改分组影响。 **补录**:历史日期写入 `StoreId` 与快照一致;回退路径可观测。 **非功能**:大批量算薪性能、无 N+1、对账抽样。 ### F. 待业务/财务拍板(开放问题) 1. **调店当日**业绩/考勤/工资算旧店还是新店(决定 `EndDate`/`StartDate` 是否含当日)。 2. **工资月已锁定**后是否允许改快照、是否允许重算、审批链。 3. **月中多店**时工资汇总表 **主门店** 规则(业绩最大 / 归属天数最长 / 其他财务口径)。 4. **无快照且无业务门店**:报错阻断还是兜底 `F_Mdid` 并打待补标。 5. **历史回填**以工资表为准还是以人事台账为准;争议数据谁签字确认。 6. **R-001 保留年限**、是否允许导出审计。 7. **兼职/多店同时有效**是否首期支持。 8. **离职当月**:归属段是否截到离职日、与「离职当月仍计薪」是否冲突。