用户主档与薪酬考勤改造记录
文档说明:本文档由原《用户主档与薪酬考勤改造记录》与《员工门店归属变更问题分析与解决方案》合并而成,统一描述用户主档(
BASE_USER)、门店主档(lq_mdxx)与门店归属、薪酬、考勤相关的问题分析、方案、改造项,以及主档变更对计算与历史查看的影响。
更新原则:每确认一项需求或方案结论,在「改造项清单」追加或更新状态。
文档版本:v2.4(R-003 门店主档版本化)
创建/合并日期:2026-01-09(门店归属分析) / 2026-04-03(R-001/R-002) / 2026-04-03(R-003 门店版本)
目录
- 关联文档
- 改造项清单
- R-001 用户档案字段级变更履历(审计)
- R-001 详细设计(完整)
- 用户信息修改后对数据计算的影响(闭环梳理)
- 员工门店归属变更:问题描述与现状
- 员工门店归属:解决方案与对比
- 员工门店归属:技术实现要点与注意事项
- 员工门店归属:总结
- R-003 门店主档版本化(完整设计)
- 附录:正式环境设计审视与评审结论
- 数据库变更与 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更新用户时仅维护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. 字段比对与写入策略
- 时机:在
Updateable/Insertable执行前读取库中旧实体(更新/删除);新增无旧实体则只写F_NewValue(F_OldValue用null或(空))。 - 参与比对的列:仅业务会真正更新的列(与
UsersService.Update的UpdateColumns列表对齐,避免把未提交字段算成变更)。 - 相等判定:
null与""在展示层可按业务约定视为相同(可选归一化后再比);日期比较建议统一到日或 UTC。 - 输出:每个变化字段插入 一行
lq_user_profile_audit,共享同一F_BatchId。 - 无字段变化:可不插审计行;若需审计「空保存」,可插一行
F_FieldKey=__META__,F_Remark=NoFieldChange(可选)。 - 软删除:至少记
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<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 | LqReimbursementWorkflowConfigService 等 Insert 用户 |
若存在 | 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(页码)、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 通常 |
| 容量 | 按员工数 × 年均变更 × 行数估算;超期归档冷表(策略同附录评审) |
| 安全 | 接口防纵向越权(禁止通过遍历 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. 闭环建议(与改造项对应)
- 门店:实施 R-002 归属快照 后,算薪与补录优先按发生月/日查归属,减少对当前
F_Mdid的依赖。 - 审计:实施 R-001 后,主档变更可追溯,便于解释「为何重算结果变化」。
- 组织/岗位:科技部、大项主管等若需历史一致,需明确是否引入按月任职/组织快照或在工资结果表中固化当时维度(与快照表同类思路)。
员工门店归属变更:问题描述与现状
业务场景
员工每个月的归属门店可能会发生变化。当发生改变后,在以下场景中会遇到门店归属问题:
- 工资计算场景:员工 A 在 1 月在门店 A 工作,2 月调到门店 B(
BASE_USER.F_Mdid更新为门店 B),计算 1 月工资时如何确定该员工 1 月的归属门店? - 数据补录场景:员工 A 在 1 月在门店 A 产生开单/消耗数据,2 月调到门店 B;若 1 月数据在 2 月补录,应归属哪个门店?
核心矛盾
- 时间维度:数据应按实际发生时间归属门店。
- 当前归属:
BASE_USER.F_Mdid只记录当前门店,无法追溯历史。 - 数据完整性:补录历史数据需要正确的门店归属。
当前数据存储情况
- 员工门店归属:
BASE_USER.F_Mdid,仅当前门店 ID。 - 开单/消耗数据:
lq_kd_jksyj/lq_xh_jksyj有F_StoreId;开单主表lq_kd_kdjlb有djmd。 - 工资计算逻辑(以健康师工资为例):优先业绩
StoreId→ 其次消耗StoreId→ 再回退BASE_USER.F_Mdid。
存在的问题
- 业务数据无门店时回退到当前
F_Mdid,历史月可能错档。 - 补录历史数据时门店易被错归。
- 历史月工资、门店统计、新店判断等可能偏差。
员工门店归属:解决方案与对比
方案一:时间维度优先 + 门店归属快照表(推荐)
核心思路:以时间为标准,用门店归属快照表记录员工每月(可精确到日)的归属。
实现要点:
- 创建门店归属表(表名建议
lq_employee_store_assignment;字段与索引见员工门店归属快照表方案详细设计.md)。 - 调店时自动维护记录;支持月中调店(同一月多条记录);支持历史导入。
- 工资计算:按统计月(及日期范围)查快照 → 无记录再回退现有逻辑。
- 数据补录:按单据/业绩发生日期查当月归属,写入
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_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 当天有效版本:
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<LqMdxxEntity> 取 StoreType/StoreCategory 处,改为:对统计月取 时点 T(如月末最后一秒或业务规定的「计薪锁定日」),LEFT JOIN 或子查询 lq_store_profile_version 解析该日有效版本;无版本行则回退当前 lq_mdxx(兼容冷启动)。 |
| 已落库工资表 | 历史结果行若已写 StoreType/StoreCategory,以结果表为准做展示;重算时才用 R-003 纠正口径。 |
**zxzt 过滤** |
如 LqDailyReportService 等 SQL 中 store.zxzt = '开店':历史报表若需严格一致,应改为「事实日 + 版本表 F_Zxzt」或接受「按当前主档」的产品口径并在文档声明。 |
8. 冷启动与回填
- 建表后对每个门店插入 V1:
F_ValidFrom = 门店创建时间或系统上线日,F_ValidTo = NULL,内容从当前lq_mdxx拷贝。 - 之后每次变更按 §5 追加版本。
- 不保证回填前历史与真实完全一致,需在发布说明中声明;争议月可人工插版本行修正。
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 门店主档版本化(完整设计):版本区间、F_AttendanceSnapshotJson、算薪冗余列 F_StoreType / F_StoreCategory / F_Zxzt / F_Dhhm 等。
建议执行顺序
- R-002 建表 → 维护与双写(可先不改算薪读路径)。
- R-003 建表 → 门店保存接版本写入 → 考勤快照嵌入 / 算薪 JOIN 版本(可分阶段开关)。
- R-001 建表 → 各用户写入点接审计。
- 方案 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 用户档案审计(生产级要点)
- 触发范围:凡最终
Updateable<UserEntity>/ 写BASE_USER的路径均需纳入(含批量);须做静态清单 + Code Review;禁止旁路 SQL 改用户表或统一走带审计门面。 - 字段策略:白名单至少含主文档所列薪酬/考勤敏感字段 +
RealName/Account(排查串改);密码等永不记原值;手机等脱敏;可忽略LastModifyTime等纯元数据噪声。 - 事务:默认 审计与主档更新同事务,失败则回滚;若主档优先,须书面接受审计缺失并配补偿/告警。
- 权限:查询与用户编辑数据范围一致;敏感列可拆子权限;导出需额外约束。
- 性能与留存:高负载再评估异步(Outbox,禁止裸 fire-and-forget);索引
(UserId, OperateTime)或(BizType, BizId, OperateTime);保留期与归档策略由人事/法务定。
C. R-002 门店归属快照(生产级要点)
- 双写真值:建议
**F_Mdid= 当前归属;历史月算薪/补录以快照段为准**。列表/选人是否展示「当前」仍以F_Mdid为主,除非产品另定。 - 调店切段(事务内):截断当前有效开放段
EndDate = 调店日-1(当日归属旧店或新店须业务拍板)→ 插入新段StartDate = 调店日→ 更新F_Mdid。 - 回退优先级(建议定稿):P1 快照覆盖业务日 → P2 当月有快照但日无覆盖须可观测(告警/人工,勿静默乱兜底)→ P3 多月多段时 主门店合并规则 全服务统一 → P4 无快照则现有逻辑(明细门店 →
F_Mdid)→ P5 仍无则业务定报错或待补队列。 - 历史回填:优先从工资结果反推;注意一月多店时单行工资会丢中段;再用业务众数等补洞;当前
F_Mdid仅冷启动兜底;全量后跑对账报表。 - 发布阶段:Phase0 建表+维护+只写快照不算薪 → Phase1 调店双写+对账 → Phase2 算薪/补录双读与开关 → Phase3 全量新逻辑+fallback 监控 → Phase4 收敛。建议按模块灰度(如先补录、再某一类工资)。
- 对账思路:段重叠、与在职区间空洞、
F_Mdidvs 最新开放段、锁定月跳过重算 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. 待业务/财务拍板(开放问题)
- 调店当日业绩/考勤/工资算旧店还是新店(决定
EndDate/StartDate是否含当日)。 - 工资月已锁定后是否允许改快照、是否允许重算、审批链。
- 月中多店时工资汇总表 主门店 规则(业绩最大 / 归属天数最长 / 其他财务口径)。
- 无快照且无业务门店:报错阻断还是兜底
F_Mdid并打待补标。 - 历史回填以工资表为准还是以人事台账为准;争议数据谁签字确认。
- R-001 保留年限、是否允许导出审计。
- 兼职/多店同时有效是否首期支持。
- 离职当月:归属段是否截到离职日、与「离职当月仍计薪」是否冲突。