diff --git a/antis-ncc-admin/src/views/wageManagement/director-detail-dialog.vue b/antis-ncc-admin/src/views/wageManagement/director-detail-dialog.vue index 2774b45..e4645c5 100644 --- a/antis-ncc-admin/src/views/wageManagement/director-detail-dialog.vue +++ b/antis-ncc-admin/src/views/wageManagement/director-detail-dialog.vue @@ -288,7 +288,7 @@ export default { getFieldsByCategory(category) { if (!this.detailData) return [] - const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'GoldTriangleTeam', 'IsNewStore', 'NewStoreProtectionStage', 'IsLocked'] + const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'GoldTriangleTeam', 'IsNewStore', 'NewStoreProtectionStage', 'IsLocked', 'StoreId', 'EmployeeId', 'SalesPerformance'] const categoryMap = { performance: ['Performance', 'Lifeline', 'CompletionRate', 'Reached', 'HeadCount', 'Target', 'Consume'], @@ -356,10 +356,11 @@ export default { const moneyFields = ['performance', 'commission', 'salary', 'subsidy', 'deduction', 'amount', 'fee', 'bonus', 'deposit', 'supplement', 'payment', 'consumption', 'reward', 'handwork', 'gross', 'guaranteed', 'transportation', 'allowance', 'total'] const percentFields = ['point', 'rate', 'percentage', 'percent'] - if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) { - return 'money' - } else if (percentFields.some(field => lowerKey.includes(field))) { + // 注意:先检查百分比字段,避免 CommissionRate 等字段被误识别为金额 + if (percentFields.some(field => lowerKey.includes(field))) { return 'percent' + } else if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) { + return 'money' } return 'text' }, @@ -371,6 +372,12 @@ export default { 'StoreTotalPerformance': '门店总业绩', 'StoreBillingPerformance': '门店开单业绩', 'StoreRefundPerformance': '门店退卡业绩', + 'SalesPerformance': '销售业绩', + 'ProductMaterial': '产品物料', + 'CooperationCost': '合作项目成本', + 'StoreExpense': '店内支出', + 'LaundryCost': '洗毛巾费用', + 'GrossProfit': '毛利', 'StoreLifeline': '门店生命线', 'PerformanceCompletionRate': '业绩完成率', 'TotalPerformance': '总业绩', @@ -456,7 +463,13 @@ export default { 'StoreType': '门店类型', 'StoreCategory': '门店类别', 'IsNewStore': '是否新店', - 'NewStoreProtectionStage': '新店保护阶段' + 'NewStoreProtectionStage': '新店保护阶段', + 'StoreId': '门店ID', + 'EmployeeId': '员工ID', + 'StatisticsMonth': '统计月份', + 'EmployeeConfirmStatus': '确认状态', + 'EmployeeConfirmTime': '员工确认时间', + 'EmployeeConfirmRemark': '员工确认备注' } // 如果映射中存在,直接返回中文 @@ -571,14 +584,24 @@ export default { return highlightFields.includes(key) }, // 获取字段顺序 + // 注意:毛利(GrossProfit)需要放在生命线提成比例(CommissionRateBelowLifeline)前面 getFieldOrder(key) { const orderMap = { 'TotalPerformance': 1, 'BasePerformance': 2, 'CooperationPerformance': 3, - 'TotalCommission': 10, - 'BasePerformanceCommission': 11, - 'CooperationPerformanceCommission': 12, + 'StoreTotalPerformance': 4, + 'SalesPerformance': 5, + 'GrossProfit': 6, // 毛利放在生命线提成比例前面 + 'StoreLifeline': 7, + 'CommissionRateBelowLifeline': 8, // 生命线提成比例 + 'CommissionRateAboveLifeline': 9, + 'CommissionAmountBelowLifeline': 10, + 'CommissionAmountAboveLifeline': 11, + 'TotalCommissionAmount': 12, + 'TotalCommission': 13, + 'BasePerformanceCommission': 14, + 'CooperationPerformanceCommission': 15, 'FinalGrossSalary': 20, 'ActualSalary': 21, 'TotalSubsidy': 30, diff --git a/antis-ncc-admin/src/views/wageManagement/director.vue b/antis-ncc-admin/src/views/wageManagement/director.vue index 1cfaeae..a454ea1 100644 --- a/antis-ncc-admin/src/views/wageManagement/director.vue +++ b/antis-ncc-admin/src/views/wageManagement/director.vue @@ -418,7 +418,7 @@ export default { } const columns = {} - const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth'] + const excludeFields = ['StoreName', 'EmployeeName', 'Position', 'Id', 'CreateTime', 'UpdateTime', 'CreateUser', 'UpdateUser', 'StatisticsMonth', 'StoreId', 'EmployeeId', 'SalesPerformance'] // 金额字段关键词 const moneyFields = ['Performance', 'Commission', 'Salary', 'Subsidy', 'Deduction', 'Amount', 'Fee', 'Bonus', 'Deposit', 'Supplement', 'Payment', 'Consumption', 'Reward', 'Handwork', 'Gross', 'Guaranteed', 'Transportation', 'Allowance', 'Total'] @@ -426,8 +426,12 @@ export default { const percentFields = ['Point', 'Rate', 'Percentage', 'Percent'] // 定义字段顺序(重要字段优先) + // 注意:毛利(GrossProfit)需要放在生命线提成比例(CommissionRateBelowLifeline)前面 const fieldOrder = [ 'GoldTriangleTeam', 'TotalPerformance', 'BasePerformance', 'CooperationPerformance', + 'StoreTotalPerformance', 'GrossProfit', + 'StoreLifeline', 'CommissionRateBelowLifeline', 'CommissionRateAboveLifeline', + 'CommissionAmountBelowLifeline', 'CommissionAmountAboveLifeline', 'TotalCommissionAmount', 'TotalCommission', 'BasePerformanceCommission', 'CooperationPerformanceCommission', 'FinalGrossSalary', 'ActualSalary', 'TotalSubsidy', 'TotalDeduction', 'IsLocked', 'IsNewStore', 'NewStoreProtectionStage' @@ -487,11 +491,12 @@ export default { const moneyFields = ['performance', 'commission', 'salary', 'subsidy', 'deduction', 'amount', 'fee', 'bonus', 'deposit', 'supplement', 'payment', 'consumption', 'reward', 'handwork', 'gross', 'guaranteed', 'transportation', 'allowance', 'total'] const percentFields = ['point', 'rate', 'percentage', 'percent'] - // 注意:payment 需要排除 PaymentStatus 这种状态字段 - if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) { - return 'money' - } else if (percentFields.some(field => lowerKey.includes(field))) { + // 注意:先检查百分比字段,避免 CommissionRate 等字段被误识别为金额 + // payment 需要排除 PaymentStatus 这种状态字段 + if (percentFields.some(field => lowerKey.includes(field))) { return 'percent' + } else if (moneyFields.some(field => lowerKey.includes(field)) && !lowerKey.includes('status')) { + return 'money' } return 'text' }, @@ -504,6 +509,12 @@ export default { 'StoreTotalPerformance': '门店总业绩', 'StoreBillingPerformance': '门店开单业绩', 'StoreRefundPerformance': '门店退卡业绩', + 'SalesPerformance': '销售业绩', + 'ProductMaterial': '产品物料', + 'CooperationCost': '合作项目成本', + 'StoreExpense': '店内支出', + 'LaundryCost': '洗毛巾费用', + 'GrossProfit': '毛利', 'StoreLifeline': '门店生命线', 'PerformanceCompletionRate': '业绩完成率', 'TotalPerformance': '总业绩', @@ -584,7 +595,13 @@ export default { 'StoreType': '门店类型', 'StoreCategory': '门店类别', 'IsNewStore': '是否新店', - 'NewStoreProtectionStage': '新店保护阶段' + 'NewStoreProtectionStage': '新店保护阶段', + 'StoreId': '门店ID', + 'EmployeeId': '员工ID', + 'StatisticsMonth': '统计月份', + 'EmployeeConfirmStatus': '确认状态', + 'EmployeeConfirmTime': '员工确认时间', + 'EmployeeConfirmRemark': '员工确认备注' } return labelMap[key] || key diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDirectorSalary/DirectorSalaryOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDirectorSalary/DirectorSalaryOutput.cs index b661810..3d50ab0 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDirectorSalary/DirectorSalaryOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqDirectorSalary/DirectorSalaryOutput.cs @@ -236,6 +236,121 @@ namespace NCC.Extend.Entitys.Dto.LqDirectorSalary /// 新店保护阶段 /// public int NewStoreProtectionStage { get; set; } + + /// + /// 门店ID + /// + public string StoreId { get; set; } + + /// + /// 员工ID + /// + public string EmployeeId { get; set; } + + /// + /// 统计月份(YYYYMM) + /// + public string StatisticsMonth { get; set; } + + /// + /// 销售业绩(开单业绩-退款业绩) + /// + public decimal SalesPerformance { get; set; } + + /// + /// 产品物料(仓库领用金额) + /// + public decimal ProductMaterial { get; set; } + + /// + /// 合作项目成本 + /// + public decimal CooperationCost { get; set; } + + /// + /// 店内支出 + /// + public decimal StoreExpense { get; set; } + + /// + /// 洗毛巾费用 + /// + public decimal LaundryCost { get; set; } + + /// + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾) + /// + public decimal GrossProfit { get; set; } + + /// + /// 缺卡扣款 + /// + public decimal MissingCard { get; set; } + + /// + /// 迟到扣款 + /// + public decimal LateArrival { get; set; } + + /// + /// 请假扣款 + /// + public decimal LeaveDeduction { get; set; } + + /// + /// 扣社保 + /// + public decimal SocialInsuranceDeduction { get; set; } + + /// + /// 扣除奖励 + /// + public decimal RewardDeduction { get; set; } + + /// + /// 扣住宿费 + /// + public decimal AccommodationDeduction { get; set; } + + /// + /// 扣学习期费用 + /// + public decimal StudyPeriodDeduction { get; set; } + + /// + /// 扣工作服费用 + /// + public decimal WorkClothesDeduction { get; set; } + + /// + /// 当月培训补贴 + /// + public decimal MonthlyTrainingSubsidy { get; set; } + + /// + /// 当月交通补贴 + /// + public decimal MonthlyTransportSubsidy { get; set; } + + /// + /// 上月培训补贴 + /// + public decimal LastMonthTrainingSubsidy { get; set; } + + /// + /// 上月交通补贴 + /// + public decimal LastMonthTransportSubsidy { get; set; } + + /// + /// 员工确认时间 + /// + public DateTime? EmployeeConfirmTime { get; set; } + + /// + /// 员工确认备注 + /// + public string EmployeeConfirmRemark { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs index 09e3973..d2f03c9 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs @@ -80,12 +80,24 @@ namespace NCC.Extend var list = await query.Select(x => new DirectorSalaryOutput { Id = x.Id, + StoreId = x.StoreId, StoreName = x.StoreName, + EmployeeId = x.EmployeeId, EmployeeName = x.EmployeeName, Position = x.Position, + StatisticsMonth = x.StatisticsMonth, + StoreType = x.StoreType, + StoreCategory = x.StoreCategory, + IsNewStore = x.IsNewStore, + NewStoreProtectionStage = x.NewStoreProtectionStage, StoreTotalPerformance = x.StoreTotalPerformance, StoreBillingPerformance = x.StoreBillingPerformance, StoreRefundPerformance = x.StoreRefundPerformance, + ProductMaterial = x.ProductMaterial, + CooperationCost = x.CooperationCost, + StoreExpense = x.StoreExpense, + LaundryCost = x.LaundryCost, + GrossProfit = x.GrossProfit, StoreLifeline = x.StoreLifeline, PerformanceCompletionRate = x.PerformanceCompletionRate, PerformanceReached = x.PerformanceReached, @@ -108,7 +120,19 @@ namespace NCC.Extend LeaveDays = x.LeaveDays, GrossSalary = x.GrossSalary, ActualSalary = x.ActualSalary, + 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, + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy, + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy, + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy, + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy, TotalSubsidy = x.TotalSubsidy, Bonus = x.Bonus, ReturnPhoneDeposit = x.ReturnPhoneDeposit, @@ -120,11 +144,9 @@ namespace NCC.Extend MonthlyTotalPayment = x.MonthlyTotalPayment, IsLocked = x.IsLocked, EmployeeConfirmStatus = x.EmployeeConfirmStatus, - UpdateTime = x.UpdateTime, - StoreType = x.StoreType, - StoreCategory = x.StoreCategory, - IsNewStore = x.IsNewStore, - NewStoreProtectionStage = x.NewStoreProtectionStage + EmployeeConfirmTime = x.EmployeeConfirmTime, + EmployeeConfirmRemark = x.EmployeeConfirmRemark, + UpdateTime = x.UpdateTime }) .ToPagedListAsync(input.currentPage, input.pageSize); @@ -147,36 +169,72 @@ namespace NCC.Extend .Select(x => new DirectorSalaryOutput { Id = x.Id, + StoreId = x.StoreId, StoreName = x.StoreName, + EmployeeId = x.EmployeeId, EmployeeName = x.EmployeeName, Position = x.Position, + StatisticsMonth = x.StatisticsMonth, + StoreType = x.StoreType, + StoreCategory = x.StoreCategory, + IsNewStore = x.IsNewStore, + NewStoreProtectionStage = x.NewStoreProtectionStage, StoreTotalPerformance = x.StoreTotalPerformance, StoreBillingPerformance = x.StoreBillingPerformance, StoreRefundPerformance = x.StoreRefundPerformance, + ProductMaterial = x.ProductMaterial, + CooperationCost = x.CooperationCost, + StoreExpense = x.StoreExpense, + LaundryCost = x.LaundryCost, + GrossProfit = x.GrossProfit, StoreLifeline = x.StoreLifeline, PerformanceCompletionRate = x.PerformanceCompletionRate, + PerformanceReached = x.PerformanceReached, + HeadCountReached = x.HeadCountReached, + ConsumeReached = x.ConsumeReached, + AssessmentDeduction = x.AssessmentDeduction, + UnreachedIndicatorCount = x.UnreachedIndicatorCount, + HeadCount = x.HeadCount, + TargetHeadCount = x.TargetHeadCount, + StoreConsume = x.StoreConsume, + TargetConsume = x.TargetConsume, CommissionRateBelowLifeline = x.CommissionRateBelowLifeline, CommissionRateAboveLifeline = x.CommissionRateAboveLifeline, CommissionAmountBelowLifeline = x.CommissionAmountBelowLifeline, CommissionAmountAboveLifeline = x.CommissionAmountAboveLifeline, TotalCommissionAmount = x.TotalCommissionAmount, BaseSalary = x.BaseSalary, + ActualBaseSalary = x.ActualBaseSalary, WorkingDays = x.WorkingDays, LeaveDays = x.LeaveDays, GrossSalary = x.GrossSalary, ActualSalary = x.ActualSalary, + 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, + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy, + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy, + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy, + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy, TotalSubsidy = x.TotalSubsidy, Bonus = x.Bonus, + ReturnPhoneDeposit = x.ReturnPhoneDeposit, + ReturnAccommodationDeposit = x.ReturnAccommodationDeposit, MonthlyPaymentStatus = x.MonthlyPaymentStatus, PaidAmount = x.PaidAmount, PendingAmount = x.PendingAmount, + LastMonthSupplement = x.LastMonthSupplement, + MonthlyTotalPayment = x.MonthlyTotalPayment, IsLocked = x.IsLocked, EmployeeConfirmStatus = x.EmployeeConfirmStatus, - StoreType = x.StoreType, - StoreCategory = x.StoreCategory, - IsNewStore = x.IsNewStore, - NewStoreProtectionStage = x.NewStoreProtectionStage, + EmployeeConfirmTime = x.EmployeeConfirmTime, + EmployeeConfirmRemark = x.EmployeeConfirmRemark, UpdateTime = x.UpdateTime }) .FirstAsync(); @@ -478,8 +536,8 @@ namespace NCC.Extend // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost; - // 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算) - salary.StoreTotalPerformance = salary.GrossProfit; + // 2.7 StoreTotalPerformance保存开单业绩-退卡业绩(销售业绩),提成计算使用毛利(GrossProfit) + salary.StoreTotalPerformance = salary.SalesPerformance; // 2.8 计算业绩完成率(基于毛利与生命线比较) if (salary.StoreLifeline > 0) @@ -670,9 +728,10 @@ namespace NCC.Extend return; } - // 提成计算基于毛利(StoreTotalPerformance存储的是毛利) + // 提成计算基于毛利(GrossProfit) // 重要:提成基数使用毛利,不是销售业绩(开单-退卡) - decimal grossProfit = salary.StoreTotalPerformance; // 这里已经是毛利了 + // StoreTotalPerformance存储的是开单业绩-退卡业绩,而提成需要使用毛利 + decimal grossProfit = salary.GrossProfit; decimal lifeline = salary.StoreLifeline; // 确定提成比例(根据新店/老店和门店分类) @@ -1057,59 +1116,159 @@ namespace NCC.Extend CreateUser = "" }; - // Excel字段映射(主任工资43列,Excel顺序:门店名称,员工姓名,岗位,实发工资,补贴合计,扣款合计,是否锁定,是否新店,新店保护阶段,门店总业绩...) + // Excel字段映射 + // 向后兼容:先按旧格式读取(43列格式) + // 如果Excel列数更多,则按新格式读取(支持新增字段) entity.StoreName = storeName; entity.EmployeeName = employeeName; entity.Position = GetColumnValue(2 + offset); - entity.ActualSalary = ParseDecimal(GetColumnValue(3 + offset)); - entity.TotalSubsidy = ParseDecimal(GetColumnValue(4 + offset)); - entity.TotalDeduction = ParseDecimal(GetColumnValue(5 + offset)); - // 跳过"是否锁定"字段(第7列),在最后处理 - entity.IsNewStore = GetColumnValue(7 + offset) == "是" ? "是" : "否"; - entity.NewStoreProtectionStage = ParseInt(GetColumnValue(8 + offset)); - entity.StoreTotalPerformance = ParseDecimal(GetColumnValue(9 + offset)); - entity.StoreBillingPerformance = ParseDecimal(GetColumnValue(10 + offset)); - entity.StoreRefundPerformance = ParseDecimal(GetColumnValue(11 + offset)); - entity.StoreLifeline = ParseDecimal(GetColumnValue(12 + offset)); - entity.PerformanceCompletionRate = ParseDecimal(GetColumnValue(13 + offset)); - entity.PerformanceReached = GetColumnValue(14 + offset); - entity.HeadCountReached = GetColumnValue(15 + offset); - entity.ConsumeReached = GetColumnValue(16 + offset); - entity.AssessmentDeduction = ParseDecimal(GetColumnValue(17 + offset)); - entity.UnreachedIndicatorCount = ParseInt(GetColumnValue(18 + offset)); - entity.HeadCount = ParseInt(GetColumnValue(19 + offset)); - entity.TargetHeadCount = ParseDecimal(GetColumnValue(20 + offset)); - entity.StoreConsume = ParseDecimal(GetColumnValue(21 + offset)); - entity.TargetConsume = ParseDecimal(GetColumnValue(22 + offset)); - entity.CommissionRateBelowLifeline = ParseDecimal(GetColumnValue(23 + offset)); - entity.CommissionRateAboveLifeline = ParseDecimal(GetColumnValue(24 + offset)); - entity.CommissionAmountBelowLifeline = ParseDecimal(GetColumnValue(25 + offset)); - entity.CommissionAmountAboveLifeline = ParseDecimal(GetColumnValue(26 + offset)); - entity.TotalCommissionAmount = ParseDecimal(GetColumnValue(27 + offset)); - entity.BaseSalary = ParseDecimal(GetColumnValue(28 + offset)); - entity.ActualBaseSalary = ParseDecimal(GetColumnValue(29 + offset)); - entity.WorkingDays = ParseInt(GetColumnValue(30 + offset)); - entity.LeaveDays = ParseInt(GetColumnValue(31 + offset)); - entity.GrossSalary = ParseDecimal(GetColumnValue(32 + offset)); - entity.Bonus = ParseDecimal(GetColumnValue(33 + offset)); - entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(34 + offset)); - entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(35 + offset)); - entity.MonthlyPaymentStatus = GetColumnValue(36 + offset); - entity.PaidAmount = ParseDecimal(GetColumnValue(37 + offset)); - entity.PendingAmount = ParseDecimal(GetColumnValue(38 + offset)); - entity.LastMonthSupplement = ParseDecimal(GetColumnValue(39 + offset)); - entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(40 + offset)); - // 处理门店类型和类别 - var storeTypeStr = GetColumnValue(41 + offset); - if (!string.IsNullOrWhiteSpace(storeTypeStr) && int.TryParse(storeTypeStr, out int storeType)) - entity.StoreType = storeType; - var storeCategoryStr = GetColumnValue(42 + offset); - if (!string.IsNullOrWhiteSpace(storeCategoryStr) && int.TryParse(storeCategoryStr, out int storeCategory)) - entity.StoreCategory = storeCategory; - // 处理锁定状态(第7列) - var isLockedStr = GetColumnValue(6 + offset); - if (isLockedStr == "已锁定" || isLockedStr == "1" || isLockedStr == "锁定") entity.IsLocked = 1; - else entity.IsLocked = 0; + + // 旧格式兼容(保持向后兼容,Excel顺序:门店名称,员工姓名,岗位,实发工资,补贴合计,扣款合计,是否锁定,是否新店,新店保护阶段,门店总业绩...) + if (dataTable.Columns.Count <= 43 + offset) + { + // 旧格式(43列) + entity.ActualSalary = ParseDecimal(GetColumnValue(3 + offset)); + entity.TotalSubsidy = ParseDecimal(GetColumnValue(4 + offset)); + entity.TotalDeduction = ParseDecimal(GetColumnValue(5 + offset)); + var isLockedStr = GetColumnValue(6 + offset); + if (isLockedStr == "已锁定" || isLockedStr == "1" || isLockedStr == "锁定") entity.IsLocked = 1; + else entity.IsLocked = 0; + entity.IsNewStore = GetColumnValue(7 + offset) == "是" ? "是" : "否"; + entity.NewStoreProtectionStage = ParseInt(GetColumnValue(8 + offset)); + entity.StoreTotalPerformance = ParseDecimal(GetColumnValue(9 + offset)); + entity.StoreBillingPerformance = ParseDecimal(GetColumnValue(10 + offset)); + entity.StoreRefundPerformance = ParseDecimal(GetColumnValue(11 + offset)); + entity.StoreLifeline = ParseDecimal(GetColumnValue(12 + offset)); + entity.PerformanceCompletionRate = ParseDecimal(GetColumnValue(13 + offset)); + entity.PerformanceReached = GetColumnValue(14 + offset); + entity.HeadCountReached = GetColumnValue(15 + offset); + entity.ConsumeReached = GetColumnValue(16 + offset); + entity.AssessmentDeduction = ParseDecimal(GetColumnValue(17 + offset)); + entity.UnreachedIndicatorCount = ParseInt(GetColumnValue(18 + offset)); + entity.HeadCount = ParseInt(GetColumnValue(19 + offset)); + entity.TargetHeadCount = ParseDecimal(GetColumnValue(20 + offset)); + entity.StoreConsume = ParseDecimal(GetColumnValue(21 + offset)); + entity.TargetConsume = ParseDecimal(GetColumnValue(22 + offset)); + entity.CommissionRateBelowLifeline = ParseDecimal(GetColumnValue(23 + offset)); + entity.CommissionRateAboveLifeline = ParseDecimal(GetColumnValue(24 + offset)); + entity.CommissionAmountBelowLifeline = ParseDecimal(GetColumnValue(25 + offset)); + entity.CommissionAmountAboveLifeline = ParseDecimal(GetColumnValue(26 + offset)); + entity.TotalCommissionAmount = ParseDecimal(GetColumnValue(27 + offset)); + entity.BaseSalary = ParseDecimal(GetColumnValue(28 + offset)); + entity.ActualBaseSalary = ParseDecimal(GetColumnValue(29 + offset)); + entity.WorkingDays = ParseInt(GetColumnValue(30 + offset)); + entity.LeaveDays = ParseInt(GetColumnValue(31 + offset)); + entity.GrossSalary = ParseDecimal(GetColumnValue(32 + offset)); + entity.Bonus = ParseDecimal(GetColumnValue(33 + offset)); + entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(34 + offset)); + entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(35 + offset)); + entity.MonthlyPaymentStatus = GetColumnValue(36 + offset); + entity.PaidAmount = ParseDecimal(GetColumnValue(37 + offset)); + entity.PendingAmount = ParseDecimal(GetColumnValue(38 + offset)); + entity.LastMonthSupplement = ParseDecimal(GetColumnValue(39 + offset)); + entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(40 + offset)); + var storeTypeStr = GetColumnValue(41 + offset); + if (!string.IsNullOrWhiteSpace(storeTypeStr) && int.TryParse(storeTypeStr, out int storeType)) + entity.StoreType = storeType; + var storeCategoryStr = GetColumnValue(42 + offset); + if (!string.IsNullOrWhiteSpace(storeCategoryStr) && int.TryParse(storeCategoryStr, out int storeCategory)) + entity.StoreCategory = storeCategory; + } + else + { + // 新格式:按列名匹配(更灵活) + // 由于前端导出是基于 tableColumns 动态生成,列顺序可能变化 + // 这里使用列名匹配方式,提高兼容性 + var columnNameMap = new Dictionary(); + for (int colIdx = 0; colIdx < dataTable.Columns.Count; colIdx++) + { + var colName = dataTable.Columns[colIdx].ColumnName?.Trim() ?? ""; + if (!string.IsNullOrWhiteSpace(colName)) + { + columnNameMap[colName] = colIdx; + } + } + + // 辅助方法:根据列名获取值 + Func GetValueByColumnName = (columnName) => + { + if (columnNameMap.ContainsKey(columnName)) + { + var colIdx = columnNameMap[columnName]; + return GetColumnValue(colIdx); + } + return ""; + }; + + // 按列名读取字段(支持新字段) + entity.StoreTotalPerformance = ParseDecimal(GetValueByColumnName("门店总业绩")); + entity.StoreBillingPerformance = ParseDecimal(GetValueByColumnName("门店开单业绩")); + entity.StoreRefundPerformance = ParseDecimal(GetValueByColumnName("门店退卡业绩")); + // 销售业绩字段不在导入中处理,仅在计算时使用 + entity.ProductMaterial = ParseDecimal(GetValueByColumnName("产品物料")); + entity.CooperationCost = ParseDecimal(GetValueByColumnName("合作项目成本")); + entity.StoreExpense = ParseDecimal(GetValueByColumnName("店内支出")); + entity.LaundryCost = ParseDecimal(GetValueByColumnName("洗毛巾费用")); + entity.GrossProfit = ParseDecimal(GetValueByColumnName("毛利")); + entity.StoreLifeline = ParseDecimal(GetValueByColumnName("门店生命线")); + entity.PerformanceCompletionRate = ParseDecimal(GetValueByColumnName("业绩完成率")); + entity.PerformanceReached = GetValueByColumnName("业绩是否达标"); + entity.HeadCountReached = GetValueByColumnName("人头是否达标"); + entity.ConsumeReached = GetValueByColumnName("消耗是否达标"); + entity.AssessmentDeduction = ParseDecimal(GetValueByColumnName("考核扣款金额")); + entity.UnreachedIndicatorCount = ParseInt(GetValueByColumnName("未达标指标数量")); + entity.HeadCount = ParseInt(GetValueByColumnName("进店消耗人数")); + entity.TargetHeadCount = ParseDecimal(GetValueByColumnName("目标人头数")); + entity.StoreConsume = ParseDecimal(GetValueByColumnName("门店消耗金额")); + entity.TargetConsume = ParseDecimal(GetValueByColumnName("目标消耗金额")); + entity.CommissionRateBelowLifeline = ParseDecimal(GetValueByColumnName("≤生命线部分提成比例")); + entity.CommissionRateAboveLifeline = ParseDecimal(GetValueByColumnName(">生命线部分提成比例")); + entity.CommissionAmountBelowLifeline = ParseDecimal(GetValueByColumnName("≤生命线部分提成金额")); + entity.CommissionAmountAboveLifeline = ParseDecimal(GetValueByColumnName(">生命线部分提成金额")); + entity.TotalCommissionAmount = ParseDecimal(GetValueByColumnName("提成总金额")); + entity.BaseSalary = ParseDecimal(GetValueByColumnName("底薪金额")); + entity.ActualBaseSalary = ParseDecimal(GetValueByColumnName("实际底薪")); + entity.WorkingDays = ParseInt(GetValueByColumnName("在店天数")); + entity.LeaveDays = ParseInt(GetValueByColumnName("请假天数")); + entity.GrossSalary = ParseDecimal(GetValueByColumnName("应发工资")); + entity.ActualSalary = ParseDecimal(GetValueByColumnName("实发工资")); + entity.MissingCard = ParseDecimal(GetValueByColumnName("缺卡扣款")); + entity.LateArrival = ParseDecimal(GetValueByColumnName("迟到扣款")); + entity.LeaveDeduction = ParseDecimal(GetValueByColumnName("请假扣款")); + entity.SocialInsuranceDeduction = ParseDecimal(GetValueByColumnName("扣社保")); + entity.RewardDeduction = ParseDecimal(GetValueByColumnName("扣除奖励")); + entity.AccommodationDeduction = ParseDecimal(GetValueByColumnName("扣住宿费")); + entity.StudyPeriodDeduction = ParseDecimal(GetValueByColumnName("扣学习期费用")); + entity.WorkClothesDeduction = ParseDecimal(GetValueByColumnName("扣工作服费用")); + entity.TotalDeduction = ParseDecimal(GetValueByColumnName("扣款合计")); + entity.MonthlyTrainingSubsidy = ParseDecimal(GetValueByColumnName("当月培训补贴")); + entity.MonthlyTransportSubsidy = ParseDecimal(GetValueByColumnName("当月交通补贴")); + entity.LastMonthTrainingSubsidy = ParseDecimal(GetValueByColumnName("上月培训补贴")); + entity.LastMonthTransportSubsidy = ParseDecimal(GetValueByColumnName("上月交通补贴")); + entity.TotalSubsidy = ParseDecimal(GetValueByColumnName("补贴合计")); + entity.Bonus = ParseDecimal(GetValueByColumnName("发奖金")); + entity.ReturnPhoneDeposit = ParseDecimal(GetValueByColumnName("退手机押金")); + entity.ReturnAccommodationDeposit = ParseDecimal(GetValueByColumnName("退住宿押金")); + entity.MonthlyPaymentStatus = GetValueByColumnName("当月是否发放"); + entity.PaidAmount = ParseDecimal(GetValueByColumnName("支付金额")); + entity.PendingAmount = ParseDecimal(GetValueByColumnName("待支付金额")); + entity.LastMonthSupplement = ParseDecimal(GetValueByColumnName("补发上月")); + entity.MonthlyTotalPayment = ParseDecimal(GetValueByColumnName("当月支付总额")); + var isLockedStrNew = GetValueByColumnName("锁定状态"); + if (isLockedStrNew == "已锁定" || isLockedStrNew == "1" || isLockedStrNew == "锁定") entity.IsLocked = 1; + else entity.IsLocked = 0; + var confirmStatusStr = GetValueByColumnName("确认状态"); + if (confirmStatusStr == "已确认" || confirmStatusStr == "1") entity.EmployeeConfirmStatus = 1; + else entity.EmployeeConfirmStatus = 0; + var storeTypeStrNew = GetValueByColumnName("门店类型"); + if (!string.IsNullOrWhiteSpace(storeTypeStrNew) && int.TryParse(storeTypeStrNew, out int storeTypeNew)) + entity.StoreType = storeTypeNew; + var storeCategoryStrNew = GetValueByColumnName("门店类别"); + if (!string.IsNullOrWhiteSpace(storeCategoryStrNew) && int.TryParse(storeCategoryStrNew, out int storeCategoryNew)) + entity.StoreCategory = storeCategoryNew; + entity.IsNewStore = GetValueByColumnName("是否新店") == "是" ? "是" : "否"; + entity.NewStoreProtectionStage = ParseInt(GetValueByColumnName("新店保护阶段")); + } if (existing != null) { @@ -1159,10 +1318,12 @@ namespace NCC.Extend if (recordsToInsert.Any()) await _db.Insertable(recordsToInsert).ExecuteCommandAsync(); if (recordsToUpdate.Any()) { - // 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新 + // 使用IgnoreColumns排除CreateTime、CreateUser、StoreId和EmployeeId,确保这些字段不会被更新 await _db.Updateable(recordsToUpdate) .IgnoreColumns(x => x.CreateTime) .IgnoreColumns(x => x.CreateUser) + .IgnoreColumns(x => x.StoreId) + .IgnoreColumns(x => x.EmployeeId) .ExecuteCommandAsync(); } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs index 68d1ed8..2fc079a 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs @@ -2152,10 +2152,121 @@ namespace NCC.Extend.LqKhxx var itemDetailIds = itemDetails.Select(x => (string)x.Id).ToList(); - // 5. 批量查询消耗品项 + // 4.5. 双向查询:直接查询会员的所有耗卡记录,确保不遗漏数据 + // 通过耗卡记录反向查找可能遗漏的开单品项明细和开单记录 + var allConsumedItemIds = new HashSet(); + var additionalBillingItemIds = new HashSet(); + + if (memberIdsList.Any()) + { + // 查询会员的所有耗卡记录 + var allConsumedItemsFromConsume = await _db.Queryable( + (pxmx, hyhk) => new JoinQueryInfos(JoinType.Inner, pxmx.ConsumeInfoId == hyhk.Id)) + .Where((pxmx, hyhk) => memberIdsList.Contains(hyhk.Hy) + && pxmx.IsEffective == StatusEnum.有效.GetHashCode() + && hyhk.IsEffective == StatusEnum.有效.GetHashCode()) + .Where((pxmx, hyhk) => !string.IsNullOrEmpty(pxmx.BillingItemId)) + .Select((pxmx, hyhk) => new + { + pxmx.BillingItemId, + pxmx.Px, + pxmx.Pxmc, + pxmx.Pxjg, + pxmx.ProjectNumber, + pxmx.TotalPrice + }) + .ToListAsync(); + + // 收集所有耗卡记录关联的开单品项明细ID + foreach (var consumedItem in allConsumedItemsFromConsume) + { + if (!string.IsNullOrEmpty(consumedItem.BillingItemId)) + { + allConsumedItemIds.Add(consumedItem.BillingItemId); + // 如果这个BillingItemId不在当前的itemDetailIds中,需要额外查询 + if (!itemDetailIds.Contains(consumedItem.BillingItemId)) + { + additionalBillingItemIds.Add(consumedItem.BillingItemId); + } + } + } + + // 如果有额外的开单品项明细ID,查询这些品项明细对应的开单记录 + if (additionalBillingItemIds.Any()) + { + // 查询这些开单品项明细 + var additionalItemDetailsData = await _db.Queryable() + .Where(x => additionalBillingItemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + x.Id, + x.Glkdbh, + x.Px, + x.Pxmc, + x.Pxjg, + x.SourceType, + x.ProjectNumber, + x.TotalPrice + }) + .ToListAsync(); + + // 将额外的品项明细添加到itemDetails中 + itemDetails.AddRange(additionalItemDetailsData.Cast()); + itemDetailIds.AddRange(additionalItemDetailsData.Select(x => x.Id)); + + // 查询这些品项明细对应的开单记录(如果不在当前的billingIds中) + var additionalBillingIds = additionalItemDetailsData + .Select(x => x.Glkdbh) + .Where(x => !string.IsNullOrEmpty(x) && !billingIds.Contains(x)) + .Distinct() + .ToList(); + + if (additionalBillingIds.Any()) + { + var additionalBillingRecords = await _db.Queryable() + .Where(x => additionalBillingIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + x.Id, + x.Kdhy, + x.Kdrq, + x.Djmd, + x.CreateUser + }) + .ToListAsync(); + + // 将额外的开单记录添加到billingRecords中(转换为dynamic以便后续使用) + foreach (var record in additionalBillingRecords) + { + billingRecords.Add(record); + } + billingIds.AddRange(additionalBillingIds); + } + } + } + + // 5. 批量查询消耗品项(包括所有通过耗卡找到的BillingItemId) var consumedItems = new List(); - if (itemDetailIds.Any()) + if (allConsumedItemIds.Any()) { + // 使用allConsumedItemIds(来自耗卡记录的所有BillingItemId)查询消耗品项 + var consumedItemsData = await _db.Queryable() + .Where(x => allConsumedItemIds.Contains(x.BillingItemId) && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + x.BillingItemId, + x.Px, + x.Pxmc, + x.Pxjg, + x.ProjectNumber, + x.TotalPrice + }) + .ToListAsync(); + consumedItems = consumedItemsData.Cast().ToList(); + } + else if (itemDetailIds.Any()) + { + // 兼容原有逻辑:如果没有耗卡记录,使用itemDetailIds查询 var consumedItemsData = await _db.Queryable() .Where(x => itemDetailIds.Contains(x.BillingItemId) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new @@ -2171,7 +2282,7 @@ namespace NCC.Extend.LqKhxx consumedItems = consumedItemsData.Cast().ToList(); } - // 6. 批量查询退卡品项 + // 6. 批量查询退卡品项(使用itemDetailIds,因为退卡也是关联开单品项明细) var refundedItems = new List(); if (itemDetailIds.Any()) { @@ -2190,6 +2301,113 @@ namespace NCC.Extend.LqKhxx refundedItems = refundedItemsData.Cast().ToList(); } + // 6.5. 双向查询退卡品项:直接从会员的退卡记录查询,确保不遗漏 + if (memberIdsList.Any()) + { + var allRefundedItemsFromRefund = await _db.Queryable( + (mx, hytk) => new JoinQueryInfos(JoinType.Inner, mx.RefundInfoId == hytk.Id)) + .Where((mx, hytk) => memberIdsList.Contains(hytk.Hy) + && mx.IsEffective == StatusEnum.有效.GetHashCode() + && hytk.IsEffective == StatusEnum.有效.GetHashCode()) + .Where((mx, hytk) => !string.IsNullOrEmpty(mx.BillingItemId)) + .Select((mx, hytk) => new + { + mx.BillingItemId, + mx.Px, + mx.Pxmc, + mx.Pxjg, + mx.ProjectNumber, + mx.Tkje + }) + .ToListAsync(); + + // 收集退卡记录关联的开单品项明细ID + var refundedBillingItemIds = allRefundedItemsFromRefund + .Select(x => x.BillingItemId) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + .ToList(); + + // 如果退卡关联的开单品项明细不在当前的itemDetailIds中,也需要查询对应的开单记录 + var missingRefundedBillingItemIds = refundedBillingItemIds + .Where(x => !itemDetailIds.Contains(x)) + .ToList(); + + if (missingRefundedBillingItemIds.Any()) + { + // 查询这些开单品项明细 + var refundedAdditionalItemDetailsData = await _db.Queryable() + .Where(x => missingRefundedBillingItemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + x.Id, + x.Glkdbh, + x.Px, + x.Pxmc, + x.Pxjg, + x.SourceType, + x.ProjectNumber, + x.TotalPrice + }) + .ToListAsync(); + + // 将额外的品项明细添加到itemDetails中 + itemDetails.AddRange(refundedAdditionalItemDetailsData.Cast()); + itemDetailIds.AddRange(refundedAdditionalItemDetailsData.Select(x => x.Id)); + + // 查询这些品项明细对应的开单记录(如果不在当前的billingIds中) + var refundedAdditionalBillingIds = refundedAdditionalItemDetailsData + .Select(x => x.Glkdbh) + .Where(x => !string.IsNullOrEmpty(x) && !billingIds.Contains(x)) + .Distinct() + .ToList(); + + if (refundedAdditionalBillingIds.Any()) + { + var refundedAdditionalBillingRecords = await _db.Queryable() + .Where(x => refundedAdditionalBillingIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + x.Id, + x.Kdhy, + x.Kdrq, + x.Djmd, + x.CreateUser + }) + .ToListAsync(); + + // 将额外的开单记录添加到billingRecords中(转换为dynamic以便后续使用) + foreach (var record in refundedAdditionalBillingRecords) + { + billingRecords.Add(record); + } + billingIds.AddRange(refundedAdditionalBillingIds); + } + + // 将退卡数据添加到refundedItems中(去重) + foreach (var refundedItem in allRefundedItemsFromRefund) + { + var refundedItemDynamic = refundedItem as dynamic; + if (!refundedItems.Any(x => x.BillingItemId?.ToString() == refundedItemDynamic.BillingItemId?.ToString())) + { + refundedItems.Add(refundedItemDynamic); + } + } + } + else + { + // 如果所有退卡记录关联的开单品项明细都在itemDetailIds中,只需要合并退卡数据(去重) + foreach (var refundedItem in allRefundedItemsFromRefund) + { + var refundedItemDynamic = refundedItem as dynamic; + if (!refundedItems.Any(x => x.BillingItemId?.ToString() == refundedItemDynamic.BillingItemId?.ToString())) + { + refundedItems.Add(refundedItemDynamic); + } + } + } + } + // 7. 批量查询储扣品项 var deductedItems = new List(); if (billingIds.Any()) diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs index db2e3c0..cf54677 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs @@ -262,6 +262,44 @@ namespace NCC.Extend.LqReimbursementApplication List queryCompletionTime = input.completionTime != null ? input.completionTime.Split(',').ToObeject>() : null; DateTime? startCompletionTime = queryCompletionTime != null ? Ext.GetDateTime(queryCompletionTime.First()) : null; DateTime? endCompletionTime = queryCompletionTime != null ? Ext.GetDateTime(queryCompletionTime.Last()) : null; + // 如果提供了完成时间筛选,需要先查询符合条件的申请ID,然后在主查询中过滤 + List filteredApplicationIdsByCompletionTime = null; + if (queryCompletionTime != null) + { + var startDate = new DateTime(startCompletionTime.ToDate().Year, startCompletionTime.ToDate().Month, startCompletionTime.ToDate().Day, 0, 0, 0); + var endDate = new DateTime(endCompletionTime.ToDate().Year, endCompletionTime.ToDate().Month, endCompletionTime.ToDate().Day, 23, 59, 59); + + // 先查询实体类中有CompletionTime字段且符合条件的申请ID + var entitiesWithCompletionTime = await _db.Queryable() + .Where(x => x.CompletionTime.HasValue + && x.CompletionTime.Value >= startDate + && x.CompletionTime.Value <= endDate) + .Select(x => x.Id) + .ToListAsync(); + + // 查询实体类中没有CompletionTime字段的申请,需要从审批记录中获取完成时间 + var entitiesWithoutCompletionTime = await _db.Queryable() + .Where(x => !x.CompletionTime.HasValue) + .Select(x => x.Id) + .ToListAsync(); + + var applicationIdsFromRecords = new List(); + if (entitiesWithoutCompletionTime.Any()) + { + // 从审批记录中查询符合条件的申请ID + applicationIdsFromRecords = await _db.Queryable() + .Where(x => entitiesWithoutCompletionTime.Contains(x.ApplicationId) && x.ApprovalResult == "通过") + .GroupBy(x => x.ApplicationId) + .Having(x => SqlFunc.AggregateMax(x.ApprovalTime) >= startDate && + SqlFunc.AggregateMax(x.ApprovalTime) <= endDate) + .Select(x => x.ApplicationId) + .ToListAsync(); + } + + // 合并两种情况的申请ID + filteredApplicationIdsByCompletionTime = entitiesWithCompletionTime.Union(applicationIdsFromRecords).ToList(); + } + var query = _db.Queryable() .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) .WhereIF(!string.IsNullOrEmpty(input.applicationUserId), p => p.ApplicationUserId.Contains(input.applicationUserId)) @@ -274,7 +312,11 @@ namespace NCC.Extend.LqReimbursementApplication .WhereIF(!string.IsNullOrEmpty(input.approveStatus), p => (p.ApprovalStatus ?? p.ApproveStatus).Contains(input.approveStatus)) // .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0)) //.WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59)) - .WhereIF(!string.IsNullOrEmpty(input.purchaseRecordsId), p => p.PurchaseRecordsId.Contains(input.purchaseRecordsId)); + .WhereIF(!string.IsNullOrEmpty(input.purchaseRecordsId), p => p.PurchaseRecordsId.Contains(input.purchaseRecordsId)) + // 如果提供了完成时间筛选,在主查询中过滤 + .WhereIF(filteredApplicationIdsByCompletionTime != null && filteredApplicationIdsByCompletionTime.Any(), p => filteredApplicationIdsByCompletionTime.Contains(p.Id)) + // 如果没有符合条件的申请,返回空结果 + .WhereIF(filteredApplicationIdsByCompletionTime != null && !filteredApplicationIdsByCompletionTime.Any(), p => false); // 处理排序(兼容前端传入的字段名) if (string.IsNullOrEmpty(input.sidx)) @@ -343,31 +385,6 @@ namespace NCC.Extend.LqReimbursementApplication .GroupBy(x => (string)x.applicationId) .ToDictionary(g => g.Key, g => string.Join(", ", g.Select(x => (string)x.approverName))); - // 如果提供了完成时间筛选,需要先查询完成时间,然后过滤 - if (queryCompletionTime != null && applicationIds.Any()) - { - var completionTimeRecords = await _db.Queryable() - .Where(x => applicationIds.Contains(x.ApplicationId) && x.ApprovalResult == "通过") - .GroupBy(x => x.ApplicationId) - .Select(x => new - { - ApplicationId = x.ApplicationId, - MaxApprovalTime = SqlFunc.AggregateMax(x.ApprovalTime) - }) - .ToListAsync(); - - var filteredApplicationIds = completionTimeRecords - .Where(x => x.MaxApprovalTime.HasValue && - x.MaxApprovalTime.Value >= new DateTime(startCompletionTime.ToDate().Year, startCompletionTime.ToDate().Month, startCompletionTime.ToDate().Day, 0, 0, 0) && - x.MaxApprovalTime.Value <= new DateTime(endCompletionTime.ToDate().Year, endCompletionTime.ToDate().Month, endCompletionTime.ToDate().Day, 23, 59, 59)) - .Select(x => x.ApplicationId) - .ToList(); - - entities = entities.Where(x => filteredApplicationIds.Contains(x.Id)).ToList(); - total = entities.Count; - applicationIds = entities.Select(x => x.Id).ToList(); - } - // 获取门店名称 var storeIds = entities.Where(x => !string.IsNullOrEmpty(x.ApplicationStoreId)).Select(x => x.ApplicationStoreId).Distinct().ToList(); var storeDict = new Dictionary(); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs index cb2905f..8765b4d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs @@ -201,6 +201,24 @@ namespace NCC.Extend /// /// 计算科技部总经理工资 /// + /// + /// 计算科技部总经理的工资,包括底薪、溯源金额提成、Cell金额提成等。 + /// + /// 计算规则: + /// - 底薪:固定4000元 + /// - 溯源金额提成:根据管理的所有门店的溯源金额总和分段累进计算 + /// - Cell金额提成:根据管理的所有门店的Cell金额总和分段累进计算 + /// + /// 数据统计说明: + /// - 时间范围:严格按照当月范围(startDate 到 endDate 23:59:59),不包含下个月的数据 + /// - 数据查询:使用原生SQL在数据库层面转换和求和,确保与科技部驾驶舱接口的计算方式一致 + /// - 门店范围:通过门店的kjb字段确定科技部总经理管理的门店 + /// + /// 溯源金额和Cell金额的计算方式: + /// - 开单金额:从 lq_kd_jksyj 表统计(使用原生SQL CAST转换) + /// - 退卡金额:从 lq_hytk_jksyj 表统计(使用SqlSugar聚合查询) + /// - 净金额 = 开单金额 - 退卡金额 + /// /// 年份 /// 月份 /// @@ -210,6 +228,12 @@ namespace NCC.Extend var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var monthStr = $"{year}{month:D2}"; + // 使用与科技部驾驶舱接口相同的时间范围处理方式 + var endDateTime = monthStr == DateTime.Now.ToString("yyyyMM") + ? DateTime.Now + : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + + _logger.LogInformation($"[科技部总经理工资计算] 开始计算,月份: {monthStr}, 时间范围: {startDate.ToString("yyyy-MM-dd HH:mm:ss")} 到 {endDateTime.ToString("yyyy-MM-dd HH:mm:ss")}"); // 1. 获取基础数据 @@ -229,9 +253,9 @@ namespace NCC.Extend var techOrganizeIds = techOrganizeList.Select(x => x.Id).ToList(); var techOrganizeDict = techOrganizeList.ToDictionary(x => x.Id, x => x.FullName); - // 1.2 从BASE_USER表查询岗位为"总经理"且组织ID在科技一部或科技二部的员工 + // 1.2 从BASE_USER表查询岗位为"总经理"或"科技部总经理"且组织ID在科技一部或科技二部的员工 var techGeneralManagerUserList = await _db.Queryable() - .Where(x => x.Gw == "总经理" + .Where(x => (x.Gw == "总经理" || 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 }) @@ -239,13 +263,8 @@ namespace NCC.Extend if (!techGeneralManagerUserList.Any()) { - // 如果没有科技部总经理员工,直接返回 - return; - } - - if (!techGeneralManagerUserList.Any()) - { - // 如果没有科技部总经理员工,直接返回 + // 如果没有科技部总经理员工,记录日志并返回 + _logger.LogWarning($"[科技部总经理工资计算] 未找到科技部总经理员工,科技部组织ID: {string.Join(",", techOrganizeIds)}"); return; } @@ -301,51 +320,110 @@ namespace NCC.Extend var storeDetailDict = new Dictionary>(); // 按门店统计溯源和Cell金额(如果有管理的门店) + // 使用与科技部驾驶舱接口完全相同的查询方式:批量查询所有门店,然后按门店分组 if (allManagedStoreIds.Any()) { + // 时间格式化字符串(使用与科技部驾驶舱接口相同的格式化方式) + var startDateStr = startDate.ToString("yyyy-MM-dd HH:mm:ss"); + var endDateTimeStr = endDateTime.ToString("yyyy-MM-dd HH:mm:ss"); + + // 批量查询所有门店的开单溯源金额(与科技部驾驶舱接口保持一致) + var allStoreTraceabilityBillingSql = $@" + SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount + FROM lq_kd_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN ('{string.Join("','", allManagedStoreIds)}') + AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源') + AND yjsj >= '{startDateStr}' + AND yjsj <= '{endDateTimeStr}' + GROUP BY F_StoreId"; + var allStoreTraceabilityBillingResult = await _db.Ado.SqlQueryAsync(allStoreTraceabilityBillingSql); + var storeTraceabilityBillingDict = new Dictionary(); + if (allStoreTraceabilityBillingResult != null) + { + foreach (var item in allStoreTraceabilityBillingResult) + { + var storeId = item.F_StoreId?.ToString() ?? ""; + var amount = Convert.ToDecimal(item.Amount ?? 0); + storeTraceabilityBillingDict[storeId] = amount; + } + } + + // 批量查询所有门店的开单Cell金额(与科技部驾驶舱接口保持一致) + var allStoreCellBillingSql = $@" + SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount + FROM lq_kd_jksyj + WHERE F_IsEffective = 1 + AND F_StoreId IN ('{string.Join("','", allManagedStoreIds)}') + AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell') + AND yjsj >= '{startDateStr}' + AND yjsj <= '{endDateTimeStr}' + GROUP BY F_StoreId"; + var allStoreCellBillingResult = await _db.Ado.SqlQueryAsync(allStoreCellBillingSql); + var storeCellBillingDict = new Dictionary(); + if (allStoreCellBillingResult != null) + { + foreach (var item in allStoreCellBillingResult) + { + var storeId = item.F_StoreId?.ToString() ?? ""; + var amount = Convert.ToDecimal(item.Amount ?? 0); + storeCellBillingDict[storeId] = amount; + } + } + + // 批量查询所有门店的退卡溯源金额 + var allStoreTraceabilityRefundList = await _db.Queryable() + .Where(x => x.IsEffective == 1) + .Where(x => allManagedStoreIds.Contains(x.StoreId)) + .Where(x => (x.BeautyType == "溯源系统" || x.BeautyType == "溯源")) + .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date) + .GroupBy(x => x.StoreId) + .Select(x => new { StoreId = x.StoreId, Amount = SqlFunc.AggregateSum(x.Jksyj) }) + .ToListAsync(); + var storeTraceabilityRefundDict = new Dictionary(); + if (allStoreTraceabilityRefundList != null) + { + foreach (var item in allStoreTraceabilityRefundList) + { + var storeId = item.StoreId ?? ""; + var amount = Convert.ToDecimal(item.Amount ?? 0); + storeTraceabilityRefundDict[storeId] = amount; + } + } + + // 批量查询所有门店的退卡Cell金额 + var allStoreCellRefundList = await _db.Queryable() + .Where(x => x.IsEffective == 1) + .Where(x => allManagedStoreIds.Contains(x.StoreId)) + .Where(x => (x.BeautyType == "cell" || x.BeautyType == "Cell")) + .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date) + .GroupBy(x => x.StoreId) + .Select(x => new { StoreId = x.StoreId, Amount = SqlFunc.AggregateSum(x.Jksyj) }) + .ToListAsync(); + var storeCellRefundDict = new Dictionary(); + if (allStoreCellRefundList != null) + { + foreach (var item in allStoreCellRefundList) + { + var storeId = item.StoreId ?? ""; + var amount = Convert.ToDecimal(item.Amount ?? 0); + storeCellRefundDict[storeId] = amount; + } + } + + // 遍历所有门店,构建门店明细 foreach (var storeId in allManagedStoreIds) { - // 该门店的开单溯源金额(从健康师业绩表统计) - var storeTraceabilityBillingList = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.StoreId == storeId - && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源") - && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1)) - .Select(x => x.Jksyj) - .ToListAsync(); - - var storeTraceabilityBilling = storeTraceabilityBillingList - .Where(x => !string.IsNullOrEmpty(x)) - .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); - - // 该门店的退卡溯源金额(从退卡健康师业绩表统计) - var storeTraceabilityRefund = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.StoreId == storeId - && (x.BeautyType == "溯源系统" || x.BeautyType == "溯源") - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) - .SumAsync(x => (decimal?)x.Jksyj) ?? 0m; - - // 该门店的开单Cell金额(从健康师业绩表统计) - var storeCellBillingList = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.StoreId == storeId - && (x.BeautyType == "cell" || x.BeautyType == "Cell") - && x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1)) - .Select(x => x.Jksyj) - .ToListAsync(); - - var storeCellBilling = storeCellBillingList - .Where(x => !string.IsNullOrEmpty(x)) - .Sum(x => decimal.TryParse(x, out var val) ? val : 0m); - - // 该门店的退卡Cell金额(从退卡健康师业绩表统计) - var storeCellRefund = await _db.Queryable() - .Where(x => x.IsEffective == 1 - && x.StoreId == storeId - && (x.BeautyType == "cell" || x.BeautyType == "Cell") - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1)) - .SumAsync(x => (decimal?)x.Jksyj) ?? 0m; + var storeTraceabilityBilling = storeTraceabilityBillingDict.ContainsKey(storeId) ? storeTraceabilityBillingDict[storeId] : 0m; + var storeTraceabilityRefund = storeTraceabilityRefundDict.ContainsKey(storeId) ? storeTraceabilityRefundDict[storeId] : 0m; + var storeCellBilling = storeCellBillingDict.ContainsKey(storeId) ? storeCellBillingDict[storeId] : 0m; + var storeCellRefund = storeCellRefundDict.ContainsKey(storeId) ? storeCellRefundDict[storeId] : 0m; + + // 调试日志:记录关键门店的计算结果 + if (storeId == "1649328471923847173" || storeId == "1649328471923847175" || storeId == "1649328471923847187") + { + _logger.LogInformation($"[科技部总经理工资计算] 门店ID: {storeId}, 开单Cell金额: {storeCellBilling}, 退卡Cell金额: {storeCellRefund}, Cell金额: {storeCellBilling - storeCellRefund}"); + } // 获取该门店属于哪些科技部总经理 // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理 @@ -445,6 +523,8 @@ namespace NCC.Extend salary.TraceabilityAmount = totalTraceabilityAmount; salary.CellAmount = totalCellAmount; + _logger.LogInformation($"[科技部总经理工资计算] 员工: {salary.EmployeeName}, 溯源金额: {totalTraceabilityAmount}, Cell金额: {totalCellAmount}, 门店数: {storeDetails.Count}"); + // 2.5 保存门店明细(JSON格式) salary.StoreDetail = JsonConvert.SerializeObject(storeDetails); @@ -493,6 +573,8 @@ namespace NCC.Extend managerStats[managerId] = salary; } + _logger.LogInformation($"[科技部总经理工资计算] 共计算了 {managerStats.Count} 个科技部总经理的工资数据"); + // 3. 保存数据 if (managerStats.Any()) { @@ -508,7 +590,12 @@ namespace NCC.Extend if (existingDict.ContainsKey(salary.EmployeeId)) { var existing = existingDict[salary.EmployeeId]; - if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1) { skippedCount++; continue; } + if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1) + { + _logger.LogWarning($"[科技部总经理工资计算] 跳过更新,员工: {salary.EmployeeName}, IsLocked: {existing.IsLocked}, EmployeeConfirmStatus: {existing.EmployeeConfirmStatus}"); + skippedCount++; + continue; + } salary.Id = existing.Id; salary.EmployeeConfirmStatus = existing.EmployeeConfirmStatus; salary.EmployeeConfirmTime = existing.EmployeeConfirmTime; @@ -516,6 +603,8 @@ namespace NCC.Extend salary.IsLocked = existing.IsLocked; salary.CreateTime = existing.CreateTime; salary.CreateUser = existing.CreateUser; + salary.UpdateTime = DateTime.Now; // 强制更新UpdateTime + _logger.LogInformation($"[科技部总经理工资计算] 准备更新,员工: {salary.EmployeeName}, 旧Cell金额: {existing.CellAmount}, 新Cell金额: {salary.CellAmount}"); recordsToUpdate.Add(salary); } else @@ -529,7 +618,15 @@ namespace NCC.Extend } } if (recordsToInsert.Any()) await _db.Insertable(recordsToInsert).ExecuteCommandAsync(); - if (recordsToUpdate.Any()) await _db.Updateable(recordsToUpdate).ExecuteCommandAsync(); + if (recordsToUpdate.Any()) + { + // 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新 + await _db.Updateable(recordsToUpdate) + .IgnoreColumns(x => x.CreateTime) + .IgnoreColumns(x => x.CreateUser) + .ExecuteCommandAsync(); + _logger.LogInformation($"已更新 {recordsToUpdate.Count} 条科技部总经理工资记录(月份:{monthStr})"); + } if (skippedCount > 0) _logger.LogWarning($"计算工资时跳过了 {skippedCount} 条已锁定或已确认的记录(月份:{monthStr})"); } } @@ -626,7 +723,7 @@ namespace NCC.Extend public decimal TraceabilityAmount { get; set; } public decimal CellBillingAmount { get; set; } public decimal CellRefundAmount { get; set; } - public decimal CellAmount { get; set; } + public decimal CellAmount { get; set; } } #region 员工工资确认 @@ -793,11 +890,11 @@ namespace NCC.Extend if (lockedCount > 0 || unlockedCount > 0) { - var salariesToUpdate = salaries.Where(s => - (input.IsLocked && s.IsLocked == 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) @@ -809,10 +906,10 @@ namespace NCC.Extend 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}条(已确认的记录不能解锁)"; @@ -851,7 +948,7 @@ namespace NCC.Extend { 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)) @@ -901,7 +998,7 @@ namespace NCC.Extend var firstColumnValue = GetColumnValue(0); bool isOldFormat = !string.IsNullOrWhiteSpace(firstColumnValue) && (firstColumnValue == "员工姓名" || (!long.TryParse(firstColumnValue, out _) && firstColumnValue.Length > 20)); - + int employeeNameIndex = isOldFormat ? 0 : 1; int offset = isOldFormat ? 0 : 1; @@ -932,7 +1029,7 @@ namespace NCC.Extend { existing = await _db.Queryable() .Where(x => x.Id == id).FirstAsync(); - + if (existing != null && (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1)) { skippedCount++; @@ -1008,7 +1105,7 @@ namespace NCC.Extend if (user != null) entity.EmployeeId = user.Id; } } - + entity.UpdateTime = DateTime.Now; if (existing != null) recordsToUpdate.Add(entity); else recordsToInsert.Add(entity); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs index e8b6d95..b0273ed 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs @@ -416,7 +416,7 @@ namespace NCC.Extend // 总业绩 > 30,000元:只计算2%提成 salary.BaseSalary = 0; salary.BaseSalaryLevel = 0; - salary.PerformanceCommissionRate = 2m; + salary.PerformanceCommissionRate = 0.02m; // 保存为小数形式,0.02表示2% salary.PerformanceCommissionAmount = salary.TotalPerformance * 0.02m; salary.ConsumeCommissionRate = 0; salary.ConsumeCommissionAmount = 0; @@ -438,23 +438,45 @@ namespace NCC.Extend { // 在职员工正常计算 - // 3.1 计算底薪(根据项目数和总业绩) - var baseSalaryResult = CalculateBaseSalary(salary.ProjectCount, salary.TotalPerformance); - salary.BaseSalary = baseSalaryResult.BaseSalary; - salary.BaseSalaryLevel = baseSalaryResult.Level; + // 判断是否为T区员工(员工姓名包含"T区") + bool isTZoneEmployee = !string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区"); - // 3.2 计算业绩提成(分段累进) - var performanceCommissionResult = CalculatePerformanceCommission(salary.TotalPerformance); - salary.PerformanceCommissionRate = performanceCommissionResult.Rate; - salary.PerformanceCommissionAmount = performanceCommissionResult.Amount; + 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.3 计算消耗提成(分段累进,可能为负数) - var consumeCommissionResult = CalculateConsumeCommission(salary.ConsumeAchievement); - salary.ConsumeCommissionRate = consumeCommissionResult.Rate; - salary.ConsumeCommissionAmount = consumeCommissionResult.Amount; + // 3.2 计算业绩提成(分段累进,门槛改为3万) + var performanceCommissionResult = CalculatePerformanceCommission(salary.TotalPerformance); + salary.PerformanceCommissionRate = performanceCommissionResult.Rate; + salary.PerformanceCommissionAmount = performanceCommissionResult.Amount; - // 3.4 提成合计 - salary.TotalCommission = salary.PerformanceCommissionAmount + salary.ConsumeCommissionAmount; + // 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) @@ -603,30 +625,30 @@ namespace NCC.Extend /// 提成比例和金额 /// /// 提成规则(分段累进式): - /// 1. 前提条件:业绩必须大于1万才能进行提成 + /// 1. 前提条件:整月业绩必须大于等于3万才能进行提成(门槛从1万提高到3万) /// 2. 如果有提成资格后,分段计算: /// - 0-7万部分:2%(整个0-7万部分都按2%计算) /// - 7万-15万部分:2.5% /// - 15万以上部分:3% /// /// 计算公式(分段累进): - /// - 如果业绩 ≤ 1万:提成 = 0(无提成资格) - /// - 如果 1万 < 业绩 ≤ 7万:提成 = 业绩 × 2% + /// - 如果业绩 < 3万:提成 = 0(无提成资格) + /// - 如果 3万 ≤ 业绩 ≤ 7万:提成 = 业绩 × 2% /// - 如果 7万 < 业绩 ≤ 15万:提成 = 7万 × 2% + (业绩 - 7万) × 2.5% /// - 如果业绩 > 15万:提成 = 7万 × 2% + (15万 - 7万) × 2.5% + (业绩 - 15万) × 3% /// /// 示例: - /// - 总业绩 = 5,000元 → 提成 = 0(无提成资格) + /// - 总业绩 = 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) { - // 提成前提:业绩必须大于1万才能进行提成 - if (totalPerformance <= 10000m) + // 提成前提:整月业绩必须大于等于3万才能进行提成 + if (totalPerformance < 30000m) { - // ≤ 10,000元 → 0%(无提成资格) + // < 30,000元 → 0%(无提成资格) return (0m, 0m); } @@ -655,43 +677,61 @@ namespace NCC.Extend } else { - // 业绩 > 1万 且 ≤ 7万:整个业绩按2%计算 + // 业绩 ≥ 3万 且 ≤ 7万:整个业绩按2%计算 totalCommission = totalPerformance * 0.02m; } - // 计算平均提成比例(用于显示) - decimal averageRate = totalCommission > 0 && totalPerformance > 0 ? (totalCommission / totalPerformance) * 100m : 0m; + // 计算平均提成比例(保存为小数形式,如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 < 80000m) + if (consumeAchievement < 100000m) { - // < 80,000元 → 扣除300元(负数) + // < 100,000元(未完成10万底标)→ 扣除300元(负数) // 比例显示为0,金额为-300 return (0m, -300m); } - else if (consumeAchievement < 100000m) - { - // 80,000-100,000元 → 0.5% - return (0.5m, consumeAchievement * 0.005m); - } - else if (consumeAchievement < 200000m) + else if (consumeAchievement <= 200000m) { - // 100,000-200,000元 → 0.5% - return (0.5m, consumeAchievement * 0.005m); + // ≥ 100,000元 且 ≤ 200,000元 → 1-20万部分按0.5% + return (0.005m, consumeAchievement * 0.005m); // 保存为小数形式,0.005表示0.5% } else { - // > 200,000元 → 1% - return (1m, consumeAchievement * 0.01m); + // > 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); } } diff --git a/netcore/src/Modularity/System/NCC.System.Entitys/Mapper/SystemMapper.cs b/netcore/src/Modularity/System/NCC.System.Entitys/Mapper/SystemMapper.cs index 0dbcc45..27eb11c 100644 --- a/netcore/src/Modularity/System/NCC.System.Entitys/Mapper/SystemMapper.cs +++ b/netcore/src/Modularity/System/NCC.System.Entitys/Mapper/SystemMapper.cs @@ -2,6 +2,7 @@ using NCC.System.Entitys.Dto.System.DbBackup; using NCC.System.Entitys.Dto.System.Province; using NCC.System.Entitys.Model.System.DataBase; +using NCC.System.Entitys.Model.Permission.UsersCurrent; using NCC.System.Entitys.System; using Mapster; using SqlSugar; @@ -51,6 +52,8 @@ namespace NCC.System.Entitys.Mapper .Map(dest => dest.dataLength, src => src.Length.ToString()) .Map(dest => dest.primaryKey, src => src.IsPrimarykey ? 1 : 0) .Map(dest => dest.allowNull, src => src.IsNullable ? 1 : 0); + config.ForType() + .Map(dest => dest.description, src => src.Description); } } } diff --git a/netcore/src/Modularity/System/NCC.System.Entitys/Model/Permission/UsersCurrent/UsersCurrentAuthorizeMoldel.cs b/netcore/src/Modularity/System/NCC.System.Entitys/Model/Permission/UsersCurrent/UsersCurrentAuthorizeMoldel.cs index 3fff1cc..c12396e 100644 --- a/netcore/src/Modularity/System/NCC.System.Entitys/Model/Permission/UsersCurrent/UsersCurrentAuthorizeMoldel.cs +++ b/netcore/src/Modularity/System/NCC.System.Entitys/Model/Permission/UsersCurrent/UsersCurrentAuthorizeMoldel.cs @@ -30,5 +30,10 @@ namespace NCC.System.Entitys.Model.Permission.UsersCurrent /// [JsonIgnore] public string moduleId { get; set; } + + /// + /// 备注 + /// + public string description { get; set; } } }