using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqSalary; using NCC.Extend.Entitys.Dto.LqTechTeacherSalary; using NCC.Extend.Entitys.lq_hytk_kjbsyj; using NCC.Extend.Entitys.lq_kd_kjbsyj; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_md_xdbhsj; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_tech_teacher_salary_statistics; using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_xh_kjbsyj; using NCC.Extend.Entitys.lq_person_times_record; using NCC.FriendlyException; using NCC.System.Entitys.Permission; using SqlSugar; using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Threading.Tasks; using Yitter.IdGenerator; namespace NCC.Extend { /// /// 科技老师薪酬服务 /// [ApiDescriptionSettings(Tag = "科技老师薪酬服务", Name = "LqTechTeacherSalary", Order = 302)] [Route("api/Extend/[controller]")] public class LqTechTeacherSalaryService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly ILogger _logger; /// /// 初始化一个类型的新实例 /// public LqTechTeacherSalaryService(ISqlSugarClient db, ILogger logger) { _db = db; _logger = logger; } /// /// 获取科技老师工资列表 /// /// 查询参数 /// 科技老师工资分页列表 [HttpGet("tech-teacher")] public async Task GetTechTeacherSalaryList([FromQuery] TechTeacherSalaryInput input) { var monthStr = $"{input.Year}{input.Month:D2}"; // 查询数据 var query = _db.Queryable() .Where(x => x.StatisticsMonth == monthStr); if (!string.IsNullOrEmpty(input.StoreId)) { query = query.Where(x => x.StoreId == input.StoreId); } if (!string.IsNullOrEmpty(input.Keyword)) { query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword)); } var list = await query.Select(x => new TechTeacherSalaryOutput { Id = x.Id, StatisticsMonth = x.StatisticsMonth, StoreId = x.StoreId, StoreName = x.StoreName, Position = x.Position, EmployeeName = x.EmployeeName, EmployeeId = x.EmployeeId, EmployeeAccount = x.EmployeeAccount, OrderAchievement = x.OrderAchievement, ConsumeAchievement = x.ConsumeAchievement, RefundAchievement = x.RefundAchievement, TotalPerformance = x.TotalPerformance, ProjectCount = x.ProjectCount, BaseSalaryLevel = x.BaseSalaryLevel, BaseSalary = x.BaseSalary, PerformanceCommissionRate = x.PerformanceCommissionRate, PerformanceCommissionAmount = x.PerformanceCommissionAmount, ConsumeCommissionRate = x.ConsumeCommissionRate, ConsumeCommissionAmount = x.ConsumeCommissionAmount, HandworkFee = x.HandworkFee, WorkingDays = x.WorkingDays, LeaveDays = x.LeaveDays, TotalCommission = x.TotalCommission, TransportationAllowance = x.TransportationAllowance, LessRest = x.LessRest, FullAttendance = x.FullAttendance, CalculatedGrossSalary = x.CalculatedGrossSalary, GuaranteedSalary = x.GuaranteedSalary, GuaranteedLeaveDeduction = x.GuaranteedLeaveDeduction, GuaranteedBaseSalary = x.GuaranteedBaseSalary, GuaranteedSupplement = x.GuaranteedSupplement, FinalGrossSalary = x.FinalGrossSalary, MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy, MonthlyTransportSubsidy = x.MonthlyTransportSubsidy, LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy, LastMonthTransportSubsidy = x.LastMonthTransportSubsidy, TotalSubsidy = x.TotalSubsidy, MissingCard = x.MissingCard, LateArrival = x.LateArrival, LeaveDeduction = x.LeaveDeduction, SocialInsuranceDeduction = x.SocialInsuranceDeduction, RewardDeduction = x.RewardDeduction, AccommodationDeduction = x.AccommodationDeduction, StudyPeriodDeduction = x.StudyPeriodDeduction, WorkClothesDeduction = x.WorkClothesDeduction, TotalDeduction = x.TotalDeduction, Bonus = x.Bonus, ReturnPhoneDeposit = x.ReturnPhoneDeposit, ReturnAccommodationDeposit = x.ReturnAccommodationDeposit, ActualSalary = x.ActualSalary, MonthlyPaymentStatus = x.MonthlyPaymentStatus, PaidAmount = x.PaidAmount, PendingAmount = x.PendingAmount, LastMonthSupplement = x.LastMonthSupplement, MonthlyTotalPayment = x.MonthlyTotalPayment, IsLocked = x.IsLocked, EmployeeConfirmStatus = x.EmployeeConfirmStatus, IsTerminated = x.IsTerminated, UpdateTime = x.UpdateTime, StoreType = x.StoreType, StoreCategory = x.StoreCategory, IsNewStore = x.IsNewStore, NewStoreProtectionStage = x.NewStoreProtectionStage }) .ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(list); } /// /// 通过月份和员工ID查询工资(仅查询已锁定且未确认的工资) /// /// /// **重要**:此接口只能查询已锁定(IsLocked=1)且未确认(EmployeeConfirmStatus=0或null)的工资记录 /// **注意**:已确认的工资记录无法通过此接口查询 /// [HttpGet("query-by-employee")] public async Task GetSalaryByEmployee([FromQuery] SalaryQueryByEmployeeInput input) { if (input.Year <= 0 || input.Month <= 0 || input.Month > 12) throw NCCException.Oh("年份和月份参数不正确"); if (string.IsNullOrWhiteSpace(input.EmployeeId)) throw NCCException.Oh("员工ID不能为空"); var monthStr = $"{input.Year}{input.Month:D2}"; var salary = await _db.Queryable() .Where(x => x.StatisticsMonth == monthStr && x.EmployeeId == input.EmployeeId && x.IsLocked == 1 && x.EmployeeConfirmStatus != 1) .Select(x => new TechTeacherSalaryOutput { Id = x.Id, StatisticsMonth = x.StatisticsMonth, StoreId = x.StoreId, StoreName = x.StoreName, Position = x.Position, EmployeeName = x.EmployeeName, EmployeeId = x.EmployeeId, EmployeeAccount = x.EmployeeAccount, OrderAchievement = x.OrderAchievement, ConsumeAchievement = x.ConsumeAchievement, RefundAchievement = x.RefundAchievement, TotalPerformance = x.TotalPerformance, ProjectCount = x.ProjectCount, BaseSalaryLevel = x.BaseSalaryLevel, BaseSalary = x.BaseSalary, PerformanceCommissionRate = x.PerformanceCommissionRate, PerformanceCommissionAmount = x.PerformanceCommissionAmount, ConsumeCommissionRate = x.ConsumeCommissionRate, ConsumeCommissionAmount = x.ConsumeCommissionAmount, HandworkFee = x.HandworkFee, WorkingDays = x.WorkingDays, LeaveDays = x.LeaveDays, TotalCommission = x.TotalCommission, TransportationAllowance = x.TransportationAllowance, LessRest = x.LessRest, FullAttendance = x.FullAttendance, CalculatedGrossSalary = x.CalculatedGrossSalary, GuaranteedSalary = x.GuaranteedSalary, GuaranteedLeaveDeduction = x.GuaranteedLeaveDeduction, GuaranteedBaseSalary = x.GuaranteedBaseSalary, GuaranteedSupplement = x.GuaranteedSupplement, FinalGrossSalary = x.FinalGrossSalary, MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy, MonthlyTransportSubsidy = x.MonthlyTransportSubsidy, LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy, LastMonthTransportSubsidy = x.LastMonthTransportSubsidy, TotalSubsidy = x.TotalSubsidy, MissingCard = x.MissingCard, LateArrival = x.LateArrival, LeaveDeduction = x.LeaveDeduction, SocialInsuranceDeduction = x.SocialInsuranceDeduction, RewardDeduction = x.RewardDeduction, AccommodationDeduction = x.AccommodationDeduction, StudyPeriodDeduction = x.StudyPeriodDeduction, WorkClothesDeduction = x.WorkClothesDeduction, TotalDeduction = x.TotalDeduction, Bonus = x.Bonus, ReturnPhoneDeposit = x.ReturnPhoneDeposit, ReturnAccommodationDeposit = x.ReturnAccommodationDeposit, ActualSalary = x.ActualSalary, MonthlyPaymentStatus = x.MonthlyPaymentStatus, PaidAmount = x.PaidAmount, PendingAmount = x.PendingAmount, LastMonthSupplement = x.LastMonthSupplement, MonthlyTotalPayment = x.MonthlyTotalPayment, IsLocked = x.IsLocked, EmployeeConfirmStatus = x.EmployeeConfirmStatus, IsTerminated = x.IsTerminated, UpdateTime = x.UpdateTime, StoreType = x.StoreType, StoreCategory = x.StoreCategory, IsNewStore = x.IsNewStore, NewStoreProtectionStage = x.NewStoreProtectionStage }) .FirstAsync(); if (salary == null) throw NCCException.Oh($"未找到员工{input.EmployeeId}在{input.Year}年{input.Month}月的工资记录"); return salary; } /// /// 计算科技老师工资 /// /// 年份 /// 月份 /// [HttpPost("calculate/tech-teacher")] public async Task CalculateTechTeacherSalary(int year, int month) { var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var monthStr = $"{year}{month:D2}"; // 1. 获取基础数据 // 1.1 获取科技老师员工列表(从BASE_USER表,岗位为"科技老师") var techTeacherUserList = await _db.Queryable() .Where(x => x.Gw == "科技老师" && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => new { x.Id, x.RealName, x.Account, x.Mdid, x.IsOnJob }) .ToListAsync(); if (!techTeacherUserList.Any()) { // 如果没有科技老师员工,直接返回 return; } // 1.2 门店信息 (lq_mdxx) var storeList = await _db.Queryable().ToListAsync(); var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x); // 1.3 门店新店保护信息 (lq_md_xdbhsj) var newStoreProtectionList = await _db.Queryable() .Where(x => x.Sfqy == 1) .ToListAsync(); var newStoreProtectionDict = newStoreProtectionList .Where(x => x.Bhkssj <= startDate && x.Bhjssj >= startDate) .GroupBy(x => x.Mdid) .ToDictionary(g => g.Key, g => g.First()); // 1.4 开单业绩数据 (lq_kd_kjbsyj) var orderPerformanceList = await _db.Queryable() .Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1) .ToListAsync(); // 1.5 消耗业绩和项目数数据 (lq_xh_kjbsyj,关联lq_xh_hyhk获取时间) var consumePerformanceList = await _db.Queryable( (kjbsyj, hyhk) => kjbsyj.Glkdbh == hyhk.Id && hyhk.IsEffective == 1) .Where((kjbsyj, hyhk) => kjbsyj.IsEffective == 1 && hyhk.Hksj >= startDate && hyhk.Hksj <= endDate.AddDays(1)) .Select((kjbsyj, hyhk) => new { kjbsyj.Kjblszh, kjbsyj.Kjblsyj, kjbsyj.HdpxNumber, kjbsyj.LaborCost, hyhk.Md }) .ToListAsync(); // 1.6 退卡业绩数据 (lq_hytk_kjbsyj) var refundPerformanceList = await _db.Queryable() .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) .ToListAsync(); // 1.7 考勤数据 (lq_attendance_summary) var attendanceList = await _db.Queryable() .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1) .ToListAsync(); var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x); // 2. 按科技老师聚合数据 var techTeacherStats = new Dictionary(); foreach (var techTeacherUser in techTeacherUserList) { var teacherId = techTeacherUser.Id; var isTerminated = techTeacherUser.IsOnJob == 0; // 2.1 创建工资统计对象 var salary = new LqTechTeacherSalaryStatisticsEntity { Id = YitIdHelper.NextId().ToString(), StatisticsMonth = monthStr, EmployeeId = teacherId, EmployeeName = techTeacherUser.RealName, EmployeeAccount = techTeacherUser.Account ?? "", Position = "科技部老师", IsTerminated = isTerminated ? 1 : 0, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsLocked = 0 }; // 2.2 填充门店信息 var storeId = techTeacherUser.Mdid; if (!string.IsNullOrEmpty(storeId) && storeDict.ContainsKey(storeId)) { var store = storeDict[storeId]; salary.StoreId = storeId; salary.StoreName = store.Dm ?? ""; salary.StoreType = store.StoreType; salary.StoreCategory = store.StoreCategory; } else { // 如果用户没有门店,尝试从业绩数据中获取 var firstOrderStore = orderPerformanceList.FirstOrDefault(x => x.Kjbls == teacherId || x.Kjblszh == teacherId); var firstConsumeStore = consumePerformanceList.FirstOrDefault(x => x.Kjblszh == teacherId); var firstRefundStore = refundPerformanceList.FirstOrDefault(x => x.Kjbls == teacherId || x.Kjblszh == teacherId); if (firstOrderStore != null && !string.IsNullOrEmpty(firstOrderStore.StoreId)) { storeId = firstOrderStore.StoreId; } else if (firstConsumeStore != null && !string.IsNullOrEmpty(firstConsumeStore.Md)) { storeId = firstConsumeStore.Md; } else if (firstRefundStore != null && !string.IsNullOrEmpty(firstRefundStore.StoreId)) { storeId = firstRefundStore.StoreId; } if (!string.IsNullOrEmpty(storeId) && storeDict.ContainsKey(storeId)) { var store = storeDict[storeId]; salary.StoreId = storeId; salary.StoreName = store.Dm ?? ""; salary.StoreType = store.StoreType; salary.StoreCategory = store.StoreCategory; } } // 2.3 新店保护信息 if (!string.IsNullOrEmpty(salary.StoreId) && newStoreProtectionDict.ContainsKey(salary.StoreId)) { var protection = newStoreProtectionDict[salary.StoreId]; salary.IsNewStore = "是"; salary.NewStoreProtectionStage = protection.Stage; } else { salary.IsNewStore = "否"; salary.NewStoreProtectionStage = 0; } // 2.4 统计业绩数据 // 开单业绩(注意:kjblsyj字段是string类型,需要转换) var orderPerformance = orderPerformanceList .Where(x => (x.Kjbls == teacherId || x.Kjblszh == teacherId) && !string.IsNullOrEmpty(x.Kjblsyj)) .Sum(x => decimal.TryParse(x.Kjblsyj, out var val) ? val : 0m); salary.OrderAchievement = orderPerformance; // 消耗业绩和项目数 var consumeData = consumePerformanceList .Where(x => x.Kjblszh == teacherId) .ToList(); salary.ConsumeAchievement = consumeData.Sum(x => x.Kjblsyj ?? 0m); salary.ProjectCount = consumeData.Sum(x => x.HdpxNumber ?? 0m); salary.HandworkFee = consumeData.Sum(x => x.LaborCost ?? 0m); // 退卡业绩 var refundPerformance = refundPerformanceList .Where(x => (x.Kjbls == teacherId || x.Kjblszh == teacherId)) .Sum(x => x.Kjblsyj ?? 0m); salary.RefundAchievement = refundPerformance; // 总业绩 = 开单业绩 - 退卡业绩(退卡是扣除) salary.TotalPerformance = salary.OrderAchievement - salary.RefundAchievement; // 2.5 考勤数据 var attendance = attendanceDict.ContainsKey(teacherId) ? attendanceDict[teacherId] : null; salary.WorkingDays = attendance?.WorkDays ?? 0; salary.LeaveDays = attendance?.LeaveDays ?? 0; // 3. 工资计算 if (isTerminated) { // 离职员工特殊处理 if (salary.TotalPerformance > 30000m) { // 总业绩 > 30,000元:只计算2%提成 salary.BaseSalary = 0; salary.BaseSalaryLevel = 0; salary.PerformanceCommissionRate = 0.02m; // 保存为小数形式,0.02表示2% salary.PerformanceCommissionAmount = salary.TotalPerformance * 0.02m; salary.ConsumeCommissionRate = 0; salary.ConsumeCommissionAmount = 0; salary.TotalCommission = salary.PerformanceCommissionAmount; } else { // 总业绩 ≤ 30,000元:无任何工资 salary.BaseSalary = 0; salary.BaseSalaryLevel = 0; salary.PerformanceCommissionRate = 0; salary.PerformanceCommissionAmount = 0; salary.ConsumeCommissionRate = 0; salary.ConsumeCommissionAmount = 0; salary.TotalCommission = 0; } } else { // 在职员工正常计算 // 判断是否为T区员工(员工姓名包含"T区") bool isTZoneEmployee = !string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区"); if (isTZoneEmployee) { // T区员工:按照开单减去退款之后的业绩统一按照2%提成 // 总业绩 = 开单业绩 - 退卡业绩(已在前面计算) salary.PerformanceCommissionRate = 0.02m; // 保存为小数形式,0.02表示2% salary.PerformanceCommissionAmount = salary.TotalPerformance * 0.02m; salary.ConsumeCommissionRate = 0; salary.ConsumeCommissionAmount = 0; salary.TotalCommission = salary.PerformanceCommissionAmount; // T区员工也计算底薪(根据项目数和总业绩) var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance); salary.BaseSalary = baseSalaryResult.BaseSalary; salary.BaseSalaryLevel = baseSalaryResult.Level; } else { // 非T区员工正常计算 // 3.1 计算底薪(根据项目数和总业绩) var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance); salary.BaseSalary = baseSalaryResult.BaseSalary; salary.BaseSalaryLevel = baseSalaryResult.Level; // 3.2 计算业绩提成(分段累进,门槛改为3万) var performanceCommissionResult = CalculatePerformanceCommission(salary.TotalPerformance); salary.PerformanceCommissionRate = performanceCommissionResult.Rate; salary.PerformanceCommissionAmount = performanceCommissionResult.Amount; // 3.3 计算消耗提成(新规则:10万门槛,阶梯式) var consumeCommissionResult = CalculateConsumeCommission(salary.ConsumeAchievement); salary.ConsumeCommissionRate = consumeCommissionResult.Rate; salary.ConsumeCommissionAmount = consumeCommissionResult.Amount; // 3.4 提成合计 salary.TotalCommission = salary.PerformanceCommissionAmount + salary.ConsumeCommissionAmount; } } // 3.5 初始化其他字段(默认值为0) salary.TransportationAllowance = 0; salary.LessRest = 0; salary.FullAttendance = 0; salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission + salary.HandworkFee; salary.GuaranteedSalary = 0; salary.GuaranteedLeaveDeduction = 0; salary.GuaranteedBaseSalary = 0; salary.GuaranteedSupplement = 0; salary.FinalGrossSalary = salary.CalculatedGrossSalary; salary.MonthlyTrainingSubsidy = 0; salary.MonthlyTransportSubsidy = 0; salary.LastMonthTrainingSubsidy = 0; salary.LastMonthTransportSubsidy = 0; salary.TotalSubsidy = 0; salary.MissingCard = 0; salary.LateArrival = 0; salary.LeaveDeduction = 0; salary.SocialInsuranceDeduction = 0; salary.RewardDeduction = 0; salary.AccommodationDeduction = 0; salary.StudyPeriodDeduction = 0; salary.WorkClothesDeduction = 0; salary.TotalDeduction = 0; salary.Bonus = 0; salary.ReturnPhoneDeposit = 0; salary.ReturnAccommodationDeposit = 0; salary.ActualSalary = salary.FinalGrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus; salary.MonthlyPaymentStatus = ""; salary.PaidAmount = 0; salary.PendingAmount = salary.ActualSalary; salary.LastMonthSupplement = 0; salary.MonthlyTotalPayment = 0; techTeacherStats[teacherId] = salary; } // 4. 保存数据 if (techTeacherStats.Any()) { // 4.1 先删除计算月的未锁定且未确认的工资记录 var deletedCount = await _db.Deleteable() .Where(x => x.StatisticsMonth == monthStr && x.IsLocked == 0 && x.EmployeeConfirmStatus == 0) .ExecuteCommandAsync(); if (deletedCount > 0) { _logger.LogInformation($"计算工资前删除了 {deletedCount} 条未锁定且未确认的记录(月份:{monthStr})"); } // 4.2 查询已存在的记录(只查询已锁定或已确认的记录) var existingRecords = await _db.Queryable() .Where(x => x.StatisticsMonth == monthStr && (x.IsLocked == 1 || x.EmployeeConfirmStatus == 1)) .ToListAsync(); var existingDict = existingRecords .Where(x => !string.IsNullOrEmpty(x.EmployeeId)) .GroupBy(x => x.EmployeeId) .ToDictionary(g => g.Key, g => g.First()); // 分离需要插入的新记录和需要更新的记录 var recordsToInsert = new List(); var recordsToUpdate = new List(); var updatedCount = 0; var skippedCount = 0; foreach (var salary in techTeacherStats.Values) { if (existingDict.ContainsKey(salary.EmployeeId)) { // 检查记录是否已锁定或已确认 var existing = existingDict[salary.EmployeeId]; // 如果已锁定或已确认,跳过不更新(保留所有原有数据,包括扣款项目) if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1) { skippedCount++; continue; // 跳过,不进行任何更新 } // 未锁定且未确认的记录,可以做更新操作 salary.Id = existing.Id; salary.EmployeeConfirmStatus = existing.EmployeeConfirmStatus; // 应该是0 salary.EmployeeConfirmTime = existing.EmployeeConfirmTime; salary.EmployeeConfirmRemark = existing.EmployeeConfirmRemark; salary.IsLocked = existing.IsLocked; // 保留锁定状态(应该是0) salary.CreateTime = existing.CreateTime; salary.CreateUser = existing.CreateUser; recordsToUpdate.Add(salary); updatedCount++; } else { // 不存在的记录,做插入操作 salary.Id = YitIdHelper.NextId().ToString(); salary.EmployeeConfirmStatus = 0; salary.IsLocked = 0; salary.CreateTime = DateTime.Now; salary.CreateUser = ""; // 新记录,创建人为空或系统 recordsToInsert.Add(salary); } } // 批量插入新记录 if (recordsToInsert.Any()) { await _db.Insertable(recordsToInsert).ExecuteCommandAsync(); _logger.LogInformation($"插入了 {recordsToInsert.Count} 条新的工资记录(月份:{monthStr})"); } // 批量更新现有记录 if (recordsToUpdate.Any()) { await _db.Updateable(recordsToUpdate).ExecuteCommandAsync(); _logger.LogInformation($"更新了 {recordsToUpdate.Count} 条未锁定且未确认的工资记录(月份:{monthStr})"); } if (skippedCount > 0) { _logger.LogInformation($"跳过了 {skippedCount} 条已锁定或已确认的工资记录,保留原有数据(月份:{monthStr})"); } } } /// /// 计算底薪(根据项目数和总业绩) /// /// 项目数 /// 总业绩 /// 底薪金额和档位 private (decimal BaseSalary, int Level) CalculateBaseSalary(decimal projectCount, decimal totalPerformance) { // 从高到低判断档位,同时满足项目数和总业绩两个条件 // 第三档:≥ 110个 且 ≥ 100,000元 → 3,500元 if (projectCount >= 110m && totalPerformance >= 100000m) { return (3500m, 3); } // 第二档:≥ 95个 且 ≥ 80,000元 → 3,000元 else if (projectCount >= 95m && totalPerformance >= 80000m) { return (3000m, 2); } // 第一档:≥ 80个 且 ≥ 40,000元 → 2,500元 else if (projectCount >= 80m && totalPerformance >= 40000m) { return (2500m, 1); } // 都不满足:默认第一档 2,500元 else { return (2500m, 1); } } /// /// 计算业绩提成(分段累进式) /// /// 总业绩 /// 提成比例和金额 /// /// 提成规则(分段累进式): /// 1. 前提条件:整月业绩必须大于等于3万才能进行提成(门槛从1万提高到3万) /// 2. 如果有提成资格后,分段计算: /// - 0-7万部分:2%(整个0-7万部分都按2%计算) /// - 7万-15万部分:2.5% /// - 15万以上部分:3% /// /// 计算公式(分段累进): /// - 如果业绩 < 3万:提成 = 0(无提成资格) /// - 如果 3万 ≤ 业绩 ≤ 7万:提成 = 业绩 × 2% /// - 如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5% /// - 如果业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3% /// /// 示例: /// - 总业绩 = 25,000元 → 提成 = 0(无提成资格,未达到3万门槛) /// - 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元 /// - 总业绩 = 100,000元 → 提成 = 70,000 × 2% + (100,000 - 70,000) × 2.5% = 1,400 + 750 = 2,150元 /// - 总业绩 = 200,000元 → 提成 = 70,000 × 2% + (150,000 - 70,000) × 2.5% + (200,000 - 150,000) × 3% = 1,400 + 2,000 + 1,500 = 4,900元 /// private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance) { // 提成前提:整月业绩必须大于等于3万才能进行提成 if (totalPerformance < 30000m) { // < 30,000元 → 0%(无提成资格) return (0m, 0m); } decimal totalCommission = 0m; // 分段累进式提成计算(已通过提成资格检查) if (totalPerformance > 150000m) { // 业绩 > 15万:分段计算 // 0-7万部分:2% decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元 // 7万-15万部分:2.5% decimal part2 = (150000m - 70000m) * 0.025m; // 8万 × 2.5% = 2,000元 // 15万以上部分:3% decimal part3 = (totalPerformance - 150000m) * 0.03m; totalCommission = part1 + part2 + part3; } else if (totalPerformance > 70000m) { // 业绩 > 7万 且 ≤ 15万:分段计算 // 0-7万部分:2% decimal part1 = 70000m * 0.02m; // 7万 × 2% = 1,400元 // 7万以上部分:2.5% decimal part2 = (totalPerformance - 70000m) * 0.025m; totalCommission = part1 + part2; } else { // 业绩 ≥ 3万 且 ≤ 7万:整个业绩按2%计算 totalCommission = totalPerformance * 0.02m; } // 计算平均提成比例(保存为小数形式,如0.02表示2%,前端会乘以100显示) decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) : 0m; return (averageRate, totalCommission); } /// /// 计算消耗提成(阶梯式,可能为负数) /// /// 消耗业绩 /// 提成比例和金额(金额可能为负数,比例用于显示) /// /// 消耗提成规则: /// 1. 未完成10万底标:负激励300元(扣除300元) /// 2. 达到10万条件后,按照阶梯式提成: /// - 1-20万部分:0.5% /// - 超过20万部分:1% /// /// 计算公式(阶梯式): /// - 如果消耗业绩 < 10万:提成 = -300元(扣除300元) /// - 如果消耗业绩 ≥ 10万 且 ≤ 20万:提成 = 消耗业绩 × 0.5% /// - 如果消耗业绩 > 20万:提成 = 20万 × 0.5% + (消耗业绩 - 20万) × 1% /// /// 示例: /// - 消耗业绩 = 50,000元 → 提成 = -300元(未完成10万底标) /// - 消耗业绩 = 100,000元 → 提成 = 100,000 × 0.5% = 500元 /// - 消耗业绩 = 150,000元 → 提成 = 150,000 × 0.5% = 750元 /// - 消耗业绩 = 250,000元 → 提成 = 200,000 × 0.5% + (250,000 - 200,000) × 1% = 1,000 + 500 = 1,500元 /// private (decimal Rate, decimal Amount) CalculateConsumeCommission(decimal consumeAchievement) { if (consumeAchievement < 100000m) { // < 100,000元(未完成10万底标)→ 扣除300元(负数) // 比例显示为0,金额为-300 return (0m, -300m); } else if (consumeAchievement <= 200000m) { // ≥ 100,000元 且 ≤ 200,000元 → 1-20万部分按0.5% return (0.005m, consumeAchievement * 0.005m); // 保存为小数形式,0.005表示0.5% } else { // > 200,000元 → 阶梯式:1-20万部分0.5%,超过20万部分1% decimal part1 = 200000m * 0.005m; // 20万 × 0.5% = 1,000元 decimal part2 = (consumeAchievement - 200000m) * 0.01m; // 超过20万部分 × 1% decimal totalCommission = part1 + part2; // 计算平均比例(保存为小数形式,前端会乘以100显示) decimal averageRate = totalCommission > 0 && consumeAchievement > 0 ? (totalCommission / consumeAchievement) : 0m; return (averageRate, totalCommission); } } /// /// 统计科技部老师某个月的数据 /// /// /// 实时统计科技部老师某个月的数据,包括: /// - 开单业绩:从 lq_kd_kjbsyj 表统计 /// - 消耗业绩:从 lq_xh_kjbsyj 表统计(关联 lq_xh_hyhk 获取时间) /// - 退卡业绩:从 lq_hytk_kjbsyj 表统计 /// - 手工费:从耗卡表(lq_xh_kjbsyj)的 F_LaborCost 字段统计 /// - 人头:从 lq_person_times_record 表统计(按月份+客户去重) /// - 人次:从 lq_person_times_record 表统计(按日期+客户去重) /// /// 示例请求: /// ```json /// { /// "year": 2025, /// "month": 11 /// } /// ``` /// /// 查询参数(年份、月份) /// 科技部老师统计数据列表 [HttpGet("statistics")] public async Task> GetTechTeacherStatistics([FromQuery] TechTeacherStatisticsInput input) { // 1. 参数验证 if (input.Year < 2020 || input.Year > 2100) { throw new Exception("年份范围不正确"); } if (input.Month < 1 || input.Month > 12) { throw new Exception("月份范围不正确(1-12)"); } // 2. 计算时间范围 var startDate = new DateTime(input.Year, input.Month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var monthStr = $"{input.Year}{input.Month:D2}"; // 3. 获取科技部组织列表(科技一部/科技二部) var techOrganizeList = await _db.Queryable() .Where(x => x.FullName != null && (x.FullName.Contains("科技一部") || x.FullName.Contains("科技二部")) && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => new { x.Id, x.FullName }) .ToListAsync(); var techOrganizeIds = techOrganizeList?.Select(x => x.Id).ToList() ?? new List(); var techOrganizeDict = techOrganizeList?.ToDictionary(x => x.Id, x => x.FullName) ?? new Dictionary(); // 4. 获取所有科技部老师(岗位为"科技老师"),并关联部门信息 // 先筛选科技部组织,只查询属于科技一部或科技二部的老师 var techTeacherQuery = _db.Queryable() .LeftJoin((user, org) => user.OrganizeId == org.Id) .Where((user, org) => user.Gw == "科技老师" && user.DeleteMark == null && user.EnabledMark == 1 && (techOrganizeIds.Count == 0 || techOrganizeIds.Contains(user.OrganizeId))); // 如果指定了部门ID,则进行筛选 if (!string.IsNullOrEmpty(input.DepartmentId)) { techTeacherQuery = techTeacherQuery.Where((user, org) => user.OrganizeId == input.DepartmentId); } var techTeacherList = await techTeacherQuery .Select((user, org) => new { EmployeeId = user.Id, EmployeeName = user.RealName, EmployeeAccount = user.Account, OrganizeId = user.OrganizeId, DepartmentName = org.FullName ?? "" }) .ToListAsync(); if (techTeacherList == null || !techTeacherList.Any()) { return new List(); } var teacherIds = techTeacherList.Select(x => x.EmployeeId).ToList(); var teacherAccounts = techTeacherList.Where(x => !string.IsNullOrEmpty(x.EmployeeAccount)).Select(x => x.EmployeeAccount).ToList(); // 5. 使用聚合查询统计开单业绩和手工费(优化性能) // 注意:kjblsyj字段是varchar类型,需要转换 var orderStatsList = await _db.Queryable() .Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1) .Where(x => teacherIds.Contains(x.Kjbls) || teacherAccounts.Contains(x.Kjblszh)) .ToListAsync(); var orderStats = orderStatsList .Where(x => !string.IsNullOrEmpty(x.Kjbls)) .GroupBy(x => x.Kjbls) .Select(g => new { TeacherId = g.Key, OrderAchievement = g.Sum(x => decimal.TryParse(x.Kjblsyj, out var val) ? val : 0m) }) .ToList(); // 6. 使用聚合查询统计消耗业绩和手工费(关联耗卡主表获取时间) var consumeStatsList = await _db.Queryable( (kjbsyj, hyhk) => kjbsyj.Glkdbh == hyhk.Id && hyhk.IsEffective == 1) .Where((kjbsyj, hyhk) => kjbsyj.IsEffective == 1 && hyhk.Hksj >= startDate && hyhk.Hksj <= endDate.AddDays(1)) .Where((kjbsyj, hyhk) => teacherIds.Contains(kjbsyj.Kjbls) || teacherAccounts.Contains(kjbsyj.Kjblszh)) .Select((kjbsyj, hyhk) => new { TeacherId = kjbsyj.Kjbls, ConsumeAchievement = kjbsyj.Kjblsyj, LaborCost = kjbsyj.LaborCost }) .ToListAsync(); var consumeStats = consumeStatsList .Where(x => !string.IsNullOrEmpty(x.TeacherId)) .GroupBy(x => x.TeacherId) .Select(g => new { TeacherId = g.Key, ConsumeAchievement = g.Sum(x => x.ConsumeAchievement ?? 0m), LaborCost = g.Sum(x => x.LaborCost ?? 0m) }) .ToList(); // 7. 使用聚合查询统计退卡业绩和手工费 var refundStatsList = await _db.Queryable() .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) .Where(x => teacherIds.Contains(x.Kjbls) || teacherAccounts.Contains(x.Kjblszh)) .ToListAsync(); var refundStats = refundStatsList .Where(x => !string.IsNullOrEmpty(x.Kjbls)) .GroupBy(x => x.Kjbls) .Select(g => new { TeacherId = g.Key, RefundAchievement = g.Sum(x => x.Kjblsyj ?? 0m) }) .ToList(); // 8. 统计人头(按月份+客户去重) // 注意:数据库中可能存在"科技老师"和"科技部老师"两种PersonType值,需要同时查询 var personCountRecords = await _db.Queryable() .Where(x => (x.PersonType == "科技老师" || x.PersonType == "科技部老师") && x.WorkMonth == monthStr && x.IsEffective == 1 && teacherIds.Contains(x.PersonId) && !string.IsNullOrEmpty(x.PersonId) && !string.IsNullOrEmpty(x.MemberId)) .Select(x => new { PersonId = x.PersonId, MemberId = x.MemberId }) .ToListAsync(); // 按PersonId和MemberId去重,然后按PersonId分组统计不同MemberId的数量 var personCountDict = personCountRecords .GroupBy(x => new { x.PersonId, x.MemberId }) .Select(g => new { TeacherId = g.Key.PersonId, MemberId = g.Key.MemberId }) .GroupBy(x => x.TeacherId) .ToDictionary( g => g.Key ?? "", g => g.Select(x => x.MemberId).Distinct().Count()); // 9. 统计人次(按日期+客户去重,汇总数量) // 注意:数据库中可能存在"科技老师"和"科技部老师"两种PersonType值,需要同时查询 var personTimesRecords = await _db.Queryable() .Where(x => (x.PersonType == "科技老师" || x.PersonType == "科技部老师") && x.WorkMonth == monthStr && x.IsEffective == 1 && teacherIds.Contains(x.PersonId)) .ToListAsync(); // 先按日期+客户去重,取最大数量,然后按老师汇总 var personTimesStats = personTimesRecords .GroupBy(x => new { x.PersonId, x.WorkDate, x.MemberId }) .Select(g => new { TeacherId = g.Key.PersonId, Quantity = g.Max(x => x.Quantity ?? 0m) // 按日期+客户去重,取最大数量 }) .GroupBy(x => x.TeacherId) .Select(g => new { TeacherId = g.Key, PersonTimes = g.Sum(x => x.Quantity) // 汇总所有去重后的数量 }) .ToList(); // 10. 统计开单人头(同一天去重) // 从开单科技老师业绩表关联开单主表,按科技老师ID + 开单日期 + 客户ID去重统计 var orderPersonCountList = await _db.Queryable( (kjbsyj, kdjlb) => kjbsyj.Glkdbh == kdjlb.Id && kdjlb.IsEffective == 1) .Where((kjbsyj, kdjlb) => kjbsyj.IsEffective == 1 && kdjlb.Kdrq >= startDate && kdjlb.Kdrq <= endDate.AddDays(1)) .Where((kjbsyj, kdjlb) => teacherIds.Contains(kjbsyj.Kjbls) || teacherAccounts.Contains(kjbsyj.Kjblszh)) .Select((kjbsyj, kdjlb) => new { TeacherId = kjbsyj.Kjbls, OrderDate = kdjlb.Kdrq, MemberId = kdjlb.Kdhy }) .ToListAsync(); // 按科技老师ID + 开单日期 + 客户ID去重统计开单人头 var orderPersonCountDict = orderPersonCountList .Where(x => !string.IsNullOrEmpty(x.TeacherId) && !string.IsNullOrEmpty(x.MemberId) && x.OrderDate.HasValue) .GroupBy(x => new { x.TeacherId, OrderDate = x.OrderDate.Value.Date, x.MemberId }) .Select(g => new { TeacherId = g.Key.TeacherId, MemberId = g.Key.MemberId }) .GroupBy(x => x.TeacherId) .ToDictionary( g => g.Key ?? "", g => g.Select(x => x.MemberId).Distinct().Count()); // 11. 构建结果字典(优化查找性能) var orderDict = orderStats.ToDictionary(x => x.TeacherId, x => x); var consumeDict = consumeStats.ToDictionary(x => x.TeacherId, x => x); var refundDict = refundStats.ToDictionary(x => x.TeacherId, x => x); // personCountDict 已经在上面构建了 var personTimesDict = personTimesStats.ToDictionary(x => x.TeacherId, x => x); // 12. 组装结果 var result = new List(); foreach (var teacher in techTeacherList) { var orderStat = orderDict.ContainsKey(teacher.EmployeeId) ? orderDict[teacher.EmployeeId] : null; var consumeStat = consumeDict.ContainsKey(teacher.EmployeeId) ? consumeDict[teacher.EmployeeId] : null; var refundStat = refundDict.ContainsKey(teacher.EmployeeId) ? refundDict[teacher.EmployeeId] : null; var personCount = personCountDict.ContainsKey(teacher.EmployeeId) ? personCountDict[teacher.EmployeeId] : 0; var personTimesStat = personTimesDict.ContainsKey(teacher.EmployeeId) ? personTimesDict[teacher.EmployeeId] : null; var orderPersonCount = orderPersonCountDict.ContainsKey(teacher.EmployeeId) ? orderPersonCountDict[teacher.EmployeeId] : 0; // 计算消耗开单转化率(开单人头/消耗人头) var consumeOrderConversionRate = personCount > 0 ? (decimal)orderPersonCount / personCount : 0m; result.Add(new TechTeacherStatisticsOutput { EmployeeId = teacher.EmployeeId, EmployeeName = teacher.EmployeeName, OrderAchievement = orderStat?.OrderAchievement ?? 0m, ConsumeAchievement = consumeStat?.ConsumeAchievement ?? 0m, RefundAchievement = refundStat?.RefundAchievement ?? 0m, PersonCount = personCount, PersonTimes = personTimesStat?.PersonTimes ?? 0m, LaborCost = consumeStat?.LaborCost ?? 0m, // 手工费只统计耗卡中的手工费 DepartmentId = teacher.OrganizeId, DepartmentName = !string.IsNullOrEmpty(teacher.DepartmentName) ? teacher.DepartmentName : (techOrganizeDict.ContainsKey(teacher.OrganizeId) ? techOrganizeDict[teacher.OrganizeId] : ""), OrderPersonCount = orderPersonCount, ConsumeOrderConversionRate = consumeOrderConversionRate }); } return result; } #region 员工工资确认 /// /// 员工确认工资条 /// /// /// 员工确认自己的工资条,确认后工资数据不可再修改 /// /// 示例请求: /// /// { /// "id": "工资记录ID", /// "employeeId": "员工ID", /// "remark": "确认备注(可选)" /// } /// /// /// 参数说明: /// - id: 工资记录ID(必填) /// - employeeId: 员工ID(必填) /// - remark: 确认备注(可选) /// /// 注意事项: /// - 只能确认自己的工资条 /// - 只能确认已锁定的工资条(IsLocked = 1) /// - 已确认的工资条不能重复确认 /// /// 确认参数 /// 操作结果 /// 确认成功 /// 参数错误或记录不存在 [HttpPost("confirm")] public async Task ConfirmSalary([FromBody] SalaryConfirmInput input) { try { if (string.IsNullOrWhiteSpace(input.Id)) { throw NCCException.Oh("工资记录ID不能为空"); } if (string.IsNullOrWhiteSpace(input.EmployeeId)) { throw NCCException.Oh("员工ID不能为空"); } // 查询工资记录 var salary = await _db.Queryable() .Where(s => s.Id == input.Id && s.EmployeeId == input.EmployeeId) .FirstAsync(); if (salary == null) { throw NCCException.Oh("工资记录不存在或不属于该员工"); } // 检查是否已确认 if (salary.EmployeeConfirmStatus == 1) { throw NCCException.Oh("该工资条已确认,不能重复确认"); } // 检查是否已锁定(员工只能确认已锁定的工资条) if (salary.IsLocked != 1) { throw NCCException.Oh("该工资条尚未锁定,请等待管理员锁定后再确认"); } // 更新确认状态 salary.EmployeeConfirmStatus = 1; salary.EmployeeConfirmTime = DateTime.Now; salary.EmployeeConfirmRemark = input.Remark; // 注意:IsLocked 保持为 1(因为本来就是管理员锁定的) salary.UpdateTime = DateTime.Now; await _db.Updateable(salary).ExecuteCommandAsync(); return "确认成功"; } catch (Exception ex) { throw NCCException.Oh($"确认工资条失败: {ex.Message}"); } } #endregion #region 工资锁定/解锁 /// /// 批量锁定/解锁工资条 /// [HttpPost("lock")] public async Task LockSalary([FromBody] SalaryLockInput input) { try { if (input == null || input.Ids == null || !input.Ids.Any()) throw NCCException.Oh("工资记录ID列表不能为空"); var salaries = await _db.Queryable() .Where(s => input.Ids.Contains(s.Id)) .ToListAsync(); if (!salaries.Any()) throw NCCException.Oh("未找到指定的工资记录"); var lockedCount = 0; var unlockedCount = 0; var skippedCount = 0; foreach (var salary in salaries) { if (salary.EmployeeConfirmStatus == 1 && !input.IsLocked) { skippedCount++; continue; } salary.IsLocked = input.IsLocked ? 1 : 0; salary.UpdateTime = DateTime.Now; if (input.IsLocked) lockedCount++; else unlockedCount++; } await _db.Updateable(salaries).ExecuteCommandAsync(); var action = input.IsLocked ? "锁定" : "解锁"; var count = input.IsLocked ? lockedCount : unlockedCount; var message = $"{action}成功:{count}条"; if (skippedCount > 0) message += $",跳过{skippedCount}条(已确认的记录不能解锁)"; return message; } catch (Exception ex) { throw NCCException.Oh($"锁定/解锁工资条失败: {ex.Message}"); } } /// /// 批量锁定当月所有工资 /// /// /// 批量锁定指定月份的所有科技部老师工资记录 /// /// 批量锁定输入参数 /// 锁定结果 [HttpPost("lock-by-month")] public async Task LockSalaryByMonth([FromBody] SalaryLockByMonthInput input) { try { if (input == null) throw NCCException.Oh("参数不能为空"); if (input.Year <= 0 || input.Month <= 0 || input.Month > 12) throw NCCException.Oh("年份和月份参数不正确"); var monthStr = $"{input.Year}{input.Month:D2}"; var salaries = await _db.Queryable() .Where(s => s.StatisticsMonth == monthStr) .ToListAsync(); if (!salaries.Any()) throw NCCException.Oh($"未找到{input.Year}年{input.Month}月的工资记录"); var lockedCount = 0; var unlockedCount = 0; var skippedCount = 0; var alreadyLockedCount = 0; foreach (var salary in salaries) { if (salary.EmployeeConfirmStatus == 1 && !input.IsLocked) { skippedCount++; continue; } if (salary.IsLocked == 1 && input.IsLocked) { alreadyLockedCount++; continue; } if (salary.IsLocked == 0 && !input.IsLocked) { alreadyLockedCount++; continue; } salary.IsLocked = input.IsLocked ? 1 : 0; salary.UpdateTime = DateTime.Now; if (input.IsLocked) lockedCount++; else unlockedCount++; } if (lockedCount > 0 || unlockedCount > 0) { var salariesToUpdate = salaries.Where(s => (input.IsLocked && s.IsLocked == 0) || (!input.IsLocked && s.IsLocked == 1 && s.EmployeeConfirmStatus != 1) ).ToList(); if (salariesToUpdate.Any()) { await _db.Updateable(salariesToUpdate) .UpdateColumns(s => new { s.IsLocked, s.UpdateTime }) .ExecuteCommandAsync(); } } var action = input.IsLocked ? "锁定" : "解锁"; var count = input.IsLocked ? lockedCount : unlockedCount; var message = $"{action}成功:{count}条"; if (alreadyLockedCount > 0) message += $",跳过{alreadyLockedCount}条(已是{action}状态)"; if (skippedCount > 0) message += $",跳过{skippedCount}条(已确认的记录不能解锁)"; return new { success = true, message = message, total = salaries.Count, locked = lockedCount, unlocked = unlockedCount, skipped = skippedCount, alreadyLocked = alreadyLockedCount }; } catch (Exception ex) { _logger.LogError(ex, "批量锁定当月工资失败"); var action = input?.IsLocked == true ? "锁定" : "解锁"; throw NCCException.Oh($"批量{action}当月工资失败: {ex.Message}"); } } #endregion #region 导入工资 /// /// 从Excel导入科技部老师工资数据 /// /// /// 从Excel文件导入科技部老师工资数据,Excel第一列必须是ID(主键) /// /// 导入规则: /// 1. Excel第一列是ID(主键),如果为空则自动生成新ID /// 2. 如果ID在数据库中存在,则更新记录(需检查是否已锁定或已确认) /// 3. 如果ID在数据库中不存在,则新增记录 /// 4. 已锁定(IsLocked=1)或已确认(EmployeeConfirmStatus=1)的记录不能导入覆盖 /// /// Excel字段顺序(第一列为ID): /// ID, 门店名称, 员工姓名, 岗位, 开单业绩, ...(共54列) /// /// Excel文件 /// 导入结果 /// 导入成功 /// 文件格式错误或数据验证失败 [HttpPost("import")] public async Task ImportSalaryFromExcel(IFormFile file) { try { if (file == null || file.Length == 0) { throw NCCException.Oh("请选择要上传的Excel文件"); } // 检查文件格式 var allowedExtensions = new[] { ".xlsx", ".xls" }; var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(fileExtension)) { throw NCCException.Oh("只支持.xlsx和.xls格式的Excel文件"); } var recordsToInsert = new List(); var recordsToUpdate = new List(); var errorMessages = new List(); var successCount = 0; var failCount = 0; var skippedCount = 0; // 保存临时文件 var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); try { using (var stream = new FileStream(tempFilePath, FileMode.Create)) { await file.CopyToAsync(stream); } // 使用ExcelImportHelper读取Excel文件 var dataTable = ExcelImportHelper.ToDataTable(tempFilePath, 0, 0); if (dataTable.Rows.Count == 0) { throw NCCException.Oh("Excel文件中没有数据行"); } // ExcelImportHelper.ToDataTable(filePath, 0, 0)会将第一行作为标题行,数据从第二行开始 // 所以DataTable.Rows[0]是Excel的第一条数据行,应该从i=0开始循环 for (int i = 0; i < dataTable.Rows.Count; i++) { try { var row = dataTable.Rows[i]; // 安全获取列值 Func GetColumnValue = (colIndex) => { if (colIndex < row.ItemArray.Length) { return row[colIndex]?.ToString()?.Trim() ?? ""; } return ""; }; // 第一列是ID var id = GetColumnValue(0); // 判断Excel格式:如果第一列是"门店名称"等中文,说明是旧格式(没有ID列) var firstColumnValue = GetColumnValue(0); bool isOldFormat = false; if (!string.IsNullOrWhiteSpace(firstColumnValue) && (firstColumnValue == "门店名称" || firstColumnValue.Contains("门店") || (!string.IsNullOrWhiteSpace(firstColumnValue) && !long.TryParse(firstColumnValue, out _) && firstColumnValue.Length > 20))) { isOldFormat = true; id = ""; } // 根据Excel格式确定字段索引 int storeNameIndex = isOldFormat ? 0 : 1; int employeeNameIndex = isOldFormat ? 1 : 2; // 跳过空行 var employeeName = GetColumnValue(employeeNameIndex); var storeName = GetColumnValue(storeNameIndex); if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(employeeName)) { continue; } // 如果ID为空,尝试根据员工姓名和门店名称匹配现有记录的ID if (string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(employeeName)) { var matchedRecord = await _db.Queryable() .Where(x => x.EmployeeName == employeeName) .WhereIF(!string.IsNullOrWhiteSpace(storeName), x => x.StoreName == storeName) .OrderBy(x => x.CreateTime, OrderByType.Desc) .FirstAsync(); if (matchedRecord != null) { id = matchedRecord.Id; } } // 验证必填字段 if (string.IsNullOrWhiteSpace(employeeName)) { errorMessages.Add($"第{i + 1}行:员工姓名不能为空"); failCount++; continue; } // 辅助方法:清理数值字符串 Func ParseDecimal = (str) => { if (string.IsNullOrWhiteSpace(str)) return 0; var cleaned = str.Trim() .Replace(",", "") .Replace(",", "") .Replace("¥", "") .Replace("$", "") .Replace("元", "") .Replace("%", "") .Replace(" ", ""); if (decimal.TryParse(cleaned, out decimal result)) return result; return 0; }; Func ParseInt = (str) => { if (string.IsNullOrWhiteSpace(str)) return 0; var cleaned = str.Trim().Replace(",", "").Replace(",", "").Replace(" ", ""); if (int.TryParse(cleaned, out int result)) return result; return 0; }; // 如果Excel中有ID,查找现有记录 LqTechTeacherSalaryStatisticsEntity existing = null; if (!string.IsNullOrWhiteSpace(id)) { existing = await _db.Queryable() .Where(x => x.Id == id) .FirstAsync(); if (existing != null) { // 检查是否已锁定或已确认 if (existing.IsLocked == 1) { errorMessages.Add($"第{i + 1}行:员工 {existing.EmployeeName} (ID: {id}) 的工资已锁定,不能导入覆盖"); skippedCount++; failCount++; continue; } if (existing.EmployeeConfirmStatus == 1) { errorMessages.Add($"第{i + 1}行:员工 {existing.EmployeeName} (ID: {id}) 的工资已确认,不能导入覆盖"); skippedCount++; failCount++; continue; } } } // 创建或更新实体 LqTechTeacherSalaryStatisticsEntity entity; if (existing != null) { entity = existing; entity.EmployeeConfirmStatus = 0; entity.EmployeeConfirmTime = null; entity.EmployeeConfirmRemark = null; } else { entity = new LqTechTeacherSalaryStatisticsEntity { Id = string.IsNullOrWhiteSpace(id) ? YitIdHelper.NextId().ToString() : id, EmployeeConfirmStatus = 0, IsLocked = 0, CreateTime = DateTime.Now, UpdateTime = DateTime.Now }; } // 根据Excel格式计算字段索引偏移量 int offset = isOldFormat ? 0 : 1; // 映射Excel字段到实体属性(根据Excel列顺序,第一列是ID,所以业务字段从索引1开始) // Excel列顺序:ID, 门店名称, 员工姓名, 岗位, 开单业绩, 消耗业绩, 退卡业绩, 总业绩, 项目数, 底薪档位, 底薪, ... entity.StoreName = storeName; entity.EmployeeName = employeeName; entity.Position = GetColumnValue(2 + offset); entity.OrderAchievement = ParseDecimal(GetColumnValue(3 + offset)); entity.ConsumeAchievement = ParseDecimal(GetColumnValue(4 + offset)); entity.RefundAchievement = ParseDecimal(GetColumnValue(5 + offset)); entity.TotalPerformance = ParseDecimal(GetColumnValue(6 + offset)); entity.ProjectCount = ParseDecimal(GetColumnValue(7 + offset)); entity.BaseSalaryLevel = ParseInt(GetColumnValue(8 + offset)); entity.BaseSalary = ParseDecimal(GetColumnValue(9 + offset)); entity.PerformanceCommissionRate = ParseDecimal(GetColumnValue(10 + offset)); entity.PerformanceCommissionAmount = ParseDecimal(GetColumnValue(11 + offset)); entity.ConsumeCommissionRate = ParseDecimal(GetColumnValue(12 + offset)); entity.ConsumeCommissionAmount = ParseDecimal(GetColumnValue(13 + offset)); entity.HandworkFee = ParseDecimal(GetColumnValue(14 + offset)); entity.TotalCommission = ParseDecimal(GetColumnValue(15 + offset)); entity.WorkingDays = ParseDecimal(GetColumnValue(16 + offset)); entity.LeaveDays = ParseDecimal(GetColumnValue(17 + offset)); entity.TransportationAllowance = ParseDecimal(GetColumnValue(18 + offset)); entity.LessRest = ParseDecimal(GetColumnValue(19 + offset)); entity.FullAttendance = ParseDecimal(GetColumnValue(20 + offset)); entity.MonthlyTrainingSubsidy = ParseDecimal(GetColumnValue(21 + offset)); entity.MonthlyTransportSubsidy = ParseDecimal(GetColumnValue(22 + offset)); entity.LastMonthTrainingSubsidy = ParseDecimal(GetColumnValue(23 + offset)); entity.LastMonthTransportSubsidy = ParseDecimal(GetColumnValue(24 + offset)); entity.TotalSubsidy = ParseDecimal(GetColumnValue(25 + offset)); entity.CalculatedGrossSalary = ParseDecimal(GetColumnValue(26 + offset)); entity.GuaranteedSalary = ParseDecimal(GetColumnValue(27 + offset)); entity.GuaranteedLeaveDeduction = ParseDecimal(GetColumnValue(28 + offset)); entity.GuaranteedBaseSalary = ParseDecimal(GetColumnValue(29 + offset)); entity.GuaranteedSupplement = ParseDecimal(GetColumnValue(30 + offset)); entity.FinalGrossSalary = ParseDecimal(GetColumnValue(31 + offset)); entity.MissingCard = ParseDecimal(GetColumnValue(32 + offset)); entity.LateArrival = ParseDecimal(GetColumnValue(33 + offset)); entity.LeaveDeduction = ParseDecimal(GetColumnValue(34 + offset)); entity.SocialInsuranceDeduction = ParseDecimal(GetColumnValue(35 + offset)); entity.RewardDeduction = ParseDecimal(GetColumnValue(36 + offset)); entity.AccommodationDeduction = ParseDecimal(GetColumnValue(37 + offset)); entity.StudyPeriodDeduction = ParseDecimal(GetColumnValue(38 + offset)); entity.WorkClothesDeduction = ParseDecimal(GetColumnValue(39 + offset)); entity.TotalDeduction = ParseDecimal(GetColumnValue(40 + offset)); entity.Bonus = ParseDecimal(GetColumnValue(41 + offset)); entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(42 + offset)); entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(43 + offset)); entity.LastMonthSupplement = ParseDecimal(GetColumnValue(44 + offset)); entity.ActualSalary = ParseDecimal(GetColumnValue(45 + offset)); entity.MonthlyPaymentStatus = GetColumnValue(46 + offset); entity.PaidAmount = ParseDecimal(GetColumnValue(47 + offset)); entity.PendingAmount = ParseDecimal(GetColumnValue(48 + offset)); entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(49 + offset)); // 处理"是否新店"字段 var isNewStoreStr = GetColumnValue(50 + offset); entity.IsNewStore = (isNewStoreStr == "是" || isNewStoreStr == "1" || isNewStoreStr.ToLower() == "true") ? "是" : "否"; // 处理"新店保护阶段"字段 var newStoreProtectionStageStr = GetColumnValue(51 + offset); if (int.TryParse(newStoreProtectionStageStr, out int protectionStage)) { entity.NewStoreProtectionStage = protectionStage; } // 处理"离职状态"字段(第54列,索引53+offset) var terminatedStatus = GetColumnValue(53 + offset); entity.IsTerminated = (terminatedStatus == "离职" || terminatedStatus == "1") ? 1 : 0; // 处理Excel中没有的字段 if (existing != null) { entity.StoreId = existing.StoreId; entity.EmployeeId = existing.EmployeeId; entity.StatisticsMonth = existing.StatisticsMonth; } else { // 新增记录:尝试根据员工姓名和门店名称查找EmployeeId和StoreId if (!string.IsNullOrWhiteSpace(employeeName)) { var user = await _db.Queryable() .Where(u => u.RealName == employeeName) .FirstAsync(); if (user != null) { entity.EmployeeId = user.Id; entity.EmployeeAccount = user.Account; if (!string.IsNullOrEmpty(user.Mdid) && string.IsNullOrWhiteSpace(storeName)) { entity.StoreId = user.Mdid; } } if (!string.IsNullOrWhiteSpace(storeName) && string.IsNullOrEmpty(entity.StoreId)) { var store = await _db.Queryable() .Where(s => s.Dm == storeName) .FirstAsync(); if (store != null) { entity.StoreId = store.Id; } } } } entity.UpdateTime = DateTime.Now; if (existing != null) { recordsToUpdate.Add(entity); } else { recordsToInsert.Add(entity); } successCount++; } catch (Exception ex) { errorMessages.Add($"第{i + 1}行数据处理失败: {ex.Message}"); failCount++; } } } finally { // 清理临时文件 if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } // 批量插入新记录 if (recordsToInsert.Any()) { await _db.Insertable(recordsToInsert).ExecuteCommandAsync(); } // 批量更新现有记录 if (recordsToUpdate.Any()) { // 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新 await _db.Updateable(recordsToUpdate) .IgnoreColumns(x => x.CreateTime) .IgnoreColumns(x => x.CreateUser) .ExecuteCommandAsync(); } return new { success = true, message = $"导入完成:成功 {successCount} 条,失败 {failCount} 条,跳过 {skippedCount} 条(已锁定或已确认)", successCount = successCount, failCount = failCount, skippedCount = skippedCount, errors = errorMessages }; } catch (Exception ex) { throw NCCException.Oh($"导入科技部老师工资数据失败: {ex.Message}"); } } #endregion } }