用户主档与薪酬考勤改造记录.md 54.3 KB

用户主档与薪酬考勤改造记录

文档说明:本文档由原《用户主档与薪酬考勤改造记录》与《员工门店归属变更问题分析与解决方案》合并而成,统一描述用户主档(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 用户档案字段级变更履历(审计)
  4. R-001 详细设计(完整)
  5. 用户信息修改后对数据计算的影响(闭环梳理)
  6. 员工门店归属变更:问题描述与现状
  7. 员工门店归属:解决方案与对比
  8. 员工门店归属:技术实现要点与注意事项
  9. 员工门店归属:总结
  10. R-003 门店主档版本化(完整设计)
  11. 附录:正式环境设计审视与评审结论
  12. 数据库变更与 SQL 脚本

关联文档

主题 文档
门店归属快照表字段、索引、业务规则(详细设计) 员工门店归属快照表方案详细设计.md
数据库表与字段说明(维护时同步) 数据库说明.md

改造项清单

序号 状态 提出/更新日期 主题 摘要
R-001 详细设计已完成·待开发 2026-04-03 用户档案字段级变更履历(审计) 见下文 R-001 详细设计(完整);表 lq_user_profile_audit,与 UsersService 等写入点同事务落库。
R-002 待实施 2026-01-09 员工门店归属快照表(时间维度) 见本文第六节起;详细表结构见 员工门店归属快照表方案详细设计.md
R-003 详细设计已完成·待开发 2026-04-03 门店主档时点版本 R-003 门店主档版本化(完整设计);表 lq_store_profile_version;解决考勤回看与算薪取历史门店属性。

R-001 用户档案字段级变更履历(审计)

业务目标

  • 管理员在用户详情/编辑相关界面可查看该用户的历史修改记录
  • 每条记录至少包含:操作时间操作人(用户 ID 或可解析姓名)、字段标识/中文名变更前值变更后值(敏感字段如密码仅记「已修改」或脱敏策略另定)。

现状(结论)

  • UsersService 更新用户时仅维护 LastModifyTimeLastModifyUserId字段级历史表或审计中间件。
  • 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_logF_BizType=base_userF_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),如 MdidOrganizeId
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_NewValueF_OldValuenull(空))。
  2. 参与比对的列:仅业务会真正更新的列(与 UsersService.UpdateUpdateColumns 列表对齐,避免把未提交字段算成变更)。
  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

中优先级(人事资料,建议记)GenderBirthdayMobilePhoneEmailCertificatesTypeCertificatesNumberNationNativePlaceEducationUrgentContactsUrgentTelePhonePostalAddressTelePhoneLandlineManagerIdSortCodeDescriptionHeadIconZwFyftGwflOpenId 等。

低优先级 / 默认忽略(降噪)QuickQuery(随姓名自动生成可不计或合并到 RealName)、LastModifyTimeLastModifyUserIdPasswordSecretkey、各类 登录统计字段FirstLogTimeLastLogTimeLogSuccessCount 等)、ChangePasswordDate(若已单独记密码动作可忽略)、PropertyJson/Theme/Language/CommonMenu 等按产品要求:主题语言头像若需审计已包含在 CURRENT_* 动作中

实现建议:维护 HashSet<string> IgnoredPropertyNames + Dictionary<string,string> 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 LqReimbursementWorkflowConfigServiceInsert 用户 若存在 PROFILE_CREATE + F_Source=SYSTEM

要求:新增任何 Updateable<UserEntity> 必须 Code Review 是否接审计;禁止脚本直连改 BASE_USER 而不留痕。

9. 事务与一致性

  • 审计插入与 用户表同一数据库事务:审计失败则 整单回滚(推荐)。
  • 批量 UserRelationService 多用户更新:每个用户独立事务块内写审计,或整批一个大事务(与现网一致)。

10. 后端接口设计(建议挂在权限模块)

约定
路由 GET api/permission/Users/{targetUserId}/profile-audit-logs(挂在 Users/UsersService,与现有用户模块同权限体系)
查询参数 current(页码)、pageSizefieldKey(可选)、action(可选)、startTimeendTime(可选)
权限 编辑该用户 相同数据范围:user.dataScope 含目标用户 organizeIdEdit,或管理员;可增设 user:audit:view
响应行 idoperateTimeoperatorUserIdoperatorUserNameactionfieldKeyfieldLabeloldValuenewValuebatchIdsourceclientIp(可选);可选 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 通常
容量 按员工数 × 年均变更 × 行数估算;超期归档冷表(策略同附录评审)
安全 接口防纵向越权(禁止通过遍历 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 → 职位表填展示岗位名。 GwPositionId 不一致时,可能出现「入了算薪池但展示名不同」或反之;改岗后重算会改变是否计入某类工资。
考勤分组 F_AttendanceGroupId 考勤规则:迟到早退、补卡、批量生成/重算等,记录上可带分组快照,缺省回用户当前分组LqAttendanceRecordService)。设置:分组下绑定人数统计。 改分组主要影响之后生成/重算的记录;已落库且带快照的记录行为以记录为准。
在职 F_IsOnJob(及启用等) 工资:多处服务过滤 DeleteMarkEnabledMark;部分统计用在职标识(如事业总工资里 IsTerminated 来自用户在职状态)。列表与选人:一般仅在职/未删用户。 标离职后可能不再进入算薪名单;若需「离职当月仍计薪」需单独业务规则(本文不展开)。
入职时间 F_EntryDate 假期/工龄等业务(用户编辑已强调必填):与请假额度、工龄类规则相关;若其它模块按入职日算系数需单独核对。 改入职日会改变按入职推算的统计;与门店/岗位无关但影响合规类计算。
角色 F_RoleId 权限与菜单,不直接参与薪酬公式;间接影响「谁能操作算薪/导入」而非个人工资数额。 一般不计入个人业绩/工资算法。

2. 按业务模块汇总(便于测试回归)

模块 与用户主档的关系
健康师工资 LqSalaryService Gw 过滤入池;门店优先明细,否则 Mdid
健康师额外计算 LqSalaryExtraCalculationService 新店等场景下 Mdid + Gw(健康师)。
店长 / 主任 / 店助工资 Gw 入池;按 Mdid 关联门店与业绩。
科技老师工资 LqTechTeacherSalaryService Gw;按 Mdid 等。
科技部总经理工资 LqTechGeneralManagerSalaryService Gw + OrganizeIdlq_md_target
大项目部主管工资 LqMajorProjectDirectorSalaryService Gw(主管)+ OrganizeIdlq_md_target
大项目部老师工资 LqMajorProjectTeacherSalaryService 名单来自 lq_md_major_project_teacher_assignment;用户表补姓名、OrganizeId、在职;不依赖 Gw 入池
事业部总经理/经理工资 LqBusinessUnitManagerSalaryService 名单来自 lq_md_general_manager_lifeline;用户表主要补姓名、账号、在职。
考勤记录/汇总 MdidAttendanceGroupId;汇总表按 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_jksyjF_StoreId;开单主表 lq_kd_kdjlbdjmd
  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 须在应用层生成。

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. 工资计算逻辑调整要点(伪代码)

// 伪代码示例
public async Task CalculateSalary(int year, int month)
{
    var monthStr = $"{year}{month:D2}";

    var storeAssignments = await _db.Queryable<EmployeeStoreAssignmentEntity>()
        .Where(x => x.StatisticsMonth == monthStr)
        .ToListAsync();

    if (!storeAssignments.Any())
    {
        // 回退:现有逻辑(业务数据或 BASE_USER)
    }

    foreach (var assignment in storeAssignments)
    {
        // 按归属查询业绩并计算
    }
}

3. 数据补录逻辑调整要点(伪代码)

public async Task ImportBillingData(DateTime billingDate, string employeeId)
{
    var monthStr = $"{billingDate.Year}{billingDate.Month:D2}";

    var assignment = await _db.Queryable<EmployeeStoreAssignmentEntity>()
        .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_StoreTypeF_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_DmF_StoreTypeF_StoreCategoryF_ZxztF_DhhmF_Status
F_AttendanceSnapshotJson LqAttendanceRecordService.BuildStoreFenceSnapshot 同结构并含扩展:围栏、WiFi 成对、经纬度、地址、AttendanceCheckFence/WifiBusinessHoursTrafficTips
F_FullExtensionJson 其余字段可选打包(图片、标签、目标等),按需填充

查询某日 d 当天有效版本

WHERE F_StoreId = @sid
  AND F_ValidFrom <= @dEndOfDay
  AND (F_ValidTo IS NULL OR F_ValidTo > @dStartOfDay)

(若一天内多次变更,应用层应保证同一自然日仅一条有效或约定「按最后一次」;推荐 ValidFrom 精确到秒,同日多条时取 MAX(F_ValidFrom) 仍落在当日的行。)

4. 纳入版本化的字段集合(首期)

必须进入 F_AttendanceSnapshotJson(或等价列)longitudelatitudefence_polygonsF_AttendanceCheckFenceF_AttendanceCheckWifiF_AttendanceWifiPairsF_AttendanceWifiVerifyPairdmdzF_BusinessHoursF_TrafficTips

必须进入冗余列(算薪/统计)F_StoreTypeF_StoreCategoryzxzt → F_Zxztdhhm → F_Dhhmstatus → F_Statusdm → 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 = nowF_VersionNo = max+1;插入新行并填充快照 JSON;(3) 再执行 lq_mdxxUpdate。若仅非版本字段变更(如纯展示图),可不产生新版本(需在实现里列白名单)。

6. 考勤侧改造要点

说明
记录落库 在打卡/生成记录/重算考勤写入 RuleSnapshotJson 时,将 **F_AttendanceSnapshotJson** 嵌入 F_AllRuleSnapshotJson 的节点 storeFenceSnapshot(或新增列 F_StorePunchSnapshotJson 二选一);推荐先扩 JSON 减少 ALTER 表次数。
详情接口 考勤记录详情展示「当时规则」时:优先用记录内快照;legacy 无快照则按 AttendanceDate + StoreId 回查 lq_store_profile_version
CurrentPunchConfig 仍读当前 lq_mdxx;无需版本表。

7. 算薪侧改造要点

说明
取门店属性 *SalaryServiceQueryable<LqMdxxEntity>StoreType/StoreCategory 处,改为:对统计月取 时点 T(如月末最后一秒或业务规定的「计薪锁定日」),LEFT JOIN 或子查询 lq_store_profile_version 解析该日有效版本;无版本行则回退当前 lq_mdxx(兼容冷启动)。
已落库工资表 历史结果行若已写 StoreType/StoreCategory,以结果表为准做展示;重算时才用 R-003 纠正口径。
**zxzt 过滤** LqDailyReportService 等 SQL 中 store.zxzt = '开店':历史报表若需严格一致,应改为「事实日 + 版本表 F_Zxzt」或接受「按当前主档」的产品口径并在文档声明。

8. 冷启动与回填

  1. 建表后对每个门店插入 V1F_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_UPDATEADMIN_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 表结构说明

员工门店归属快照表方案详细设计.mdR-002_...sql 一致(含 F_IsEffectiveF_RemarkF_UpdateUser 等)。

R-003 表结构说明(lq_store_profile_version

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_UpdateUserF_IsEffectiveF_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<UserEntity> / 写 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. 离职当月:归属段是否截到离职日、与「离职当月仍计薪」是否冲突。