diff --git a/antis-ncc-admin/src/components/kpi-drill-dialog.vue b/antis-ncc-admin/src/components/kpi-drill-dialog.vue new file mode 100644 index 0000000..5b55bcf --- /dev/null +++ b/antis-ncc-admin/src/components/kpi-drill-dialog.vue @@ -0,0 +1,1216 @@ + + + + + + + {{ dateRangeText || '全部时间' }} + + + + {{ storeNamesText }} + + + + 本页金额合计:{{ formatMoney(summary.totalAmount) }} + + + + 本页记录:{{ summary.totalCount }} 条 + + + + 净额=开单-退款:{{ formatMoney((extra.actualAmount || 0) - (extra.refundAmount || 0)) }} + + + + 本月实际:{{ formatMoney(extra.actualAmount || 0) }} + + + + + + + + + + + + + + + + + + + 每日开单金额 & 人数 + + + + + + + + + 本月成交明细 + + + + + + + + + + + + + ¥{{ formatMoney(scope.row[col.prop]) }} + {{ scope.row[col.prop] || '—' }} + + + + + + + + + + + + + + + + + + 开单金额最高会员 + + + {{ billingStats.topMemberAmount.name || '无' }} + ¥{{ formatMoney(billingStats.topMemberAmount.value) }} + + + + + + + + + + + 开单次数最多会员 + + + {{ billingStats.topMemberTimes.name || '无' }} + {{ billingStats.topMemberTimes.count || 0 }} 次 + + + + + + + + + 品项类型雷达图 + + + + + + + + 业绩类型占比 + + + + + + + + 科美类型业绩 + + + + + + + + + + {{ item.name }} + {{ formatMoney(item.value) }} + + + + + + + + + ¥{{ formatMoney(scope.row[col.prop]) }} + {{ scope.row[col.prop] || '—' }} + + + + + + + + + + + + + + diff --git a/antis-ncc-admin/src/views/statisticsList/form9.vue b/antis-ncc-admin/src/views/statisticsList/form9.vue index 131963f..5321ec2 100644 --- a/antis-ncc-admin/src/views/statisticsList/form9.vue +++ b/antis-ncc-admin/src/views/statisticsList/form9.vue @@ -34,7 +34,7 @@ - + {{ kpi.label }} @@ -422,6 +422,10 @@ + + + @@ -474,9 +478,11 @@ import request from '@/utils/request' import * as echarts from 'echarts' import dayjs from 'dayjs' +import KpiDrillDialog from '@/components/kpi-drill-dialog.vue' export default { name: 'LeadershipCockpit', + components: { KpiDrillDialog }, data() { return { query: { @@ -484,6 +490,19 @@ export default { storeIds: [] }, storeOptions: [], + // KPI 穿透 + drillDialog: { + visible: false, + type: 'billing', + title: '数据穿透', + filters: {}, + extra: {} + }, + currentDateParams: { + startTime: null, + endTime: null, + month: null + }, // 核心 KPI 数据 kpiData: {}, trendType: 'day', @@ -551,13 +570,14 @@ export default { // 6大核心 KPI 装饰器 kpiList() { const d = this.kpiData || {} + const tkCount = (this.tkStatisticsData && this.tkStatisticsData.TkCount) ? this.tkStatisticsData.TkCount : 0 return [ - { label: '本月成交总额', value: this.formatMoney(d.TotalBillingAmount), icon: 'el-icon-wallet', type: 'primary', isMoney: true }, - { label: '本月消耗金额', value: this.formatMoney(d.TotalConsumeAmount), icon: 'el-icon-medal', type: 'success', isMoney: true }, - { label: '完成业绩(净额)', value: this.formatMoney(d.CompletedBillingAmount), icon: 'el-icon-trophy', type: 'warning', isMoney: true }, - { label: '开单目标达成', value: d.BillingCompletionRate || 0, icon: 'el-icon-pie-chart', type: 'info', isPercent: true, target: this.formatMoney(d.TargetBillingAmount), status: (d.BillingCompletionRate >= 100) ? 'up' : 'down' }, - { label: '本月拓客人数', value: (this.tkStatisticsData && this.tkStatisticsData.TkCount) ? this.tkStatisticsData.TkCount : 0, icon: 'el-icon-user-solid', type: 'danger', isPercent: false, target: null, status: null }, - { label: '退卡总计', value: this.formatMoney(d.TotalRefundAmount), icon: 'el-icon-warning-outline', type: 'error', isMoney: true } + { key: 'billing', label: '本月成交总额', value: this.formatMoney(d.TotalBillingAmount), raw: d.TotalBillingAmount || 0, icon: 'el-icon-wallet', type: 'primary', isMoney: true }, + { key: 'consume', label: '本月消耗金额', value: this.formatMoney(d.TotalConsumeAmount), raw: d.TotalConsumeAmount || 0, icon: 'el-icon-medal', type: 'success', isMoney: true }, + { key: 'net', label: '完成业绩(净额)', value: this.formatMoney(d.CompletedBillingAmount), raw: d.CompletedBillingAmount || 0, icon: 'el-icon-trophy', type: 'warning', isMoney: true }, + { key: 'target', label: '开单目标达成', value: d.BillingCompletionRate || 0, raw: d.BillingCompletionRate || 0, icon: 'el-icon-pie-chart', type: 'info', isPercent: true, target: this.formatMoney(d.TargetBillingAmount), targetRaw: d.TargetBillingAmount || 0, status: (d.BillingCompletionRate >= 100) ? 'up' : 'down' }, + { key: 'tk', label: '本月拓客人数', value: tkCount, raw: tkCount, icon: 'el-icon-user-solid', type: 'danger', isPercent: false, target: null, status: null }, + { key: 'refund', label: '退卡总计', value: this.formatMoney(d.TotalRefundAmount), raw: d.TotalRefundAmount || 0, icon: 'el-icon-warning-outline', type: 'error', isMoney: true } ] }, // 健康师排行榜数据切换 @@ -623,6 +643,29 @@ export default { openTechModal() { this.showTechModal = true }, + // 打开KPI穿透弹窗 + openKpiDrill(kpi) { + if (!kpi) return + const filters = { + startTime: this.currentDateParams.startTime, + endTime: this.currentDateParams.endTime, + storeIds: this.query.storeIds || [], + month: this.currentDateParams.month + } + const extra = { + actualAmount: kpi.raw || 0, + targetAmount: kpi.targetRaw || 0, + refundAmount: this.kpiData ? (this.kpiData.TotalRefundAmount || 0) : 0 + } + this.drillDialog = { + ...this.drillDialog, + visible: true, + type: kpi.key, + title: `${kpi.label}穿透`, + filters, + extra + } + }, // 重置弹窗大小与标题 resetTechModal() { this.techModalTitle = '会员画像穿透预览' @@ -670,6 +713,12 @@ export default { endTime: monthEnd.endOf('day').format('YYYY-MM-DD HH:mm:ss'), storeIds: this.query.storeIds || [] } + // 存储当前时间范围与月份,供穿透使用 + this.currentDateParams = { + startTime: dateParams.startTime, + endTime: dateParams.endTime, + month: currentMonth + } // 根据趋势类型构建不同的查询参数 let trendParams = {} @@ -1596,6 +1645,7 @@ export default { height: 100px; box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04); transition: transform 0.3s; + cursor: pointer; &:hover { transform: translateY(-4px); diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs index 652453e..a0d3e54 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs @@ -63,6 +63,16 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb public string sourceType { get; set; } /// + /// 业绩类型(来源:品项表fl3,对应F_PerformanceType) + /// + public string performanceType { get; set; } + + /// + /// 科美类型(来源:lq_xmzl.F_BeautyType) + /// + public string beautyType { get; set; } + + /// /// 备注 /// public string remark { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/BillingDrillStatisticsDto.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/BillingDrillStatisticsDto.cs new file mode 100644 index 0000000..29ec360 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/BillingDrillStatisticsDto.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 本月成交总额穿透统计请求参数 + /// + public class BillingDrillStatisticsInput + { + /// + /// 统计月份(格式:yyyyMM) + /// + public string StatisticsMonth { get; set; } + + /// + /// 门店ID列表 + /// + public List StoreIds { get; set; } = new List(); + } + + /// + /// 本月成交总额穿透统计返回结果 + /// + public class BillingDrillStatisticsOutput + { + /// + /// 每日开单趋势 + /// + public List DailyTrend { get; set; } = new List(); + + /// + /// 会员极值统计 + /// + public BillingMemberStatsOutput MemberStats { get; set; } = new BillingMemberStatsOutput(); + + /// + /// 品项类型雷达图数据 + /// + public List ItemTypeRadar { get; set; } = new List(); + + /// + /// 来源类型统计 + /// + public List SourceTypeStats { get; set; } = new List(); + + /// + /// 业绩类型统计 + /// + public List PerformanceTypeStats { get; set; } = new List(); + + /// + /// 科美类型统计 + /// + public List BeautyTypeStats { get; set; } = new List(); + } + + /// + /// 每日开单趋势 + /// + public class BillingDailyTrendOutput + { + /// + /// 日期(yyyy-MM-dd) + /// + public string Date { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + + /// + /// 会员人数 + /// + public int MemberCount { get; set; } + } + + /// + /// 会员极值统计 + /// + public class BillingMemberStatsOutput + { + /// + /// 开单金额最高会员姓名 + /// + public string TopAmountMemberName { get; set; } + + /// + /// 开单金额最高会员的金额 + /// + public decimal TopAmountValue { get; set; } + + /// + /// 开单次数最多会员姓名 + /// + public string TopTimesMemberName { get; set; } + + /// + /// 开单次数最多会员的次数 + /// + public int TopTimesCount { get; set; } + + /// + /// 整月欠款总额 + /// + public decimal DebtTotal { get; set; } + + /// + /// 欠款最多会员姓名 + /// + public string TopDebtMemberName { get; set; } + + /// + /// 欠款最多会员金额 + /// + public decimal TopDebtValue { get; set; } + } + + /// + /// 通用名称+金额 + /// + public class CommonNameAmountOutput + { + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + } + + /// + /// 通用名称+数量+金额 + /// + public class CommonNameCountAmountOutput + { + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 数量 + /// + public int Count { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + } +} + + diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs index 577b317..b2b0bfd 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs @@ -163,8 +163,8 @@ namespace NCC.Extend.LqHytkHytk .WhereIF(!string.IsNullOrEmpty(input.tkzt), p => p.Tkzt.Contains(input.tkzt)) .WhereIF(!string.IsNullOrEmpty(input.tkyy), p => p.Tkyy.Contains(input.tkyy)) .WhereIF(!string.IsNullOrEmpty(input.fileUrl), p => p.FileUrl.Contains(input.fileUrl)) - .WhereIF(queryTksj != null, p => p.Tksj >= new DateTime(startTksj.ToDate().Year, startTksj.ToDate().Month, startTksj.ToDate().Day, 0, 0, 0)) - .WhereIF(queryTksj != null, p => p.Tksj <= new DateTime(endTksj.ToDate().Year, endTksj.ToDate().Month, endTksj.ToDate().Day, 23, 59, 59)) + .WhereIF(startTksj.HasValue, p => p.Tksj >= new DateTime(startTksj.Value.Year, startTksj.Value.Month, startTksj.Value.Day, 0, 0, 0)) + .WhereIF(endTksj.HasValue, p => p.Tksj <= new DateTime(endTksj.Value.Year, endTksj.Value.Month, endTksj.Value.Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.czry), p => p.Czry.Equals(input.czry)) .WhereIF(input.isEffective != 0, p => p.IsEffective == input.isEffective) .Select(it => new LqHytkHytkListOutput diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs index f05c8a7..83e31e8 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs @@ -3805,26 +3805,22 @@ namespace NCC.Extend.LqKdKdjlb string startTimeStr = !string.IsNullOrEmpty(input.StartBillingTime) ? input.StartBillingTime : input.startTime; string endTimeStr = !string.IsNullOrEmpty(input.EndBillingTime) ? input.EndBillingTime : input.endTime; - if (!string.IsNullOrEmpty(startTimeStr) && !string.IsNullOrEmpty(endTimeStr)) + // 开始时间:单独解析,解析失败则忽略过滤,避免直接抛500 + if (!string.IsNullOrWhiteSpace(startTimeStr)) { - // 尝试解析日期字符串(支持多种格式) if (DateTime.TryParse(startTimeStr, out DateTime startDate)) { startBillingTime = startDate; } - else - { - throw NCCException.Oh($"开始时间格式错误:{startTimeStr}"); - } + } + // 结束时间:单独解析,解析失败则忽略过滤 + if (!string.IsNullOrWhiteSpace(endTimeStr)) + { if (DateTime.TryParse(endTimeStr, out DateTime endDate)) { endBillingTime = endDate; } - else - { - throw NCCException.Oh($"结束时间格式错误:{endTimeStr}"); - } } // 优化查询:先分页查询主表,再批量查询关联数据,避免子查询性能问题 @@ -3850,6 +3846,27 @@ namespace NCC.Extend.LqKdKdjlb // 3. 先分页查询主表数据(查询实体类,提高性能) var pagedData = await baseQuery.OrderBy(sidx + " " + sort).ToPagedListAsync(input.currentPage, input.pageSize); + // 检查分页结果 + if (pagedData == null) + { + throw NCCException.Oh("分页查询返回结果为空"); + } + + if (pagedData.list == null) + { + pagedData.list = new List(); + } + + if (pagedData.pagination == null) + { + pagedData.pagination = new PagedModel + { + PageIndex = input.currentPage, + PageSize = input.pageSize, + Total = 0 + }; + } + // 4. 批量查询关联数据 var itemIds = pagedData.list.Select(x => x.Id).ToList(); var memberIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.MemberId)).Select(x => x.MemberId).Distinct().ToList(); @@ -3925,6 +3942,8 @@ namespace NCC.Extend.LqKdKdjlb actualPrice = pxmx.ActualPrice, projectNumber = pxmx.ProjectNumber, sourceType = pxmx.SourceType, + performanceType = pxmx.PerformanceType, + beautyType = pxmx.BeautyType, remark = pxmx.Remark, storeId = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) ? billingStoreDict[pxmx.Glkdbh] : "", storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : "", @@ -3946,8 +3965,8 @@ namespace NCC.Extend.LqKdKdjlb } catch (Exception ex) { - _logger.LogError(ex, $"获取开单品项明细记录列表失败: {ex.ToString()}"); - throw NCCException.Oh(ErrorCode.COM1005, $"获取开单品项明细记录列表失败: {ex.Message}"); + _logger.LogError(ex, $"获取开单品项明细记录列表失败 - 开始时间: {input?.StartBillingTime}, 结束时间: {input?.EndBillingTime}, 页码: {input?.currentPage}, 页大小: {input?.pageSize}, 错误详情: {ex.ToString()}"); + throw NCCException.Oh($"获取开单品项明细记录列表失败: {ex.Message}"); } } #endregion diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs index 397132c..1ce0574 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs @@ -23,6 +23,7 @@ using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_ycsd_jsj; using NCC.Extend.Entitys.lq_jinsanjiao_user; using NCC.Extend.Entitys.lq_kd_kdjlb; +using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_khxx; using NCC.Extend.Entitys.Dto.LqReport; @@ -467,6 +468,268 @@ namespace NCC.Extend #endregion + #region 本月成交总额穿透统计 + /// + /// 本月成交总额穿透统计 + /// + /// + /// 用于 /statisticsList/form9 页面“本月成交总额”KPI 的穿透分析,基于整月完整数据做统计。 + /// + /// 统计范围: + /// - 只统计有效开单:lq_kd_kdjlb.F_IsEffective = 1、lq_kd_pxmx.F_IsEffective = 1 + /// - 按业绩时间 yjsj(品项表)落在指定月份内 + /// - 可按门店ID列表筛选(Djmd) + /// + /// 示例请求: + /// ```json + /// { + /// "statisticsMonth": "202512", + /// "storeIds": ["门店ID1","门店ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - statisticsMonth: 统计月份,格式yyyyMM(必填) + /// - storeIds: 门店ID列表(可选,不传则统计所有门店) + /// + /// 返回字段说明: + /// - DailyTrend: 每日开单金额 & 人数趋势 + /// - Date: 日期(yyyy-MM-dd) + /// - Amount: 当日实付金额合计(开单品项实付金额合计) + /// - MemberCount: 当日开单会员数(按会员ID去重) + /// - MemberStats: 会员相关极值 + /// - TopAmountMemberName: 开单金额最高会员姓名 + /// - TopAmountValue: 该会员整月实付金额 + /// - TopTimesMemberName: 开单次数最多会员姓名 + /// - TopTimesCount: 该会员整月开单次数 + /// - DebtTotal: 整月欠款总额(sfyj+ck金额以外的qk累加) + /// - TopDebtMemberName: 欠款最多会员姓名 + /// - TopDebtValue: 欠款最多会员的欠款金额 + /// - ItemTypeRadar: 品项类型维度金额(用于雷达图) + /// - Name: 品项类型名称 + /// - Value: 实付金额合计 + /// - SourceTypeStats: 来源类型统计 + /// - Name: 来源类型(购买/赠送/体验等) + /// - Count: 该来源的品项条数 + /// - Amount: 该来源的实付金额合计 + /// - PerformanceTypeStats: 业绩类型统计 + /// - Name: 业绩类型 + /// - Amount: 实付金额合计 + /// - BeautyTypeStats: 科美类型统计 + /// - Name: 科美类型 + /// - Amount: 实付金额合计 + /// + /// 查询参数 + /// 本月成交总额穿透统计结果 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-billing-drill-statistics")] + public async Task GetBillingDrillStatistics([FromBody] BillingDrillStatisticsInput input) + { + try + { + if (input == null || string.IsNullOrWhiteSpace(input.StatisticsMonth)) + { + throw NCCException.Oh("统计月份不能为空,格式为yyyyMM"); + } + + if (!DateTime.TryParseExact(input.StatisticsMonth + "01", "yyyyMMdd", null, + global::System.Globalization.DateTimeStyles.None, out var monthStart)) + { + throw NCCException.Oh($"统计月份格式错误:{input.StatisticsMonth},应为yyyyMM"); + } + + var startTime = monthStart; + var endTime = monthStart.AddMonths(1).AddSeconds(-1); + + // 1. 基础查询:开单品项明细 + 开单主表(用于获取门店和欠款等) + var storeIds = input.StoreIds ?? new List(); + + var baseQuery = _db.Queryable((px, kd) => new JoinQueryInfos( + JoinType.Inner, px.Glkdbh == kd.Id)) + .Where((px, kd) => px.IsEffective == StatusEnum.有效.GetHashCode() + && kd.IsEffective == StatusEnum.有效.GetHashCode()) + .Where((px, kd) => px.Yjsj >= startTime && px.Yjsj <= endTime) + .WhereIF(storeIds.Any(), (px, kd) => storeIds.Contains(kd.Djmd)); + + // 为后续多次统计复用,先拉到内存(注意:仅限当月+门店内的数据量) + var rawList = await baseQuery.Select((px, kd) => new + { + BillingId = kd.Id, + BillingDate = kd.Kdrq, + StoreId = kd.Djmd, + MemberId = px.MemberId, + MemberName = "", // 如需姓名可再Join lq_khxx,这里留空给前端展示列表时使用原接口 + Amount = px.ActualPrice, + Debt = kd.Qk, + ItemType = px.ItemCategory, + SourceType = px.SourceType, + PerformanceType = px.PerformanceType, + BeautyType = px.BeautyType + }).ToListAsync(); + + // 保护:无数据时直接返回空结构 + if (!rawList.Any()) + { + return new BillingDrillStatisticsOutput + { + DailyTrend = new List(), + MemberStats = new BillingMemberStatsOutput(), + ItemTypeRadar = new List(), + SourceTypeStats = new List(), + PerformanceTypeStats = new List(), + BeautyTypeStats = new List() + }; + } + + // 2. 每日趋势:按yjsj日期聚合金额和会员数 + var dailyTrend = rawList + .GroupBy(x => x.BillingDate.HasValue ? x.BillingDate.Value.ToString("yyyy-MM-dd") : "") + .Where(g => !string.IsNullOrEmpty(g.Key)) + .Select(g => new BillingDailyTrendOutput + { + Date = g.Key, + Amount = g.Sum(x => x.Amount), + MemberCount = g.Select(x => x.MemberId).Where(m => !string.IsNullOrEmpty(m)).Distinct().Count() + }) + .OrderBy(x => x.Date) + .ToList(); + + // 3. 会员极值:按会员聚合金额和次数 + var memberAgg = rawList + .Where(x => !string.IsNullOrEmpty(x.MemberId)) + .GroupBy(x => x.MemberId) + .Select(g => new + { + MemberId = g.Key, + Amount = g.Sum(x => x.Amount), + Times = g.Count() + }) + .ToList(); + + var topAmount = memberAgg + .OrderByDescending(x => x.Amount) + .FirstOrDefault(); + + var topTimes = memberAgg + .OrderByDescending(x => x.Times) + .FirstOrDefault(); + + // 查询会员姓名 + var memberIds = new List(); + if (topAmount != null && !string.IsNullOrEmpty(topAmount.MemberId)) + { + memberIds.Add(topAmount.MemberId); + } + if (topTimes != null && !string.IsNullOrEmpty(topTimes.MemberId) && !memberIds.Contains(topTimes.MemberId)) + { + memberIds.Add(topTimes.MemberId); + } + + var memberNameDict = new Dictionary(); + if (memberIds.Any()) + { + var members = await _db.Queryable() + .Where(x => memberIds.Contains(x.Id)) + .Select(x => new { x.Id, x.Khmc }) + .ToListAsync(); + memberNameDict = members.ToDictionary(x => x.Id, x => x.Khmc ?? ""); + } + + // 欠款统计:按会员聚合欠款(使用主表Qk) + var debtAgg = rawList + .GroupBy(x => x.MemberId) + .Select(g => new + { + MemberId = g.Key, + Debt = g.Max(x => x.Debt) // 每个会员按最大欠款估算,避免重复累加 + }) + .Where(x => !string.IsNullOrEmpty(x.MemberId)) + .ToList(); + + var debtTotal = debtAgg.Sum(x => x.Debt); + var topDebt = debtAgg + .OrderByDescending(x => x.Debt) + .FirstOrDefault(); + + var memberStats = new BillingMemberStatsOutput + { + TopAmountMemberName = topAmount != null && memberNameDict.ContainsKey(topAmount.MemberId) ? memberNameDict[topAmount.MemberId] : "", + TopAmountValue = topAmount?.Amount ?? 0m, + TopTimesMemberName = topTimes != null && memberNameDict.ContainsKey(topTimes.MemberId) ? memberNameDict[topTimes.MemberId] : "", + TopTimesCount = topTimes?.Times ?? 0, + DebtTotal = debtTotal, + TopDebtMemberName = "", + TopDebtValue = topDebt?.Debt ?? 0m + }; + + // 4. 品项类型雷达:按ItemType汇总金额 + var itemTypeRadar = rawList + .GroupBy(x => string.IsNullOrEmpty(x.ItemType) ? "未分类" : x.ItemType) + .Select(g => new CommonNameAmountOutput + { + Name = g.Key, + Amount = g.Sum(x => x.Amount) + }) + .OrderByDescending(x => x.Amount) + .ToList(); + + // 5. 来源类型统计 + var sourceTypeStats = rawList + .GroupBy(x => string.IsNullOrEmpty(x.SourceType) ? "未设置" : x.SourceType) + .Select(g => new CommonNameCountAmountOutput + { + Name = g.Key, + Count = g.Count(), + Amount = g.Sum(x => x.Amount) + }) + .OrderByDescending(x => x.Amount) + .ToList(); + + // 6. 业绩类型统计 + var performanceTypeStats = rawList + .GroupBy(x => string.IsNullOrEmpty(x.PerformanceType) ? "未设置" : x.PerformanceType) + .Select(g => new CommonNameAmountOutput + { + Name = g.Key, + Amount = g.Sum(x => x.Amount) + }) + .OrderByDescending(x => x.Amount) + .ToList(); + + // 7. 科美类型统计(只显示有设置的,过滤掉未设置和空值) + var beautyTypeStats = rawList + .Where(x => !string.IsNullOrEmpty(x.BeautyType)) + .GroupBy(x => x.BeautyType) + .Select(g => new CommonNameAmountOutput + { + Name = g.Key, + Amount = g.Sum(x => x.Amount) + }) + .OrderByDescending(x => x.Amount) + .ToList(); + + var result = new BillingDrillStatisticsOutput + { + DailyTrend = dailyTrend, + MemberStats = memberStats, + ItemTypeRadar = itemTypeRadar, + SourceTypeStats = sourceTypeStats, + PerformanceTypeStats = performanceTypeStats, + BeautyTypeStats = beautyTypeStats + }; + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取本月成交总额穿透统计失败 - 统计月份: {input?.StatisticsMonth}"); + throw NCCException.Oh($"获取本月成交总额穿透统计失败: {ex.Message}"); + } + } + #endregion + #region 人员业绩报表 ///