using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Dependency; using NCC.DynamicApiController; using NCC.FriendlyException; using NCC.Extend.Entitys.Dto.LqTechDepartmentDashboard; using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_hytk_jksyj; using NCC.Extend.Entitys.lq_md_target; using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_xh_pxmx; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_tech_teacher_salary_statistics; using NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics; using NCC.Extend.Entitys.lq_xh_kjbsyj; using NCC.Extend.Entitys.lq_xmzl; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_kd_kjbsyj; using NCC.Extend.Entitys.lq_hytk_kjbsyj; using NCC.Extend.Entitys.lq_kd_deductinfo; using NCC.Extend.Entitys.lq_khxx; using NCC.Extend.Entitys; using NCC.Common.Filter; using NCC.System.Entitys.Permission; using SqlSugar; namespace NCC.Extend { /// /// 科技部驾驶舱服务 /// [ApiDescriptionSettings(Tag = "科技部驾驶舱服务", Name = "LqTechDepartmentDashboard", Order = 202)] [Route("api/Extend/[controller]")] public class LqTechDepartmentDashboardService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly ILogger _logger; /// /// 初始化科技部驾驶舱服务 /// public LqTechDepartmentDashboardService(ISqlSugarClient db, ILogger logger) { _db = db; _logger = logger; } // 辅助方法:获取门店列表 private async Task> GetStoreIdsAsync(string techDepartmentId, List storeIds, string statisticsMonth) { if (!string.IsNullOrWhiteSpace(techDepartmentId)) { return await _db.Queryable() .Where(x => x.TechDepartment == techDepartmentId && x.Month == statisticsMonth) .Select(x => x.StoreId) .Distinct() .ToListAsync(); } return storeIds ?? new List(); } // 辅助方法:获取门店列表(重载) private async Task> GetStoreIdsAsync(TechDepartmentDashboardStatisticsInput input) { return await GetStoreIdsAsync(input.TechDepartmentId, input.StoreIds, input.StatisticsMonth); } // 辅助方法:获取指定月份的门店列表 private async Task> GetStoreIdsForMonthAsync(string techDepartmentId, List storeIds, string month) { if (!string.IsNullOrWhiteSpace(techDepartmentId)) { return await _db.Queryable() .Where(x => x.TechDepartment == techDepartmentId && x.Month == month) .Select(x => x.StoreId) .Distinct() .ToListAsync(); } return storeIds ?? new List(); } // 辅助方法:计算单月业绩指标 private async Task<(decimal traceabilityAmount, decimal cellAmount, decimal totalKemeiIncome)> CalculateMonthlyPerformance(List storeIds, string month) { if (!storeIds.Any()) return (0, 0, 0); var year = int.Parse(month.Substring(0, 4)); var monthNum = int.Parse(month.Substring(4, 2)); var startDate = new DateTime(year, monthNum, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = month == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIdsStr = string.Join("','", storeIds); // 溯源金额 var billingTraceabilitySql = $@" SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount FROM lq_kd_jksyj WHERE F_IsEffective = 1 AND F_StoreId IN ('{storeIdsStr}') AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源') AND yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingTraceabilityResult = await _db.Ado.SqlQueryAsync(billingTraceabilitySql); var billingTraceabilityAmount = billingTraceabilityResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingTraceabilityResult.FirstOrDefault().Amount ?? 0) : 0m; var refundTraceabilityAmount = await _db.Queryable() .Where(x => x.IsEffective == 1) .Where(x => storeIds.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) .SumAsync(x => (decimal?)(x.Jksyj ?? 0)) ?? 0m; var traceabilityAmount = billingTraceabilityAmount - refundTraceabilityAmount; // Cell金额 var billingCellSql = $@" SELECT COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as Amount FROM lq_kd_jksyj WHERE F_IsEffective = 1 AND F_StoreId IN ('{storeIdsStr}') AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell') AND yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingCellResult = await _db.Ado.SqlQueryAsync(billingCellSql); var billingCellAmount = billingCellResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingCellResult.FirstOrDefault().Amount ?? 0) : 0m; var refundCellAmount = await _db.Queryable() .Where(x => x.IsEffective == 1) .Where(x => storeIds.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) .SumAsync(x => (decimal?)(x.Jksyj ?? 0)) ?? 0m; var cellAmount = billingCellAmount - refundCellAmount; var totalKemeiIncome = traceabilityAmount + cellAmount; return (traceabilityAmount, cellAmount, totalKemeiIncome); } /// /// 获取科技部驾驶舱统计数据 /// /// /// 获取指定科技部在指定月份的核心指标:溯源金额、Cell金额、科美总收入、管理的门店数、活跃门店数 /// /// 示例请求: /// ```json /// { /// "techDepartmentId": "1649328471923847169", /// "statisticsMonth": "202512" /// } /// ``` /// /// 或者: /// ```json /// { /// "storeIds": ["1649328471923847169", "1649328471923847170"], /// "statisticsMonth": "202512" /// } /// ``` /// /// 参数说明: /// - techDepartmentId: 科技部ID(BASE_ORGANIZE表的组织ID),与storeIds两者必填其一 /// - storeIds: 门店ID列表,与techDepartmentId两者必填其一 /// - statisticsMonth: 统计月份,格式为YYYYMM(必填) /// /// 返回数据说明: /// - TraceabilityAmount: 溯源金额(开单溯源金额 - 退卡溯源金额) /// - CellAmount: Cell金额(开单Cell金额 - 退卡Cell金额) /// - TotalKemeiIncome: 科美总收入(溯源金额 + Cell金额) /// - ManagedStoreCount: 管理的门店数 /// - ActiveStoreCount: 活跃门店数(有科美开单或消耗的门店数量) /// /// 查询参数 /// 科技部驾驶舱统计数据 /// 成功返回统计数据 /// 参数错误 /// 服务器错误 [HttpPost("GetStatistics")] public async Task GetStatistics([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) { throw NCCException.Oh("请求参数不能为空"); } if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) { throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); } // 参数验证:科技部ID和门店ID列表两者必填其一 if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) { throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); } if (!string.IsNullOrWhiteSpace(input.TechDepartmentId) && input.StoreIds != null && input.StoreIds.Any()) { throw NCCException.Oh("科技部ID和门店ID列表不能同时传入,请只传入其中一个"); } _logger.LogInformation("开始查询科技部驾驶舱统计数据,科技部ID:{TechDepartmentId},门店ID列表:{StoreIds},统计月份:{StatisticsMonth}", input.TechDepartmentId, input.StoreIds != null ? string.Join(",", input.StoreIds) : "", input.StatisticsMonth); // 解析月份获取时间范围 var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); // 获取门店列表 List storeIds; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { // 方式1:从lq_md_target表查询该月份、该科技部归属的门店列表 storeIds = await _db.Queryable() .Where(x => x.TechDepartment == input.TechDepartmentId && x.Month == input.StatisticsMonth) .Select(x => x.StoreId) .Distinct() .ToListAsync(); if (!storeIds.Any()) { _logger.LogWarning("科技部ID:{TechDepartmentId} 在 {StatisticsMonth} 月份没有管理的门店", input.TechDepartmentId, input.StatisticsMonth); // 返回空数据 return new TechDepartmentDashboardStatisticsOutput(); } } else { // 方式2:直接使用传入的门店ID列表 storeIds = input.StoreIds; } // 1. 统计开单溯源金额(使用SQL查询,因为jksyj字段是字符串类型) var billingTraceabilitySql = $@" SELECT 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("','", storeIds)}') AND (F_BeautyType = '溯源系统' OR F_BeautyType = '溯源') AND yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingTraceabilityResult = await _db.Ado.SqlQueryAsync(billingTraceabilitySql); var billingTraceabilityAmount = billingTraceabilityResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingTraceabilityResult.FirstOrDefault().Amount ?? 0) : 0m; // 2. 统计退卡溯源金额 var refundTraceabilityAmount = await _db.Queryable() .Where(x => x.IsEffective == 1) .Where(x => storeIds.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) .SumAsync(x => (decimal?)(x.Jksyj ?? 0)) ?? 0m; // 3. 计算净溯源金额 var traceabilityAmount = billingTraceabilityAmount - refundTraceabilityAmount; // 4. 统计开单Cell金额(使用SQL查询,因为jksyj字段是字符串类型) var billingCellSql = $@" SELECT 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("','", storeIds)}') AND (F_BeautyType = 'cell' OR F_BeautyType = 'Cell') AND yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingCellResult = await _db.Ado.SqlQueryAsync(billingCellSql); var billingCellAmount = billingCellResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingCellResult.FirstOrDefault().Amount ?? 0) : 0m; // 5. 统计退卡Cell金额 var refundCellAmount = await _db.Queryable() .Where(x => x.IsEffective == 1) .Where(x => storeIds.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) .SumAsync(x => (decimal?)(x.Jksyj ?? 0)) ?? 0m; // 6. 计算净Cell金额 var cellAmount = billingCellAmount - refundCellAmount; // 7. 计算科美总收入 var totalKemeiIncome = traceabilityAmount + cellAmount; // 8. 管理的门店数 var managedStoreCount = storeIds.Count; // 9. 统计活跃门店数(有科美开单或消耗的门店数量) // 先获取有科美开单的门店 var storesWithBilling = await _db.Queryable() .Where(x => x.IsEffective == 1) .Where(x => storeIds.Contains(x.StoreId)) .Where(x => (x.BeautyType == "溯源系统" || x.BeautyType == "溯源" || x.BeautyType == "cell" || x.BeautyType == "Cell")) .Where(x => x.Yjsj.HasValue && x.Yjsj.Value >= startDate && x.Yjsj.Value <= endDateTime) .Select(x => x.StoreId) .Distinct() .ToListAsync(); // 获取有科美消耗的门店(通过关联lq_xh_pxmx表,筛选科美类型) var storesWithConsumeSql = $@" SELECT DISTINCT xh.Md as StoreId FROM lq_xh_hyhk xh INNER JOIN lq_xh_pxmx px ON px.F_ConsumeInfoId = xh.F_Id INNER JOIN lq_xmzl xm ON px.px = xm.F_Id WHERE xh.F_IsEffective = 1 AND px.F_IsEffective = 1 AND xh.Md IN ('{string.Join("','", storeIds)}') AND xm.qt2 = '科美' AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var storesWithConsumeData = await _db.Ado.SqlQueryAsync(storesWithConsumeSql); var storesWithConsume = new List(); if (storesWithConsumeData != null) { foreach (var row in storesWithConsumeData) { var storeId = row?.StoreId?.ToString(); if (!string.IsNullOrEmpty(storeId) && !storesWithConsume.Contains(storeId)) { storesWithConsume.Add(storeId); } } } // 合并并去重 var allActiveStoreIds = storesWithBilling.Union(storesWithConsume).Distinct().ToList(); var activeStoreCount = allActiveStoreIds.Count; // 10. 统计消耗金额(科技部老师消耗业绩) var consumeAmount = 0m; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var consumeSql = $@" SELECT COALESCE(SUM(kjbsyj.kjblsyj), 0) as ConsumeAmount FROM lq_xh_kjbsyj kjbsyj WHERE kjbsyj.F_IsEffective = 1 AND kjbsyj.F_ItemCategory = '科美' AND kjbsyj.F_StoreId IN ('{storeIdsStr}') AND kjbsyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND kjbsyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var consumeResult = await _db.Ado.SqlQueryAsync(consumeSql); consumeAmount = consumeResult?.FirstOrDefault() != null ? Convert.ToDecimal(consumeResult.FirstOrDefault().ConsumeAmount ?? 0) : 0m; } // 11. 统计手工费(科技部老师手工费) var handworkFee = 0m; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var handworkFeeSql = $@" SELECT COALESCE(SUM(kjbsyj.F_LaborCost), 0) as HandworkFee FROM lq_xh_kjbsyj kjbsyj WHERE kjbsyj.F_IsEffective = 1 AND kjbsyj.F_ItemCategory = '科美' AND kjbsyj.F_StoreId IN ('{storeIdsStr}') AND kjbsyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND kjbsyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var handworkFeeResult = await _db.Ado.SqlQueryAsync(handworkFeeSql); handworkFee = handworkFeeResult?.FirstOrDefault() != null ? Convert.ToDecimal(handworkFeeResult.FirstOrDefault().HandworkFee ?? 0) : 0m; } // 12. 统计开单金额(科美开单实付金额) var billingAmount = 0m; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var billingAmountSql = $@" SELECT COALESCE(SUM(billing.sfyj), 0) as BillingAmount FROM lq_kd_kdjlb billing INNER JOIN lq_kd_kjbsyj kjbsyj ON billing.F_Id = kjbsyj.glkdbh WHERE billing.F_IsEffective = 1 AND kjbsyj.F_IsEffective = 1 AND kjbsyj.F_ItemCategory = '科美' AND billing.djmd IN ('{storeIdsStr}') AND billing.kdrq >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND billing.kdrq <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingAmountResult = await _db.Ado.SqlQueryAsync(billingAmountSql); billingAmount = billingAmountResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingAmountResult.FirstOrDefault().BillingAmount ?? 0) : 0m; } // 13. 统计退款金额(科美退卡金额) // 先找出包含科美品项的退卡记录ID,然后统计这些退卡记录的总金额(避免重复计算) var refundAmount = 0m; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var refundAmountSql = $@" SELECT COALESCE(SUM(COALESCE(refund.F_ActualRefundAmount, refund.tkje, 0)), 0) as RefundAmount FROM lq_hytk_hytk refund WHERE refund.F_IsEffective = 1 AND refund.md IN ('{storeIdsStr}') AND refund.tksj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND refund.tksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' AND EXISTS ( SELECT 1 FROM lq_hytk_kjbsyj kjbsyj WHERE kjbsyj.gltkbh = refund.F_Id AND kjbsyj.F_IsEffective = 1 AND kjbsyj.F_ItemCategory = '科美' )"; var refundAmountResult = await _db.Ado.SqlQueryAsync(refundAmountSql); refundAmount = refundAmountResult?.FirstOrDefault() != null ? Convert.ToDecimal(refundAmountResult.FirstOrDefault().RefundAmount ?? 0) : 0m; } // 14. 统计储扣金额(科美开单的储扣金额) var deductAmount = 0m; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var deductAmountSql = $@" SELECT COALESCE(SUM(deduct.F_Amount), 0) as DeductAmount FROM lq_kd_deductinfo deduct INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id LEFT JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id AND item.F_IsEffective = 1 WHERE deduct.F_IsEffective = 1 AND billing.F_IsEffective = 1 AND COALESCE(item.qt2, '其他') = '科美' AND billing.djmd IN ('{storeIdsStr}') AND COALESCE(deduct.F_BillingTime, billing.kdrq) >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND COALESCE(deduct.F_BillingTime, billing.kdrq) <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var deductAmountResult = await _db.Ado.SqlQueryAsync(deductAmountSql); deductAmount = deductAmountResult?.FirstOrDefault() != null ? Convert.ToDecimal(deductAmountResult.FirstOrDefault().DeductAmount ?? 0) : 0m; } var result = new TechDepartmentDashboardStatisticsOutput { TraceabilityAmount = traceabilityAmount, CellAmount = cellAmount, TotalKemeiIncome = totalKemeiIncome, ManagedStoreCount = managedStoreCount, ActiveStoreCount = activeStoreCount, BillingTraceabilityAmount = billingTraceabilityAmount, RefundTraceabilityAmount = refundTraceabilityAmount, BillingCellAmount = billingCellAmount, RefundCellAmount = refundCellAmount, BillingAmount = billingAmount, DeductAmount = deductAmount, RefundAmount = refundAmount, ConsumeAmount = consumeAmount, HandworkFee = handworkFee }; _logger.LogInformation($"科技部驾驶舱统计数据查询完成,统计月份:{input.StatisticsMonth},管理的门店数:{managedStoreCount},活跃门店数:{activeStoreCount},溯源金额:{traceabilityAmount:F2},Cell金额:{cellAmount:F2},科美总收入:{totalKemeiIncome:F2},开单金额:{billingAmount:F2},储扣金额:{deductAmount:F2},退款金额:{refundAmount:F2},消耗金额:{consumeAmount:F2},手工费:{handworkFee:F2}"); return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱统计数据失败,科技部ID:{TechDepartmentId},统计月份:{StatisticsMonth}", input?.TechDepartmentId, input?.StatisticsMonth); throw NCCException.Oh($"查询科技部驾驶舱统计数据失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱股份统计数据 /// /// /// 获取指定科技部在指定月份的股份相关指标:收入、成本(报销、人工各项明细)、利润 /// /// 示例请求: /// ```json /// { /// "techDepartmentId": "1649328471923847169", /// "statisticsMonth": "202512" /// } /// ``` /// /// 参数说明: /// - techDepartmentId: 科技部ID(BASE_ORGANIZE表的组织ID),与storeIds两者必填其一 /// - storeIds: 门店ID列表,与techDepartmentId两者必填其一 /// - statisticsMonth: 统计月份,格式为YYYYMM(必填) /// /// 返回数据说明: /// - Income: 收入(科美开单30%,需减去退款) /// - CostReimbursement: 成本-报销 /// - CostTeacherBase: 成本-人工-老师底薪 /// - CostTeacherManual: 成本-人工-老师手工费 /// - CostTeacherBillingComm: 成本-人工-老师开单提成 /// - CostTeacherConsumeComm: 成本-人工-老师消耗提成 /// - CostGMBase: 成本-人工-总经理底薪 /// - CostGMComm: 成本-人工-总经理提成 /// - Profit: 利润(收入 - 所有成本) /// /// 查询参数 /// 科技部驾驶舱股份统计数据 /// 成功返回统计数据 /// 参数错误 /// 服务器错误 [HttpPost("GetShareStatistics")] public async Task GetShareStatistics([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) { throw NCCException.Oh("请求参数不能为空"); } if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) { throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); } if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) { throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); } _logger.LogInformation("开始查询科技部驾驶舱股份统计数据,科技部ID:{TechDepartmentId},门店ID列表:{StoreIds},统计月份:{StatisticsMonth}", input.TechDepartmentId, input.StoreIds != null ? string.Join(",", input.StoreIds) : "", input.StatisticsMonth); // 解析月份获取时间范围 var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); // 获取门店列表 List storeIds; string deptName = null; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { // 从lq_md_target表查询该月份、该科技部归属的门店列表 storeIds = await _db.Queryable() .Where(x => x.TechDepartment == input.TechDepartmentId && x.Month == input.StatisticsMonth) .Select(x => x.StoreId) .Distinct() .ToListAsync(); if (!storeIds.Any()) { _logger.LogWarning("科技部ID:{TechDepartmentId} 在 {StatisticsMonth} 月份没有管理的门店", input.TechDepartmentId, input.StatisticsMonth); return new TechDepartmentDashboardShareStatisticsOutput(); } // 获取组织名称(用于查询总经理工资) var organize = await _db.Queryable() .Where(x => x.Id == input.TechDepartmentId && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => x.FullName) .FirstAsync(); deptName = organize; } else { storeIds = input.StoreIds; } // 1. 计算收入:科美开单30%(需减去退款) decimal kemeiIncome = 0; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var kemeiIncomeSql = $@" SELECT COALESCE(SUM(px.F_TotalPrice), 0) as Total FROM lq_kd_pxmx px INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id WHERE kd.djmd IN ('{storeIdsStr}') AND px.yjsj >= '{startDate:yyyy-MM-dd} 00:00:00' AND px.yjsj < '{endDate:yyyy-MM-dd} 23:59:59' AND px.F_IsEffective = 1 AND px.F_ItemCategory = '科美'"; var kemeiIncomeResult = await _db.Ado.SqlQueryAsync(kemeiIncomeSql); if (kemeiIncomeResult != null && kemeiIncomeResult.Any()) { var first = kemeiIncomeResult.FirstOrDefault(); if (first != null) { kemeiIncome = Convert.ToDecimal(first.Total ?? 0); } } } // 减去对应的科美项目实退金额 decimal kemeiRefund = 0; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var kemeiRefundSql = $@" SELECT COALESCE(SUM(jksyj), 0) as Total FROM lq_hytk_jksyj WHERE F_StoreId IN ('{storeIdsStr}') AND tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND tksj < '{endDate:yyyy-MM-dd} 23:59:59' AND F_IsEffective = 1 AND F_ItemCategory = '科美'"; var kemeiRefundResult = await _db.Ado.SqlQueryAsync(kemeiRefundSql); if (kemeiRefundResult != null && kemeiRefundResult.Any()) { var first = kemeiRefundResult.FirstOrDefault(); if (first != null) { kemeiRefund = Convert.ToDecimal(first.Total ?? 0); } } } // 收入 = (科美开单 - 科美退卡) * 30% var income = (kemeiIncome - kemeiRefund) * 0.3m; // 2. 成本-报销:筛选一级分类为"科技部费用"的申请 var reimbursementAmount = 0m; if (storeIds.Any()) { reimbursementAmount = await _db.Queryable( (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id) .Where((app, purchase, category) => category.Level1Name == "科技部费用" && app.ApprovalStatus == "已通过" && storeIds.Contains(app.ApplicationStoreId) && app.ApplicationTime >= startDate && app.ApplicationTime <= endDateTime) .SumAsync((app, purchase, category) => purchase.Amount); } // 3. 成本-人工-科技部老师底薪 var teacherBaseSalary = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && storeIds.Contains(x.StoreId)) .SumAsync(x => x.BaseSalary); // 4. 成本-人工-科技部手工费 var teacherManualFee = await _db.Queryable() .Where(x => x.ItemCategory == "科美" && storeIds.Contains(x.StoreId) && x.Yjsj >= startDate && x.Yjsj <= endDateTime && x.IsEffective == 1) .SumAsync(x => x.LaborCost ?? 0); // 5. 成本-人工-科技部开单提成 var teacherBillingComm = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && storeIds.Contains(x.StoreId)) .SumAsync(x => x.PerformanceCommissionAmount); // 6. 成本-人工-科技部消耗提成 var teacherConsumeComm = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && storeIds.Contains(x.StoreId)) .SumAsync(x => x.ConsumeCommissionAmount); // 7. 成本-人工-科技部总经理底薪和提成(需要组织名称) var gmBaseSalary = 0m; var gmComm = 0m; if (!string.IsNullOrWhiteSpace(deptName)) { gmBaseSalary = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && x.Position == deptName) .SumAsync(x => x.BaseSalary); gmComm = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && x.Position == deptName) .SumAsync(x => x.TotalCommission); } // 8. 计算利润 var totalCost = reimbursementAmount + teacherBaseSalary + teacherManualFee + teacherBillingComm + teacherConsumeComm + gmBaseSalary + gmComm; var profit = income - totalCost; var result = new TechDepartmentDashboardShareStatisticsOutput { Income = income, CostReimbursement = reimbursementAmount, CostTeacherBase = teacherBaseSalary, CostTeacherManual = teacherManualFee, CostTeacherBillingComm = teacherBillingComm, CostTeacherConsumeComm = teacherConsumeComm, CostGMBase = gmBaseSalary, CostGMComm = gmComm, Profit = profit }; _logger.LogInformation($"科技部驾驶舱股份统计数据查询完成,统计月份:{input.StatisticsMonth},收入:{income:F2},成本:{totalCost:F2},利润:{profit:F2}"); return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱股份统计数据失败,科技部ID:{TechDepartmentId},统计月份:{StatisticsMonth}", input?.TechDepartmentId, input?.StatisticsMonth); throw NCCException.Oh($"查询科技部驾驶舱股份统计数据失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱业绩趋势数据 /// /// /// 获取指定科技部在指定时间范围内的业绩趋势:溯源金额趋势、Cell金额趋势、科美总收入趋势 /// /// 查询参数 /// 业绩趋势数据 [HttpPost("GetPerformanceTrend")] public async Task GetPerformanceTrend([FromBody] TechDepartmentDashboardPerformanceTrendInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var monthCount = input.MonthCount; if (monthCount != 3 && monthCount != 6 && monthCount != 12) monthCount = 12; var baseMonth = DateTime.ParseExact(input.StatisticsMonth, "yyyyMM", null); var result = new TechDepartmentDashboardPerformanceTrendOutput(); decimal cumulativeTraceability = 0m; decimal cumulativeCell = 0m; decimal cumulativeTotal = 0m; for (int i = monthCount - 1; i >= 0; i--) { var trendMonth = baseMonth.AddMonths(-i); var trendMonthStr = trendMonth.ToString("yyyyMM"); List storeIds; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { storeIds = await GetStoreIdsForMonthAsync(input.TechDepartmentId, null, trendMonthStr); } else { storeIds = input.StoreIds; } var (traceabilityAmount, cellAmount, totalKemeiIncome) = await CalculateMonthlyPerformance(storeIds, trendMonthStr); cumulativeTraceability += traceabilityAmount; cumulativeCell += cellAmount; cumulativeTotal += totalKemeiIncome; result.TrendData.Add(new PerformanceTrendPoint { Month = trendMonthStr, TraceabilityAmount = traceabilityAmount, CellAmount = cellAmount, TotalKemeiIncome = totalKemeiIncome, CumulativeTraceabilityAmount = cumulativeTraceability, CumulativeCellAmount = cumulativeCell, CumulativeTotalKemeiIncome = cumulativeTotal }); } return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱业绩趋势数据失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱股份趋势数据 /// /// /// 获取指定科技部在指定时间范围内的股份趋势:收入趋势、成本趋势、利润趋势 /// /// 查询参数 /// 股份趋势数据 [HttpPost("GetShareTrend")] public async Task GetShareTrend([FromBody] TechDepartmentDashboardShareTrendInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var monthCount = input.MonthCount; if (monthCount != 3 && monthCount != 6 && monthCount != 12) monthCount = 12; var baseMonth = DateTime.ParseExact(input.StatisticsMonth, "yyyyMM", null); var result = new TechDepartmentDashboardShareTrendOutput(); string deptName = null; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { var organize = await _db.Queryable() .Where(x => x.Id == input.TechDepartmentId && x.DeleteMark == null && x.EnabledMark == 1) .Select(x => x.FullName) .FirstAsync(); deptName = organize; } decimal cumulativeIncome = 0m; decimal cumulativeCost = 0m; decimal cumulativeProfit = 0m; for (int i = monthCount - 1; i >= 0; i--) { var trendMonth = baseMonth.AddMonths(-i); var trendMonthStr = trendMonth.ToString("yyyyMM"); var year = trendMonth.Year; var month = trendMonth.Month; var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = trendMonthStr == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); List storeIds; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { storeIds = await GetStoreIdsForMonthAsync(input.TechDepartmentId, null, trendMonthStr); } else { storeIds = input.StoreIds; } if (!storeIds.Any()) { result.TrendData.Add(new ShareTrendPoint { Month = trendMonthStr }); continue; } // 计算收入 decimal kemeiIncome = 0; decimal kemeiRefund = 0; if (storeIds.Any()) { var storeIdsStr = string.Join("','", storeIds); var kemeiIncomeSql = $@" SELECT COALESCE(SUM(px.F_TotalPrice), 0) as Total FROM lq_kd_pxmx px INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id WHERE kd.djmd IN ('{storeIdsStr}') AND px.yjsj >= '{startDate:yyyy-MM-dd} 00:00:00' AND px.yjsj < '{endDate:yyyy-MM-dd} 23:59:59' AND px.F_IsEffective = 1 AND px.F_ItemCategory = '科美'"; var kemeiIncomeResult = await _db.Ado.SqlQueryAsync(kemeiIncomeSql); if (kemeiIncomeResult?.Any() == true) kemeiIncome = Convert.ToDecimal(kemeiIncomeResult.FirstOrDefault().Total ?? 0); var kemeiRefundSql = $@" SELECT COALESCE(SUM(jksyj), 0) as Total FROM lq_hytk_jksyj WHERE F_StoreId IN ('{storeIdsStr}') AND tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND tksj < '{endDate:yyyy-MM-dd} 23:59:59' AND F_IsEffective = 1 AND F_ItemCategory = '科美'"; var kemeiRefundResult = await _db.Ado.SqlQueryAsync(kemeiRefundSql); if (kemeiRefundResult?.Any() == true) kemeiRefund = Convert.ToDecimal(kemeiRefundResult.FirstOrDefault().Total ?? 0); } var income = (kemeiIncome - kemeiRefund) * 0.3m; // 计算成本 var reimbursementAmount = await _db.Queryable( (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id) .Where((app, purchase, category) => category.Level1Name == "科技部费用" && app.ApprovalStatus == "已通过" && storeIds.Contains(app.ApplicationStoreId) && app.ApplicationTime >= startDate && app.ApplicationTime <= endDateTime) .SumAsync((app, purchase, category) => purchase.Amount); var teacherBaseSalary = await _db.Queryable() .Where(x => x.StatisticsMonth == trendMonthStr && storeIds.Contains(x.StoreId)) .SumAsync(x => x.BaseSalary); var teacherManualFee = await _db.Queryable() .Where(x => x.ItemCategory == "科美" && storeIds.Contains(x.StoreId) && x.Yjsj >= startDate && x.Yjsj <= endDateTime && x.IsEffective == 1) .SumAsync(x => x.LaborCost ?? 0); var teacherBillingComm = await _db.Queryable() .Where(x => x.StatisticsMonth == trendMonthStr && storeIds.Contains(x.StoreId)) .SumAsync(x => x.PerformanceCommissionAmount); var teacherConsumeComm = await _db.Queryable() .Where(x => x.StatisticsMonth == trendMonthStr && storeIds.Contains(x.StoreId)) .SumAsync(x => x.ConsumeCommissionAmount); var gmBaseSalary = 0m; var gmComm = 0m; if (!string.IsNullOrWhiteSpace(deptName)) { gmBaseSalary = await _db.Queryable() .Where(x => x.StatisticsMonth == trendMonthStr && x.Position == deptName) .SumAsync(x => x.BaseSalary); gmComm = await _db.Queryable() .Where(x => x.StatisticsMonth == trendMonthStr && x.Position == deptName) .SumAsync(x => x.TotalCommission); } var totalCost = reimbursementAmount + teacherBaseSalary + teacherManualFee + teacherBillingComm + teacherConsumeComm + gmBaseSalary + gmComm; var profit = income - totalCost; cumulativeIncome += income; cumulativeCost += totalCost; cumulativeProfit += profit; result.TrendData.Add(new ShareTrendPoint { Month = trendMonthStr, Income = income, CostReimbursement = reimbursementAmount, CostTeacherBase = teacherBaseSalary, CostTeacherManual = teacherManualFee, CostTeacherBillingComm = teacherBillingComm, CostTeacherConsumeComm = teacherConsumeComm, CostGMBase = gmBaseSalary, CostGMComm = gmComm, Profit = profit, CumulativeIncome = cumulativeIncome, CumulativeCost = cumulativeCost, CumulativeProfit = cumulativeProfit, ProfitRate = income > 0 ? (profit / income) * 100 : 0 }); } return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱股份趋势数据失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱门店业绩排行 /// /// /// 获取指定科技部在指定月份的门店业绩排行:门店溯源金额排行、门店Cell金额排行、门店科美总收入排行 /// /// 查询参数 /// 门店业绩排行数据 [HttpPost("GetStoreRanking")] public async Task GetStoreRanking([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIds = await GetStoreIdsAsync(input); if (!storeIds.Any()) { return new TechDepartmentDashboardStoreRankingOutput(); } var storeIdsStr = string.Join("','", storeIds); // 统计各门店的业绩 var storePerformanceSql = $@" SELECT * FROM ( SELECT md.F_Id as StoreId, md.dm as StoreName, COALESCE(SUM(CASE WHEN jksyj.F_BeautyType IN ('溯源系统', '溯源') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) - COALESCE(refund_trace.RefundAmount, 0) as TraceabilityAmount, COALESCE(SUM(CASE WHEN jksyj.F_BeautyType IN ('cell', 'Cell') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) - COALESCE(refund_cell.RefundAmount, 0) as CellAmount FROM lq_mdxx md LEFT JOIN lq_kd_jksyj jksyj ON md.F_Id = jksyj.F_StoreId AND jksyj.F_IsEffective = 1 AND jksyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND jksyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' LEFT JOIN ( SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as RefundAmount FROM lq_hytk_jksyj WHERE F_IsEffective = 1 AND F_BeautyType IN ('溯源系统', '溯源') AND tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND tksj <= '{endDate:yyyy-MM-dd} 23:59:59' GROUP BY F_StoreId ) refund_trace ON md.F_Id = refund_trace.F_StoreId LEFT JOIN ( SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as RefundAmount FROM lq_hytk_jksyj WHERE F_IsEffective = 1 AND F_BeautyType IN ('cell', 'Cell') AND tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND tksj <= '{endDate:yyyy-MM-dd} 23:59:59' GROUP BY F_StoreId ) refund_cell ON md.F_Id = refund_cell.F_StoreId WHERE md.F_Id IN ('{storeIdsStr}') GROUP BY md.F_Id, md.dm, refund_trace.RefundAmount, refund_cell.RefundAmount ) t WHERE TraceabilityAmount > 0 OR CellAmount > 0 ORDER BY (TraceabilityAmount + CellAmount) DESC"; var storePerformanceData = await _db.Ado.SqlQueryAsync(storePerformanceSql); var storePerformanceList = storePerformanceData?.ToList() ?? new List(); decimal totalKemeiIncome = storePerformanceList.Sum(x => { decimal trace = 0; decimal cell = 0; trace = Convert.ToDecimal(x.TraceabilityAmount); cell = Convert.ToDecimal(x.CellAmount); return trace + cell; }); var result = new TechDepartmentDashboardStoreRankingOutput(); int rankingIndex = 1; foreach (var item in storePerformanceList) { var rankingItem = new StoreRankingItem { Ranking = rankingIndex, StoreId = item.StoreId?.ToString() ?? "", StoreName = item.StoreName?.ToString() ?? "未知门店", TraceabilityAmount = item.TraceabilityAmount, CellAmount = item.CellAmount, TotalKemeiIncome = Convert.ToDecimal(item.TraceabilityAmount ?? 0) + Convert.ToDecimal(item.CellAmount ?? 0), Percentage = totalKemeiIncome > 0 ? ((Convert.ToDecimal(item.TraceabilityAmount ?? 0) + Convert.ToDecimal(item.CellAmount ?? 0)) / totalKemeiIncome) * 100 : 0 }; result.RankingData.Add(rankingItem); rankingIndex++; } return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱门店业绩排行失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱门店业绩分布 /// /// /// 获取指定科技部在指定月份的门店业绩分布数据(用于饼图展示) /// /// 查询参数 /// 门店业绩分布数据 [HttpPost("GetStoreDistribution")] public async Task GetStoreDistribution([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIds = await GetStoreIdsAsync(input); if (!storeIds.Any()) { return new TechDepartmentDashboardStoreDistributionOutput(); } var storeIdsStr = string.Join("','", storeIds); // 统计各门店的业绩(与GetStoreRanking逻辑相同) var storePerformanceSql = $@" SELECT md.F_Id as StoreId, md.dm as StoreName, COALESCE(SUM(CASE WHEN jksyj.F_BeautyType IN ('溯源系统', '溯源') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) - COALESCE(refund_trace.RefundAmount, 0) as TraceabilityAmount, COALESCE(SUM(CASE WHEN jksyj.F_BeautyType IN ('cell', 'Cell') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) - COALESCE(refund_cell.RefundAmount, 0) as CellAmount FROM lq_mdxx md LEFT JOIN lq_kd_jksyj jksyj ON md.F_Id = jksyj.F_StoreId AND jksyj.F_IsEffective = 1 AND jksyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND jksyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' LEFT JOIN ( SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as RefundAmount FROM lq_hytk_jksyj WHERE F_IsEffective = 1 AND F_BeautyType IN ('溯源系统', '溯源') AND tksj >= '{startDate:yyyy-MM-dd}' AND tksj <= '{endDate:yyyy-MM-dd}' GROUP BY F_StoreId ) refund_trace ON md.F_Id = refund_trace.F_StoreId LEFT JOIN ( SELECT F_StoreId, COALESCE(SUM(CAST(jksyj AS DECIMAL(18,2))), 0) as RefundAmount FROM lq_hytk_jksyj WHERE F_IsEffective = 1 AND F_BeautyType IN ('cell', 'Cell') AND tksj >= '{startDate:yyyy-MM-dd}' AND tksj <= '{endDate:yyyy-MM-dd}' GROUP BY F_StoreId ) refund_cell ON md.F_Id = refund_cell.F_StoreId WHERE md.F_Id IN ('{storeIdsStr}') GROUP BY md.F_Id, md.dm, refund_trace.RefundAmount, refund_cell.RefundAmount"; var storePerformanceData = await _db.Ado.SqlQueryAsync(storePerformanceSql); var storePerformanceList = storePerformanceData?.ToList() ?? new List(); decimal totalKemeiIncome = storePerformanceList.Sum(x => { decimal trace = 0; decimal cell = 0; trace = Convert.ToDecimal(x.TraceabilityAmount); cell = Convert.ToDecimal(x.CellAmount); return trace + cell; }); var result = new TechDepartmentDashboardStoreDistributionOutput(); foreach (var item in storePerformanceList) { result.DistributionData.Add(new StoreDistributionItem { StoreId = item.StoreId?.ToString() ?? "", StoreName = item.StoreName?.ToString() ?? "未知门店", TraceabilityAmount = item.TraceabilityAmount, CellAmount = item.CellAmount, TotalKemeiIncome = Convert.ToDecimal(item.TraceabilityAmount ?? 0) + Convert.ToDecimal(item.CellAmount ?? 0), Percentage = totalKemeiIncome > 0 ? ((Convert.ToDecimal(item.TraceabilityAmount ?? 0) + Convert.ToDecimal(item.CellAmount ?? 0)) / totalKemeiIncome) * 100 : 0 }); } return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱门店业绩分布失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱老师业绩排行 /// /// /// 获取指定科技部在指定月份的科技部老师业绩排行 /// /// 查询参数 /// 老师业绩排行数据 [HttpPost("GetTeacherRanking")] public async Task GetTeacherRanking([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var storeIds = await GetStoreIdsAsync(input); if (!storeIds.Any()) { return new TechDepartmentDashboardTeacherRankingOutput(); } // 从科技部老师工资统计表获取数据 var teacherStats = await _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && storeIds.Contains(x.StoreId)) .OrderBy(x => x.TotalPerformance, OrderByType.Desc) .ToListAsync(); var result = new TechDepartmentDashboardTeacherRankingOutput(); int ranking = 1; foreach (var teacher in teacherStats) { result.RankingData.Add(new TeacherRankingItem { Ranking = ranking++, EmployeeId = teacher.EmployeeId ?? "", EmployeeName = teacher.EmployeeName ?? "", EmployeeAccount = teacher.EmployeeAccount ?? "", StoreId = teacher.StoreId ?? "", StoreName = teacher.StoreName ?? "", OrderAchievement = teacher.OrderAchievement, ConsumeAchievement = teacher.ConsumeAchievement, RefundAchievement = teacher.RefundAchievement, TotalPerformance = teacher.TotalPerformance, ProjectCount = teacher.ProjectCount, BaseSalary = teacher.BaseSalary, PerformanceCommissionAmount = teacher.PerformanceCommissionAmount, ConsumeCommissionAmount = teacher.ConsumeCommissionAmount, FinalGrossSalary = teacher.FinalGrossSalary }); } return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱老师业绩排行失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱运营统计数据 /// /// /// 获取指定科技部在指定月份的运营分析:开单分析、消耗分析、退卡分析 /// /// 查询参数 /// 运营统计数据 [HttpPost("GetOperationStatistics")] public async Task GetOperationStatistics([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIds = await GetStoreIdsAsync(input); if (!storeIds.Any()) { return new TechDepartmentDashboardOperationStatisticsOutput(); } var storeIdsStr = string.Join("','", storeIds); // 开单分析:开单次数、平均开单金额 var billingStatsSql = $@" SELECT COUNT(DISTINCT kd.F_Id) as BillingCount, COALESCE(SUM(CASE WHEN jksyj.F_BeautyType IN ('溯源系统', '溯源', 'cell', 'Cell') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as BillingAmount FROM lq_kd_kdjlb kd INNER JOIN lq_kd_jksyj jksyj ON kd.F_Id = jksyj.Glkdbh AND jksyj.F_IsEffective = 1 AND jksyj.F_BeautyType IN ('溯源系统', '溯源', 'cell', 'Cell') AND jksyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND jksyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' WHERE kd.F_IsEffective = 1 AND kd.djmd IN ('{storeIdsStr}') AND kd.kdrq >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND kd.kdrq <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var billingStatsResult = await _db.Ado.SqlQueryAsync(billingStatsSql); var billingCount = billingStatsResult?.FirstOrDefault() != null ? Convert.ToInt32(billingStatsResult.FirstOrDefault().BillingCount ?? 0) : 0; var billingAmount = billingStatsResult?.FirstOrDefault() != null ? Convert.ToDecimal(billingStatsResult.FirstOrDefault().BillingAmount ?? 0) : 0m; // 消耗分析:消耗次数、消耗金额 var consumeStatsSql = $@" SELECT COUNT(DISTINCT xh.F_Id) as ConsumeCount, COALESCE(SUM(CASE WHEN xmzl.qt2 = '科美' THEN CAST(COALESCE(xhpx.F_TotalPrice, xhpx.pxjg * xhpx.F_ProjectNumber, 0) AS DECIMAL(18,2)) ELSE 0 END), 0) as ConsumeAmount FROM lq_xh_hyhk xh INNER JOIN lq_xh_pxmx xhpx ON xh.F_Id = xhpx.F_ConsumeInfoId INNER JOIN lq_xmzl xmzl ON xhpx.px = xmzl.F_Id WHERE xh.F_IsEffective = 1 AND xhpx.F_IsEffective = 1 AND xh.Md IN ('{storeIdsStr}') AND xmzl.qt2 = '科美' AND xh.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND xh.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}'"; var consumeStatsResult = await _db.Ado.SqlQueryAsync(consumeStatsSql); var consumeCount = consumeStatsResult?.FirstOrDefault() != null ? Convert.ToInt32(consumeStatsResult.FirstOrDefault().ConsumeCount ?? 0) : 0; var consumeAmount = consumeStatsResult?.FirstOrDefault() != null ? Convert.ToDecimal(consumeStatsResult.FirstOrDefault().ConsumeAmount ?? 0) : 0m; // 退卡分析:退卡次数、退卡金额 var refundStatsSql = $@" SELECT COUNT(DISTINCT hytk.F_Id) as RefundCount, COALESCE(SUM(CASE WHEN jksyj.F_ItemCategory = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END), 0) as RefundAmount FROM lq_hytk_hytk hytk INNER JOIN lq_hytk_jksyj jksyj ON hytk.F_Id = jksyj.Gltkbh AND jksyj.F_IsEffective = 1 AND jksyj.F_ItemCategory = '科美' AND jksyj.tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND jksyj.tksj <= '{endDate:yyyy-MM-dd} 23:59:59' WHERE hytk.F_IsEffective = 1 AND hytk.md IN ('{storeIdsStr}') AND hytk.tksj >= '{startDate:yyyy-MM-dd} 00:00:00' AND hytk.tksj <= '{endDate:yyyy-MM-dd} 23:59:59'"; var refundStatsResult = await _db.Ado.SqlQueryAsync(refundStatsSql); var refundCount = refundStatsResult?.FirstOrDefault() != null ? Convert.ToInt32(refundStatsResult.FirstOrDefault().RefundCount ?? 0) : 0; var refundAmount = refundStatsResult?.FirstOrDefault() != null ? Convert.ToDecimal(refundStatsResult.FirstOrDefault().RefundAmount ?? 0) : 0m; // 计算消耗率和退卡率 var consumeRate = billingAmount > 0 ? (consumeAmount / billingAmount) * 100 : 0; var refundRate = billingAmount > 0 ? (refundAmount / billingAmount) * 100 : 0; var result = new TechDepartmentDashboardOperationStatisticsOutput { BillingAnalysis = new BillingAnalysis { BillingCount = billingCount, AverageBillingAmount = billingCount > 0 ? billingAmount / billingCount : 0 }, ConsumeAnalysis = new ConsumeAnalysis { ConsumeCount = consumeCount, ConsumeAmount = consumeAmount, ConsumeRate = consumeRate }, RefundAnalysis = new RefundAnalysis { RefundCount = refundCount, RefundAmount = refundAmount, RefundRate = refundRate } }; return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱运营统计数据失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱对比分析数据 /// /// /// 获取指定科技部在指定月份的时间对比分析:环比对比(与上月对比)、同比对比(与去年同月对比) /// /// 查询参数 /// 对比分析数据 [HttpPost("GetComparisonAnalysis")] public async Task GetComparisonAnalysis([FromBody] TechDepartmentDashboardStatisticsInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var currentMonth = DateTime.ParseExact(input.StatisticsMonth, "yyyyMM", null); var lastMonth = currentMonth.AddMonths(-1); var lastMonthStr = lastMonth.ToString("yyyyMM"); var lastYearMonth = currentMonth.AddYears(-1); var lastYearMonthStr = lastYearMonth.ToString("yyyyMM"); // 计算当月业绩 List currentStoreIds = await GetStoreIdsAsync(input); var (currentTraceability, currentCell, currentTotal) = await CalculateMonthlyPerformance(currentStoreIds, input.StatisticsMonth); // 计算上月业绩(环比) List lastMonthStoreIds; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { lastMonthStoreIds = await GetStoreIdsForMonthAsync(input.TechDepartmentId, null, lastMonthStr); } else { lastMonthStoreIds = input.StoreIds; } var (lastTraceability, lastCell, lastTotal) = await CalculateMonthlyPerformance(lastMonthStoreIds, lastMonthStr); // 计算去年同月业绩(同比) List lastYearStoreIds; if (!string.IsNullOrWhiteSpace(input.TechDepartmentId)) { lastYearStoreIds = await GetStoreIdsForMonthAsync(input.TechDepartmentId, null, lastYearMonthStr); } else { lastYearStoreIds = input.StoreIds; } var (lastYearTraceability, lastYearCell, lastYearTotal) = await CalculateMonthlyPerformance(lastYearStoreIds, lastYearMonthStr); // 计算环比变化率 var momTraceabilityRate = lastTraceability > 0 ? ((currentTraceability - lastTraceability) / lastTraceability) * 100 : 0; var momCellRate = lastCell > 0 ? ((currentCell - lastCell) / lastCell) * 100 : 0; var momTotalRate = lastTotal > 0 ? ((currentTotal - lastTotal) / lastTotal) * 100 : 0; // 计算同比变化率 var yoyTraceabilityRate = lastYearTraceability > 0 ? ((currentTraceability - lastYearTraceability) / lastYearTraceability) * 100 : 0; var yoyCellRate = lastYearCell > 0 ? ((currentCell - lastYearCell) / lastYearCell) * 100 : 0; var yoyTotalRate = lastYearTotal > 0 ? ((currentTotal - lastYearTotal) / lastYearTotal) * 100 : 0; var result = new TechDepartmentDashboardComparisonAnalysisOutput { TimeComparison = new TimeComparison { MonthOverMonth = new ComparisonData { TraceabilityAmountChangeRate = momTraceabilityRate, CellAmountChangeRate = momCellRate, TotalKemeiIncomeChangeRate = momTotalRate }, YearOverYear = new ComparisonData { TraceabilityAmountChangeRate = yoyTraceabilityRate, CellAmountChangeRate = yoyCellRate, TotalKemeiIncomeChangeRate = yoyTotalRate } } }; return result; } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱对比分析数据失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱门店明细列表 /// /// /// 获取指定科技部在指定月份的门店明细列表(支持分页、排序、筛选) /// /// 查询参数 /// 门店明细列表 [HttpPost("GetStoreDetailList")] public async Task GetStoreDetailList( [FromBody] TechDepartmentDashboardStoreDetailListInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : startDate.AddMonths(1).AddSeconds(-1); var storeIds = await GetStoreIdsAsync( input.TechDepartmentId, input.StoreIds, input.StatisticsMonth); if (!storeIds.Any()) { return PageResult .SqlSugarPageResult(new SqlSugarPagedList { list = new List(), pagination = new SqlSugar.PagedModel { PageIndex = input.currentPage, PageSize = input.pageSize, Total = 0 } }); } var pageIndex = input.currentPage <= 0 ? 1 : input.currentPage; var pageSize = input.pageSize <= 0 ? 20 : input.pageSize; /* ============================== * ① 只查【门店分页】 * ============================== */ var storeQuery = _db.Queryable() .Where(md => storeIds.Contains(md.Id)); if (!string.IsNullOrWhiteSpace(input.StoreName)) { storeQuery = storeQuery.Where(md => md.Dm.Contains(input.StoreName)); } var total = await storeQuery.CountAsync(); var pageStores = await storeQuery .OrderBy(md => md.Dm) .ToPageListAsync(pageIndex, pageSize); var pageStoreIds = pageStores.Select(x => x.Id).ToList(); if (!pageStoreIds.Any()) return PageResult.SqlSugarPageResult(new SqlSugarPagedList { list = new List(), pagination = new SqlSugar.PagedModel { PageIndex = pageIndex, PageSize = pageSize, Total = 0 } }); /* ============================== * ② 只对【当前页门店】做统计 - 开单数据 * ============================== */ var storeIdsStr = string.Join(",", pageStoreIds.Select(x => $"'{x}'")); var billingSql = $@" SELECT jksyj.F_StoreId AS StoreId, SUM(CASE WHEN jksyj.F_BeautyType IN ('溯源系统','溯源') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS BillingTraceabilityAmount, SUM(CASE WHEN jksyj.F_BeautyType IN ('cell','Cell') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS BillingCellAmount, COUNT(DISTINCT jksyj.Glkdbh) AS BillingCount FROM lq_kd_jksyj jksyj WHERE jksyj.F_IsEffective = 1 AND jksyj.F_StoreId IN ({storeIdsStr}) AND jksyj.yjsj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND jksyj.yjsj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' GROUP BY jksyj.F_StoreId"; var billingStatList = await _db.Ado.SqlQueryAsync(billingSql); var billingStatDict = billingStatList.ToDictionary(x => x.StoreId); /* ============================== * ③ 查询退卡数据 * ============================== */ var refundSql = $@" SELECT jksyj.F_StoreId AS StoreId, SUM(CASE WHEN jksyj.F_BeautyType IN ('溯源系统','溯源') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS RefundTraceabilityAmount, SUM(CASE WHEN jksyj.F_BeautyType IN ('cell','Cell') THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS RefundCellAmount FROM lq_hytk_jksyj jksyj WHERE jksyj.F_IsEffective = 1 AND jksyj.F_StoreId IN ({storeIdsStr}) AND jksyj.tksj >= '{startDate:yyyy-MM-dd 00:00:00}' AND jksyj.tksj <= '{endDate:yyyy-MM-dd 23:59:59}' GROUP BY jksyj.F_StoreId"; var refundStatList = await _db.Ado.SqlQueryAsync(refundSql); var refundStatDict = refundStatList.ToDictionary(x => x.StoreId); /* ============================== * ④ 查询消耗次数和退卡次数 * ============================== */ var consumeSql = $@" SELECT hyhk.md AS StoreId, COUNT(DISTINCT hyhk.F_Id) AS ConsumeCount FROM lq_xh_hyhk hyhk WHERE hyhk.F_IsEffective = 1 AND hyhk.md IN ({storeIdsStr}) AND hyhk.Hksj >= '{startDate:yyyy-MM-dd HH:mm:ss}' AND hyhk.Hksj <= '{endDateTime:yyyy-MM-dd HH:mm:ss}' GROUP BY hyhk.md"; var consumeStatList = await _db.Ado.SqlQueryAsync(consumeSql); var consumeStatDict = consumeStatList.ToDictionary(x => x.StoreId); var refundCountSql = $@" SELECT hytk.md AS StoreId, COUNT(DISTINCT hytk.F_Id) AS RefundCount FROM lq_hytk_hytk hytk WHERE hytk.F_IsEffective = 1 AND hytk.md IN ({storeIdsStr}) AND hytk.tksj >= '{startDate:yyyy-MM-dd 00:00:00}' AND hytk.tksj <= '{endDate:yyyy-MM-dd 23:59:59}' GROUP BY hytk.md"; var refundCountStatList = await _db.Ado.SqlQueryAsync(refundCountSql); var refundCountStatDict = refundCountStatList.ToDictionary(x => x.StoreId); /* ============================== * ⑤ 内存合并 + 计算 * ============================== */ var resultList = pageStores.Select(md => { billingStatDict.TryGetValue(md.Id, out var billingStat); refundStatDict.TryGetValue(md.Id, out var refundStat); consumeStatDict.TryGetValue(md.Id, out var consumeStat); refundCountStatDict.TryGetValue(md.Id, out var refundCountStat); var billingTrace = billingStat?.BillingTraceabilityAmount ?? 0m; var refundTrace = refundStat?.RefundTraceabilityAmount ?? 0m; var trace = billingTrace - refundTrace; var billingCell = billingStat?.BillingCellAmount ?? 0m; var refundCell = refundStat?.RefundCellAmount ?? 0m; var cell = billingCell - refundCell; return new TechDepartmentDashboardStoreDetailListOutput { StoreId = md.Id, StoreName = md.Dm, StoreType = md.StoreType, StoreCategory = md.StoreCategory, Month = input.StatisticsMonth, BillingTraceabilityAmount = billingTrace, RefundTraceabilityAmount = refundTrace, TraceabilityAmount = trace, BillingCellAmount = billingCell, RefundCellAmount = refundCell, CellAmount = cell, TotalKemeiIncome = trace + cell, BillingCount = billingStat?.BillingCount ?? 0, ConsumeCount = consumeStat?.ConsumeCount ?? 0, RefundCount = refundCountStat?.RefundCount ?? 0 }; }).ToList(); return PageResult .SqlSugarPageResult(new SqlSugarPagedList { list = resultList, pagination = new SqlSugar.PagedModel { PageIndex = pageIndex, PageSize = pageSize, Total = total } }); } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱门店明细列表失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱老师明细列表 /// /// /// 获取指定科技部在指定月份的科技部老师明细列表(支持分页、排序、筛选) /// /// 查询参数 /// 老师明细列表 [HttpPost("GetTeacherDetailList")] public async Task GetTeacherDetailList([FromBody] TechDepartmentDashboardTeacherDetailListInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var storeIds = await GetStoreIdsAsync(input.TechDepartmentId, input.StoreIds, input.StatisticsMonth); if (!storeIds.Any()) { var emptyPagination = new SqlSugarPagedList { list = new List(), pagination = new SqlSugar.PagedModel { PageIndex = input.currentPage, PageSize = input.pageSize, Total = 0 } }; return PageResult.SqlSugarPageResult(emptyPagination); } var query = _db.Queryable() .Where(x => x.StatisticsMonth == input.StatisticsMonth && storeIds.Contains(x.StoreId)) .WhereIF(!string.IsNullOrWhiteSpace(input.TeacherName), x => x.EmployeeName.Contains(input.TeacherName)) .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), x => x.StoreId == input.StoreId) .OrderBy(x => x.TotalPerformance, OrderByType.Desc); var result = await query.ToPagedListAsync(input.currentPage, input.pageSize); var outputList = result.list.Select(x => new TechDepartmentDashboardTeacherDetailListOutput { EmployeeId = x.EmployeeId ?? "", EmployeeName = x.EmployeeName ?? "", EmployeeAccount = x.EmployeeAccount ?? "", StoreId = x.StoreId ?? "", StoreName = x.StoreName ?? "", OrderAchievement = x.OrderAchievement, ConsumeAchievement = x.ConsumeAchievement, RefundAchievement = x.RefundAchievement, TotalPerformance = x.TotalPerformance, ProjectCount = x.ProjectCount, BaseSalary = x.BaseSalary, PerformanceCommissionAmount = x.PerformanceCommissionAmount, ConsumeCommissionAmount = x.ConsumeCommissionAmount, HandworkFee = x.HandworkFee, FinalGrossSalary = x.FinalGrossSalary }).ToList(); return PageResult.SqlSugarPageResult( new SqlSugarPagedList { list = outputList, pagination = result.pagination }); } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱老师明细列表失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱开单明细列表 /// /// /// 获取指定科技部在指定月份的科美开单明细列表(支持分页、排序、筛选) /// /// 查询参数 /// 开单明细列表 [HttpPost("GetBillingDetailList")] public async Task GetBillingDetailList([FromBody] TechDepartmentDashboardBillingDetailListInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIds = await GetStoreIdsAsync(input.TechDepartmentId, input.StoreIds, input.StatisticsMonth); if (!storeIds.Any()) { var emptyPagination = new SqlSugarPagedList { list = new List(), pagination = new SqlSugar.PagedModel { PageIndex = input.currentPage, PageSize = input.pageSize, Total = 0 } }; return PageResult.SqlSugarPageResult(emptyPagination); } var query = _db.Queryable( (jksyj, kd, kh, md) => jksyj.Glkdbh == kd.Id && kd.Kdhy == kh.Id && jksyj.StoreId == md.Id) .Where((jksyj, kd, kh, md) => jksyj.IsEffective == 1 && kd.IsEffective == 1) .Where((jksyj, kd, kh, md) => storeIds.Contains(jksyj.StoreId)) .Where((jksyj, kd, kh, md) => (jksyj.BeautyType == "溯源系统" || jksyj.BeautyType == "溯源" || jksyj.BeautyType == "cell" || jksyj.BeautyType == "Cell")) .Where((jksyj, kd, kh, md) => jksyj.Yjsj >= startDate && jksyj.Yjsj <= endDateTime) .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (jksyj, kd, kh, md) => jksyj.StoreId == input.StoreId) .WhereIF(!string.IsNullOrWhiteSpace(input.MemberName), (jksyj, kd, kh, md) => kh.Khmc.Contains(input.MemberName)) .WhereIF(!string.IsNullOrWhiteSpace(input.BeautyType), (jksyj, kd, kh, md) => jksyj.BeautyType.Contains(input.BeautyType)) .OrderBy((jksyj, kd, kh, md) => jksyj.Yjsj, OrderByType.Desc); var result = await query.Select((jksyj, kd, kh, md) => new TechDepartmentDashboardBillingDetailListOutput { BillingId = kd.Id ?? "", StoreId = jksyj.StoreId ?? "", StoreName = md.Dm ?? "", MemberId = kd.Kdhy ?? "", MemberName = kh.Khmc ?? "", BeautyType = jksyj.BeautyType ?? "", BillingAmount = SqlFunc.ToDecimal(jksyj.Jksyj), ActualAmount = SqlFunc.ToDecimal(jksyj.Jksyj), BillingDate = jksyj.Yjsj, HealthCoachName = "", TechTeacherName = "" }).ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(result); } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱开单明细列表失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 获取科技部驾驶舱消耗明细列表 /// /// /// 获取指定科技部在指定月份的科美消耗明细列表(支持分页、排序、筛选) /// /// 查询参数 /// 消耗明细列表 [HttpPost("GetConsumeDetailList")] public async Task GetConsumeDetailList([FromBody] TechDepartmentDashboardConsumeDetailListInput input) { try { if (input == null) throw NCCException.Oh("请求参数不能为空"); if (string.IsNullOrWhiteSpace(input.StatisticsMonth) || input.StatisticsMonth.Length != 6) throw NCCException.Oh("统计月份格式错误,必须为YYYYMM格式"); if (string.IsNullOrWhiteSpace(input.TechDepartmentId) && (input.StoreIds == null || !input.StoreIds.Any())) throw NCCException.Oh("科技部ID和门店ID列表不能同时为空,必须传入其中一个"); var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1).AddDays(-1); var endDateTime = input.StatisticsMonth == DateTime.Now.ToString("yyyyMM") ? DateTime.Now : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var storeIds = await GetStoreIdsAsync(input.TechDepartmentId, input.StoreIds, input.StatisticsMonth); if (!storeIds.Any()) { var emptyPagination = new SqlSugarPagedList { list = new List(), pagination = new SqlSugar.PagedModel { PageIndex = input.currentPage, PageSize = input.pageSize, Total = 0 } }; return PageResult.SqlSugarPageResult(emptyPagination); } var query = _db.Queryable( (xh, pxmx, xm, kh, md) => xh.Id == pxmx.ConsumeInfoId && pxmx.Px == xm.Id && xh.Hy == kh.Id && xh.Md == md.Id) .Where((xh, pxmx, xm, kh, md) => xh.IsEffective == 1 && pxmx.IsEffective == 1 && xm.Qt2 == "科美") .Where((xh, pxmx, xm, kh, md) => storeIds.Contains(xh.Md)) .Where((xh, pxmx, xm, kh, md) => xh.Hksj >= startDate && xh.Hksj <= endDateTime) .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (xh, pxmx, xm, kh, md) => xh.Md == input.StoreId) .WhereIF(!string.IsNullOrWhiteSpace(input.MemberName), (xh, pxmx, xm, kh, md) => kh.Khmc.Contains(input.MemberName)) .OrderBy((xh, pxmx, xm, kh, md) => xh.Hksj, OrderByType.Desc); var result = await query.Select((xh, pxmx, xm, kh, md) => new TechDepartmentDashboardConsumeDetailListOutput { ConsumeId = xh.Id ?? "", StoreId = xh.Md ?? "", StoreName = md.Dm ?? "", MemberId = xh.Hy ?? "", MemberName = kh.Khmc ?? "", ItemId = xm.Id ?? "", ItemName = xm.Xmmc ?? "", BeautyType = xm.Qt2 ?? "", ConsumeAmount = pxmx.TotalPrice > 0 ? pxmx.TotalPrice : (pxmx.Pxjg * pxmx.ProjectNumber), ConsumeCount = pxmx.ProjectNumber, ConsumeDate = xh.Hksj, HealthCoachName = "", TechTeacherName = "" }).ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(result); } catch (Exception ex) { _logger.LogError(ex, "查询科技部驾驶舱消耗明细列表失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } } public class StoreBaseDto { public string StoreId { get; set; } public string StoreName { get; set; } public int? StoreType { get; set; } public int? StoreCategory { get; set; } } public class StoreStatDto { public string StoreId { get; set; } public decimal BillingTraceabilityAmount { get; set; } public decimal BillingCellAmount { get; set; } public int BillingCount { get; set; } } public class StoreRefundStatDto { public string StoreId { get; set; } public decimal RefundTraceabilityAmount { get; set; } public decimal RefundCellAmount { get; set; } } public class StoreConsumeRefundCountDto { public string StoreId { get; set; } public int ConsumeCount { get; set; } public int RefundCount { get; set; } } }