using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_hytk_mx; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_md_general_manager_lifeline; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics; using NCC.Extend.Entitys.lq_xmzl; using NCC.System.Entitys.Permission; using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Yitter.IdGenerator; using Newtonsoft.Json; namespace NCC.Extend { /// /// 科技部总经理薪酬服务 /// [ApiDescriptionSettings(Tag = "科技部总经理薪酬服务", Name = "LqTechGeneralManagerSalary", Order = 305)] [Route("api/Extend/[controller]")] public class LqTechGeneralManagerSalaryService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; /// /// 初始化一个类型的新实例 /// public LqTechGeneralManagerSalaryService(ISqlSugarClient db) { _db = db; } /// /// 获取科技部总经理工资列表 /// /// 查询参数 /// 科技部总经理工资分页列表 [HttpGet("tech-general-manager")] public async Task GetTechGeneralManagerSalaryList([FromQuery] TechGeneralManagerSalaryInput input) { var monthStr = $"{input.Year}{input.Month:D2}"; // 1. 检查当月是否已生成工资数据 var exists = await _db.Queryable() .AnyAsync(x => x.StatisticsMonth == monthStr); // 2. 如果没有数据,则进行计算 if (!exists) { await CalculateTechGeneralManagerSalary(input.Year, input.Month); } // 3. 查询数据 var query = _db.Queryable() .Where(x => x.StatisticsMonth == monthStr); if (!string.IsNullOrEmpty(input.Position)) { query = query.Where(x => x.Position == input.Position); } 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 TechGeneralManagerSalaryOutput { Id = x.Id, StatisticsMonth = x.StatisticsMonth, Position = x.Position, EmployeeName = x.EmployeeName, EmployeeId = x.EmployeeId, EmployeeAccount = x.EmployeeAccount, IsTerminated = x.IsTerminated, StoreDetail = x.StoreDetail, TraceabilityAmount = x.TraceabilityAmount, CellAmount = x.CellAmount, BaseSalary = x.BaseSalary, TraceabilityCommissionRate = x.TraceabilityCommissionRate, TraceabilityCommissionAmount = x.TraceabilityCommissionAmount, CellCommissionRate = x.CellCommissionRate, CellCommissionAmount = x.CellCommissionAmount, TotalCommission = x.TotalCommission, WorkingDays = x.WorkingDays, LeaveDays = x.LeaveDays, CalculatedGrossSalary = x.CalculatedGrossSalary, 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, UpdateTime = x.UpdateTime }) .ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(list); } /// /// 计算科技部总经理工资 /// /// 年份 /// 月份 /// [HttpPost("calculate/tech-general-manager")] public async Task CalculateTechGeneralManagerSalary(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_ORGANIZE表查找组织名称包含"科技一部"或"科技二部"的组织 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(); if (!techOrganizeList.Any()) { // 如果没有找到科技部组织,直接返回 return; } var techOrganizeIds = techOrganizeList.Select(x => x.Id).ToList(); var techOrganizeDict = techOrganizeList.ToDictionary(x => x.Id, x => x.FullName); // 1.2 从BASE_USER表查询岗位为"总经理"且组织ID在科技一部或科技二部的员工 var techGeneralManagerUserList = await _db.Queryable() .Where(x => x.Gw == "总经理" && techOrganizeIds.Contains(x.OrganizeId) && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob }) .ToListAsync(); if (!techGeneralManagerUserList.Any()) { // 如果没有科技部总经理员工,直接返回 return; } if (!techGeneralManagerUserList.Any()) { // 如果没有科技部总经理员工,直接返回 return; } // 1.3 获取科技部总经理归属信息(从lq_md_general_manager_lifeline表) // 通过门店的科技部组织ID(kjb字段)找到科技一部或科技二部管理的门店 // 然后在lifeline表中找到这些门店的记录,这些记录对应的总经理就是科技部总经理 var lifelineList = await _db.Queryable( (lifeline, store) => lifeline.StoreId == store.Id) .Where((lifeline, store) => lifeline.Month == monthStr && techOrganizeIds.Contains(store.Kjb)) .Select((lifeline, store) => lifeline) .ToListAsync(); // 1.4 获取科技一部和科技二部管理的门店(通过门店的kjb字段) var techManagedStoreIds = await _db.Queryable() .Where(x => techOrganizeIds.Contains(x.Kjb)) .Select(x => x.Id) .ToListAsync(); // 1.5 按科技部总经理ID分组,获取每个科技部总经理管理的门店 // 科技部总经理管理的门店 = 科技一部/科技二部管理的所有门店(通过门店的kjb字段确定) var managerStoreDict = new Dictionary>(); foreach (var managerUser in techGeneralManagerUserList) { var managerId = managerUser.Id; var managerOrganizeId = managerUser.OrganizeId; // 如果该总经理属于科技一部,则管理所有科技一部的门店 // 如果该总经理属于科技二部,则管理所有科技二部的门店 var managedStores = await _db.Queryable() .Where(x => x.Kjb == managerOrganizeId) .Select(x => x.Id) .ToListAsync(); managerStoreDict[managerId] = managedStores; } // 1.4 门店信息 (lq_mdxx) var storeList = await _db.Queryable().ToListAsync(); var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x); // 1.6 考勤数据 (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); // 1.7 获取所有管理的门店ID列表(用于后续查询,如果没有管理的门店,则为空列表) var allManagedStoreIds = managerStoreDict.Values.SelectMany(x => x).Distinct().ToList(); // 1.8 按科技部总经理和门店分组统计(用于生成门店明细JSON和汇总数据) var storeDetailDict = new Dictionary>(); // 按门店统计溯源和Cell金额(如果有管理的门店) if (allManagedStoreIds.Any()) { foreach (var storeId in allManagedStoreIds) { // 该门店的开单溯源金额 var storeTraceabilityBilling = await _db.Queryable( (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id) .Where((pxmx, billing, item) => pxmx.IsEffective == 1 && billing.IsEffective == 1 && item.IsEffective == 1 && (pxmx.BeautyType == "溯源系统" || pxmx.BeautyType == "溯源" || item.BeautyType == "溯源系统" || item.BeautyType == "溯源") && billing.Djmd == storeId && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1)) .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m; // 该门店的退卡溯源金额 var storeTraceabilityRefund = await _db.Queryable( (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id) .Where((tkmx, refund, item) => tkmx.IsEffective == 1 && refund.IsEffective == 1 && item.IsEffective == 1 && (tkmx.BeautyType == "溯源系统" || tkmx.BeautyType == "溯源" || item.BeautyType == "溯源系统" || item.BeautyType == "溯源") && refund.Md == storeId && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1)) .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m; // 该门店的开单Cell金额 var storeCellBilling = await _db.Queryable( (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id) .Where((pxmx, billing, item) => pxmx.IsEffective == 1 && billing.IsEffective == 1 && item.IsEffective == 1 && (pxmx.BeautyType == "cell" || pxmx.BeautyType == "Cell" || item.BeautyType == "cell" || item.BeautyType == "Cell") && billing.Djmd == storeId && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1)) .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m; // 该门店的退卡Cell金额 var storeCellRefund = await _db.Queryable( (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id) .Where((tkmx, refund, item) => tkmx.IsEffective == 1 && refund.IsEffective == 1 && item.IsEffective == 1 && (tkmx.BeautyType == "cell" || tkmx.BeautyType == "Cell" || item.BeautyType == "cell" || item.BeautyType == "Cell") && refund.Md == storeId && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1)) .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m; // 获取该门店属于哪些科技部总经理 // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理 var store = storeDict.ContainsKey(storeId) ? storeDict[storeId] : null; var managersOfStore = new List(); if (store != null && !string.IsNullOrEmpty(store.Kjb)) { // 找到组织ID等于门店kjb的科技部总经理 var managers = techGeneralManagerUserList .Where(x => x.OrganizeId == store.Kjb) .Select(x => x.Id) .ToList(); managersOfStore.AddRange(managers); } foreach (var managerId in managersOfStore) { if (!storeDetailDict.ContainsKey(managerId)) { storeDetailDict[managerId] = new Dictionary(); } var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : ""; storeDetailDict[managerId][storeId] = new StoreDetailItem { StoreId = storeId, StoreName = storeName, TraceabilityBillingAmount = storeTraceabilityBilling, TraceabilityRefundAmount = storeTraceabilityRefund, TraceabilityAmount = storeTraceabilityBilling - storeTraceabilityRefund, CellBillingAmount = storeCellBilling, CellRefundAmount = storeCellRefund, CellAmount = storeCellBilling - storeCellRefund }; } } } // 2. 按科技部总经理聚合数据 var managerStats = new Dictionary(); foreach (var managerUser in techGeneralManagerUserList) { var managerId = managerUser.Id; // 获取该科技部总经理管理的门店列表 var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List(); // 2.1 创建工资统计对象 // 岗位使用组织名称(科技一部/科技二部) var position = techOrganizeDict.ContainsKey(managerUser.OrganizeId) ? techOrganizeDict[managerUser.OrganizeId] : ""; var salary = new LqTechGeneralManagerSalaryStatisticsEntity { Id = YitIdHelper.NextId().ToString(), StatisticsMonth = monthStr, EmployeeId = managerId, Position = position, EmployeeName = managerUser.RealName ?? "", EmployeeAccount = managerUser.Account ?? "", IsTerminated = managerUser.IsOnJob == 0 ? 1 : 0, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsLocked = 0 }; // 2.2 考勤数据 var attendance = attendanceDict.ContainsKey(managerId) ? attendanceDict[managerId] : null; salary.WorkingDays = attendance?.WorkDays ?? 0; salary.LeaveDays = attendance?.LeaveDays ?? 0; // 2.3 计算底薪(固定4000元) salary.BaseSalary = 4000m; // 2.4 统计该科技部总经理管理的所有门店的溯源金额和Cell金额总和 decimal totalTraceabilityAmount = 0m; decimal totalCellAmount = 0m; var storeDetails = new List(); if (managedStores.Any() && storeDetailDict.ContainsKey(managerId)) { foreach (var storeId in managedStores) { if (storeDetailDict[managerId].ContainsKey(storeId)) { var storeDetail = storeDetailDict[managerId][storeId]; totalTraceabilityAmount += storeDetail.TraceabilityAmount; totalCellAmount += storeDetail.CellAmount; storeDetails.Add(storeDetail); } } } salary.TraceabilityAmount = totalTraceabilityAmount; salary.CellAmount = totalCellAmount; // 2.5 保存门店明细(JSON格式) salary.StoreDetail = JsonConvert.SerializeObject(storeDetails); // 2.6 计算溯源金额提成(分段累进) var traceabilityCommission = CalculateTraceabilityCommission(totalTraceabilityAmount); salary.TraceabilityCommissionAmount = traceabilityCommission.Amount; salary.TraceabilityCommissionRate = traceabilityCommission.Rate; // 2.7 计算Cell金额提成(分段累进) var cellCommission = CalculateCellCommission(totalCellAmount); salary.CellCommissionAmount = cellCommission.Amount; salary.CellCommissionRate = cellCommission.Rate; // 2.8 提成合计 salary.TotalCommission = salary.TraceabilityCommissionAmount + salary.CellCommissionAmount; // 2.9 计算应发工资 salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission; salary.FinalGrossSalary = salary.CalculatedGrossSalary; // 2.10 初始化其他字段(默认值为0) 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; managerStats[managerId] = salary; } // 3. 保存数据 if (managerStats.Any()) { // 先删除当月旧数据 (防止重复) await _db.Deleteable() .Where(x => x.StatisticsMonth == monthStr) .ExecuteCommandAsync(); await _db.Insertable(managerStats.Values.ToList()).ExecuteCommandAsync(); } } /// /// 计算溯源金额提成(分段累进) /// /// 溯源金额 /// 提成金额和平均比例 private (decimal Amount, decimal? Rate) CalculateTraceabilityCommission(decimal traceabilityAmount) { if (traceabilityAmount <= 0) { return (0m, null); } decimal commissionAmount = 0m; decimal? averageRate = null; if (traceabilityAmount < 200000m) { // < 200,000元:1% commissionAmount = traceabilityAmount * 0.01m; averageRate = 1.00m; } else if (traceabilityAmount < 300000m) { // 200,000-300,000元:1.5% commissionAmount = 200000m * 0.01m + (traceabilityAmount - 200000m) * 0.015m; averageRate = (commissionAmount / traceabilityAmount) * 100m; } else if (traceabilityAmount < 500000m) { // 300,000-500,000元:2% commissionAmount = 200000m * 0.01m + 100000m * 0.015m + (traceabilityAmount - 300000m) * 0.02m; averageRate = (commissionAmount / traceabilityAmount) * 100m; } else { // ≥ 500,000元:2.5% commissionAmount = 200000m * 0.01m + 100000m * 0.015m + 200000m * 0.02m + (traceabilityAmount - 500000m) * 0.025m; averageRate = (commissionAmount / traceabilityAmount) * 100m; } return (commissionAmount, averageRate); } /// /// 计算Cell金额提成(分段累进) /// /// Cell金额 /// 提成金额和平均比例 private (decimal Amount, decimal? Rate) CalculateCellCommission(decimal cellAmount) { if (cellAmount <= 0) { return (0m, null); } if (cellAmount < 50000m) { // < 50,000元:无提成 return (0m, null); } decimal commissionAmount = 0m; decimal? averageRate = null; if (cellAmount < 400000m) { // 50,000-400,000元:1% commissionAmount = (cellAmount - 50000m) * 0.01m; averageRate = (commissionAmount / cellAmount) * 100m; } else { // ≥ 400,000元:1.5% commissionAmount = 350000m * 0.01m + (cellAmount - 400000m) * 0.015m; averageRate = (commissionAmount / cellAmount) * 100m; } return (commissionAmount, averageRate); } /// /// 门店明细项(用于JSON序列化) /// private class StoreDetailItem { public string StoreId { get; set; } public string StoreName { get; set; } public decimal TraceabilityBillingAmount { get; set; } public decimal TraceabilityRefundAmount { get; set; } public decimal TraceabilityAmount { get; set; } public decimal CellBillingAmount { get; set; } public decimal CellRefundAmount { get; set; } public decimal CellAmount { get; set; } } } }