员工门店归属快照表方案详细设计.md 30.4 KB

员工门店归属快照表方案详细设计

📋 方案概述

通过创建门店归属快照表,记录员工每个月的门店归属历史,以时间维度为唯一标准,解决工资计算和数据补录中的门店归属问题。


🗄️ 数据模型设计

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 自动维护机制

触发时机:员工调店时

操作步骤:

  1. 用户在前端操作:员工从门店A调到门店B(选择调店日期)
  2. 后端处理逻辑:
    • 查询当前员工的有效归属记录(结束日期为空的记录)
    • 如果存在,将结束日期设置为调店日期的前一天
    • 创建新的归属记录:开始日期为调店日期,结束日期为空
    • 如果调店日期是月初,更新统计月份;如果是月中,保持原月份,新增记录

2.2 手动维护机制

适用场景:

  • 历史数据补录
  • 数据修正
  • 批量导入

操作方式:

  • 提供管理界面:员工门店归属管理
  • 支持批量导入Excel
  • 支持手动添加/编辑/删除(软删除)

2.3 数据校验规则

插入/更新时的校验:

  1. 日期范围校验

    • 开始日期不能大于结束日期
    • 开始日期和结束日期必须在同一个月内(或跨月但统计月份相同)
  2. 重叠校验

    • 同一个员工,同一天不能有多条有效记录
    • 如果存在重叠,需要先处理旧记录(结束或删除)
  3. 完整性校验

    • 员工ID、门店ID必须存在
    • 统计月份格式正确(YYYYMM)
  4. 业务逻辑校验

    • 门店必须有效(未删除)
    • 员工必须有效(未删除、已启用)

🔄 与现有系统集成

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 数据来源分析

可能的数据来源:

  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:数据提取

-- 从工资统计表提取(健康师工资)
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<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 员工调店操作

操作流程:

  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 批量查询优化

场景:计算某月所有员工的工资

优化策略:

// 一次性查询所有员工的归属记录
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 数据缺失处理

场景:快照表中没有某员工某月的归属记录

处理方案:

  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
文档性质: 详细设计方案(仅思考,不修改代码)