工资条确认功能完整方案.md
10.6 KB
工资条确认功能完整方案
需求确认
业务流程
- 系统自动计算工资 → 生成工资数据
- 导出Excel → 进行线下梳理处理
- 导入Excel → 覆盖现有数据(包含调整后的数据)
- 形成工资条 → 给员工查看
- 员工确认工资条 → 确认后工资数据不可再修改
关键逻辑
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. 员工确认接口
工作流程:
- 管理员锁定工资(IsLocked = 1)
- 员工查看工资条
- 员工确认工资条(只能确认已锁定的)
- 确认后发工资
逻辑:
[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个工资服务
LqSalaryService- 健康师LqTechTeacherSalaryService- 科技部老师LqAssistantSalaryService- 店助/店助主任LqStoreManagerSalaryService- 店长LqDirectorSalaryService- 主任LqMajorProjectTeacherSalaryService- 大项目部老师LqMajorProjectDirectorSalaryService- 大项目主管LqTechGeneralManagerSalaryService- 科技部总经理LqBusinessUnitManagerSalaryService- 事业部总经理/经理
对应的9个工资表
lq_salary_statisticslq_tech_teacher_salary_statisticslq_assistant_salary_statisticslq_store_manager_salary_statisticslq_director_salary_statisticslq_major_project_teacher_salary_statisticslq_major_project_director_salary_statisticslq_tech_general_manager_salary_statisticslq_business_unit_manager_salary_statistics
已确认的逻辑
导入时已确认的记录:
- ✅ 不能导入覆盖(无论是否锁定)
- ✅ 已锁定的记录也不能导入覆盖
- ✅ 只有未锁定且未确认的记录才能导入覆盖
导出Excel格式:
- ✅ 第一列必须是ID(主键)
- ✅ 后续列是业务字段(门店名称、员工姓名、岗位等)
- ✅ 包含确认状态字段(方便线下查看)
导入Excel格式:
- ✅ 第一列是ID(主键)
- ✅ 通过ID判断是更新还是新增
- ✅ 如果Excel有ID但数据库不存在 → 新增(使用Excel中的ID)
- ✅ 如果Excel无ID → 新增(自动生成新ID)
实施步骤
- ✅ 创建SQL脚本为所有9个工资表添加确认字段
- ✅ 修改9个工资实体类,添加确认字段属性
- ⏳ 修改9个服务的计算工资方法:已锁定或已确认的跳过
- ⏳ 修改9个服务的导入方法(如果存在):已锁定的跳过,未锁定的覆盖
- ⏳ 为所有9个服务类添加员工确认接口
工作流程
完整流程
- 系统自动计算工资 → 生成工资数据(IsLocked = 0, EmployeeConfirmStatus = 0)
- 导出Excel → 进行线下梳理处理
- 导入Excel → 覆盖现有数据(已锁定或已确认的记录不能覆盖)
- 管理员锁定工资 → 设置 IsLocked = 1(准备让员工确认)
- 员工查看工资条 → 查看已锁定的工资条
- 员工确认工资条 → 设置 EmployeeConfirmStatus = 1(只能确认已锁定的)
- 发工资 → 确认后才会去发工资
请确认
请确认以上方案是否符合需求,特别是:
- ✅ 计算工资:已锁定或已确认的跳过
- ✅ 导入:已锁定或已确认的跳过,不能覆盖
- ✅ 员工确认:只能确认已锁定的工资条(IsLocked = 1 且 EmployeeConfirmStatus = 0)
- ✅ 工作流程:管理员锁定 → 员工确认 → 发工资
确认后我将继续完成所有9个服务的代码修改。