# 员工门店归属快照表方案详细设计 ## 📋 方案概述 通过创建门店归属快照表,记录员工每个月的门店归属历史,以时间维度为唯一标准,解决工资计算和数据补录中的门店归属问题。 --- ## 🗄️ 数据模型设计 ### 1. 核心表设计 #### 表名:`lq_employee_store_assignment`(员工门店归属表) #### 字段设计 | 字段名 | 类型 | 说明 | 约束 | 备注 | |--------|------|------|------|------| | F_Id | VARCHAR(50) | 主键ID | PRIMARY KEY | 使用YitIdHelper生成 | | F_EmployeeId | VARCHAR(50) | 员工ID | NOT NULL | 关联BASE_USER.F_Id | | F_StoreId | VARCHAR(50) | 门店ID | NOT NULL | 关联lq_mdxx.F_Id | | F_StoreName | VARCHAR(200) | 门店名称 | | 冗余字段,便于查询和显示 | | F_Year | INT | 年份 | NOT NULL | 如:2025 | | F_Month | INT | 月份 | NOT NULL | 1-12 | | F_StatisticsMonth | VARCHAR(6) | 统计月份 | NOT NULL | YYYYMM格式,如:202501 | | F_StartDate | DATE | 归属开始日期 | NOT NULL | 精确到天,用于月中调店 | | F_EndDate | DATE | 归属结束日期 | NULL | 为空表示当前仍在归属,精确到天 | | F_CreateTime | DATETIME | 创建时间 | | | | F_UpdateTime | DATETIME | 更新时间 | | | | F_CreateUser | VARCHAR(50) | 创建人 | | | | F_UpdateUser | VARCHAR(50) | 更新人 | | | | F_IsEffective | INT | 是否有效 | DEFAULT 1 | 1=有效,0=无效(软删除) | | F_Remark | VARCHAR(500) | 备注 | | 记录调店原因等 | #### 索引设计 ```sql -- 主键索引(自动创建) PRIMARY KEY (F_Id) -- 员工+月份查询索引(最常用) INDEX idx_employee_month (F_EmployeeId, F_StatisticsMonth, F_IsEffective) -- 门店+月份查询索引(用于门店维度统计) INDEX idx_store_month (F_StoreId, F_StatisticsMonth, F_IsEffective) -- 日期范围查询索引(用于精确查询某天的归属) INDEX idx_employee_date (F_EmployeeId, F_StartDate, F_EndDate) -- 时间范围查询索引(用于查询某个时间点的归属) INDEX idx_date_range (F_StartDate, F_EndDate, F_IsEffective) ``` #### 唯一约束 **重要考虑:是否允许一个员工在一个月内归属多个门店?** **建议:允许**(支持月中调店场景) **唯一约束设计:** - 不设置唯一约束(允许一个员工一个月有多条记录) - 通过业务逻辑保证:同一个员工的同一天只能有一条有效记录 - 通过应用层校验:插入/更新时检查是否有日期重叠 --- ### 2. 数据关系图 ``` BASE_USER (员工表) ↓ F_Id lq_employee_store_assignment (门店归属表) ↓ F_StoreId lq_mdxx (门店表) ↓ lq_salary_statistics (工资统计表) - 使用归属表的门店信息 lq_kd_jksyj (开单业绩表) - 使用归属表的门店信息 lq_xh_jksyj (消耗业绩表) - 使用归属表的门店信息 ``` --- ## 📐 业务规则设计 ### 1. 基础规则 #### 1.1 时间维度原则 - **唯一标准**:以数据实际发生的时间(年、月、日)作为门店归属的判断标准 - **不可变更**:历史归属记录一旦创建,原则上不允许修改(可通过软删除+新增的方式调整) #### 1.2 归属记录创建规则 **场景A:月初调店(整月归属一个门店)** - 员工在1月1日从门店A调到门店B - 创建两条记录: - 记录1:1月,门店A,开始日期:1月1日,结束日期:1月1日(或空,如果跨月) - 记录2:1月,门店B,开始日期:1月2日,结束日期:空(或1月31日) **场景B:月中调店(一个月归属两个门店)** - 员工在1月15日从门店A调到门店B - 创建两条记录: - 记录1:1月,门店A,开始日期:1月1日,结束日期:1月14日 - 记录2:1月,门店B,开始日期:1月15日,结束日期:1月31日(或空) **场景C:整月归属一个门店** - 员工整个月都在门店A - 创建一条记录: - 记录:1月,门店A,开始日期:1月1日,结束日期:1月31日(或空) #### 1.3 归属记录查询规则 **查询某员工某月的所有门店归属:** ```sql SELECT * FROM lq_employee_store_assignment WHERE F_EmployeeId = @EmployeeId AND F_StatisticsMonth = @StatisticsMonth AND F_IsEffective = 1 ORDER BY F_StartDate ``` **查询某员工某天的门店归属:** ```sql SELECT * FROM lq_employee_store_assignment WHERE F_EmployeeId = @EmployeeId AND F_StartDate <= @Date AND (F_EndDate >= @Date OR F_EndDate IS NULL) AND F_IsEffective = 1 ORDER BY F_StartDate DESC LIMIT 1 ``` **查询某门店某月的所有员工:** ```sql SELECT DISTINCT F_EmployeeId FROM lq_employee_store_assignment WHERE F_StoreId = @StoreId AND F_StatisticsMonth = @StatisticsMonth AND F_IsEffective = 1 ``` ### 2. 数据维护规则 #### 2.1 自动维护机制 **触发时机:员工调店时** **操作步骤:** 1. 用户在前端操作:员工从门店A调到门店B(选择调店日期) 2. 后端处理逻辑: - 查询当前员工的有效归属记录(结束日期为空的记录) - 如果存在,将结束日期设置为调店日期的前一天 - 创建新的归属记录:开始日期为调店日期,结束日期为空 - 如果调店日期是月初,更新统计月份;如果是月中,保持原月份,新增记录 #### 2.2 手动维护机制 **适用场景:** - 历史数据补录 - 数据修正 - 批量导入 **操作方式:** - 提供管理界面:员工门店归属管理 - 支持批量导入Excel - 支持手动添加/编辑/删除(软删除) #### 2.3 数据校验规则 **插入/更新时的校验:** 1. **日期范围校验**: - 开始日期不能大于结束日期 - 开始日期和结束日期必须在同一个月内(或跨月但统计月份相同) 2. **重叠校验**: - 同一个员工,同一天不能有多条有效记录 - 如果存在重叠,需要先处理旧记录(结束或删除) 3. **完整性校验**: - 员工ID、门店ID必须存在 - 统计月份格式正确(YYYYMM) 4. **业务逻辑校验**: - 门店必须有效(未删除) - 员工必须有效(未删除、已启用) --- ## 🔄 与现有系统集成 ### 1. 工资计算集成 #### 1.1 健康师工资计算 **当前逻辑:** ```csharp // 1. 优先从业绩数据中获取门店(performanceData中的StoreId) // 2. 如果没有,从消耗数据中获取门店 // 3. 如果还没有,使用 BASE_USER.F_Mdid ``` **调整后逻辑:** ```csharp // 1. 查询门店归属快照表(按统计月份) var storeAssignments = await QueryStoreAssignment(employeeId, statisticsMonth); // 2. 如果快照表有记录,使用快照表的门店归属 if (storeAssignments.Any()) { // 按时间范围拆分计算(如果一个月有多个门店) foreach (var assignment in storeAssignments) { // 查询该员工在该门店、该时间范围内的业绩数据 // 计算该部分工资 } } // 3. 如果快照表没有记录,使用当前逻辑(回退方案) else { // 使用现有逻辑:从业务数据或BASE_USER获取 } ``` **特殊处理:月中调店** - 如果一个月有多个门店归属,需要按时间范围拆分计算 - 每个时间段的业绩数据单独统计 - 工资计算结果需要合并(一个员工一个月可能有多条工资记录?还是合并为一条?) **建议:合并为一条记录** - 工资统计表仍然是一个员工一个月一条记录 - 但需要记录主要门店(归属时间最长的门店,或业绩最多的门店) #### 1.2 其他岗位工资计算 **店长工资:** - 店长通常是固定门店,但也要支持调店 - 计算逻辑与健康师类似 **主任工资:** - 类似逻辑 **其他岗位:** - 统一使用快照表逻辑 ### 2. 数据补录集成 #### 2.1 开单数据补录 **当前问题:** - 补录历史数据时,如果没有门店信息,可能使用当前门店(BASE_USER.F_Mdid) **调整后逻辑:** ```csharp // 补录开单数据时 public async Task ImportBillingData(BillingRecord record) { var billingDate = record.BillingDate; // 开单日期 var employeeId = record.EmployeeId; // 健康师ID var statisticsMonth = $"{billingDate.Year}{billingDate.Month:D2}"; // 1. 查询该日期该员工的门店归属 var assignment = await QueryStoreAssignmentByDate(employeeId, billingDate); // 2. 设置门店ID if (assignment != null) { record.StoreId = assignment.StoreId; } else { // 回退方案:使用当前逻辑 record.StoreId = GetStoreIdByCurrentLogic(employeeId); // 可选:记录警告日志,提示需要维护门店归属 _logger.LogWarning($"员工{employeeId}在{billingDate}的门店归属未找到,使用当前门店"); } // 3. 保存数据 await SaveBillingRecord(record); } ``` #### 2.2 消耗数据补录 **类似逻辑:** - 根据消耗日期(Yjsj)查询门店归属 - 设置消耗记录的StoreId ### 3. 数据查询集成 #### 3.1 员工列表查询 **当前逻辑:** - 直接从BASE_USER表查询,显示F_Mdid对应的门店 **调整后逻辑:** - 如果要显示历史门店,需要关联查询快照表 - 当前门店:BASE_USER.F_Mdid(实时) - 历史门店:从快照表查询 #### 3.2 门店员工列表查询 **场景:查询某门店某月的所有员工** **当前逻辑:** - 查询BASE_USER表,F_Mdid = 门店ID **调整后逻辑:** ```sql -- 查询某门店某月的所有员工 SELECT DISTINCT esa.F_EmployeeId, u.F_RealName FROM lq_employee_store_assignment esa INNER JOIN BASE_USER u ON esa.F_EmployeeId = u.F_Id WHERE esa.F_StoreId = @StoreId AND esa.F_StatisticsMonth = @StatisticsMonth AND esa.F_IsEffective = 1 AND u.F_DeleteMark IS NULL AND u.F_EnabledMark = 1 ``` --- ## 📊 数据迁移策略 ### 1. 历史数据来源 #### 1.1 数据来源分析 **可能的数据来源:** 1. **工资统计表(lq_salary_statistics等)** - 已有历史工资记录 - 工资记录中有门店ID(F_StoreId) - 可以反向推导:员工+月份 → 门店 2. **业务数据(开单/消耗表)** - 开单业绩表(lq_kd_jksyj)有门店ID(F_StoreId) - 消耗表(lq_xh_jksyj)有门店ID(F_StoreId) - 可以统计:员工+月份 → 门店(取出现最多的门店) 3. **BASE_USER表的变更日志(如果有)** - 如果系统有记录用户表变更日志,可以追溯 4. **人工确认** - 对于无法确定的数据,需要人工确认 #### 1.2 数据迁移优先级 **优先级1:工资统计表(最可靠)** - 理由:工资记录是最终结果,门店归属相对准确 - 方法:从工资统计表提取:员工ID + 统计月份 + 门店ID - 时间范围:最近2年的数据(或更长时间,根据业务需要) **优先级2:业务数据统计(补充)** - 理由:对于没有工资记录的月份,从业务数据推断 - 方法:统计每个员工每个月的业绩数据,取门店出现最多的 - 注意:可能存在一个月有多个门店的情况(月中调店) **优先级3:BASE_USER当前门店(兜底)** - 理由:对于完全没有数据的员工,使用当前门店 - 方法:BASE_USER.F_Mdid作为最后兜底 ### 2. 数据迁移步骤 #### 步骤1:数据提取 ```sql -- 从工资统计表提取(健康师工资) INSERT INTO lq_employee_store_assignment (F_Id, F_EmployeeId, F_StoreId, F_StoreName, F_Year, F_Month, F_StatisticsMonth, F_StartDate, F_EndDate, ...) SELECT YitIdHelper.NextId(), F_EmployeeId, F_StoreId, (SELECT F_Dm FROM lq_mdxx WHERE F_Id = F_StoreId), YEAR(STR_TO_DATE(CONCAT(F_StatisticsMonth, '01'), '%Y%m%d')), MONTH(STR_TO_DATE(CONCAT(F_StatisticsMonth, '01'), '%Y%m%d')), F_StatisticsMonth, STR_TO_DATE(CONCAT(F_StatisticsMonth, '01'), '%Y%m%d'), -- 开始日期:月初 LAST_DAY(STR_TO_DATE(CONCAT(F_StatisticsMonth, '01'), '%Y%m%d')), -- 结束日期:月末 NOW(), NOW(), 'System', 'System', 1 FROM lq_salary_statistics WHERE F_StoreId IS NOT NULL AND F_StoreId != '' GROUP BY F_EmployeeId, F_StatisticsMonth, F_StoreId ``` #### 步骤2:数据清洗 **处理重复记录:** - 如果一个员工一个月有多条记录(可能是数据异常,也可能是月中调店) - 需要人工确认:是数据异常还是确实调店 **处理缺失数据:** - 对于没有工资记录但可能有业务数据的员工 - 从业务数据统计推断 #### 步骤3:数据验证 **验证规则:** 1. 每个员工每个月至少有一条记录(如果有业务数据) 2. 门店ID必须存在 3. 统计月份格式正确 4. 日期范围合理 **验证方法:** - 抽样检查:随机抽取一定比例的数据,人工验证 - 逻辑检查:检查是否有明显异常(如同一员工同一天多个门店) #### 步骤4:数据导入 **导入方式:** - 批量导入(使用事务保证一致性) - 分批次导入(避免锁表时间过长) - 记录导入日志(成功/失败记录) ### 3. 数据迁移时间窗口 **建议:** - 选择业务低峰期(如夜间或周末) - 分批次迁移(先迁移最近3个月,验证无误后再迁移更多) - 保留回滚方案(如果迁移失败,可以删除导入的数据) --- ## 🎯 边界情况处理 ### 1. 月中调店 **场景:** 员工在1月15日从门店A调到门店B **处理方案:** - 创建两条归属记录 - 工资计算时,按时间范围拆分计算 - 最终工资记录合并为一条(记录主要门店) **问题:** - 工资统计表中的门店ID如何设置? - 方案A:设置为主要门店(归属时间最长的门店,或业绩最多的门店) - 方案B:设置为空,或记录为"多门店" - **建议:方案A**,记录为主要门店,在备注或明细中说明 ### 2. 数据缺失 **场景:** 快照表中没有某员工某月的归属记录 **处理方案:** - 回退到现有逻辑(从业务数据或BASE_USER获取) - 记录警告日志,提示需要维护门店归属 - 可选:自动创建一条归属记录(使用当前门店或推断的门店) ### 3. 数据冲突 **场景:** 同一个员工同一天有多条归属记录(数据异常) **处理方案:** - 数据校验时发现冲突,不允许插入/更新 - 提示用户先处理冲突记录 - 提供数据修正工具 ### 4. 跨月调店 **场景:** 员工在1月31日调到门店B,实际在2月1日生效 **处理方案:** - 记录1月:门店A,结束日期:1月31日 - 记录2月:门店B,开始日期:2月1日 - 如果调店日期是月末最后一天,可以考虑归属到下个月 ### 5. 离职员工 **场景:** 员工在月中离职 **处理方案:** - 创建归属记录:结束日期设置为离职日期 - 如果离职日期是月中,归属记录只到离职日期 - 工资计算时,只计算到离职日期 ### 6. 新员工入职 **场景:** 新员工在月中入职 **处理方案:** - 创建归属记录:开始日期为入职日期 - 如果入职日期是月中,从入职日期开始归属 --- ## ⚡ 性能考虑 ### 1. 查询性能优化 #### 1.1 索引优化 - 已设计多个索引(见索引设计部分) - 定期分析索引使用情况,优化索引 #### 1.2 查询优化 **批量查询:** - 工资计算时,批量查询所有员工的归属记录,避免N+1查询 ```csharp // 批量查询 var employeeIds = salaryList.Select(s => s.EmployeeId).Distinct().ToList(); var assignments = await _db.Queryable() .Where(x => employeeIds.Contains(x.EmployeeId) && x.StatisticsMonth == monthStr) .ToListAsync(); var assignmentDict = assignments.GroupBy(x => x.EmployeeId).ToDictionary(g => g.Key, g => g.ToList()); ``` **缓存策略:** - 对于频繁查询的数据(如当前月的归属记录),可以使用缓存 - 缓存失效策略:归属记录更新时,清除相关缓存 ### 2. 写入性能优化 #### 2.1 批量插入 - 数据迁移时,使用批量插入(如每次插入1000条) - 使用事务保证一致性 #### 2.2 异步处理 - 数据迁移可以异步处理 - 员工调店时,归属记录的创建可以异步处理(如果对实时性要求不高) ### 3. 表分区考虑 **是否需要分区?** - 如果数据量很大(如10年历史数据,每月几千员工),可以考虑按年份或月份分区 - 当前阶段可能不需要,但设计时要考虑扩展性 --- ## 🔒 数据一致性保证 ### 1. 事务处理 **关键操作使用事务:** - 员工调店时:更新旧记录 + 创建新记录(在一个事务中) - 数据迁移:批量导入使用事务 ### 2. 数据校验 **应用层校验:** - 插入/更新前校验数据完整性和业务逻辑 - 防止数据冲突 **数据库层约束:** - 外键约束(如果MySQL支持,但通常不使用外键,使用应用层保证) - 非空约束、唯一约束(根据业务需要) ### 3. 数据同步 **与BASE_USER表同步:** - 员工调店时,同时更新BASE_USER.F_Mdid和归属表 - 或者:归属表作为数据源,BASE_USER.F_Mdid从归属表计算(当前月份的主要门店) **建议:** - BASE_USER.F_Mdid保持为当前门店(实时) - 归属表记录历史归属 - 两者可以不一致(BASE_USER是当前状态,归属表是历史记录) --- ## 👤 用户体验设计 ### 1. 管理界面 #### 1.1 员工门店归属管理页面 **功能:** - 列表展示:员工、门店、月份、时间范围 - 查询:按员工、门店、月份查询 - 添加:手动添加归属记录 - 编辑:修改归属记录(需要校验) - 删除:软删除归属记录 - 批量导入:Excel导入 #### 1.2 员工调店操作 **操作流程:** 1. 选择员工 2. 选择目标门店 3. 选择调店日期(默认今天) 4. 系统自动: - 查询当前有效归属记录 - 更新旧记录结束日期 - 创建新记录开始日期 5. 确认保存 ### 2. 数据展示 #### 2.1 员工详情页 **显示:** - 当前门店:BASE_USER.F_Mdid(实时) - 历史归属:从归属表查询,列表展示 #### 2.2 工资计算页面 **显示:** - 如果使用了快照表的门店,显示来源标识 - 如果使用了回退逻辑,显示警告提示 --- ## 🔮 扩展性考虑 ### 1. 未来可能的业务变化 #### 1.1 多门店归属 **场景:** 一个员工可能同时归属多个门店(兼职) **扩展方案:** - 当前设计已经支持(一个员工一个月可以有多条记录) - 需要调整业务逻辑:工资计算时,如何分配业绩到不同门店 #### 1.2 部门归属 **场景:** 不仅记录门店归属,还要记录部门归属 **扩展方案:** - 可以增加字段:F_DepartmentId(部门ID) - 或者创建新的表:员工部门归属表 #### 1.3 岗位变更 **场景:** 记录员工岗位变更历史 **扩展方案:** - 类似设计:员工岗位归属表 - 或者:在归属表中增加字段:F_Position(岗位) ### 2. 性能扩展 #### 2.1 数据归档 - 历史数据(如3年前的数据)可以归档到历史表 - 当前表只保留最近2-3年的数据 #### 2.2 读写分离 - 如果查询压力大,可以考虑读写分离 - 归属表主要是查询操作,可以放到只读库 --- ## ⚠️ 风险点与应对措施 ### 1. 数据准确性风险 **风险:** 历史数据迁移可能不准确 **应对:** - 多数据源验证(工资表、业务数据、人工确认) - 分批次迁移,每批验证 - 提供数据修正工具 ### 2. 性能风险 **风险:** 查询性能可能下降 **应对:** - 合理设计索引 - 使用缓存 - 监控查询性能,及时优化 ### 3. 数据一致性风险 **风险:** 归属表与BASE_USER表可能不一致 **应对:** - 明确两者的作用:BASE_USER是当前状态,归属表是历史记录 - 提供数据同步工具(可选) - 在关键操作时校验一致性 ### 4. 业务复杂度风险 **风险:** 月中调店等复杂场景增加业务复杂度 **应对:** - 提供清晰的业务规则文档 - 在管理界面提供操作指引 - 提供数据校验和提示 --- ## 📋 实施 Checklist ### 阶段一:数据准备 - [ ] 创建归属表结构 - [ ] 设计索引 - [ ] 编写数据迁移脚本 - [ ] 准备测试数据 ### 阶段二:功能开发 - [ ] 开发归属记录管理功能(CRUD) - [ ] 开发员工调店功能(自动创建归属记录) - [ ] 调整工资计算逻辑(集成快照表) - [ ] 调整数据补录逻辑(集成快照表) - [ ] 开发数据校验逻辑 ### 阶段三:数据迁移 - [ ] 备份现有数据 - [ ] 执行数据迁移(小批量测试) - [ ] 验证数据准确性 - [ ] 全量数据迁移 - [ ] 数据验证和修正 ### 阶段四:测试验证 - [ ] 单元测试 - [ ] 集成测试 - [ ] 性能测试 - [ ] 用户验收测试 ### 阶段五:上线部署 - [ ] 部署到测试环境 - [ ] 测试环境验证 - [ ] 部署到生产环境 - [ ] 监控运行情况 - [ ] 数据修正和支持 --- ## 🔍 参考现有系统设计 ### 1. 现有归属表分析 系统中已经存在类似的归属表设计,可以参考其设计思路: #### 1.1 大项目部老师归属表(lq_md_major_project_teacher_assignment) **设计特点:** - 只记录月份级别(Year + Month),不记录日期范围 - 唯一约束:门店 + 年份 + 月份 + 老师ID - 一个老师一个月只能有一条记录(不支持月中调店) **适用场景:** - 大项目部老师通常是整月归属一个门店 - 如果需要月中调店,需要拆分月份处理 #### 1.2 总经理门店归属表(lq_md_general_manager_lifeline) **设计特点:** - 记录月份级别(Month,YYYYMM格式) - 唯一约束:门店 + 月份 + 总经理ID - 一个总经理一个月在一个门店只能有一条记录 - 但一个总经理可以管理多个门店(通过多条记录实现) **适用场景:** - 总经理/经理通常管理多个门店 - 整月归属,不涉及月中调店 ### 2. 设计对比与选择 #### 方案A:按月设计(参考现有归属表) **特点:** - 只记录月份级别(Year + Month) - 一个员工一个月只能有一条记录 - 不支持月中调店(如果需要调店,需要拆分月份) **优点:** - 设计简单,与现有归属表一致 - 查询逻辑简单 - 性能好(数据量少) **缺点:** - 不支持月中调店场景 - 如果员工在月中调店,需要手动拆分月份 #### 方案B:按日期范围设计(推荐方案) **特点:** - 记录日期范围(StartDate + EndDate) - 一个员工一个月可以有多条记录 - 支持月中调店 **优点:** - 支持月中调店等复杂场景 - 时间维度精确 - 更灵活 **缺点:** - 设计相对复杂 - 查询逻辑需要处理日期范围 - 数据量可能更多 ### 3. 推荐方案 **建议采用方案B(按日期范围设计)**,理由: 1. **业务需求**:健康师等岗位确实存在月中调店的场景 2. **数据准确性**:日期范围设计更精确,能准确反映实际归属情况 3. **扩展性**:未来如果有更细粒度的需求,日期范围设计更容易扩展 4. **兼容性**:可以与现有按月设计的归属表共存,互不影响 **但是要注意:** - 如果某些岗位(如大项目部老师、总经理)不需要支持月中调店,可以继续使用现有的按月设计 - 门店归属快照表主要用于健康师、店助、店长等需要精确时间范围的岗位 --- ## 💭 额外思考点 ### 1. 数据一致性策略 #### 1.1 BASE_USER.F_Mdid 与归属表的关系 **问题:** BASE_USER.F_Mdid 应该如何处理? **方案A:保持独立** - BASE_USER.F_Mdid 保持为当前门店(实时状态) - 归属表记录历史归属 - 两者可以不一致(BASE_USER是当前状态,归属表是历史记录) **方案B:同步更新** - 员工调店时,同时更新 BASE_USER.F_Mdid 和归属表 - 保持一致 **方案C:BASE_USER.F_Mdid 从归属表计算** - BASE_USER.F_Mdid 不再手动维护 - 从归属表计算当前月份的主要门店 - 查询时实时计算或定期同步 **推荐:方案A** - 理由:BASE_USER.F_Mdid 是实时状态,归属表是历史记录,用途不同 - BASE_USER.F_Mdid 用于当前业务查询(如:查询当前门店的员工) - 归属表用于历史数据查询(如:计算历史月份工资) - 两者可以不一致,但需要明确各自的作用 #### 1.2 数据同步策略 **如果采用方案A,需要考虑:** - 员工调店时,BASE_USER.F_Mdid 和归属表的更新是否需要在同一个事务中? - 建议:在同一个事务中更新,保证一致性 ### 2. 数据维护的便利性 #### 2.1 自动维护 vs 手动维护 **自动维护:** - 员工调店时,自动创建归属记录 - 优点:减少人工操作,数据准确 - 缺点:如果调店操作本身有问题,归属记录也会有问题 **手动维护:** - 需要手动创建/编辑归属记录 - 优点:可控性强 - 缺点:增加操作复杂度,可能遗漏 **推荐:混合模式** - 员工调店时,自动创建归属记录(默认) - 提供手动维护界面,可以修改/补录 - 支持批量导入历史数据 #### 2.2 数据修正机制 **场景:发现历史归属数据有误** **处理方案:** 1. **不允许修改历史数据**(严格模式) - 如果需要修正,只能删除(软删除)+ 新增 - 优点:保持数据可追溯性 - 缺点:操作复杂 2. **允许修改历史数据**(灵活模式) - 可以直接修改历史记录 - 优点:操作简单 - 缺点:无法追溯修改历史 **推荐:方案1(严格模式)** - 历史数据一旦创建,不允许直接修改 - 需要修正时,软删除旧记录,创建新记录 - 记录修正原因和操作人 ### 3. 查询性能优化 #### 3.1 批量查询优化 **场景:计算某月所有员工的工资** **优化策略:** ```csharp // 一次性查询所有员工的归属记录 var employeeIds = allEmployees.Select(e => e.Id).ToList(); var assignments = await _db.Queryable() .Where(x => employeeIds.Contains(x.EmployeeId) && x.StatisticsMonth == monthStr) .ToListAsync(); // 转换为字典,便于快速查找 var assignmentDict = assignments .GroupBy(x => x.EmployeeId) .ToDictionary(g => g.Key, g => g.OrderBy(x => x.StartDate).ToList()); ``` #### 3.2 缓存策略 **缓存哪些数据?** - 当前月的归属记录(变化较少) - 员工当前门店(BASE_USER.F_Mdid) **缓存失效策略:** - 归属记录更新时,清除相关缓存 - 定时刷新(如每天凌晨) #### 3.3 数据分区考虑 **是否需要分区?** - 如果数据量很大(如10年历史,每月几千员工),可以考虑按年份分区 - 当前阶段可能不需要,但设计时要考虑扩展性 ### 4. 异常情况处理 #### 4.1 数据冲突检测 **场景:同一个员工同一天有多条归属记录** **检测时机:** - 插入/更新归属记录时 - 数据迁移时 **处理方案:** - 应用层校验:插入前检查是否有日期重叠 - 如果发现冲突,拒绝操作,提示用户处理 **校验SQL示例:** ```sql -- 检查是否存在日期重叠 SELECT COUNT(*) FROM lq_employee_store_assignment WHERE F_EmployeeId = @EmployeeId AND F_IsEffective = 1 AND ( (F_StartDate <= @EndDate AND F_EndDate >= @StartDate) OR (F_StartDate IS NULL AND F_EndDate IS NULL) ) ``` #### 4.2 数据缺失处理 **场景:快照表中没有某员工某月的归属记录** **处理方案:** 1. **回退到现有逻辑**(推荐) - 从业务数据或BASE_USER获取 - 记录警告日志 - 可选:自动创建一条归属记录(使用当前门店或推断的门店) 2. **报错处理** - 如果快照表没有记录,直接报错 - 要求用户先维护归属记录 **推荐:方案1** - 保持系统的容错性 - 记录警告,便于后续补录数据 ### 5. 与现有系统的兼容性 #### 5.1 渐进式迁移 **迁移策略:** 1. **第一阶段:新建表,不影响现有逻辑** - 创建归属表 - 数据迁移 - 但工资计算仍使用现有逻辑 2. **第二阶段:双写模式** - 工资计算时,同时使用新逻辑和旧逻辑 - 对比结果,验证新逻辑的正确性 3. **第三阶段:切换到新逻辑** - 新逻辑验证无误后,切换到新逻辑 - 保留旧逻辑作为回退方案 4. **第四阶段:移除旧逻辑** - 新逻辑稳定运行一段时间后,移除旧逻辑 #### 5.2 数据迁移的准确性 **迁移数据来源优先级:** 1. 工资统计表(最可靠) 2. 业务数据统计(补充) 3. BASE_USER当前门店(兜底) **迁移后的验证:** - 抽样检查:随机抽取一定比例的数据,人工验证 - 逻辑检查:检查是否有明显异常 - 对比验证:对比迁移前后的工资计算结果 ### 6. 未来扩展性 #### 6.1 多门店归属 **场景:一个员工同时归属多个门店(兼职)** **扩展方案:** - 当前设计已经支持(一个员工一个月可以有多条记录) - 需要调整工资计算逻辑:如何分配业绩到不同门店 - 可能需要增加字段:业绩分配比例 #### 6.2 部门归属 **场景:不仅记录门店归属,还要记录部门归属** **扩展方案:** - 可以增加字段:F_DepartmentId(部门ID) - 或者创建新表:员工部门归属表 - 或者:在归属表中增加字段:F_DepartmentId(如果需要同时记录门店和部门) #### 6.3 岗位变更历史 **场景:记录员工岗位变更历史** **扩展方案:** - 可以创建新表:员工岗位归属表(类似设计) - 或者:在归属表中增加字段:F_Position(岗位) #### 6.4 其他业务属性 **场景:需要记录其他业务属性(如:职级、薪资等级等)** **扩展方案:** - 可以在归属表中增加字段 - 或者创建关联表 **设计原则:** - 优先考虑在归属表中增加字段(如果属性是时间相关的) - 如果属性与门店归属无关,考虑创建新表 --- ## 📝 总结 门店归属快照表方案是一个相对完善的解决方案,能够: 1. **解决核心问题**:以时间维度为唯一标准,准确记录历史门店归属 2. **支持复杂场景**:月中调店、跨月调店等 3. **保证数据完整性**:通过多数据源验证和数据校验 4. **考虑性能**:合理设计索引和查询优化 5. **易于扩展**:设计时考虑了未来的业务变化 **关键成功因素:** - 数据迁移的准确性 - 业务规则的清晰性 - 用户操作的简便性 - 系统性能的稳定性 --- **文档版本:** v1.0 **创建日期:** 2026-01-09 **文档性质:** 详细设计方案(仅思考,不修改代码)