员工门店归属快照表方案详细设计
📋 方案概述
通过创建门店归属快照表,记录员工每个月的门店归属历史,以时间维度为唯一标准,解决工资计算和数据补录中的门店归属问题。
🗄️ 数据模型设计
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) | 备注 | 记录调店原因等 |
索引设计
-- 主键索引(自动创建)
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 归属记录查询规则
查询某员工某月的所有门店归属:
SELECT * FROM lq_employee_store_assignment
WHERE F_EmployeeId = @EmployeeId
AND F_StatisticsMonth = @StatisticsMonth
AND F_IsEffective = 1
ORDER BY F_StartDate
查询某员工某天的门店归属:
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
查询某门店某月的所有员工:
SELECT DISTINCT F_EmployeeId FROM lq_employee_store_assignment
WHERE F_StoreId = @StoreId
AND F_StatisticsMonth = @StatisticsMonth
AND F_IsEffective = 1
2. 数据维护规则
2.1 自动维护机制
触发时机:员工调店时
操作步骤:
- 用户在前端操作:员工从门店A调到门店B(选择调店日期)
- 后端处理逻辑:
- 查询当前员工的有效归属记录(结束日期为空的记录)
- 如果存在,将结束日期设置为调店日期的前一天
- 创建新的归属记录:开始日期为调店日期,结束日期为空
- 如果调店日期是月初,更新统计月份;如果是月中,保持原月份,新增记录
2.2 手动维护机制
适用场景:
- 历史数据补录
- 数据修正
- 批量导入
操作方式:
- 提供管理界面:员工门店归属管理
- 支持批量导入Excel
- 支持手动添加/编辑/删除(软删除)
2.3 数据校验规则
插入/更新时的校验:
日期范围校验:
- 开始日期不能大于结束日期
- 开始日期和结束日期必须在同一个月内(或跨月但统计月份相同)
重叠校验:
- 同一个员工,同一天不能有多条有效记录
- 如果存在重叠,需要先处理旧记录(结束或删除)
完整性校验:
- 员工ID、门店ID必须存在
- 统计月份格式正确(YYYYMM)
业务逻辑校验:
- 门店必须有效(未删除)
- 员工必须有效(未删除、已启用)
🔄 与现有系统集成
1. 工资计算集成
1.1 健康师工资计算
当前逻辑:
// 1. 优先从业绩数据中获取门店(performanceData中的StoreId)
// 2. 如果没有,从消耗数据中获取门店
// 3. 如果还没有,使用 BASE_USER.F_Mdid
调整后逻辑:
// 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)
调整后逻辑:
// 补录开单数据时
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
调整后逻辑:
-- 查询某门店某月的所有员工
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 数据来源分析
可能的数据来源:
工资统计表(lq_salary_statistics等)
- 已有历史工资记录
- 工资记录中有门店ID(F_StoreId)
- 可以反向推导:员工+月份 → 门店
业务数据(开单/消耗表)
- 开单业绩表(lq_kd_jksyj)有门店ID(F_StoreId)
- 消耗表(lq_xh_jksyj)有门店ID(F_StoreId)
- 可以统计:员工+月份 → 门店(取出现最多的门店)
BASE_USER表的变更日志(如果有)
- 如果系统有记录用户表变更日志,可以追溯
人工确认
- 对于无法确定的数据,需要人工确认
1.2 数据迁移优先级
优先级1:工资统计表(最可靠)
- 理由:工资记录是最终结果,门店归属相对准确
- 方法:从工资统计表提取:员工ID + 统计月份 + 门店ID
- 时间范围:最近2年的数据(或更长时间,根据业务需要)
优先级2:业务数据统计(补充)
- 理由:对于没有工资记录的月份,从业务数据推断
- 方法:统计每个员工每个月的业绩数据,取门店出现最多的
- 注意:可能存在一个月有多个门店的情况(月中调店)
优先级3:BASE_USER当前门店(兜底)
- 理由:对于完全没有数据的员工,使用当前门店
- 方法:BASE_USER.F_Mdid作为最后兜底
2. 数据迁移步骤
步骤1:数据提取
-- 从工资统计表提取(健康师工资)
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:数据验证
验证规则:
- 每个员工每个月至少有一条记录(如果有业务数据)
- 门店ID必须存在
- 统计月份格式正确
- 日期范围合理
验证方法:
- 抽样检查:随机抽取一定比例的数据,人工验证
- 逻辑检查:检查是否有明显异常(如同一员工同一天多个门店)
步骤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<EmployeeStoreAssignmentEntity>() .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 员工调店操作
操作流程:
- 选择员工
- 选择目标门店
- 选择调店日期(默认今天)
- 系统自动:
- 查询当前有效归属记录
- 更新旧记录结束日期
- 创建新记录开始日期
- 确认保存
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. 数据一致性策略
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(严格模式)
- 历史数据一旦创建,不允许直接修改
- 需要修正时,软删除旧记录,创建新记录
- 记录修正原因和操作人
3. 查询性能优化
3.1 批量查询优化
场景:计算某月所有员工的工资
优化策略:
// 一次性查询所有员工的归属记录
var employeeIds = allEmployees.Select(e => e.Id).ToList();
var assignments = await _db.Queryable<EmployeeStoreAssignmentEntity>()
.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示例:
-- 检查是否存在日期重叠
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 数据缺失处理
场景:快照表中没有某员工某月的归属记录
处理方案:
回退到现有逻辑(推荐)
- 从业务数据或BASE_USER获取
- 记录警告日志
- 可选:自动创建一条归属记录(使用当前门店或推断的门店)
报错处理
- 如果快照表没有记录,直接报错
- 要求用户先维护归属记录
推荐:方案1
- 保持系统的容错性
- 记录警告,便于后续补录数据
5. 与现有系统的兼容性
5.1 渐进式迁移
迁移策略:
第一阶段:新建表,不影响现有逻辑
- 创建归属表
- 数据迁移
- 但工资计算仍使用现有逻辑
第二阶段:双写模式
- 工资计算时,同时使用新逻辑和旧逻辑
- 对比结果,验证新逻辑的正确性
第三阶段:切换到新逻辑
- 新逻辑验证无误后,切换到新逻辑
- 保留旧逻辑作为回退方案
第四阶段:移除旧逻辑
- 新逻辑稳定运行一段时间后,移除旧逻辑
5.2 数据迁移的准确性
迁移数据来源优先级:
- 工资统计表(最可靠)
- 业务数据统计(补充)
- BASE_USER当前门店(兜底)
迁移后的验证:
- 抽样检查:随机抽取一定比例的数据,人工验证
- 逻辑检查:检查是否有明显异常
- 对比验证:对比迁移前后的工资计算结果
6. 未来扩展性
6.1 多门店归属
场景:一个员工同时归属多个门店(兼职)
扩展方案:
- 当前设计已经支持(一个员工一个月可以有多条记录)
- 需要调整工资计算逻辑:如何分配业绩到不同门店
- 可能需要增加字段:业绩分配比例
6.2 部门归属
场景:不仅记录门店归属,还要记录部门归属
扩展方案:
- 可以增加字段:F_DepartmentId(部门ID)
- 或者创建新表:员工部门归属表
- 或者:在归属表中增加字段:F_DepartmentId(如果需要同时记录门店和部门)
6.3 岗位变更历史
场景:记录员工岗位变更历史
扩展方案:
- 可以创建新表:员工岗位归属表(类似设计)
- 或者:在归属表中增加字段:F_Position(岗位)
6.4 其他业务属性
场景:需要记录其他业务属性(如:职级、薪资等级等)
扩展方案:
- 可以在归属表中增加字段
- 或者创建关联表
设计原则:
- 优先考虑在归属表中增加字段(如果属性是时间相关的)
- 如果属性与门店归属无关,考虑创建新表
📝 总结
门店归属快照表方案是一个相对完善的解决方案,能够:
- 解决核心问题:以时间维度为唯一标准,准确记录历史门店归属
- 支持复杂场景:月中调店、跨月调店等
- 保证数据完整性:通过多数据源验证和数据校验
- 考虑性能:合理设计索引和查询优化
- 易于扩展:设计时考虑了未来的业务变化
关键成功因素:
- 数据迁移的准确性
- 业务规则的清晰性
- 用户操作的简便性
- 系统性能的稳定性
文档版本: v1.0
创建日期: 2026-01-09
文档性质: 详细设计方案(仅思考,不修改代码)