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 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, StoreName = x.StoreName, EmployeeName = x.EmployeeName, Position = x.Position, GoldTriangleTeam = x.GoldTriangleTeam, TotalPerformance = x.TotalPerformance, BasePerformance = x.BasePerformance, CooperationPerformance = x.CooperationPerformance, RewardPerformance = x.RewardPerformance, Consumption = x.Consumption, ProjectCount = x.ProjectCount, CustomerCount = x.CustomerCount, WorkingDays = x.WorkingDays, HealthCoachBaseSalary = x.HealthCoachBaseSalary, TotalCommission = x.TotalCommission, HandworkFee = x.HandworkFee, TotalSubsidy = x.TotalSubsidy, TotalDeduction = x.TotalDeduction, ActualSalary = x.ActualSalary, IsLocked = x.IsLocked, UpdateTime = x.UpdateTime }) .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)) .ToDictionaryAsync(x => x.Id, x => x.Sfskdd); // 1.1.2 组合数据 var performanceData = performanceList.Select(p => new { p.Jks, p.Jksxm, p.StoreId, p.Jksyj, p.ItemCategory, 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.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.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(); // 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(); foreach (var empId in allEmployeeIds) { var salary = new LqSalaryStatisticsEntity { Id = YitIdHelper.NextId().ToString(), EmployeeId = empId, StatisticsMonth = monthStr, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsLocked = 0 }; // 填充基础信息 (姓名、门店) var perfRecord = performanceData.FirstOrDefault(x => x.Jks == empId); var consRecord = consumptionList.FirstOrDefault(x => x.Jks == empId); if (perfRecord != null) { salary.EmployeeName = perfRecord.Jksxm; salary.StoreId = perfRecord.StoreId; } else if (consRecord != null) { salary.EmployeeName = consRecord.Jksxm; salary.StoreId = consRecord.StoreId; } // 填充门店名称 if (!string.IsNullOrEmpty(salary.StoreId)) { // 这里简单处理,实际可能需要缓存门店列表 // salary.StoreName = ... } // 2.1 计算个人业绩 var myPerf = performanceData.Where(x => x.Jks == empId).ToList(); salary.BasePerformance = myPerf.Where(x => x.ItemCategory == "基础业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.CooperationPerformance = myPerf.Where(x => x.ItemCategory == "合作业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.TotalPerformance = myPerf.Sum(x => decimal.Parse(x.Jksyj ?? "0")); // 新客与升单业绩 salary.NewCustomerPerformance = myPerf.Where(x => x.Sfskdd == "是").Sum(x => decimal.Parse(x.Jksyj ?? "0")); salary.UpgradePerformance = myPerf.Where(x => x.Sfskdd == "否").Sum(x => decimal.Parse(x.Jksyj ?? "0")); // 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; // 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); if (myTeam != null) { salary.GoldTriangleId = myTeam.TeamId; salary.GoldTriangleTeam = myTeam.TeamName ?? ""; } employeeStats[empId] = salary; } // 3. 处理战队逻辑 (考勤规则) // 规则:若出勤天数 < 21天,则该健康师不计入战队,按单人计算。 // 按战队分组 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 >= 21) { validMembers.Add(member); } else { invalidMembers.Add(member); } } // 对于无效成员,移除战队标识,视为单人 foreach (var member in invalidMembers) { member.GoldTriangleId = null; member.GoldTriangleTeam = null; } // 计算有效战队的总业绩 var teamTotalPerformance = validMembers.Sum(x => x.TotalPerformance); // 更新有效成员的战队业绩 foreach (var member in validMembers) { member.TeamPerformance = teamTotalPerformance; } } // 4. 计算薪资 (底薪 & 提成) foreach (var salary in employeeStats.Values) { // 4.1 底薪计算 salary.HealthCoachBaseSalary = CalculateBaseSalary(salary.Consumption, salary.ProjectCount); // 4.2 提成计算 // 单人业绩 <= 6000 无提成 if (salary.TotalPerformance <= 6000) { salary.TotalCommission = 0; salary.BasePerformanceCommission = 0; salary.CooperationPerformanceCommission = 0; salary.ConsultantCommission = 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); } else { // 单人 (或被剔除出战队) commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance); } salary.CommissionPoint = commissionPoint; // 计算基础/合作提成 salary.BasePerformanceCommission = salary.BasePerformance * 0.95m * commissionPoint; salary.CooperationPerformanceCommission = salary.CooperationPerformance * 0.95m * 0.65m * commissionPoint; // 计算顾问提成 // 检查是否是顾问 var isConsultant = teamMembers.Any(x => x.UserId == salary.EmployeeId && x.IsLeader == 1); if (isConsultant && !string.IsNullOrEmpty(salary.GoldTriangleId)) { salary.ConsultantCommission = CalculateConsultantCommission(salary.TeamPerformance, employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList()); } salary.TotalCommission = salary.BasePerformanceCommission + salary.CooperationPerformanceCommission + salary.ConsultantCommission; } // 计算占比 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 consumption, decimal projectCount) { // 0星:<1w 或 <96个 -> 1800 // 1星:>=1w 且 >=96个 -> 2000 // 2星:>=2w 且 >=126个 -> 2200 // 3星:>=4w 且 >=156个 -> 2400 // 特殊规则:若消耗或项目数中仅一项未达标(0星),底薪按1星(2000元)计算 int starCons = 0; if (consumption >= 40000) starCons = 3; else if (consumption >= 20000) starCons = 2; else if (consumption >= 10000) starCons = 1; int starProj = 0; if (projectCount >= 156) starProj = 3; else if (projectCount >= 126) starProj = 2; else if (projectCount >= 96) starProj = 1; int finalStar = Math.Min(starCons, starProj); // 特殊规则处理: 仅一项未达标(0星) -> 1星 if (finalStar == 0 && (starCons > 0 || starProj > 0)) { finalStar = 1; } switch (finalStar) { case 3: return 2400; case 2: return 2200; case 1: return 2000; default: return 1800; } } /// /// 获取战队提成点 /// private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance) { 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人 { if (teamPerformance >= 60000) return 0.06m; if (teamPerformance >= 40000) return 0.05m; if (teamPerformance >= 20000) return 0.04m; if (teamPerformance >= 10000) return 0.03m; } return 0; } /// /// 计算顾问提成 /// private decimal CalculateConsultantCommission(decimal teamPerformance, List teamMembers) { // 顾问提成规则: // 高级顾问:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8% // 普通顾问:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3% // 这里的“组员业绩达到X%以上”理解为:除顾问外的成员业绩占比?或者每个成员都达标? // 通常理解为:团队中是否有成员业绩贡献较高,或者团队整体结构健康。 // 假设“组员业绩达到X%”是指:团队中至少有一名成员(非顾问本人?)或者所有成员平均? // 鉴于规则模糊,这里先简化实现:暂只考核总业绩和消耗。 // 消耗是团队总消耗吗?假设是。 var teamConsumption = teamMembers.Sum(x => x.Consumption); // 高级顾问 if (teamPerformance >= 60000 && teamConsumption >= 60000) { return teamPerformance * 0.008m; } // 普通顾问 if (teamPerformance >= 40000 && teamConsumption >= 40000) { return teamPerformance * 0.003m; } return 0; } } }