工资条确认功能完整方案.md 10.6 KB

工资条确认功能完整方案

需求确认

业务流程

  1. 系统自动计算工资 → 生成工资数据
  2. 导出Excel → 进行线下梳理处理
  3. 导入Excel → 覆盖现有数据(包含调整后的数据)
  4. 形成工资条 → 给员工查看
  5. 员工确认工资条 → 确认后工资数据不可再修改

关键逻辑

1. 计算工资(CalculateSalary)

  • 已锁定(IsLocked = 1)的记录:跳过,不重新计算
  • 已确认(EmployeeConfirmStatus = 1)的记录:跳过,不重新计算
  • 未锁定且未确认的记录:可以重新计算并更新

2. 导入工资(Import)

  • Excel第一列是ID(主键):通过ID判断是更新还是新增
  • 导入逻辑
    • Excel有ID且数据库中存在该ID → 更新(覆盖)
    • Excel有ID但数据库中不存在 → 新增(使用Excel中的ID)
    • Excel无ID(空值) → 新增(自动生成新ID)
  • 保护机制
    • 已锁定(IsLocked = 1)的记录:跳过,不能导入覆盖
    • 已确认(EmployeeConfirmStatus = 1)的记录:跳过,不能导入覆盖(无论是否锁定)
    • 未锁定且未确认的记录:可以导入覆盖

3. 员工确认(Confirm)

  • ✅ 只能确认自己的工资条
  • 只能确认已锁定的工资条(IsLocked = 1 且 EmployeeConfirmStatus = 0)
  • 工作流程:管理员先锁定工资 → 员工确认 → 发工资
  • ✅ 确认后设置 EmployeeConfirmStatus = 1(IsLocked 保持为 1,因为本来就是管理员锁定的)
  • ✅ 确认后不能重复确认

数据库字段

为所有9个工资表添加以下字段:

F_EmployeeConfirmStatus INT NOT NULL DEFAULT 0 COMMENT '员工确认状态(0=未确认,1=已确认)',
F_EmployeeConfirmTime DATETIME NULL COMMENT '员工确认时间',
F_EmployeeConfirmRemark VARCHAR(500) NULL COMMENT '员工确认备注'

实现方案

1. 计算工资方法修改

逻辑

// 查询当月已存在的记录
var existingRecords = await _db.Queryable<SalaryEntity>()
    .Where(x => x.StatisticsMonth == monthStr)
    .ToListAsync();

// 遍历计算出的工资数据
foreach (var salary in calculatedSalaries)
{
    if (existingRecords.ContainsKey(salary.EmployeeId))
    {
        var existing = existingRecords[salary.EmployeeId];

        // 如果已锁定或已确认,跳过
        if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1)
        {
            skippedCount++;
            continue; // 跳过,不更新
        }

        // 更新现有记录(保留确认状态相关字段)
        salary.Id = existing.Id;
        salary.EmployeeConfirmStatus = existing.EmployeeConfirmStatus;
        salary.EmployeeConfirmTime = existing.EmployeeConfirmTime;
        salary.EmployeeConfirmRemark = existing.EmployeeConfirmRemark;
        salary.IsLocked = existing.IsLocked; // 保留锁定状态
        recordsToUpdate.Add(salary);
    }
    else
    {
        // 新记录,正常插入
        recordsToInsert.Add(salary);
    }
}

2. 导入方法修改

Excel结构

  • 第一列(A列):ID(主键,F_Id)
  • 第二列(B列)开始:业务字段

导入逻辑

// 使用ExcelImportHelper读取Excel文件(第一行为标题行)
var dataTable = ExcelImportHelper.ToDataTable(tempFilePath, 0, 0);

// 从第1行开始读取数据(跳过标题行)
for (int i = 1; i < dataTable.Rows.Count; i++)
{
    var row = dataTable.Rows[i];

    // 第一列是ID
    var id = row[0]?.ToString()?.Trim();
    // 第二列开始是业务字段
    var storeName = row[1]?.ToString()?.Trim();
    var employeeName = row[2]?.ToString()?.Trim();
    // ... 其他字段

    if (string.IsNullOrWhiteSpace(id))
    {
        // Excel中没有ID → 新增记录(自动生成ID)
        var newRecord = new SalaryEntity
        {
            Id = YitIdHelper.NextId().ToString(),
            StoreName = storeName,
            EmployeeName = employeeName,
            // ... 其他字段
            EmployeeConfirmStatus = 0,
            IsLocked = 0
        };
        recordsToInsert.Add(newRecord);
    }
    else
    {
        // Excel中有ID → 查找现有记录
        var existing = await _db.Queryable<SalaryEntity>()
            .Where(x => x.Id == id)
            .FirstAsync();

        if (existing != null)
        {
            // 记录存在 → 检查是否可以更新
            // 如果已锁定,跳过导入
            if (existing.IsLocked == 1)
            {
                skippedCount++;
                errorMessages.Add($"员工 {existing.EmployeeName} (ID: {id}) 的工资已锁定,不能导入覆盖");
                continue;
            }

            // 如果已确认,跳过导入
            if (existing.EmployeeConfirmStatus == 1)
            {
                skippedCount++;
                errorMessages.Add($"员工 {existing.EmployeeName} (ID: {id}) 的工资已确认,不能导入覆盖");
                continue;
            }

            // 可以更新 → 覆盖现有记录
            existing.StoreName = storeName;
            existing.EmployeeName = employeeName;
            // ... 更新所有字段
            existing.EmployeeConfirmStatus = 0; // 导入后重置确认状态
            existing.EmployeeConfirmTime = null;
            existing.EmployeeConfirmRemark = null;
            recordsToUpdate.Add(existing);
        }
        else
        {
            // Excel中有ID,但数据库中不存在 → 新增记录(使用Excel中的ID)
            var newRecord = new SalaryEntity
            {
                Id = id,
                StoreName = storeName,
                EmployeeName = employeeName,
                // ... 其他字段
                EmployeeConfirmStatus = 0,
                IsLocked = 0
            };
            recordsToInsert.Add(newRecord);
        }
    }
}

导出功能修改

  • 确保导出时,第一列是ID(主键)
  • 字段顺序:ID、门店名称、员工姓名、岗位、... 其他业务字段

3. 员工确认接口

工作流程

  1. 管理员锁定工资(IsLocked = 1)
  2. 员工查看工资条
  3. 员工确认工资条(只能确认已锁定的)
  4. 确认后发工资

逻辑

[HttpPost("confirm")]
public async Task<string> ConfirmSalary(SalaryConfirmInput input)
{
    // 1. 验证参数
    if (string.IsNullOrWhiteSpace(input.Id))
        throw NCCException.Oh("工资记录ID不能为空");
    if (string.IsNullOrWhiteSpace(input.EmployeeId))
        throw NCCException.Oh("员工ID不能为空");

    // 2. 查询工资记录
    var salary = await _db.Queryable<SalaryEntity>()
        .Where(s => s.Id == input.Id && s.EmployeeId == input.EmployeeId)
        .FirstAsync();

    // 3. 验证记录是否存在
    if (salary == null)
        throw NCCException.Oh("工资记录不存在或不属于该员工");

    // 4. 验证是否已确认
    if (salary.EmployeeConfirmStatus == 1)
        throw NCCException.Oh("该工资条已确认,不能重复确认");

    // 5. 验证是否已锁定(员工只能确认已锁定的工资条)
    if (salary.IsLocked != 1)
        throw NCCException.Oh("该工资条尚未锁定,请等待管理员锁定后再确认");

    // 6. 更新确认状态
    salary.EmployeeConfirmStatus = 1;
    salary.EmployeeConfirmTime = DateTime.Now;
    salary.EmployeeConfirmRemark = input.Remark;
    // 注意:IsLocked 保持为 1(因为本来就是管理员锁定的)

    await _db.Updateable(salary).ExecuteCommandAsync();

    return "确认成功";
}

涉及的服务和表

9个工资服务

  1. LqSalaryService - 健康师
  2. LqTechTeacherSalaryService - 科技部老师
  3. LqAssistantSalaryService - 店助/店助主任
  4. LqStoreManagerSalaryService - 店长
  5. LqDirectorSalaryService - 主任
  6. LqMajorProjectTeacherSalaryService - 大项目部老师
  7. LqMajorProjectDirectorSalaryService - 大项目主管
  8. LqTechGeneralManagerSalaryService - 科技部总经理
  9. LqBusinessUnitManagerSalaryService - 事业部总经理/经理

对应的9个工资表

  1. lq_salary_statistics
  2. lq_tech_teacher_salary_statistics
  3. lq_assistant_salary_statistics
  4. lq_store_manager_salary_statistics
  5. lq_director_salary_statistics
  6. lq_major_project_teacher_salary_statistics
  7. lq_major_project_director_salary_statistics
  8. lq_tech_general_manager_salary_statistics
  9. lq_business_unit_manager_salary_statistics

已确认的逻辑

  1. 导入时已确认的记录

    • 不能导入覆盖(无论是否锁定)
    • 已锁定的记录也不能导入覆盖
    • 只有未锁定且未确认的记录才能导入覆盖
  2. 导出Excel格式

    • ✅ 第一列必须是ID(主键)
    • ✅ 后续列是业务字段(门店名称、员工姓名、岗位等)
    • ✅ 包含确认状态字段(方便线下查看)
  3. 导入Excel格式

    • ✅ 第一列是ID(主键)
    • ✅ 通过ID判断是更新还是新增
    • ✅ 如果Excel有ID但数据库不存在 → 新增(使用Excel中的ID)
    • ✅ 如果Excel无ID → 新增(自动生成新ID)

实施步骤

  1. ✅ 创建SQL脚本为所有9个工资表添加确认字段
  2. ✅ 修改9个工资实体类,添加确认字段属性
  3. ⏳ 修改9个服务的计算工资方法:已锁定或已确认的跳过
  4. ⏳ 修改9个服务的导入方法(如果存在):已锁定的跳过,未锁定的覆盖
  5. ⏳ 为所有9个服务类添加员工确认接口

工作流程

完整流程

  1. 系统自动计算工资 → 生成工资数据(IsLocked = 0, EmployeeConfirmStatus = 0)
  2. 导出Excel → 进行线下梳理处理
  3. 导入Excel → 覆盖现有数据(已锁定或已确认的记录不能覆盖)
  4. 管理员锁定工资 → 设置 IsLocked = 1(准备让员工确认)
  5. 员工查看工资条 → 查看已锁定的工资条
  6. 员工确认工资条 → 设置 EmployeeConfirmStatus = 1(只能确认已锁定的)
  7. 发工资 → 确认后才会去发工资

请确认

请确认以上方案是否符合需求,特别是:

  • ✅ 计算工资:已锁定或已确认的跳过
  • ✅ 导入:已锁定或已确认的跳过,不能覆盖
  • ✅ 员工确认:只能确认已锁定的工资条(IsLocked = 1 且 EmployeeConfirmStatus = 0)
  • ✅ 工作流程:管理员锁定 → 员工确认 → 发工资

确认后我将继续完成所有9个服务的代码修改。