# 工资条确认功能完整方案 ## 需求确认 ### 业务流程 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个工资表添加以下字段: ```sql F_EmployeeConfirmStatus INT NOT NULL DEFAULT 0 COMMENT '员工确认状态(0=未确认,1=已确认)', F_EmployeeConfirmTime DATETIME NULL COMMENT '员工确认时间', F_EmployeeConfirmRemark VARCHAR(500) NULL COMMENT '员工确认备注' ``` --- ## 实现方案 ### 1. 计算工资方法修改 **逻辑**: ```csharp // 查询当月已存在的记录 var existingRecords = await _db.Queryable() .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列)开始:业务字段 **导入逻辑**: ```csharp // 使用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() .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. 确认后发工资 **逻辑**: ```csharp [HttpPost("confirm")] public async Task 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() .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个服务的代码修改。