using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NCC.Common.Enum; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqSalary; using Yitter.IdGenerator; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_jinsanjiao_user; using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_md_target; using NCC.Extend.Entitys.lq_person_times_record; using NCC.Extend.Entitys.lq_salary_statistics; using NCC.Extend.Entitys.lq_xh_jksyj; using NCC.Extend.Entitys.lq_ycsd_jsj; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_hytk_jksyj; using NCC.Extend.Entitys.lq_md_xdbhsj; using NCC.Extend.Entitys.lq_salary_extra_calculation; using NCC.System.Entitys.Permission; using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NCC.Extend { /// /// 健康师薪酬服务 /// [ApiDescriptionSettings(Tag = "健康师薪酬服务", Name = "LqSalary", Order = 300)] [Route("api/Extend/[controller]")] public class LqSalaryService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; /// /// 初始化一个类型的新实例 /// public LqSalaryService(ISqlSugarClient db) { _db = db; } /// /// 获取健康师工资列表 /// /// 查询参数 /// 健康师工资分页列表 [HttpGet("health-coach")] public async Task GetHealthCoachSalaryList([FromQuery] HealthCoachSalaryInput input) { var monthStr = $"{input.Year}{input.Month:D2}"; // 1. 检查当月是否已生成工资数据 var exists = await _db.Queryable() .AnyAsync(x => x.StatisticsMonth == monthStr); // 2. 如果没有数据,则进行计算 if (!exists) { await CalculateHealthCoachSalary(input.Year, input.Month); } // 3. 查询数据 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.EmployeeId.Contains(input.Keyword)); } var list = await query.Select(x => new HealthCoachSalaryOutput { Id = x.Id, StoreId = x.StoreId, StoreName = x.StoreName, EmployeeId = x.EmployeeId, EmployeeName = x.EmployeeName, Position = x.Position, GoldTriangleId = x.GoldTriangleId, GoldTriangleTeam = x.GoldTriangleTeam, TotalPerformance = x.TotalPerformance, BasePerformance = x.BasePerformance, CooperationPerformance = x.CooperationPerformance, BaseRewardPerformance = x.BaseRewardPerformance, CooperationRewardPerformance = x.CooperationRewardPerformance, ActualBasePerformance = x.ActualBasePerformance, ActualCooperationPerformance = x.ActualCooperationPerformance, RewardPerformance = x.RewardPerformance, StoreTotalPerformance = x.StoreTotalPerformance, TeamPerformance = x.TeamPerformance, Percentage = x.Percentage, NewCustomerPerformance = x.NewCustomerPerformance, NewCustomerConversionRate = x.NewCustomerConversionRate, NewCustomerPoint = x.NewCustomerPoint, UpgradeCustomerCount = x.UpgradeCustomerCount, UpgradePerformance = x.UpgradePerformance, UpgradePoint = x.UpgradePoint, NewCustomerCommission = x.NewCustomerPerformanceCommission, UpgradeCommission = x.UpgradePerformanceCommission, OtherPerformanceAdd = x.OtherPerformanceAdd, OtherPerformanceSubtract = x.OtherPerformanceSubtract, Consumption = x.Consumption, ProjectCount = x.ProjectCount, CustomerCount = x.CustomerCount, WorkingDays = x.WorkingDays, LeaveDays = x.LeaveDays, CommissionPoint = x.CommissionPoint, BasePerformanceCommission = x.BasePerformanceCommission, CooperationPerformanceCommission = x.CooperationPerformanceCommission, ConsultantCommission = x.ConsultantCommission, StoreTZoneCommission = x.StoreTZoneCommission, TotalCommission = x.TotalCommission, HealthCoachBaseSalary = x.HealthCoachBaseSalary, HandworkFee = x.HandworkFee, OutherHandworkFee = x.OutherHandworkFee, 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, StatisticsMonth = x.StatisticsMonth, IsLocked = x.IsLocked, CreateTime = x.CreateTime, CreateUser = x.CreateUser, UpdateTime = x.UpdateTime, UpdateUser = x.UpdateUser, IsNewStore = x.IsNewStore, NewStoreProtectionStage = x.NewStoreProtectionStage, StoreType = x.StoreType, StoreCategory = x.StoreCategory, DailyAverageConsumption = x.DailyAverageConsumption, DailyAverageProjectCount = x.DailyAverageProjectCount, TeamTotalConsumption = x.TeamTotalConsumption }) .ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(list); } /// /// 计算健康师工资 /// /// 年份 /// 月份 /// [HttpPost("calculate/health-coach")] public async Task CalculateHealthCoachSalary(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 业绩数据 (lq_kd_jksyj) var performanceList = await _db.Queryable() .Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1) .ToListAsync(); // 1.1.1 获取关联的开单记录(用于获取 sfskdd) var billingIds = performanceList.Select(x => x.Glkdbh).Distinct().ToList(); var billingDict = await _db.Queryable() .Where(x => billingIds.Contains(x.Id) && x.Id != null) .ToDictionaryAsync(x => x.Id, x => x.Sfskdd); // 1.1.2 组合数据 var performanceData = performanceList.Select(p => new { Jks = p.Jkszh, // 使用 Jkszh (账号/ID) 而不是 Jks (姓名) p.Jksxm, p.StoreId, p.Jksyj, p.ItemCategory, p.PerformanceType, // 新增业绩类型字段 Sfskdd = billingDict.ContainsKey(p.Glkdbh) ? billingDict[p.Glkdbh] : null }).ToList(); // 1.2 消耗数据 (lq_xh_jksyj) var consumptionList = await _db.Queryable() .Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1) .ToListAsync(); // 1.3 考勤数据 (lq_attendance_summary) var attendanceList = await _db.Queryable() .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1) .ToListAsync(); // 1.3.1 退款数据 (lq_hytk_jksyj) var refundList = await _db.Queryable() .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) .ToListAsync(); // 1.4 战队成员及顾问信息 (lq_jinsanjiao_user + lq_ycsd_jsj) var teamUserList = await _db.Queryable() .Where(x => x.Month == monthStr && x.DeleteMark == 0) .ToListAsync(); // 1.4.1 获取战队信息 var teamIds = teamUserList.Select(x => x.JsjId).Distinct().ToList(); var teamList = await _db.Queryable() .Where(x => teamIds.Contains(x.Id)) .ToListAsync(); var teamDict = teamList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x.Jsj); // 1.4.2 组合数据 var teamMembers = teamUserList.Select(user => new { user.UserId, user.IsLeader, TeamId = user.JsjId, TeamName = teamDict.ContainsKey(user.JsjId) ? teamDict[user.JsjId] : (string)null }).ToList(); // 1.5 到店人头 (lq_person_times_record) // 统计每个健康师的去重会员数 var headcountList = await _db.Queryable() .Where(x => x.WorkMonth == monthStr && x.IsEffective == 1) .GroupBy(x => x.PersonId) .Select(x => new { PersonId = x.PersonId, Count = SqlFunc.AggregateDistinctCount(x.MemberId) }) .ToListAsync(); // 1.6 门店生命线 (lq_md_target) var storeTargets = await _db.Queryable() .Where(x => x.Month == monthStr) .ToListAsync(); // 1.6.1 门店总业绩计算 (开单实付 - 退款金额) // 开单实付 var storeBillingList = await _db.Queryable() .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1) .Select(x => new { x.Djmd, x.Sfyj }) .ToListAsync(); var storeBillingDict = storeBillingList .Where(x => !string.IsNullOrEmpty(x.Djmd)) .GroupBy(x => x.Djmd) .ToDictionary(g => g.Key, g => g.Sum(x => x.Sfyj)); // 退款金额 var storeRefundList = await _db.Queryable() .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1) .Select(x => new { x.Mdbh, x.Tkje, x.ActualRefundAmount }) .ToListAsync(); var storeRefundDict = storeRefundList .Where(x => !string.IsNullOrEmpty(x.Mdbh)) .GroupBy(x => x.Mdbh) .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? 0)); // 1.7 门店信息 (lq_mdxx) var storeList = await _db.Queryable().ToListAsync(); var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x); // 1.7.1 门店新店保护信息 (lq_md_xdbhsj) var newStoreProtectionList = await _db.Queryable() .Where(x => x.Sfqy == 1) .ToListAsync(); // 构造新店保护查找字典: StoreId -> List // 因为一个门店可能有多个阶段配置,虽然通常是一个时间段,但为了严谨取符合当前月份的配置 // 逻辑: 统计月份 (startDate ~ endDate) 是否在 bhkssj ~ bhjssj 范围内 // 只要统计月份与保护期有交集,就算保护期。或者严格一点,统计月份的第一天在保护期内。 // 这里取: 统计月份的第一天 (startDate) 在保护期内 var newStoreProtectionDict = newStoreProtectionList .Where(x => x.Bhkssj <= startDate && x.Bhjssj >= startDate) .GroupBy(x => x.Mdid) .ToDictionary(g => g.Key, g => g.First()); // 取第一个匹配的配置 // 1.8 健康师工资额外计算数据 (lq_salary_extra_calculation) var extraCalculationList = await _db.Queryable() .Where(x => x.Year == year && x.Month == month) .ToListAsync(); var extraCalculationDict = extraCalculationList .Where(x => !string.IsNullOrEmpty(x.EmployeeId)) .ToDictionary(x => x.EmployeeId, x => x); // 2. 聚合每个健康师的数据对象 var employeeStats = new Dictionary(); // 获取所有涉及的健康师ID var allEmployeeIds = performanceData.Select(x => x.Jks) .Union(consumptionList.Select(x => x.Jks)) .Union(attendanceList.Select(x => x.UserId)) .Union(teamMembers.Select(x => x.UserId)) .Where(x => !string.IsNullOrEmpty(x)) .Distinct() .ToList(); // 1.8 批量获取员工信息 (BASE_USER + BASE_POSITION) // 使用 allEmployeeIds 作为驱动,查询 BASE_USER // 重要:只统计岗位为"健康师"的员工 var userList = await _db.Queryable() .Where(x => allEmployeeIds.Contains(x.Id) && x.Gw == "健康师" // 只统计岗位为"健康师"的员工 && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => new { x.Id, x.RealName, x.PositionId, x.Mdid }) .ToListAsync(); // 过滤出健康师ID列表 var healthCoachIds = userList.Select(x => x.Id).ToList(); var userDict = userList.ToDictionary(x => x.Id, x => x); var positionIds = userList.Select(x => x.PositionId).Distinct().ToList(); var positionList = await _db.Queryable().Where(x => positionIds.Contains(x.Id)).ToListAsync(); var positionLookup = positionList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x.FullName); // 只处理健康师员工 foreach (var empId in allEmployeeIds.Where(x => healthCoachIds.Contains(x))) { var salary = new LqSalaryStatisticsEntity { Id = YitIdHelper.NextId().ToString(), EmployeeId = empId, StatisticsMonth = monthStr, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsLocked = 0 }; // 填充基础信息 (优先从 BASE_USER 获取) string userMdid = null; if (userDict.ContainsKey(empId)) { var user = userDict[empId]; salary.EmployeeName = user.RealName; userMdid = user.Mdid; // 岗位 if (user.PositionId != null && positionLookup.ContainsKey(user.PositionId)) { salary.Position = positionLookup[user.PositionId]; } } // 如果 BASE_USER 没名字,尝试从业务表获取 if (string.IsNullOrEmpty(salary.EmployeeName)) { var perfRecord = performanceData.FirstOrDefault(x => x.Jks == empId); var consRecord = consumptionList.FirstOrDefault(x => x.Jks == empId); if (perfRecord != null) salary.EmployeeName = perfRecord.Jksxm; else if (consRecord != null) salary.EmployeeName = consRecord.Jksxm; } // 填充门店ID (从业务数据获取,因为 User 表的 OrganizeId 未必是门店) var perfStore = performanceData.FirstOrDefault(x => x.Jks == empId && !string.IsNullOrEmpty(x.StoreId)); var consStore = consumptionList.FirstOrDefault(x => x.Jks == empId && !string.IsNullOrEmpty(x.StoreId)); if (perfStore != null) salary.StoreId = perfStore.StoreId; else if (consStore != null) salary.StoreId = consStore.StoreId; // 如果业务数据没门店,尝试使用 User.Mdid if (string.IsNullOrEmpty(salary.StoreId) && !string.IsNullOrEmpty(userMdid)) { if (storeDict.ContainsKey(userMdid)) { salary.StoreId = userMdid; } } // 填充门店名称及分类信息 if (!string.IsNullOrEmpty(salary.StoreId) && storeDict.ContainsKey(salary.StoreId)) { var store = storeDict[salary.StoreId]; salary.StoreName = store.Dm; salary.StoreType = store.StoreType; salary.StoreCategory = store.StoreCategory; } // 填充新店保护信息 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.1 计算个人业绩 var myPerf = performanceData.Where(x => x.Jks == empId).ToList(); salary.BasePerformance = myPerf.Where(x => (x.PerformanceType ?? "").Trim() == "基础业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.CooperationPerformance = myPerf.Where(x => (x.PerformanceType ?? "").Trim() == "合作业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.TotalPerformance = myPerf.Sum(x => decimal.Parse(x.Jksyj ?? "0")); // 扣除退款 var myRefunds = refundList.Where(x => x.Jks == empId).ToList(); if (myRefunds.Any()) { decimal totalRefund = myRefunds.Sum(x => x.Jksyj ?? 0); decimal baseRefund = myRefunds.Where(x => (x.PerformanceType ?? "").Trim() == "基础业绩").Sum(x => x.Jksyj ?? 0); decimal cooperationRefund = myRefunds.Where(x => (x.PerformanceType ?? "").Trim() == "合作业绩").Sum(x => x.Jksyj ?? 0); // 如果退款记录未标记类型,且总退款大于0,则可能需要处理(当前暂不处理未分类退款的明细扣除,仅扣除总额) // 修正:如果PerformanceType为空,应当从总业绩扣除。若要从Base或Coop扣除,需确认业务规则。 // 假设:如果未分类,暂时不从Base/Coop扣(或者按比例扣?)。 // 根据现有数据,有部分是null。 // 安全起见,TotalPerformance 减去 totalRefund。 // Base 和 Coop 减去各自明确标识的部分。 salary.TotalPerformance -= totalRefund; salary.BasePerformance -= baseRefund; salary.CooperationPerformance -= cooperationRefund; } // 新客与升单业绩 salary.NewCustomerPerformance = myPerf.Where(x => string.Equals(x.Sfskdd, "是")).Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.UpgradePerformance = myPerf.Where(x => string.Equals(x.Sfskdd, "否")).Sum(x => decimal.Parse(x.Jksyj ?? "0")); // 2.1.1 填充额外计算数据 if (extraCalculationDict.ContainsKey(empId)) { var extraData = extraCalculationDict[empId]; salary.BaseRewardPerformance = extraData.BaseRewardPerformance; salary.CooperationRewardPerformance = extraData.CooperationRewardPerformance; salary.OtherPerformanceAdd = extraData.OtherPerformanceAdd; salary.OtherPerformanceSubtract = extraData.OtherPerformanceSubtract; salary.UpgradeCustomerCount = extraData.UpgradeCustomerCount; salary.NewCustomerConversionRate = extraData.NewCustomerConversionRate; salary.NewCustomerPerformance = extraData.NewCustomerPerformance; salary.UpgradePerformance = extraData.UpgradePerformance; } // 2.1.2 计算实际基础业绩和实际合作业绩 // 定义新店相关变量,供后续多处使用 bool isNewStore = salary.IsNewStore == "是"; int newStoreStage = salary.NewStoreProtectionStage; // 实际基础业绩 = 基础业绩 - 基础奖励业绩 + 其他业绩加 - 其他业绩减 decimal actualBasePerformance = salary.BasePerformance - salary.BaseRewardPerformance + salary.OtherPerformanceAdd - salary.OtherPerformanceSubtract; // 新店额外调整:根据阶段扣除新客业绩或升单业绩 if (isNewStore) { if (newStoreStage == 1) { // 第一阶段:扣除新客业绩 actualBasePerformance -= salary.NewCustomerPerformance; } else if (newStoreStage == 2) { // 第二阶段:扣除升单业绩 actualBasePerformance -= salary.UpgradePerformance; } } salary.ActualBasePerformance = actualBasePerformance; // 实际合作业绩 = 合作业绩 - 合作奖励业绩 salary.ActualCooperationPerformance = salary.CooperationPerformance - salary.CooperationRewardPerformance; // 2.2 计算消耗和项目数 var myCons = consumptionList.Where(x => x.Jks == empId).ToList(); salary.Consumption = myCons.Sum(x => x.Jksyj ?? 0); salary.ProjectCount = myCons.Sum(x => x.KdpxNumber ?? 0); salary.HandworkFee = myCons.Sum(x => x.LaborCost ?? 0); // 使用 F_LaborCost // 2.3 考勤数据 var myAtt = attendanceList.FirstOrDefault(x => x.UserId == empId); salary.WorkingDays = myAtt?.WorkDays ?? 0; salary.LeaveDays = myAtt?.LeaveDays ?? 0; // 计算日均消耗和日均项目数 (用于底薪计算) // 逻辑: 总消耗/在店天数, 总项目数/在店天数 if (salary.WorkingDays > 0) { salary.DailyAverageConsumption = salary.Consumption / salary.WorkingDays; salary.DailyAverageProjectCount = salary.ProjectCount / salary.WorkingDays; } else { salary.DailyAverageConsumption = 0; salary.DailyAverageProjectCount = 0; } // 2.4 到店人头 var myHeadcount = headcountList.FirstOrDefault(x => x.PersonId == empId); salary.CustomerCount = myHeadcount?.Count ?? 0; // 2.5 战队信息 (初始) var myTeam = teamMembers.FirstOrDefault(x => x.UserId == empId); // 初始判断岗位:如果是战队队长(IsLeader=1)则是顾问,否则是健康师 // 注意:这里先根据战队设置判断,后续考勤不足21天会降级 if (myTeam != null && myTeam.IsLeader == 1) { salary.Position = "顾问"; } else if (string.IsNullOrEmpty(salary.Position)) // 如果BASE_USER没岗位,且不是队长,默认为健康师 { salary.Position = "健康师"; } if (myTeam != null) { salary.GoldTriangleId = myTeam.TeamId; salary.GoldTriangleTeam = myTeam.TeamName ?? "个人"; } else { salary.GoldTriangleTeam = "个人"; } // 2.6 门店总业绩 if (!string.IsNullOrEmpty(salary.StoreId)) { decimal billing = storeBillingDict.ContainsKey(salary.StoreId) ? storeBillingDict[salary.StoreId] : 0; decimal refund = storeRefundDict.ContainsKey(salary.StoreId) ? storeRefundDict[salary.StoreId] : 0; salary.StoreTotalPerformance = billing - refund; } employeeStats[empId] = salary; } // 3. 处理战队逻辑 (考勤规则) // 规则:若出勤天数 < 20天,则该健康师不计入战队,按单人计算。 // 按战队分组 var teamGroups = employeeStats.Values .Where(x => !string.IsNullOrEmpty(x.GoldTriangleId)) .GroupBy(x => x.GoldTriangleId) .ToList(); foreach (var group in teamGroups) { var validMembers = new List(); var invalidMembers = new List(); foreach (var member in group) { if (member.WorkingDays >= 20) { validMembers.Add(member); } else { invalidMembers.Add(member); } } // 对于无效成员,移除战队标识,视为单人,并重置岗位为健康师 foreach (var member in invalidMembers) { member.GoldTriangleId = null; member.GoldTriangleTeam = "个人"; member.Position = "健康师"; // 降级为健康师 } // 计算有效战队的总业绩和总消耗 var teamTotalPerformance = validMembers.Sum(x => x.TotalPerformance); var teamTotalConsumption = validMembers.Sum(x => x.Consumption); // 更新有效成员的战队业绩和战队总消耗 foreach (var member in validMembers) { member.TeamPerformance = teamTotalPerformance; member.TeamTotalConsumption = teamTotalConsumption; } } // 补充处理:对于没有战队ID的(个人,或被剔除的),战队业绩等于个人总业绩 foreach (var salary in employeeStats.Values) { if (string.IsNullOrEmpty(salary.GoldTriangleId)) { salary.TeamPerformance = salary.TotalPerformance; } } // 4. 计算薪资 (底薪 & 提成) foreach (var salary in employeeStats.Values) { // 定义新店相关变量,供底薪和提成计算使用 bool isNewStore = salary.IsNewStore == "是"; int newStoreStage = salary.NewStoreProtectionStage; // 4.1 底薪计算 // 4.1 底薪计算 // 传入当月天数用于计算标准日均 int daysInMonth = DateTime.DaysInMonth(year, month); salary.HealthCoachBaseSalary = CalculateBaseSalary( salary.DailyAverageConsumption, salary.DailyAverageProjectCount, daysInMonth, salary.WorkingDays, isNewStore); // 4.2 提成计算 // 业绩门槛: 战队成员个人总业绩 <= 6000 无提成 (需按日均计算) // 规则:战队成员日均业绩 <= 6000 / 当月天数 -> 无提成 decimal memberThreshold = 6000m; if (daysInMonth > 0 && salary.WorkingDays > 0) { memberThreshold = (6000m / daysInMonth) * salary.WorkingDays; } if (!string.IsNullOrEmpty(salary.GoldTriangleId) && salary.TotalPerformance < memberThreshold) // 修正为小于校验 { salary.TotalCommission = 0; salary.BasePerformanceCommission = 0; salary.CooperationPerformanceCommission = 0; salary.ConsultantCommission = 0; salary.NewCustomerPerformanceCommission = 0; salary.UpgradePerformanceCommission = 0; } else { // 确定提成点 decimal commissionPoint = 0; if (!string.IsNullOrEmpty(salary.GoldTriangleId)) { // 是战队成员 // 获取战队人数 (注意:这里应该是有效战队人数) var teamMemberCount = employeeStats.Values.Count(x => x.GoldTriangleId == salary.GoldTriangleId); // 注意:提成点按原始基础业绩计算,不是实际基础业绩 // 战队成员不按日均考核提成点,只考核个人门槛 commissionPoint = GetTeamCommissionPoint(teamMemberCount, salary.TeamPerformance, daysInMonth, salary.WorkingDays); } else { // 单人 (或被剔除出战队) // 注意:提成点按原始总业绩计算 // 单人按日均考核提成点 commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance, daysInMonth, salary.WorkingDays); } salary.CommissionPoint = commissionPoint; // 计算基础/合作提成(使用实际业绩) salary.BasePerformanceCommission = salary.ActualBasePerformance * 0.95m * commissionPoint; salary.CooperationPerformanceCommission = salary.ActualCooperationPerformance * 0.95m * 0.65m * commissionPoint; // 计算新客转化率提成和升单人头提成(根据新店阶段) // isNewStore 和 newStoreStage 已在上面定义 // 升单提点总是赋值(根据升单人头数) decimal upgradeCommissionRate = GetUpgradeCommissionRate(salary.UpgradeCustomerCount); salary.UpgradePoint = upgradeCommissionRate; if (isNewStore) { if (newStoreStage == 1) { // 第一阶段:计算新客转化率提成 (需乘以0.95) salary.NewCustomerPerformanceCommission = CalculateNewCustomerConversionCommission( salary.NewCustomerPerformance, salary.NewCustomerConversionRate) * 0.95m; } else if (newStoreStage == 2) { // 第二阶段:计算升单人头提成 (需乘以0.95) salary.UpgradePerformanceCommission = CalculateUpgradeCustomerCommission( salary.UpgradePerformance, salary.UpgradeCustomerCount) * 0.95m; } // 第三阶段:不计算新客/升单提成 } // 计算顾问提成 // 检查是否是顾问 // 注意:这里需要重新判断是否是顾问,因为可能被降级了 if (salary.Position == "顾问" && !string.IsNullOrEmpty(salary.GoldTriangleId)) { // 获取战队人数 var teamMemberList = employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList(); var teamMemberCount = teamMemberList.Count; // 单人或者一个人的战队就没有顾问 if (teamMemberCount > 1) { salary.ConsultantCommission = CalculateConsultantCommission( salary.TeamPerformance, teamMemberList, teamMemberCount, isNewStore); } else { salary.ConsultantCommission = 0; } } // 计算门店T区提成 // 规则:姓名包含"T区" -> 门店总业绩 * 0.05 * 0.05 if (!string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区")) { salary.StoreTZoneCommission = salary.StoreTotalPerformance * 0.05m * 0.05m; // T区人员仅核算提成,其他项(底薪、手工、社保等)归零 salary.HealthCoachBaseSalary = 0; salary.HandworkFee = 0; salary.BasePerformanceCommission = 0; salary.CooperationPerformanceCommission = 0; salary.ConsultantCommission = 0; salary.NewCustomerPerformanceCommission = 0; salary.UpgradePerformanceCommission = 0; salary.TotalSubsidy = 0; salary.TotalDeduction = 0; } salary.TotalCommission = salary.BasePerformanceCommission + salary.CooperationPerformanceCommission + salary.ConsultantCommission + salary.NewCustomerPerformanceCommission + salary.UpgradePerformanceCommission + salary.StoreTZoneCommission; } // 计算占比 if (salary.TeamPerformance > 0) { salary.Percentage = salary.TotalPerformance / salary.TeamPerformance; } else if (salary.TotalPerformance > 0 && string.IsNullOrEmpty(salary.GoldTriangleId)) { salary.Percentage = 1; // 单人占比100% } // 4.3 最终工资 salary.ActualSalary = salary.HealthCoachBaseSalary + salary.TotalCommission + salary.HandworkFee + salary.TotalSubsidy - salary.TotalDeduction; } // 5. 保存数据 if (employeeStats.Any()) { // 先删除当月旧数据 (防止重复) await _db.Deleteable().Where(x => x.StatisticsMonth == monthStr).ExecuteCommandAsync(); await _db.Insertable(employeeStats.Values.ToList()).ExecuteCommandAsync(); } } /// /// 计算底薪 /// private decimal CalculateBaseSalary(decimal dailyAvgConsumption, decimal dailyAvgProjectCount, int daysInMonth, decimal workingDays, bool isNewStore) { // 规则调整:按日均计算 // 一星:月消耗 10000 / 当月天数,项目数 96 / 当月天数 // 二星:月消耗 20000 / 当月天数,项目数 126 / 当月天数 // 三星:月消耗 40000 / 当月天数,项目数 156 / 当月天数 // 计算各星级日均标准 decimal consStar1 = 10000m / daysInMonth; decimal consStar2 = 20000m / daysInMonth; decimal consStar3 = 40000m / daysInMonth; decimal projStar1 = 96m / daysInMonth; decimal projStar2 = 126m / daysInMonth; decimal projStar3 = 156m / daysInMonth; // 0星:未达标 -> 1800 // 1星:>=1w标准 且 >=96个标准 -> 2000 // 2星:>=2w标准 且 >=126个标准 -> 2200 // 3星:>=4w标准 且 >=156个标准 -> 2400 // 特殊规则:若消耗或项目数中仅一项未达标(0星),底薪按1星(2000元)计算 // 新店规则:新店底薪最低为1星(2000元),不满足1星按1星算 int starCons = 0; if (dailyAvgConsumption >= consStar3) starCons = 3; else if (dailyAvgConsumption >= consStar2) starCons = 2; else if (dailyAvgConsumption >= consStar1) starCons = 1; int starProj = 0; if (dailyAvgProjectCount >= projStar3) starProj = 3; else if (dailyAvgProjectCount >= projStar2) starProj = 2; else if (dailyAvgProjectCount >= projStar1) starProj = 1; int finalStar = Math.Min(starCons, starProj); // 特殊规则处理: 仅一项未达标(0星) -> 1星 if (finalStar == 0 && (starCons > 0 || starProj > 0)) { finalStar = 1; } decimal baseSalary = finalStar switch { 3 => 2400, 2 => 2200, 1 => 2000, _ => 1800 }; // 新店保底1星(2000元) if (isNewStore && baseSalary < 2000) { baseSalary = 2000; } // 最终计算:(底薪 / 当月天数) * 在店天数 if (daysInMonth > 0) { baseSalary = (baseSalary / daysInMonth) * workingDays; } return Math.Round(baseSalary, 2); } /// /// 获取战队提成点 /// private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance, int daysInMonth, decimal workingDays) { if (memberCount >= 3) { if (teamPerformance >= 150000) return 0.07m; if (teamPerformance >= 120000) return 0.06m; if (teamPerformance >= 90000) return 0.05m; if (teamPerformance >= 60000) return 0.04m; if (teamPerformance >= 30000) return 0.03m; } else if (memberCount == 2) { if (teamPerformance >= 80000) return 0.06m; if (teamPerformance >= 60000) return 0.05m; if (teamPerformance >= 40000) return 0.04m; if (teamPerformance >= 20000) return 0.03m; } else // 1人 { // 单人按照日均考核 decimal p1 = 60000m; decimal p2 = 40000m; decimal p3 = 20000m; decimal p4 = 10000m; if (daysInMonth > 0 && workingDays > 0) { p1 = (p1 / daysInMonth) * workingDays; p2 = (p2 / daysInMonth) * workingDays; p3 = (p3 / daysInMonth) * workingDays; p4 = (p4 / daysInMonth) * workingDays; } if (teamPerformance >= p1) return 0.06m; if (teamPerformance >= p2) return 0.05m; if (teamPerformance >= p3) return 0.04m; if (teamPerformance >= p4) return 0.03m; } return 0; } /// /// 计算顾问提成 /// /// 战队总业绩 /// 战队成员列表 /// 战队人数 /// 是否为新店 /// 顾问提成金额 private decimal CalculateConsultantCommission(decimal teamPerformance, List teamMembers, int teamMemberCount, bool isNewStore) { // 顾问提成规则: // 战队人数3人:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8% // 战队人数2人:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3% // 如果3人以上,就按照3人的规则来 // 单人或者一个人的战队就没有顾问(已在调用处处理) // 注意: // 1. "组员业绩"指除顾问外的其他成员业绩总和 // 2. 只统计有效战队成员(考勤≥20天,未被剔除的成员) // 3. "达到X%以上"指:组员业绩总和 ≥ 团队总业绩 × X% // 4. 新店顾问不考核消耗 // 5. 消耗达标:3人战队整组消耗>=6万,2人战队整组消耗>=4万 // 使用传入的 teamMembers 计算总消耗 var teamConsumption = teamMembers.Sum(x => x.Consumption); // 计算组员(非顾问)业绩总和 var memberPerformance = teamMembers.Where(x => x.Position != "顾问").Sum(x => x.TotalPerformance); // 3人及以上战队:业绩≥6万 且 组员业绩≥40% 且 (新店 或 消耗≥6万) → 0.8% if (teamMemberCount >= 3) { if (teamPerformance >= 60000 && memberPerformance >= teamPerformance * 0.4m) { if (isNewStore || teamConsumption >= 60000) { return teamPerformance * 0.008m; } } } // 2人战队:业绩≥4万 且 组员业绩≥30% 且 (新店 或 消耗≥4万) → 0.3% else if (teamMemberCount == 2) { if (teamPerformance >= 40000 && memberPerformance >= teamPerformance * 0.3m) { if (isNewStore || teamConsumption >= 40000) { return teamPerformance * 0.003m; } } } // 1人战队:没有顾问,返回0(已在调用处处理,这里不会执行到) return 0; } /// /// 计算新客转化率提成 /// private decimal CalculateNewCustomerConversionCommission(decimal newCustomerPerformance, decimal conversionRate) { decimal commissionRate = 0; if (conversionRate >= 0.5m) commissionRate = 0.20m; else if (conversionRate >= 0.45m) commissionRate = 0.15m; else if (conversionRate >= 0.35m) commissionRate = 0.10m; else if (conversionRate >= 0) commissionRate = 0.06m; return newCustomerPerformance * commissionRate; } /// /// 获取升单提成比例 /// private decimal GetUpgradeCommissionRate(decimal upgradeCustomerCount) { if (upgradeCustomerCount >= 10) return 0.12m; // 大于等于10:12% else if (upgradeCustomerCount >= 7 && upgradeCustomerCount < 10) return 0.10m; // 大于等于7且小于10:10% else if (upgradeCustomerCount >= 4 && upgradeCustomerCount < 7) return 0.07m; // 大于等于4且小于7:7% // 小于4:0% return 0m; } /// /// 计算升单人头提成 /// private decimal CalculateUpgradeCustomerCommission(decimal upgradePerformance, decimal upgradeCustomerCount) { decimal commissionRate = GetUpgradeCommissionRate(upgradeCustomerCount); return upgradePerformance * commissionRate; } } }