Commit 6b697cd30b094929283aaf42948dd516c1041a63

Authored by “wangming”
1 parent a30ca3d5

feat: 添加营销活动统计功能(按门店/品项/门店品项);邀约记录列表添加门店筛选;事业部开单统计按门店分组编号;开单后自动播报统计

netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsByItemOutput.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqPackageInfo
  4 +{
  5 + /// <summary>
  6 + /// 营销活动按品项统计输出
  7 + /// </summary>
  8 + public class ActivityStatisticsByItemOutput
  9 + {
  10 + /// <summary>
  11 + /// 营销活动ID
  12 + /// </summary>
  13 + public string ActivityId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 营销活动名称
  17 + /// </summary>
  18 + public string ActivityName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 品项统计列表
  22 + /// </summary>
  23 + public List<ItemStatisticsItem> ItemList { get; set; }
  24 + }
  25 +
  26 + /// <summary>
  27 + /// 品项统计项
  28 + /// </summary>
  29 + public class ItemStatisticsItem
  30 + {
  31 + /// <summary>
  32 + /// 品项ID
  33 + /// </summary>
  34 + public string ItemId { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 品项名称
  38 + /// </summary>
  39 + public string ItemName { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 销售数量(项目次数总和)
  43 + /// </summary>
  44 + public decimal SalesQuantity { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 销售金额
  48 + /// </summary>
  49 + public decimal SalesAmount { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 开单数量(包含该品项的开单数)
  53 + /// </summary>
  54 + public int BillingCount { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 销售次数(品项明细记录数)
  58 + /// </summary>
  59 + public int SalesCount { get; set; }
  60 + }
  61 +}
  62 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsByStoreItemOutput.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqPackageInfo
  4 +{
  5 + /// <summary>
  6 + /// 营销活动按门店品项统计输出
  7 + /// </summary>
  8 + public class ActivityStatisticsByStoreItemOutput
  9 + {
  10 + /// <summary>
  11 + /// 营销活动ID
  12 + /// </summary>
  13 + public string ActivityId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 营销活动名称
  17 + /// </summary>
  18 + public string ActivityName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店品项统计列表
  22 + /// </summary>
  23 + public List<StoreItemStatisticsItem> StoreItemList { get; set; }
  24 + }
  25 +
  26 + /// <summary>
  27 + /// 门店品项统计项
  28 + /// </summary>
  29 + public class StoreItemStatisticsItem
  30 + {
  31 + /// <summary>
  32 + /// 门店ID
  33 + /// </summary>
  34 + public string StoreId { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 门店名称
  38 + /// </summary>
  39 + public string StoreName { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 品项ID
  43 + /// </summary>
  44 + public string ItemId { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 品项名称
  48 + /// </summary>
  49 + public string ItemName { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 销售数量(项目次数总和)
  53 + /// </summary>
  54 + public decimal SalesQuantity { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 销售金额
  58 + /// </summary>
  59 + public decimal SalesAmount { get; set; }
  60 +
  61 + /// <summary>
  62 + /// 开单数量(包含该品项的开单数)
  63 + /// </summary>
  64 + public int BillingCount { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 销售次数(品项明细记录数)
  68 + /// </summary>
  69 + public int SalesCount { get; set; }
  70 + }
  71 +}
  72 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsByStoreOutput.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqPackageInfo
  4 +{
  5 + /// <summary>
  6 + /// 营销活动按门店统计输出
  7 + /// </summary>
  8 + public class ActivityStatisticsByStoreOutput
  9 + {
  10 + /// <summary>
  11 + /// 营销活动ID
  12 + /// </summary>
  13 + public string ActivityId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 营销活动名称
  17 + /// </summary>
  18 + public string ActivityName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店统计列表
  22 + /// </summary>
  23 + public List<StoreStatisticsItem> StoreList { get; set; }
  24 + }
  25 +
  26 + /// <summary>
  27 + /// 门店统计项
  28 + /// </summary>
  29 + public class StoreStatisticsItem
  30 + {
  31 + /// <summary>
  32 + /// 门店ID
  33 + /// </summary>
  34 + public string StoreId { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 门店名称
  38 + /// </summary>
  39 + public string StoreName { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 开单数量
  43 + /// </summary>
  44 + public int BillingCount { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 开单金额
  48 + /// </summary>
  49 + public decimal BillingAmount { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 欠款金额
  53 + /// </summary>
  54 + public decimal DebtAmount { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 人头数(去重客户数)
  58 + /// </summary>
  59 + public int CustomerCount { get; set; }
  60 +
  61 + /// <summary>
  62 + /// 项目数(品项项目次数总和)
  63 + /// </summary>
  64 + public decimal ItemCount { get; set; }
  65 + }
  66 +}
  67 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqYaoyjl/LqYaoyjlListQueryInput.cs
... ... @@ -17,47 +17,50 @@ namespace NCC.Extend.Entitys.Dto.LqYaoyjl
17 17 ///
18 18 /// </summary>
19 19 public int dataType { get; set; }
20   -
21   -
22 20 /// <summary>
23 21 /// 邀约编号
24 22 /// </summary>
25 23 public string id { get; set; }
26   -
  24 +
27 25 /// <summary>
28 26 /// 邀约人
29 27 /// </summary>
30 28 public string yyr { get; set; }
31   -
  29 +
32 30 /// <summary>
33 31 /// 邀约时间
34 32 /// </summary>
35 33 public string yysj { get; set; }
36   -
  34 +
37 35 /// <summary>
38 36 /// 邀约客户
39 37 /// </summary>
40 38 public string yykh { get; set; }
41   -
  39 +
42 40 /// <summary>
43 41 /// 邀约客户姓名
44 42 /// </summary>
45 43 public string yykhxm { get; set; }
46   -
  44 +
47 45 /// <summary>
48 46 /// 电话是否有效
49 47 /// </summary>
50 48 public string dhsfyx { get; set; }
51   -
  49 +
52 50 /// <summary>
53 51 /// 联系时间
54 52 /// </summary>
55 53 public string lxsj { get; set; }
56   -
  54 +
57 55 /// <summary>
58 56 /// 联系记录
59 57 /// </summary>
60 58 public string lxjl { get; set; }
61   -
  59 +
  60 + /// <summary>
  61 + /// 门店ID
  62 + /// </summary>
  63 + public string storeId { get; set; }
  64 +
62 65 }
63 66 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -1095,15 +1095,27 @@ namespace NCC.Extend
1095 1095 ///
1096 1096 /// 返回格式:
1097 1097 /// ```
1098   - /// 事业一部业绩:xxxx
1099   - /// 事业一部总单量:xxx
1100   - /// 第一单:xxx店、xxx健康师,金额xxx
1101   - /// 第二单:xxx店、xxx健康师,金额xxx
  1098 + /// 11月8日
1102 1099 ///
1103   - /// 事业二部业绩:xxxx
1104   - /// 事业二部总单量:xxx
1105   - /// 第一单:xxx店、xxx健康师,金额xxx
1106   - /// 第二单:xxx店、xxx健康师,金额xxx
  1100 + /// 今日总业绩:32317
  1101 + ///
  1102 + /// 今日总单量:18
  1103 + ///
  1104 + /// 绿纤人用结果捍卫尊严
  1105 + ///
  1106 + /// 业绩捷报
  1107 + ///
  1108 + /// ▃▃▃▃▃▃▃▃▃▃
  1109 + ///
  1110 + /// 事业1部总业绩:4722
  1111 + ///
  1112 + /// 事业1部总单量:4
  1113 + ///
  1114 + /// 第1单:红光店马丽亚1000
  1115 + ///
  1116 + /// 第2单:468店陈小琴、苟小春500
  1117 + ///
  1118 + /// ...
1107 1119 /// ```
1108 1120 /// </remarks>
1109 1121 /// <param name="input">查询参数</param>
... ... @@ -1125,23 +1137,80 @@ namespace NCC.Extend
1125 1137 return $"日期 {input.Date} 暂无开单数据";
1126 1138 }
1127 1139  
1128   - // 2. 格式化为中文文本
  1140 + // 2. 解析日期
  1141 + if (!DateTime.TryParse(input.Date, out var targetDate))
  1142 + {
  1143 + throw NCCException.Oh("日期格式错误,请使用 yyyy-MM-dd 格式");
  1144 + }
  1145 +
  1146 + // 3. 计算总业绩和总单量
  1147 + var totalPerformance = statistics.Sum(x => x.TotalPerformance);
  1148 + var totalOrderCount = statistics.Sum(x => x.TotalOrderCount);
  1149 +
  1150 + // 4. 格式化为中文文本
1129 1151 var textBuilder = new StringBuilder();
1130 1152  
  1153 + // 日期(格式:11月8日)
  1154 + textBuilder.AppendLine($"{targetDate.Month}月{targetDate.Day}日");
  1155 + textBuilder.AppendLine();
  1156 +
  1157 + // 今日总业绩和总单量
  1158 + textBuilder.AppendLine($"今日总业绩:{totalPerformance:F0}");
  1159 + textBuilder.AppendLine();
  1160 + textBuilder.AppendLine($"今日总单量:{totalOrderCount}");
  1161 + textBuilder.AppendLine();
  1162 +
  1163 + // 标语
  1164 + textBuilder.AppendLine("绿纤人用结果捍卫尊严 ");
  1165 + textBuilder.AppendLine();
  1166 + textBuilder.AppendLine("业绩捷报 ");
  1167 + textBuilder.AppendLine();
  1168 +
  1169 + // 每个事业部的数据
1131 1170 foreach (var unit in statistics)
1132 1171 {
1133   - textBuilder.AppendLine($"{unit.BusinessUnitName}业绩:{unit.TotalPerformance:F2}");
  1172 + // 分隔线
  1173 + textBuilder.AppendLine("▃▃▃▃▃▃▃▃▃▃");
  1174 + textBuilder.AppendLine();
  1175 +
  1176 + // 事业部业绩和单量
  1177 + textBuilder.AppendLine($"{unit.BusinessUnitName}总业绩:{unit.TotalPerformance:F0}");
  1178 + textBuilder.AppendLine();
1134 1179 textBuilder.AppendLine($"{unit.BusinessUnitName}总单量:{unit.TotalOrderCount}");
  1180 + textBuilder.AppendLine();
  1181 +
  1182 + // 按门店分组订单
  1183 + var ordersByStore = unit.Orders
  1184 + .GroupBy(x => x.StoreName)
  1185 + .OrderBy(g => g.Key) // 按门店名称排序
  1186 + .ToList();
1135 1187  
1136   - int orderIndex = 1;
1137   - foreach (var order in unit.Orders)
  1188 + // 遍历每个门店的订单
  1189 + foreach (var storeGroup in ordersByStore)
1138 1190 {
1139   - var teacherNames = string.IsNullOrEmpty(order.HealthTeacherNames) ? "无健康师" : order.HealthTeacherNames;
1140   - textBuilder.AppendLine($"第{orderIndex}单:{order.StoreName}、{teacherNames},金额{order.Amount:F2}");
1141   - orderIndex++;
  1191 + // 每个门店的订单按时间排序
  1192 + var storeOrders = storeGroup
  1193 + .OrderBy(x => x.OrderTime ?? DateTime.MinValue)
  1194 + .ToList();
  1195 +
  1196 + // 每个门店的订单单独编号(从第1单开始)
  1197 + int orderIndex = 1;
  1198 + foreach (var order in storeOrders)
  1199 + {
  1200 + var teacherNames = string.IsNullOrEmpty(order.HealthTeacherNames) ? "" : order.HealthTeacherNames;
  1201 + // 格式:第1单:门店名健康师名金额(没有"、",没有"金额"字样,直接数字)
  1202 + if (string.IsNullOrEmpty(teacherNames))
  1203 + {
  1204 + textBuilder.AppendLine($"第{orderIndex}单:{order.StoreName}{order.Amount:F0}");
  1205 + }
  1206 + else
  1207 + {
  1208 + textBuilder.AppendLine($"第{orderIndex}单:{order.StoreName}{teacherNames}{order.Amount:F0}");
  1209 + }
  1210 + textBuilder.AppendLine();
  1211 + orderIndex++;
  1212 + }
1142 1213 }
1143   -
1144   - textBuilder.AppendLine(); // 空行分隔
1145 1214 }
1146 1215  
1147 1216 return textBuilder.ToString().TrimEnd();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -45,6 +45,7 @@ using Yitter.IdGenerator;
45 45 using NCC.Extend.Entitys.lq_package_info;
46 46 using NCC.Extend.Entitys.lq_mdxx;
47 47 using NCC.Extend.Entitys.lq_card_transfer_log;
  48 +using NCC.Extend.Entitys.Dto.LqDailyReport;
48 49  
49 50 namespace NCC.Extend.LqKdKdjlb
50 51 {
... ... @@ -64,6 +65,7 @@ namespace NCC.Extend.LqKdKdjlb
64 65 private readonly WeChatBotService _weChatBotService;
65 66 private readonly LqKdKdjlbStringGenerator _stringGenerator;
66 67 private readonly ILogger<LqKdKdjlbService> _logger;
  68 + private readonly LqDailyReportService _dailyReportService;
67 69  
68 70 /// <summary>
69 71 /// 初始化一个<see cref="LqKdKdjlbService"/>类型的新实例
... ... @@ -76,7 +78,8 @@ namespace NCC.Extend.LqKdKdjlb
76 78 IUserManager userManager,
77 79 WeChatBotService weChatBotService,
78 80 LqKdKdjlbStringGenerator stringGenerator,
79   - ILogger<LqKdKdjlbService> logger
  81 + ILogger<LqKdKdjlbService> logger,
  82 + LqDailyReportService dailyReportService
80 83 )
81 84 {
82 85 _lqKdKdjlbRepository = lqKdKdjlbRepository;
... ... @@ -88,6 +91,7 @@ namespace NCC.Extend.LqKdKdjlb
88 91 _weChatBotService = weChatBotService;
89 92 _stringGenerator = stringGenerator;
90 93 _logger = logger;
  94 + _dailyReportService = dailyReportService;
91 95 }
92 96  
93 97 #region 获取开单记录表
... ... @@ -966,6 +970,47 @@ namespace NCC.Extend.LqKdKdjlb
966 970 // 字符串生成失败不影响主流程,只记录日志
967 971 Console.WriteLine($"生成开单记录字符串失败: {ex.Message}");
968 972 }
  973 +
  974 + // 播报事业部开单统计数据
  975 + try
  976 + {
  977 + //判断如果开单金额大于0,则播报事业部开单统计数据
  978 + if (newEntity.Sfyj > 0 && newEntity.Kdrq.HasValue)
  979 + {
  980 + var billingDate = newEntity.Kdrq.Value.ToString("yyyy-MM-dd");
  981 + var statisticsInput = new BusinessUnitBillingStatisticsInput
  982 + {
  983 + Date = billingDate
  984 + };
  985 + var statisticsText = await _dailyReportService.GetBusinessUnitBillingStatisticsText(statisticsInput);
  986 +
  987 + if (!string.IsNullOrEmpty(statisticsText) && !statisticsText.Contains("暂无开单数据"))
  988 + {
  989 + // 发送到企业微信群
  990 + try
  991 + {
  992 + var sendResult = await _weChatBotService.SendTextMessage(statisticsText);
  993 + if (sendResult)
  994 + {
  995 + Console.WriteLine("事业部开单统计数据已成功发送到企业微信群");
  996 + }
  997 + else
  998 + {
  999 + Console.WriteLine("事业部开单统计数据发送到企业微信群失败");
  1000 + }
  1001 + }
  1002 + catch (Exception wechatEx)
  1003 + {
  1004 + Console.WriteLine($"发送事业部开单统计数据异常: {wechatEx.Message}");
  1005 + }
  1006 + }
  1007 + }
  1008 + }
  1009 + catch (Exception ex)
  1010 + {
  1011 + // 播报失败不影响主流程,只记录日志
  1012 + Console.WriteLine($"播报事业部开单统计数据失败: {ex.Message}");
  1013 + }
969 1014 }
970 1015 catch (Exception ex)
971 1016 {
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs
... ... @@ -14,6 +14,9 @@ using NCC.Extend.Entitys.Enum;
14 14 using NCC.Extend.Entitys.lq_package_info;
15 15 using NCC.Extend.Entitys.lq_package_item_detail;
16 16 using NCC.Extend.Entitys.lq_xmzl;
  17 +using NCC.Extend.Entitys.lq_kd_kdjlb;
  18 +using NCC.Extend.Entitys.lq_kd_pxmx;
  19 +using NCC.Extend.Entitys.lq_mdxx;
17 20 using NCC.Extend.Interfaces.LqPackageInfo;
18 21 using NCC.FriendlyException;
19 22 using SqlSugar;
... ... @@ -693,5 +696,404 @@ namespace NCC.Extend.LqPackageInfo
693 696 }
694 697 #endregion
695 698  
  699 + #region 营销活动按门店统计
  700 + /// <summary>
  701 + /// 获取营销活动按门店统计数据
  702 + /// </summary>
  703 + /// <remarks>
  704 + /// 统计指定营销活动在各个门店的开单数据
  705 + /// 包括:开单数量、开单金额、欠款金额、人头数、项目数
  706 + ///
  707 + /// 示例请求:
  708 + /// ```json
  709 + /// {
  710 + /// "activityId": "营销活动ID",
  711 + /// "startTime": "2025-10-01",
  712 + /// "endTime": "2025-10-31",
  713 + /// "storeIds": ["门店ID1", "门店ID2"]
  714 + /// }
  715 + /// ```
  716 + ///
  717 + /// 返回字段说明:
  718 + /// - ActivityId: 营销活动ID
  719 + /// - ActivityName: 营销活动名称
  720 + /// - StoreList: 门店统计列表
  721 + /// - StoreId: 门店ID
  722 + /// - StoreName: 门店名称
  723 + /// - BillingCount: 开单数量
  724 + /// - BillingAmount: 开单金额(实付业绩总和)
  725 + /// - DebtAmount: 欠款金额
  726 + /// - CustomerCount: 人头数(去重客户数)
  727 + /// - ItemCount: 项目数(品项项目次数总和,使用F_ProjectNumber字段)
  728 + /// </remarks>
  729 + /// <param name="input">查询参数</param>
  730 + /// <returns>按门店统计数据</returns>
  731 + /// <response code="200">成功返回统计数据</response>
  732 + /// <response code="400">参数错误</response>
  733 + /// <response code="500">服务器错误</response>
  734 + [HttpPost("get-activity-statistics-by-store")]
  735 + public async Task<object> GetActivityStatisticsByStore(ActivityStatisticsInput input)
  736 + {
  737 + try
  738 + {
  739 + // 1. 获取营销活动信息
  740 + var activity = await _db.Queryable<LqPackageInfoEntity>()
  741 + .Where(x => x.Id == input.ActivityId && x.IsEffective == StatusEnum.有效.GetHashCode())
  742 + .FirstAsync();
  743 +
  744 + if (activity == null)
  745 + {
  746 + throw NCCException.Oh("营销活动不存在或已失效");
  747 + }
  748 +
  749 + // 2. 设置时间范围(如果未提供,使用活动时间范围)
  750 + var startTime = input.StartTime ?? activity.StartTime;
  751 + var endTime = input.EndTime ?? activity.EndTime;
  752 +
  753 + // 3. 构建基础查询:开单记录 JOIN 门店表
  754 + var baseQuery = _db.Queryable<LqKdKdjlbEntity, LqMdxxEntity>(
  755 + (kd, md) => kd.Djmd == md.Id)
  756 + .Where((kd, md) => kd.IsEffective == StatusEnum.有效.GetHashCode())
  757 + .Where((kd, md) => kd.ActivityId == input.ActivityId)
  758 + .Where((kd, md) => kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  759 +
  760 + // 4. 门店筛选
  761 + if (input.StoreIds != null && input.StoreIds.Any())
  762 + {
  763 + baseQuery = baseQuery.Where((kd, md) => input.StoreIds.Contains(kd.Djmd));
  764 + }
  765 +
  766 + // 5. 按门店分组统计(先获取基础统计数据)
  767 + var storeStatistics = await baseQuery
  768 + .GroupBy((kd, md) => new { StoreId = kd.Djmd, StoreName = md.Dm })
  769 + .Select((kd, md) => new
  770 + {
  771 + StoreId = kd.Djmd,
  772 + StoreName = md.Dm ?? "",
  773 + BillingCount = SqlFunc.AggregateCount(kd.Id),
  774 + BillingAmount = SqlFunc.AggregateSum(kd.Sfyj),
  775 + DebtAmount = SqlFunc.AggregateSum(kd.Qk)
  776 + })
  777 + .ToListAsync();
  778 +
  779 + // 6. 单独统计每个门店的人头数和项目数
  780 + var storeList = new List<StoreStatisticsItem>();
  781 + foreach (var stat in storeStatistics)
  782 + {
  783 + // 统计人头数(去重客户数)
  784 + var customerCountQuery = _db.Queryable<LqKdKdjlbEntity>()
  785 + .Where(kd => kd.Djmd == stat.StoreId
  786 + && kd.ActivityId == input.ActivityId
  787 + && kd.IsEffective == StatusEnum.有效.GetHashCode()
  788 + && kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  789 +
  790 + if (input.StoreIds != null && input.StoreIds.Any())
  791 + {
  792 + customerCountQuery = customerCountQuery.Where(kd => input.StoreIds.Contains(kd.Djmd));
  793 + }
  794 +
  795 + var customerCount = await customerCountQuery
  796 + .GroupBy(kd => kd.Kdhy)
  797 + .Select(kd => kd.Kdhy)
  798 + .CountAsync();
  799 +
  800 + // 统计项目数(品项项目次数总和,使用F_ProjectNumber字段)
  801 + var itemCountQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>(
  802 + (px, kd) => px.Glkdbh == kd.Id)
  803 + .Where((px, kd) => kd.Djmd == stat.StoreId
  804 + && px.IsEffective == StatusEnum.有效.GetHashCode()
  805 + && kd.IsEffective == StatusEnum.有效.GetHashCode()
  806 + && kd.ActivityId == input.ActivityId
  807 + && kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  808 +
  809 + if (input.StoreIds != null && input.StoreIds.Any())
  810 + {
  811 + itemCountQuery = itemCountQuery.Where((px, kd) => input.StoreIds.Contains(kd.Djmd));
  812 + }
  813 +
  814 + var itemCount = await itemCountQuery
  815 + .Select((px, kd) => SqlFunc.AggregateSum(px.ProjectNumber))
  816 + .FirstAsync();
  817 +
  818 + storeList.Add(new StoreStatisticsItem
  819 + {
  820 + StoreId = stat.StoreId ?? "",
  821 + StoreName = stat.StoreName ?? "",
  822 + BillingCount = stat.BillingCount,
  823 + BillingAmount = stat.BillingAmount,
  824 + DebtAmount = stat.DebtAmount,
  825 + CustomerCount = customerCount,
  826 + ItemCount = itemCount
  827 + });
  828 + }
  829 +
  830 + // 7. 排序
  831 + storeList = storeList.OrderBy(x => x.StoreName).ToList();
  832 +
  833 + // 7. 返回统计结果
  834 + return new ActivityStatisticsByStoreOutput
  835 + {
  836 + ActivityId = input.ActivityId,
  837 + ActivityName = activity.ActivityName,
  838 + StoreList = storeList
  839 + };
  840 + }
  841 + catch (Exception ex)
  842 + {
  843 + throw NCCException.Oh($"获取营销活动按门店统计数据失败: {ex.Message}");
  844 + }
  845 + }
  846 + #endregion
  847 +
  848 + #region 营销活动按品项统计
  849 + /// <summary>
  850 + /// 获取营销活动按品项统计数据
  851 + /// </summary>
  852 + /// <remarks>
  853 + /// 统计指定营销活动各个品项的销售情况
  854 + /// 包括:销售数量、销售金额、开单数量、销售次数
  855 + ///
  856 + /// 示例请求:
  857 + /// ```json
  858 + /// {
  859 + /// "activityId": "营销活动ID",
  860 + /// "startTime": "2025-10-01",
  861 + /// "endTime": "2025-10-31"
  862 + /// }
  863 + /// ```
  864 + /// </remarks>
  865 + /// <param name="input">查询参数</param>
  866 + /// <returns>按品项统计数据</returns>
  867 + /// <response code="200">成功返回统计数据</response>
  868 + /// <response code="400">参数错误</response>
  869 + /// <response code="500">服务器错误</response>
  870 + [HttpPost("get-activity-statistics-by-item")]
  871 + public async Task<object> GetActivityStatisticsByItem(ActivityStatisticsInput input)
  872 + {
  873 + try
  874 + {
  875 + // 1. 获取营销活动信息
  876 + var activity = await _db.Queryable<LqPackageInfoEntity>()
  877 + .Where(x => x.Id == input.ActivityId && x.IsEffective == StatusEnum.有效.GetHashCode())
  878 + .FirstAsync();
  879 +
  880 + if (activity == null)
  881 + {
  882 + throw NCCException.Oh("营销活动不存在或已失效");
  883 + }
  884 +
  885 + // 2. 设置时间范围(如果未提供,使用活动时间范围)
  886 + var startTime = input.StartTime ?? activity.StartTime;
  887 + var endTime = input.EndTime ?? activity.EndTime;
  888 +
  889 + // 3. 构建基础查询:品项明细 JOIN 开单记录(用于过滤活动ID和时间)
  890 + var baseQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>(
  891 + (px, kd) => px.Glkdbh == kd.Id)
  892 + .Where((px, kd) => px.IsEffective == StatusEnum.有效.GetHashCode())
  893 + .Where((px, kd) => kd.IsEffective == StatusEnum.有效.GetHashCode())
  894 + .Where((px, kd) => kd.ActivityId == input.ActivityId)
  895 + .Where((px, kd) => kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  896 +
  897 + // 4. 门店筛选(如果提供)
  898 + if (input.StoreIds != null && input.StoreIds.Any())
  899 + {
  900 + baseQuery = baseQuery.Where((px, kd) => input.StoreIds.Contains(kd.Djmd));
  901 + }
  902 +
  903 + // 5. 按品项分组统计(先获取基础统计数据)
  904 + var itemStatistics = await baseQuery
  905 + .GroupBy((px, kd) => new { ItemId = px.Px, ItemName = px.Pxmc })
  906 + .Select((px, kd) => new
  907 + {
  908 + ItemId = px.Px ?? "",
  909 + ItemName = px.Pxmc ?? "",
  910 + SalesQuantity = SqlFunc.AggregateSum(px.ProjectNumber),
  911 + SalesAmount = SqlFunc.AggregateSum(px.ActualPrice > 0 ? px.ActualPrice : px.TotalPrice),
  912 + SalesCount = SqlFunc.AggregateCount(px.Id)
  913 + })
  914 + .ToListAsync();
  915 +
  916 + // 6. 单独统计每个品项的开单数量(去重开单ID)
  917 + var itemList = new List<ItemStatisticsItem>();
  918 + foreach (var stat in itemStatistics)
  919 + {
  920 + var billingCountQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>(
  921 + (px, kd) => px.Glkdbh == kd.Id)
  922 + .Where((px, kd) => px.Px == stat.ItemId
  923 + && px.IsEffective == StatusEnum.有效.GetHashCode()
  924 + && kd.IsEffective == StatusEnum.有效.GetHashCode()
  925 + && kd.ActivityId == input.ActivityId
  926 + && kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  927 +
  928 + if (input.StoreIds != null && input.StoreIds.Any())
  929 + {
  930 + billingCountQuery = billingCountQuery.Where((px, kd) => input.StoreIds.Contains(kd.Djmd));
  931 + }
  932 +
  933 + var billingCount = await billingCountQuery
  934 + .GroupBy((px, kd) => px.Glkdbh)
  935 + .Select((px, kd) => px.Glkdbh)
  936 + .CountAsync();
  937 +
  938 + itemList.Add(new ItemStatisticsItem
  939 + {
  940 + ItemId = stat.ItemId ?? "",
  941 + ItemName = stat.ItemName ?? "",
  942 + SalesQuantity = stat.SalesQuantity,
  943 + SalesAmount = stat.SalesAmount,
  944 + BillingCount = billingCount,
  945 + SalesCount = stat.SalesCount
  946 + });
  947 + }
  948 +
  949 + // 7. 排序
  950 + itemList = itemList.OrderBy(x => x.ItemName).ToList();
  951 +
  952 + // 7. 返回统计结果
  953 + return new ActivityStatisticsByItemOutput
  954 + {
  955 + ActivityId = input.ActivityId,
  956 + ActivityName = activity.ActivityName,
  957 + ItemList = itemList
  958 + };
  959 + }
  960 + catch (Exception ex)
  961 + {
  962 + throw NCCException.Oh($"获取营销活动按品项统计数据失败: {ex.Message}");
  963 + }
  964 + }
  965 + #endregion
  966 +
  967 + #region 营销活动按门店品项统计
  968 + /// <summary>
  969 + /// 获取营销活动按门店品项统计数据
  970 + /// </summary>
  971 + /// <remarks>
  972 + /// 统计指定营销活动在各个门店的各个品项销售情况
  973 + /// 包括:销售数量、销售金额、开单数量、销售次数
  974 + ///
  975 + /// 示例请求:
  976 + /// ```json
  977 + /// {
  978 + /// "activityId": "营销活动ID",
  979 + /// "startTime": "2025-10-01",
  980 + /// "endTime": "2025-10-31",
  981 + /// "storeIds": ["门店ID1", "门店ID2"]
  982 + /// }
  983 + /// ```
  984 + /// </remarks>
  985 + /// <param name="input">查询参数</param>
  986 + /// <returns>按门店品项统计数据</returns>
  987 + /// <response code="200">成功返回统计数据</response>
  988 + /// <response code="400">参数错误</response>
  989 + /// <response code="500">服务器错误</response>
  990 + [HttpPost("get-activity-statistics-by-store-item")]
  991 + public async Task<object> GetActivityStatisticsByStoreItem(ActivityStatisticsInput input)
  992 + {
  993 + try
  994 + {
  995 + // 1. 获取营销活动信息
  996 + var activity = await _db.Queryable<LqPackageInfoEntity>()
  997 + .Where(x => x.Id == input.ActivityId && x.IsEffective == StatusEnum.有效.GetHashCode())
  998 + .FirstAsync();
  999 +
  1000 + if (activity == null)
  1001 + {
  1002 + throw NCCException.Oh("营销活动不存在或已失效");
  1003 + }
  1004 +
  1005 + // 2. 设置时间范围(如果未提供,使用活动时间范围)
  1006 + var startTime = input.StartTime ?? activity.StartTime;
  1007 + var endTime = input.EndTime ?? activity.EndTime;
  1008 +
  1009 + // 3. 构建基础查询:品项明细 JOIN 开单记录 JOIN 门店表
  1010 + var baseQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity, LqMdxxEntity>(
  1011 + (px, kd, md) => px.Glkdbh == kd.Id && kd.Djmd == md.Id)
  1012 + .Where((px, kd, md) => px.IsEffective == StatusEnum.有效.GetHashCode())
  1013 + .Where((px, kd, md) => kd.IsEffective == StatusEnum.有效.GetHashCode())
  1014 + .Where((px, kd, md) => kd.ActivityId == input.ActivityId)
  1015 + .Where((px, kd, md) => kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  1016 +
  1017 + // 4. 门店筛选(如果提供)
  1018 + if (input.StoreIds != null && input.StoreIds.Any())
  1019 + {
  1020 + baseQuery = baseQuery.Where((px, kd, md) => input.StoreIds.Contains(kd.Djmd));
  1021 + }
  1022 +
  1023 + // 5. 按门店和品项分组统计(先获取基础统计数据)
  1024 + var storeItemStatistics = await baseQuery
  1025 + .GroupBy((px, kd, md) => new
  1026 + {
  1027 + StoreId = kd.Djmd,
  1028 + StoreName = md.Dm,
  1029 + ItemId = px.Px,
  1030 + ItemName = px.Pxmc
  1031 + })
  1032 + .Select((px, kd, md) => new
  1033 + {
  1034 + StoreId = kd.Djmd ?? "",
  1035 + StoreName = md.Dm ?? "",
  1036 + ItemId = px.Px ?? "",
  1037 + ItemName = px.Pxmc ?? "",
  1038 + SalesQuantity = SqlFunc.AggregateSum(px.ProjectNumber),
  1039 + SalesAmount = SqlFunc.AggregateSum(px.ActualPrice > 0 ? px.ActualPrice : px.TotalPrice),
  1040 + SalesCount = SqlFunc.AggregateCount(px.Id)
  1041 + })
  1042 + .ToListAsync();
  1043 +
  1044 + // 6. 单独统计每个门店品项的开单数量(去重开单ID)
  1045 + var storeItemList = new List<StoreItemStatisticsItem>();
  1046 + foreach (var stat in storeItemStatistics)
  1047 + {
  1048 + var billingCountQuery = _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>(
  1049 + (px, kd) => px.Glkdbh == kd.Id)
  1050 + .Where((px, kd) => px.Px == stat.ItemId
  1051 + && kd.Djmd == stat.StoreId
  1052 + && px.IsEffective == StatusEnum.有效.GetHashCode()
  1053 + && kd.IsEffective == StatusEnum.有效.GetHashCode()
  1054 + && kd.ActivityId == input.ActivityId
  1055 + && kd.Kdrq.HasValue && kd.Kdrq.Value >= startTime && kd.Kdrq.Value <= endTime);
  1056 +
  1057 + if (input.StoreIds != null && input.StoreIds.Any())
  1058 + {
  1059 + billingCountQuery = billingCountQuery.Where((px, kd) => input.StoreIds.Contains(kd.Djmd));
  1060 + }
  1061 +
  1062 + var billingCount = await billingCountQuery
  1063 + .GroupBy((px, kd) => px.Glkdbh)
  1064 + .Select((px, kd) => px.Glkdbh)
  1065 + .CountAsync();
  1066 +
  1067 + storeItemList.Add(new StoreItemStatisticsItem
  1068 + {
  1069 + StoreId = stat.StoreId ?? "",
  1070 + StoreName = stat.StoreName ?? "",
  1071 + ItemId = stat.ItemId ?? "",
  1072 + ItemName = stat.ItemName ?? "",
  1073 + SalesQuantity = stat.SalesQuantity,
  1074 + SalesAmount = stat.SalesAmount,
  1075 + BillingCount = billingCount,
  1076 + SalesCount = stat.SalesCount
  1077 + });
  1078 + }
  1079 +
  1080 + // 7. 排序
  1081 + storeItemList = storeItemList.OrderBy(x => x.StoreName).ThenBy(x => x.ItemName).ToList();
  1082 +
  1083 + // 7. 返回统计结果
  1084 + return new ActivityStatisticsByStoreItemOutput
  1085 + {
  1086 + ActivityId = input.ActivityId,
  1087 + ActivityName = activity.ActivityName,
  1088 + StoreItemList = storeItemList
  1089 + };
  1090 + }
  1091 + catch (Exception ex)
  1092 + {
  1093 + throw NCCException.Oh($"获取营销活动按门店品项统计数据失败: {ex.Message}");
  1094 + }
  1095 + }
  1096 + #endregion
  1097 +
696 1098 }
697 1099 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqYaoyjlService.cs
... ... @@ -93,6 +93,7 @@ namespace NCC.Extend.LqYaoyjl
93 93 .WhereIF(queryLxsj != null, p => p.Lxsj >= new DateTime(startLxsj.ToDate().Year, startLxsj.ToDate().Month, startLxsj.ToDate().Day, 0, 0, 0))
94 94 .WhereIF(queryLxsj != null, p => p.Lxsj <= new DateTime(endLxsj.ToDate().Year, endLxsj.ToDate().Month, endLxsj.ToDate().Day, 23, 59, 59))
95 95 .WhereIF(!string.IsNullOrEmpty(input.lxjl), p => p.Lxjl.Contains(input.lxjl))
  96 + .WhereIF(!string.IsNullOrEmpty(input.storeId), p => p.StoreId.Equals(input.storeId))
96 97 .Select(it => new LqYaoyjlListOutput
97 98 {
98 99 id = it.Id,
... ... @@ -158,6 +159,7 @@ namespace NCC.Extend.LqYaoyjl
158 159 .WhereIF(queryLxsj != null, p => p.Lxsj >= new DateTime(startLxsj.ToDate().Year, startLxsj.ToDate().Month, startLxsj.ToDate().Day, 0, 0, 0))
159 160 .WhereIF(queryLxsj != null, p => p.Lxsj <= new DateTime(endLxsj.ToDate().Year, endLxsj.ToDate().Month, endLxsj.ToDate().Day, 23, 59, 59))
160 161 .WhereIF(!string.IsNullOrEmpty(input.lxjl), p => p.Lxjl.Contains(input.lxjl))
  162 + .WhereIF(!string.IsNullOrEmpty(input.storeId), p => p.StoreId.Equals(input.storeId))
161 163 .Select(it => new LqYaoyjlListOutput
162 164 {
163 165 id = it.Id,
... ...