Commit d19ef8a50f9f253d7d01ea9d33835dd990b990c5

Authored by 李宇
2 parents fb47691a bc35ccd1

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

Showing 29 changed files with 2693 additions and 93 deletions
antis-ncc-admin/.env.development
... ... @@ -2,6 +2,6 @@
2 2  
3 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com'
5   -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
6   -# VUE_APP_BASE_API = 'http://localhost:2011'
  5 +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
  6 +VUE_APP_BASE_API = 'http://localhost:2011'
7 7 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs 0 → 100644
  1 +using System.ComponentModel.DataAnnotations;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqJinsanjiaoUser
  4 +{
  5 + /// <summary>
  6 + /// 删除金三角用户绑定关系输入
  7 + /// </summary>
  8 + public class LqJinsanjiaoUserDeleteInput
  9 + {
  10 + /// <summary>
  11 + /// 金三角用户关系ID
  12 + /// </summary>
  13 + [Required(ErrorMessage = "ID不能为空")]
  14 + public string Id { get; set; }
  15 + }
  16 +}
  17 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqReport
  5 +{
  6 + /// <summary>
  7 + /// 客户到店次数统计输入
  8 + /// </summary>
  9 + public class CustomerVisitFrequencyInput
  10 + {
  11 + /// <summary>
  12 + /// 开始时间(可选,默认为当月1号)
  13 + /// </summary>
  14 + public DateTime? StartTime { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 结束时间(可选,默认为当前时间)
  18 + /// </summary>
  19 + public DateTime? EndTime { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 门店ID列表(可选)
  23 + /// </summary>
  24 + public List<string> StoreIds { get; set; }
  25 + }
  26 +}
  27 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqReport
  2 +{
  3 + /// <summary>
  4 + /// 客户到店次数统计输出
  5 + /// </summary>
  6 + public class CustomerVisitFrequencyOutput
  7 + {
  8 + /// <summary>
  9 + /// 消耗次数(客户到店次数)
  10 + /// </summary>
  11 + public int VisitCount { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 人数(达到该次数的客户人数)
  15 + /// </summary>
  16 + public int CustomerCount { get; set; }
  17 + }
  18 +}
  19 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqReport
  4 +{
  5 + /// <summary>
  6 + /// 健康师排行榜输出
  7 + /// </summary>
  8 + public class HealthCoachRankingOutput
  9 + {
  10 + /// <summary>
  11 + /// 开单业绩排行榜(前20名)
  12 + /// </summary>
  13 + public List<HealthCoachStatisticsOutput> BillingRanking { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 耗卡业绩排行榜(前20名)
  17 + /// </summary>
  18 + public List<HealthCoachStatisticsOutput> ConsumeRanking { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 退卡业绩排行榜(前20名)
  22 + /// </summary>
  23 + public List<HealthCoachStatisticsOutput> RefundRanking { get; set; }
  24 + }
  25 +}
  26 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqReport
  5 +{
  6 + /// <summary>
  7 + /// 健康师统计数据输入
  8 + /// </summary>
  9 + public class HealthCoachStatisticsInput
  10 + {
  11 + /// <summary>
  12 + /// 开始时间(可选,默认为当月1号)
  13 + /// </summary>
  14 + public DateTime? StartTime { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 结束时间(可选,默认为当前时间)
  18 + /// </summary>
  19 + public DateTime? EndTime { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 门店ID列表(可选)
  23 + /// </summary>
  24 + public List<string> StoreIds { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 健康师ID列表(可选)
  28 + /// </summary>
  29 + public List<string> HealthCoachIds { get; set; }
  30 + }
  31 +}
  32 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqReport
  2 +{
  3 + /// <summary>
  4 + /// 健康师统计数据输出
  5 + /// </summary>
  6 + public class HealthCoachStatisticsOutput
  7 + {
  8 + /// <summary>
  9 + /// 健康师ID
  10 + /// </summary>
  11 + public string HealthCoachId { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 健康师姓名
  15 + /// </summary>
  16 + public string HealthCoachName { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 开单业绩
  20 + /// </summary>
  21 + public decimal BillingPerformance { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 消耗业绩
  25 + /// </summary>
  26 + public decimal ConsumePerformance { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 退单业绩
  30 + /// </summary>
  31 + public decimal RefundPerformance { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 开单项目数
  35 + /// </summary>
  36 + public int BillingProjectCount { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 消耗项目数
  40 + /// </summary>
  41 + public int ConsumeProjectCount { get; set; }
  42 +
  43 + /// <summary>
  44 + /// 退单项目数
  45 + /// </summary>
  46 + public int RefundProjectCount { get; set; }
  47 + }
  48 +}
  49 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqReport
  4 +{
  5 + /// <summary>
  6 + /// 门店项目指标统计输入
  7 + /// </summary>
  8 + public class StoreItemStatisticsInput
  9 + {
  10 + /// <summary>
  11 + /// 开始时间
  12 + /// </summary>
  13 + public DateTime? StartTime { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 结束时间
  17 + /// </summary>
  18 + public DateTime? EndTime { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店ID列表
  22 + /// </summary>
  23 + public string[] StoreIds { get; set; }
  24 + }
  25 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqReport
  2 +{
  3 + /// <summary>
  4 + /// 门店项目指标统计输出
  5 + /// </summary>
  6 + public class StoreItemStatisticsOutput
  7 + {
  8 + /// <summary>
  9 + /// 门店ID
  10 + /// </summary>
  11 + public string StoreId { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 门店名称
  15 + /// </summary>
  16 + public string StoreName { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 消耗项目数(项目次数总和)
  20 + /// </summary>
  21 + public int ConsumeProjectCount { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 消耗率(消耗金额/开单金额 × 100%)
  25 + /// </summary>
  26 + public decimal ConsumeRate { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 客单项目数(项目数/消耗人次)
  30 + /// </summary>
  31 + public decimal AvgProjectPerConsume { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 消耗客单价(消耗业绩/消耗人次)
  35 + /// </summary>
  36 + public decimal AvgAmountPerConsume { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 开单金额
  40 + /// </summary>
  41 + public decimal BillingAmount { get; set; }
  42 +
  43 + /// <summary>
  44 + /// 消耗金额
  45 + /// </summary>
  46 + public decimal ConsumeAmount { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 消耗人次
  50 + /// </summary>
  51 + public int ConsumePersonCount { get; set; }
  52 + }
  53 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StorePerformanceComparisonOutput.cs
... ... @@ -39,5 +39,10 @@ namespace NCC.Extend.Entitys.Dto.LqReport
39 39 /// 是否达标(实际业绩 >= 目标业绩)
40 40 /// </summary>
41 41 public bool IsTargetAchieved { get; set; }
  42 +
  43 + /// <summary>
  44 + /// 实际消耗业绩
  45 + /// </summary>
  46 + public decimal ActualConsumePerformance { get; set; }
42 47 }
43 48 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqReport
  4 +{
  5 + /// <summary>
  6 + /// 门店剩余权益统计输入
  7 + /// </summary>
  8 + public class StoreRemainingRightsInput
  9 + {
  10 + /// <summary>
  11 + /// 开始时间(可选,默认为当月1号)
  12 + /// </summary>
  13 + public DateTime? StartTime { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 结束时间(可选,默认为当前时间)
  17 + /// </summary>
  18 + public DateTime? EndTime { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店ID列表(可选,不传则统计所有门店)
  22 + /// </summary>
  23 + public string[] StoreIds { get; set; }
  24 + }
  25 +}
  26 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqReport
  2 +{
  3 + /// <summary>
  4 + /// 门店剩余权益统计输出
  5 + /// </summary>
  6 + public class StoreRemainingRightsOutput
  7 + {
  8 + /// <summary>
  9 + /// 门店ID
  10 + /// </summary>
  11 + public string StoreId { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 门店名称
  15 + /// </summary>
  16 + public string StoreName { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 剩余权益累计金额(开单总金额 - 消耗总金额 - 退卡总金额)
  20 + /// </summary>
  21 + public decimal RemainingRightsAmount { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 剩余权益累计人数(有剩余权益的会员数量)
  25 + /// </summary>
  26 + public int RemainingRightsPersonCount { get; set; }
  27 + }
  28 +}
  29 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs
... ... @@ -21,6 +21,11 @@ namespace NCC.Extend.Entitys.Dto.LqTkjlb
21 21 /// 活动ID(可选)
22 22 /// </summary>
23 23 public string EventId { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 门店ID(可选)
  27 + /// </summary>
  28 + public string[] StoreId { get; set; }
24 29 }
25 30 }
26 31  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs
... ... @@ -31,6 +31,12 @@ namespace NCC.Extend.Entitys.lq_hytk_mx
31 31 public string BillingItemId { get; set; }
32 32  
33 33 /// <summary>
  34 + /// 会员id
  35 + /// </summary>
  36 + [SugarColumn(ColumnName = "F_MemberId")]
  37 + public string MemberId { get; set; }
  38 +
  39 + /// <summary>
34 40 /// 品项
35 41 /// </summary>
36 42 [SugarColumn(ColumnName = "px")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs
... ... @@ -19,10 +19,5 @@ namespace NCC.Extend.Entitys.Enum
19 19 [Description("有效")]
20 20 有效 = 1,
21 21  
22   - /// <summary>
23   - /// 删除
24   - /// </summary>
25   - [Description("删除")]
26   - 删除 = 99,
27 22 }
28 23 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs
... ... @@ -18,7 +18,7 @@ namespace NCC.Extend.LqCardTransferLog
18 18 /// <summary>
19 19 /// 转卡日志服务
20 20 /// </summary>
21   - [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 100)]
  21 + [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 200)]
22 22 [Route("api/Extend/[controller]")]
23 23 public class LqCardTransferLogService : IDynamicApiController, ILqCardTransferLogService, ITransient
24 24 {
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -22,6 +22,7 @@ using NCC.DataEncryption;
22 22 using NCC.ClayObject;
23 23 using NCC.Extend.Entitys.Dto.LqDailyReport;
24 24 using NCC.Extend.Entitys.lq_kd_kdjlb;
  25 +using Microsoft.AspNetCore.Authorization;
25 26  
26 27 namespace NCC.Extend
27 28 {
... ... @@ -103,6 +104,7 @@ namespace NCC.Extend
103 104 /// <response code="200">成功返回门店统计列表</response>
104 105 /// <response code="400">日期格式错误或参数无效</response>
105 106 /// <response code="500">服务器内部错误</response>
  107 + [AllowAnonymous]
106 108 [HttpPost("get-store-daily-statistics")]
107 109 public async Task<List<StoreDailyStatisticsOutput>> GetStoreDailyStatistics(StoreDailyStatisticsInput input)
108 110 {
... ... @@ -198,6 +200,7 @@ namespace NCC.Extend
198 200 /// <response code="200">成功返回门店业绩完成情况列表</response>
199 201 /// <response code="400">日期格式错误或参数无效</response>
200 202 /// <response code="500">服务器内部错误</response>
  203 + [AllowAnonymous]
201 204 [HttpPost("get-store-performance-completion")]
202 205 public async Task<List<StorePerformanceCompletionOutput>> GetStorePerformanceCompletion(StorePerformanceCompletionInput input)
203 206 {
... ... @@ -288,6 +291,7 @@ namespace NCC.Extend
288 291 /// <response code="200">成功返回事业部业绩完成情况列表</response>
289 292 /// <response code="400">日期格式错误或参数无效</response>
290 293 /// <response code="500">服务器内部错误</response>
  294 + [AllowAnonymous]
291 295 [HttpPost("get-business-unit-performance-completion")]
292 296 public async Task<List<BusinessUnitPerformanceCompletionOutput>> GetBusinessUnitPerformanceCompletion(BusinessUnitPerformanceCompletionInput input)
293 297 {
... ... @@ -421,6 +425,7 @@ namespace NCC.Extend
421 425 /// <response code="200">成功返回天王团业绩完成情况列表</response>
422 426 /// <response code="400">日期格式错误或参数无效</response>
423 427 /// <response code="500">服务器内部错误</response>
  428 + [AllowAnonymous]
424 429 [HttpPost("get-tianwang-group-performance-completion")]
425 430 public async Task<List<TianwangGroupPerformanceCompletionOutput>> GetTianwangGroupPerformanceCompletion(TianwangGroupPerformanceCompletionInput input)
426 431 {
... ... @@ -595,6 +600,7 @@ namespace NCC.Extend
595 600 /// <response code="200">成功返回经理业绩完成情况列表</response>
596 601 /// <response code="400">日期格式错误或参数无效</response>
597 602 /// <response code="500">服务器内部错误</response>
  603 + [AllowAnonymous]
598 604 [HttpPost("get-manager-performance-completion")]
599 605 public async Task<List<ManagerPerformanceCompletionOutput>> GetManagerPerformanceCompletion(ManagerPerformanceCompletionInput input)
600 606 {
... ... @@ -699,6 +705,7 @@ namespace NCC.Extend
699 705 /// <response code="200">成功返回经理汇总业绩完成情况列表</response>
700 706 /// <response code="400">日期格式错误或参数无效</response>
701 707 /// <response code="500">服务器内部错误</response>
  708 + [AllowAnonymous]
702 709 [HttpPost("get-manager-summary-performance-completion")]
703 710 public async Task<List<ManagerSummaryPerformanceCompletionOutput>> GetManagerSummaryPerformanceCompletion(ManagerPerformanceCompletionInput input)
704 711 {
... ... @@ -808,6 +815,7 @@ namespace NCC.Extend
808 815 /// <response code="200">成功返回科技部老师统计列表</response>
809 816 /// <response code="400">日期格式错误或参数无效</response>
810 817 /// <response code="500">服务器内部错误</response>
  818 + [AllowAnonymous]
811 819 [HttpPost("get-tech-teacher-daily-statistics")]
812 820 public async Task<List<TechTeacherDailyStatisticsOutput>> GetTechTeacherDailyStatistics(TechTeacherDailyStatisticsInput input)
813 821 {
... ... @@ -841,7 +849,7 @@ namespace NCC.Extend
841 849 techDept.F_Id as TechDepartmentId,
842 850 techDept.F_FullName as TechDepartmentName,
843 851 consume.kjbls as TeacherId,
844   - consume.kjblsxm as TeacherName,
  852 + user.F_RealName as TeacherName,
845 853 COUNT(DISTINCT hyhk.hy) as CustomerCount,
846 854 SUM(consume.F_hdpxNumber) as ConsumeProjectCount,
847 855 SUM(consume.kjblsyj) as ConsumeAchievement
... ... @@ -849,13 +857,14 @@ namespace NCC.Extend
849 857 INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id
850 858 INNER JOIN lq_mdxx store ON hyhk.md = store.F_Id
851 859 LEFT JOIN base_organize techDept ON store.kjb = techDept.F_Id
  860 + LEFT JOIN BASE_USER user ON consume.kjbls = user.F_Id
852 861 WHERE consume.F_IsEffective = 1
853 862 AND hyhk.F_IsEffective = 1
854 863 AND DATE(hyhk.hksj) >= '{startDate:yyyy-MM-dd}'
855 864 AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}'
856 865 {techFilter}
857 866 {teacherFilter}
858   - GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, consume.kjblsxm";
  867 + GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, user.F_RealName";
859 868  
860 869 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql);
861 870  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqGzService.cs
... ... @@ -913,10 +913,5 @@ namespace NCC.Extend.LqGz
913 913 }
914 914  
915 915 #endregion
916   -
917   -
918   -
919   -
920   -
921 916 }
922 917 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs
... ... @@ -24,6 +24,7 @@ using NCC.Extend.Entitys.lq_hytk_hytk;
24 24 using NCC.Extend.Entitys.lq_hytk_jksyj;
25 25 using NCC.Extend.Entitys.lq_hytk_kjbsyj;
26 26 using NCC.Extend.Entitys.lq_hytk_mx;
  27 +using NCC.Extend.Entitys.lq_kd_pxmx;
27 28 using NCC.Extend.Interfaces.LqHytkHytk;
28 29 using NCC.FriendlyException;
29 30 using NCC.JsonSerialization;
... ... @@ -230,6 +231,7 @@ namespace NCC.Extend.LqHytkHytk
230 231 Id = YitIdHelper.NextId().ToString(),
231 232 RefundInfoId = newEntity.Id,
232 233 BillingItemId = item.billingItemId,
  234 + MemberId = newEntity.Hy,
233 235 CreateTime = DateTime.Now,
234 236 CreateUser = userInfo.userId,
235 237 Px = item.px,
... ... @@ -368,18 +370,19 @@ namespace NCC.Extend.LqHytkHytk
368 370 var allMxEntities = new List<LqHytkMxEntity>();
369 371 var allJksyjEntities = new List<LqHytkJksyjEntity>();
370 372 var allKjbsyjEntities = new List<LqHytkKjbsyjEntity>();
371   -
372 373 // 处理品项明细列表
373 374 if (input.lqHytkMxList != null && input.lqHytkMxList.Any())
374 375 {
375 376 foreach (var item in input.lqHytkMxList)
376 377 {
  378 +
377 379 // 创建品项明细实体
378 380 var lqHytkMxEntity = new LqHytkMxEntity
379 381 {
380 382 Id = YitIdHelper.NextId().ToString(),
381 383 RefundInfoId = id,
382 384 BillingItemId = item.billingItemId,
  385 + MemberId = entity.Hy,
383 386 CreateTime = DateTime.Now,
384 387 CreateUser = userInfo.userId,
385 388 Tksj = input.tksj,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -2478,6 +2478,7 @@ namespace NCC.Extend.LqKdKdjlb
2478 2478 Id = YitIdHelper.NextId().ToString(),
2479 2479 RefundInfoId = refundId,
2480 2480 BillingItemId = item.BillingItemId,
  2481 + MemberId = input.FromMemberId, // 转卡时使用转出方会员ID
2481 2482 CreateTime = transferTime,
2482 2483 CreateUser = userInfo.userId,
2483 2484 Px = refundPxmxEntity.Px,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -106,8 +106,8 @@ namespace NCC.Extend
106 106  
107 107 if (input.StoreIds != null && input.StoreIds.Any())
108 108 {
109   - sql += " AND s.F_StoreId IN @storeIds";
110   - parameters = new { startMonth = input.StartMonth, endMonth = input.EndMonth, storeIds = input.StoreIds };
  109 + var storeIdsStr = string.Join("','", input.StoreIds);
  110 + sql += $" AND s.F_StoreId IN ('{storeIdsStr}')";
111 111 }
112 112  
113 113 sql += " ORDER BY s.F_StoreName, s.F_StatisticsMonth";
... ... @@ -751,15 +751,12 @@ namespace NCC.Extend
751 751 AND kd.kdrq >= @startTime
752 752 AND kd.kdrq <= @endTime";
753 753  
754   - object billingParameters;
  754 + object billingParameters = new { startTime, endTime };
  755 +
755 756 if (input.StoreIds != null && input.StoreIds.Any())
756 757 {
757   - billingSql += " AND kd.djmd IN @storeIds";
758   - billingParameters = new { startTime, endTime, storeIds = input.StoreIds };
759   - }
760   - else
761   - {
762   - billingParameters = new { startTime, endTime };
  758 + var storeIdsStr = string.Join("','", input.StoreIds);
  759 + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')";
763 760 }
764 761  
765 762 var billingResult = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters);
... ... @@ -776,15 +773,12 @@ namespace NCC.Extend
776 773 AND xh.hksj >= @startTime
777 774 AND xh.hksj <= @endTime";
778 775  
779   - object consumeParameters;
  776 + object consumeParameters = new { startTime, endTime };
  777 +
780 778 if (input.StoreIds != null && input.StoreIds.Any())
781 779 {
782   - consumeSql += " AND xh.md IN @storeIds";
783   - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds };
784   - }
785   - else
786   - {
787   - consumeParameters = new { startTime, endTime };
  780 + var storeIdsStr = string.Join("','", input.StoreIds);
  781 + consumeSql += $" AND xh.md IN ('{storeIdsStr}')";
788 782 }
789 783  
790 784 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters);
... ... @@ -801,15 +795,12 @@ namespace NCC.Extend
801 795 AND hytk.tksj >= @startTime
802 796 AND hytk.tksj <= @endTime";
803 797  
804   - object refundParameters;
  798 + object refundParameters = new { startTime, endTime };
  799 +
805 800 if (input.StoreIds != null && input.StoreIds.Any())
806 801 {
807   - refundSql += " AND hytk.md IN @storeIds";
808   - refundParameters = new { startTime, endTime, storeIds = input.StoreIds };
809   - }
810   - else
811   - {
812   - refundParameters = new { startTime, endTime };
  802 + var storeIdsStr = string.Join("','", input.StoreIds);
  803 + refundSql += $" AND hytk.md IN ('{storeIdsStr}')";
813 804 }
814 805  
815 806 var refundResult = await _db.Ado.SqlQueryAsync<dynamic>(refundSql, refundParameters);
... ... @@ -818,28 +809,26 @@ namespace NCC.Extend
818 809  
819 810 // 第四步:获取消耗目标业绩(所有门店xhyj字段的总和)
820 811 var targetConsumeSql = "SELECT COALESCE(SUM(CAST(md.xhyj AS DECIMAL(18,2))), 0) as target_consume_amount FROM lq_mdxx md WHERE 1=1";
821   - object targetConsumeParameters = null;
822 812  
823 813 if (input.StoreIds != null && input.StoreIds.Any())
824 814 {
825   - targetConsumeSql += " AND md.F_Id IN @storeIds";
826   - targetConsumeParameters = new { storeIds = input.StoreIds };
  815 + var storeIdsStr = string.Join("','", input.StoreIds);
  816 + targetConsumeSql += $" AND md.F_Id IN ('{storeIdsStr}')";
827 817 }
828 818  
829   - var targetConsumeResult = await _db.Ado.SqlQueryAsync<dynamic>(targetConsumeSql, targetConsumeParameters);
  819 + var targetConsumeResult = await _db.Ado.SqlQueryAsync<dynamic>(targetConsumeSql);
830 820 var targetConsumeAmount = Convert.ToDecimal(targetConsumeResult?.FirstOrDefault()?.target_consume_amount ?? 0m);
831 821  
832 822 // 第五步:获取开单目标业绩(所有门店xsyj字段的总和)
833 823 var targetBillingSql = "SELECT COALESCE(SUM(CAST(md.xsyj AS DECIMAL(18,2))), 0) as target_billing_amount FROM lq_mdxx md WHERE 1=1";
834   - object targetBillingParameters = null;
835 824  
836 825 if (input.StoreIds != null && input.StoreIds.Any())
837 826 {
838   - targetBillingSql += " AND md.F_Id IN @storeIds";
839   - targetBillingParameters = new { storeIds = input.StoreIds };
  827 + var storeIdsStr = string.Join("','", input.StoreIds);
  828 + targetBillingSql += $" AND md.F_Id IN ('{storeIdsStr}')";
840 829 }
841 830  
842   - var targetBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(targetBillingSql, targetBillingParameters);
  831 + var targetBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(targetBillingSql);
843 832 var targetBillingAmount = Convert.ToDecimal(targetBillingResult?.FirstOrDefault()?.target_billing_amount ?? 0m);
844 833  
845 834 // 计算开单完成业绩(开单总金额 - 退卡总金额)
... ... @@ -958,15 +947,12 @@ namespace NCC.Extend
958 947 WHERE tk.F_CreateTime >= @startTime
959 948 AND tk.F_CreateTime <= @endTime";
960 949  
961   - object inviteParameters;
  950 + object inviteParameters = new { startTime, endTime };
  951 +
962 952 if (input.StoreIds != null && input.StoreIds.Any())
963 953 {
964   - inviteSql += " AND tk.F_StoreId IN @storeIds";
965   - inviteParameters = new { startTime, endTime, storeIds = input.StoreIds };
966   - }
967   - else
968   - {
969   - inviteParameters = new { startTime, endTime };
  954 + var storeIdsStr = string.Join("','", input.StoreIds);
  955 + inviteSql += $" AND tk.F_StoreId IN ('{storeIdsStr}')";
970 956 }
971 957  
972 958 var inviteResult = await _db.Ado.SqlQueryAsync<dynamic>(inviteSql, inviteParameters);
... ... @@ -981,15 +967,12 @@ namespace NCC.Extend
981 967 AND xh.hksj <= @endTime
982 968 AND xh.xfje > 0";
983 969  
984   - object consumeParameters;
  970 + object consumeParameters = new { startTime, endTime };
  971 +
985 972 if (input.StoreIds != null && input.StoreIds.Any())
986 973 {
987   - consumeSql += " AND xh.md IN @storeIds";
988   - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds };
989   - }
990   - else
991   - {
992   - consumeParameters = new { startTime, endTime };
  974 + var storeIdsStr = string.Join("','", input.StoreIds);
  975 + consumeSql += $" AND xh.md IN ('{storeIdsStr}')";
993 976 }
994 977  
995 978 var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters);
... ... @@ -1047,6 +1030,7 @@ namespace NCC.Extend
1047 1030 /// - StoreName: 门店名称
1048 1031 /// - TargetPerformance: 目标业绩(来自门店资料表xsyj字段)
1049 1032 /// - ActualPerformance: 实际开单业绩(统计期间内的开单业绩总和)
  1033 + /// - ActualConsumePerformance: 实际消耗业绩(统计期间内的消耗业绩总和)
1050 1034 /// - CompletionRate: 完成率(实际业绩/目标业绩 × 100%)
1051 1035 /// - Difference: 差额(实际业绩-目标业绩)
1052 1036 /// - IsTargetAchieved: 是否达标(实际业绩 >= 目标业绩)
... ... @@ -1065,42 +1049,106 @@ namespace NCC.Extend
1065 1049 var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
1066 1050 var endTime = input.EndTime ?? DateTime.Now;
1067 1051  
1068   - // 构建SQL查询
1069   - var sql = @"
  1052 + // 先获取门店基础信息和目标业绩
  1053 + var storeSql = "SELECT F_Id, dm, xsyj FROM lq_mdxx WHERE 1=1";
  1054 +
  1055 + if (input.StoreIds != null && input.StoreIds.Any())
  1056 + {
  1057 + var storeIdsStr = string.Join("','", input.StoreIds);
  1058 + storeSql += $" AND F_Id IN ('{storeIdsStr}')";
  1059 + }
  1060 +
  1061 + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql);
  1062 +
  1063 + // 构建门店字典
  1064 + var storeDict = new Dictionary<string, string>();
  1065 + var targetDict = new Dictionary<string, decimal>();
  1066 + foreach (var store in stores)
  1067 + {
  1068 + var storeId = store.F_Id?.ToString();
  1069 + if (!string.IsNullOrEmpty(storeId))
  1070 + {
  1071 + storeDict[storeId] = store.dm?.ToString() ?? "未知门店";
  1072 + targetDict[storeId] = Convert.ToDecimal(store.xsyj ?? 0);
  1073 + }
  1074 + }
  1075 +
  1076 + // 统计开单业绩
  1077 + var billingSql = @"
1070 1078 SELECT
1071   - md.F_Id as store_id,
1072   - md.dm as store_name,
1073   - COALESCE(CAST(md.xsyj AS DECIMAL(18,2)), 0) as target_performance,
  1079 + kd.djmd as store_id,
1074 1080 COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as actual_performance
1075   - FROM lq_mdxx md
1076   - LEFT JOIN lq_kd_kdjlb kd ON md.F_Id = kd.djmd
1077   - AND kd.F_IsEffective = 1
  1081 + FROM lq_kd_kdjlb kd
  1082 + WHERE kd.F_IsEffective = 1
1078 1083 AND kd.kdrq >= @startTime
1079 1084 AND kd.kdrq <= @endTime";
1080 1085  
1081   - object parameters;
  1086 + object billingParameters = new { startTime, endTime };
  1087 +
1082 1088 if (input.StoreIds != null && input.StoreIds.Any())
1083 1089 {
1084   - sql += " AND md.F_Id IN @storeIds";
1085   - parameters = new { startTime, endTime, storeIds = input.StoreIds };
  1090 + var storeIdsStr = string.Join("','", input.StoreIds);
  1091 + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')";
1086 1092 }
1087   - else
  1093 +
  1094 + billingSql += " GROUP BY kd.djmd";
  1095 +
  1096 + var billingResults = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters);
  1097 +
  1098 + // 构建开单业绩字典
  1099 + var billingDict = new Dictionary<string, decimal>();
  1100 + foreach (var item in billingResults)
1088 1101 {
1089   - parameters = new { startTime, endTime };
  1102 + var storeId = item.store_id?.ToString();
  1103 + if (!string.IsNullOrEmpty(storeId))
  1104 + {
  1105 + billingDict[storeId] = Convert.ToDecimal(item.actual_performance ?? 0);
  1106 + }
1090 1107 }
1091 1108  
1092   - sql += " GROUP BY md.F_Id, md.dm, md.xsyj ORDER BY actual_performance DESC";
  1109 + // 统计消耗业绩
  1110 + var consumeSql = @"
  1111 + SELECT
  1112 + xh.md as store_id,
  1113 + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as actual_consume_performance
  1114 + FROM lq_xh_hyhk xh
  1115 + WHERE xh.F_IsEffective = 1
  1116 + AND xh.hksj >= @startTime
  1117 + AND xh.hksj <= @endTime";
1093 1118  
1094   - var results = await _db.Ado.SqlQueryAsync<dynamic>(sql, parameters);
  1119 + object consumeParameters = new { startTime, endTime };
1095 1120  
1096   - var storePerformanceList = new List<StorePerformanceComparisonOutput>();
  1121 + if (input.StoreIds != null && input.StoreIds.Any())
  1122 + {
  1123 + var storeIdsStr = string.Join("','", input.StoreIds);
  1124 + consumeSql += $" AND xh.md IN ('{storeIdsStr}')";
  1125 + }
  1126 +
  1127 + consumeSql += " GROUP BY xh.md";
1097 1128  
1098   - foreach (var item in results)
  1129 + var consumeResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters);
  1130 +
  1131 + // 构建消耗业绩字典
  1132 + var consumeDict = new Dictionary<string, decimal>();
  1133 + foreach (var item in consumeResults)
1099 1134 {
1100 1135 var storeId = item.store_id?.ToString();
1101   - var storeName = item.store_name?.ToString();
1102   - var targetPerformance = Convert.ToDecimal(item.target_performance ?? 0);
1103   - var actualPerformance = Convert.ToDecimal(item.actual_performance ?? 0);
  1136 + if (!string.IsNullOrEmpty(storeId))
  1137 + {
  1138 + consumeDict[storeId] = Convert.ToDecimal(item.actual_consume_performance ?? 0);
  1139 + }
  1140 + }
  1141 +
  1142 + // 合并数据
  1143 + var storePerformanceList = new List<StorePerformanceComparisonOutput>();
  1144 +
  1145 + foreach (var kvp in storeDict)
  1146 + {
  1147 + var storeId = kvp.Key;
  1148 + var storeName = kvp.Value;
  1149 + var targetPerformance = targetDict.ContainsKey(storeId) ? targetDict[storeId] : 0;
  1150 + var actualPerformance = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0;
  1151 + var actualConsumePerformance = consumeDict.ContainsKey(storeId) ? consumeDict[storeId] : 0;
1104 1152  
1105 1153 // 计算完成率
1106 1154 var completionRate = targetPerformance > 0
... ... @@ -1119,12 +1167,16 @@ namespace NCC.Extend
1119 1167 StoreName = storeName,
1120 1168 TargetPerformance = targetPerformance,
1121 1169 ActualPerformance = actualPerformance,
  1170 + ActualConsumePerformance = actualConsumePerformance,
1122 1171 CompletionRate = completionRate,
1123 1172 Difference = difference,
1124 1173 IsTargetAchieved = isTargetAchieved
1125 1174 });
1126 1175 }
1127 1176  
  1177 + // 按实际业绩排序
  1178 + storePerformanceList = storePerformanceList.OrderByDescending(x => x.ActualPerformance).ToList();
  1179 +
1128 1180 return storePerformanceList;
1129 1181 }
1130 1182 catch (Exception ex)
... ... @@ -1266,7 +1318,7 @@ namespace NCC.Extend
1266 1318 SELECT F_Id FROM lq_xh_hyhk
1267 1319 WHERE hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
1268 1320 AND hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
1269   - {(input.StoreIds != null && input.StoreIds.Any() ? $"AND F_StoreId IN ('{string.Join("','", input.StoreIds)}')" : "")}
  1321 + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND md IN ('{string.Join("','", input.StoreIds)}')" : "")}
1270 1322 )
1271 1323 GROUP BY xh.px";
1272 1324  
... ... @@ -1325,5 +1377,1537 @@ namespace NCC.Extend
1325 1377 }
1326 1378  
1327 1379 #endregion
  1380 +
  1381 + #region 获取门店项目指标统计数据
  1382 +
  1383 + /// <summary>
  1384 + /// 获取门店项目指标统计数据
  1385 + /// </summary>
  1386 + /// <remarks>
  1387 + /// 统计指定时间范围内各门店的项目指标数据
  1388 + /// 包括:消耗项目数、消耗率、客单项目数、消耗客单价、开单金额、消耗人次
  1389 + ///
  1390 + /// 示例请求:
  1391 + /// ```json
  1392 + /// {
  1393 + /// "startTime": "2025-10-01",
  1394 + /// "endTime": "2025-10-31",
  1395 + /// "storeIds": ["门店ID1", "门店ID2"]
  1396 + /// }
  1397 + /// ```
  1398 + ///
  1399 + /// 参数说明:
  1400 + /// - startTime: 开始时间(可选,默认为当月1号)
  1401 + /// - endTime: 结束时间(可选,默认为当前时间)
  1402 + /// - storeIds: 门店ID列表(可选)
  1403 + ///
  1404 + /// 返回字段说明:
  1405 + /// - StoreId: 门店ID
  1406 + /// - StoreName: 门店名称
  1407 + /// - ConsumeProjectCount: 消耗项目数(项目次数总和)
  1408 + /// - ConsumeRate: 消耗率(消耗金额/开单金额 × 100%)
  1409 + /// - AvgProjectPerConsume: 客单项目数(项目数/消耗人次)
  1410 + /// - AvgAmountPerConsume: 消耗客单价(消耗业绩/消耗人次)
  1411 + /// - BillingAmount: 开单金额
  1412 + /// - ConsumeAmount: 消耗金额
  1413 + /// - ConsumePersonCount: 消耗人次(去重客户数)
  1414 + /// </remarks>
  1415 + /// <param name="input">查询参数</param>
  1416 + /// <returns>门店项目指标统计数据列表</returns>
  1417 + /// <response code="200">成功返回统计数据</response>
  1418 + /// <response code="400">参数错误</response>
  1419 + /// <response code="500">服务器错误</response>
  1420 + [HttpPost("get-store-item-statistics")]
  1421 + public async Task<object> GetStoreItemStatistics(StoreItemStatisticsInput input)
  1422 + {
  1423 + try
  1424 + {
  1425 + // 设置默认时间范围(如果未提供,默认为当月)
  1426 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  1427 + var endTime = input.EndTime ?? DateTime.Now;
  1428 +
  1429 + // 第一步:获取门店基础信息
  1430 + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1";
  1431 +
  1432 + if (input.StoreIds != null && input.StoreIds.Any())
  1433 + {
  1434 + var storeIdsStr = string.Join("','", input.StoreIds);
  1435 + storeSql += $" AND F_Id IN ('{storeIdsStr}')";
  1436 + }
  1437 +
  1438 + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql);
  1439 +
  1440 + // 构建门店字典
  1441 + var storeDict = new Dictionary<string, string>();
  1442 + foreach (var store in stores)
  1443 + {
  1444 + var storeId = store.F_Id?.ToString();
  1445 + if (!string.IsNullOrEmpty(storeId))
  1446 + {
  1447 + storeDict[storeId] = store.dm?.ToString() ?? "未知门店";
  1448 + }
  1449 + }
  1450 +
  1451 + // 第二步:统计开单金额
  1452 + var billingSql = @"
  1453 + SELECT
  1454 + kd.djmd as store_id,
  1455 + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount
  1456 + FROM lq_kd_kdjlb kd
  1457 + WHERE kd.F_IsEffective = 1
  1458 + AND kd.kdrq >= @startTime
  1459 + AND kd.kdrq <= @endTime";
  1460 +
  1461 + object billingParameters = new { startTime, endTime };
  1462 +
  1463 + if (input.StoreIds != null && input.StoreIds.Any())
  1464 + {
  1465 + var storeIdsStr = string.Join("','", input.StoreIds);
  1466 + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')";
  1467 + }
  1468 +
  1469 + billingSql += " GROUP BY kd.djmd";
  1470 +
  1471 + var billingResults = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters);
  1472 +
  1473 + // 构建开单金额字典
  1474 + var billingDict = new Dictionary<string, decimal>();
  1475 + foreach (var item in billingResults)
  1476 + {
  1477 + var storeId = item.store_id?.ToString();
  1478 + if (!string.IsNullOrEmpty(storeId))
  1479 + {
  1480 + billingDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0);
  1481 + }
  1482 + }
  1483 +
  1484 + // 第三步:统计消耗金额和消耗人次(直接从lq_xh_hyhk表,避免JOIN导致重复计算)
  1485 + var consumeAmountSql = @"
  1486 + SELECT
  1487 + xh.md as store_id,
  1488 + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount,
  1489 + COUNT(DISTINCT xh.hy) as consume_person_count
  1490 + FROM lq_xh_hyhk xh
  1491 + WHERE xh.F_IsEffective = 1
  1492 + AND xh.hksj >= @startTime
  1493 + AND xh.hksj <= @endTime";
  1494 +
  1495 + object consumeAmountParameters = new { startTime, endTime };
  1496 +
  1497 + if (input.StoreIds != null && input.StoreIds.Any())
  1498 + {
  1499 + var storeIdsStr = string.Join("','", input.StoreIds);
  1500 + consumeAmountSql += $" AND xh.md IN ('{storeIdsStr}')";
  1501 + }
  1502 +
  1503 + consumeAmountSql += " GROUP BY xh.md";
  1504 +
  1505 + var consumeAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeAmountSql, consumeAmountParameters);
  1506 +
  1507 + // 构建消耗金额和人次字典
  1508 + var consumeAmountDict = new Dictionary<string, decimal>();
  1509 + var consumePersonCountDict = new Dictionary<string, int>();
  1510 +
  1511 + foreach (var item in consumeAmountResults)
  1512 + {
  1513 + var storeId = item.store_id?.ToString();
  1514 + if (!string.IsNullOrEmpty(storeId))
  1515 + {
  1516 + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0);
  1517 + consumePersonCountDict[storeId] = Convert.ToInt32(item.consume_person_count ?? 0);
  1518 + }
  1519 + }
  1520 +
  1521 + // 第四步:统计消耗项目数(需要JOIN品项明细表)
  1522 + var consumeProjectSql = @"
  1523 + SELECT
  1524 + xh.md as store_id,
  1525 + COALESCE(SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))), 0) as consume_project_count
  1526 + FROM lq_xh_hyhk xh
  1527 + INNER JOIN lq_xh_pxmx xhpx ON xhpx.F_ConsumeInfoId = xh.F_Id AND xhpx.F_IsEffective = 1
  1528 + WHERE xh.F_IsEffective = 1
  1529 + AND xh.hksj >= @startTime
  1530 + AND xh.hksj <= @endTime";
  1531 +
  1532 + object consumeProjectParameters = new { startTime, endTime };
  1533 +
  1534 + if (input.StoreIds != null && input.StoreIds.Any())
  1535 + {
  1536 + var storeIdsStr = string.Join("','", input.StoreIds);
  1537 + consumeProjectSql += $" AND xh.md IN ('{storeIdsStr}')";
  1538 + }
  1539 +
  1540 + consumeProjectSql += " GROUP BY xh.md";
  1541 +
  1542 + var consumeProjectResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeProjectSql, consumeProjectParameters);
  1543 +
  1544 + // 构建消耗项目数字典
  1545 + var consumeProjectCountDict = new Dictionary<string, decimal>();
  1546 +
  1547 + foreach (var item in consumeProjectResults)
  1548 + {
  1549 + var storeId = item.store_id?.ToString();
  1550 + if (!string.IsNullOrEmpty(storeId))
  1551 + {
  1552 + consumeProjectCountDict[storeId] = Convert.ToDecimal(item.consume_project_count ?? 0);
  1553 + }
  1554 + }
  1555 +
  1556 + // 第五步:合并数据并计算指标
  1557 + var resultList = new List<StoreItemStatisticsOutput>();
  1558 +
  1559 + foreach (var kvp in storeDict)
  1560 + {
  1561 + var storeId = kvp.Key;
  1562 + var storeName = kvp.Value;
  1563 + var billingAmount = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0;
  1564 +
  1565 + var consumeProjectCount = consumeProjectCountDict.ContainsKey(storeId) ? consumeProjectCountDict[storeId] : 0;
  1566 + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0;
  1567 + var consumePersonCount = consumePersonCountDict.ContainsKey(storeId) ? consumePersonCountDict[storeId] : 0;
  1568 +
  1569 + // 如果有消耗数据或开单数据,都返回该门店
  1570 + if (consumeAmount > 0 || consumeProjectCount > 0 || billingAmount > 0)
  1571 + {
  1572 + // 计算消耗率
  1573 + var consumeRate = billingAmount > 0
  1574 + ? decimal.Round(consumeAmount / billingAmount * 100m, 2)
  1575 + : 0;
  1576 +
  1577 + // 计算客单项目数
  1578 + var avgProjectPerConsume = consumePersonCount > 0
  1579 + ? decimal.Round(consumeProjectCount / consumePersonCount, 2)
  1580 + : 0;
  1581 +
  1582 + // 计算消耗客单价
  1583 + var avgAmountPerConsume = consumePersonCount > 0
  1584 + ? decimal.Round(consumeAmount / consumePersonCount, 2)
  1585 + : 0;
  1586 +
  1587 + resultList.Add(new StoreItemStatisticsOutput
  1588 + {
  1589 + StoreId = storeId,
  1590 + StoreName = storeName,
  1591 + ConsumeProjectCount = Convert.ToInt32(consumeProjectCount),
  1592 + ConsumeRate = consumeRate,
  1593 + AvgProjectPerConsume = avgProjectPerConsume,
  1594 + AvgAmountPerConsume = avgAmountPerConsume,
  1595 + BillingAmount = billingAmount,
  1596 + ConsumeAmount = consumeAmount,
  1597 + ConsumePersonCount = consumePersonCount
  1598 + });
  1599 + }
  1600 + }
  1601 +
  1602 + // 按消耗人次排序
  1603 + resultList = resultList.OrderByDescending(x => x.ConsumePersonCount).ToList();
  1604 +
  1605 + return resultList;
  1606 + }
  1607 + catch (Exception ex)
  1608 + {
  1609 + _logger.LogError(ex, $"获取门店项目指标统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  1610 + throw NCCException.Oh($"获取门店项目指标统计数据失败: {ex.Message}");
  1611 + }
  1612 + }
  1613 +
  1614 + #endregion
  1615 +
  1616 + #region 客户到店次数统计
  1617 +
  1618 + /// <summary>
  1619 + /// 获取客户到店次数统计
  1620 + /// </summary>
  1621 + /// <remarks>
  1622 + /// 统计指定时间范围内,客户按到店次数(消耗次数)的分布情况
  1623 + /// 一次消耗开单即为一次到店
  1624 + /// 支持门店筛选,但统计结果不分门店,按次数汇总所有门店的人数
  1625 + ///
  1626 + /// 示例请求:
  1627 + /// ```json
  1628 + /// {
  1629 + /// "startTime": "2025-10-01",
  1630 + /// "endTime": "2025-10-31",
  1631 + /// "storeIds": ["门店ID1", "门店ID2"]
  1632 + /// }
  1633 + /// ```
  1634 + ///
  1635 + /// 参数说明:
  1636 + /// - startTime: 开始时间(可选,默认为当月1号)
  1637 + /// - endTime: 结束时间(可选,默认为当前时间)
  1638 + /// - storeIds: 门店ID列表(可选,用于筛选)
  1639 + ///
  1640 + /// 返回说明:
  1641 + /// - VisitCount: 消耗次数(客户到店次数)
  1642 + /// - CustomerCount: 人数(达到该次数的客户人数)
  1643 + /// </remarks>
  1644 + /// <param name="input">查询参数</param>
  1645 + /// <returns>客户到店次数统计数据列表</returns>
  1646 + /// <response code="200">成功返回统计数据</response>
  1647 + /// <response code="400">参数错误</response>
  1648 + /// <response code="500">服务器错误</response>
  1649 + [HttpPost("get-customer-visit-frequency")]
  1650 + public async Task<List<CustomerVisitFrequencyOutput>> GetCustomerVisitFrequency(CustomerVisitFrequencyInput input)
  1651 + {
  1652 + try
  1653 + {
  1654 + // 设置默认时间范围(如果未提供,默认为当月)
  1655 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  1656 + var endTime = input.EndTime ?? DateTime.Now;
  1657 +
  1658 + // 统计每个客户的消耗次数(跨门店汇总)
  1659 + var visitSql = $@"
  1660 + SELECT
  1661 + xh.hy as customer_id,
  1662 + COUNT(DISTINCT CONCAT(xh.md, '_', DATE(xh.hksj))) as visit_count
  1663 + FROM lq_xh_hyhk xh
  1664 + WHERE xh.F_IsEffective = 1
  1665 + AND xh.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  1666 + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
  1667 +
  1668 + if (input.StoreIds != null && input.StoreIds.Any())
  1669 + {
  1670 + var storeIdsStr = string.Join("','", input.StoreIds);
  1671 + visitSql += $" AND xh.md IN ('{storeIdsStr}')";
  1672 + }
  1673 +
  1674 + visitSql += " GROUP BY xh.hy";
  1675 +
  1676 + var visitData = await _db.Ado.SqlQueryAsync<dynamic>(visitSql);
  1677 +
  1678 + // 统计各次数的人数分布
  1679 + var visitCountDict = new Dictionary<int, int>();
  1680 +
  1681 + foreach (var item in visitData)
  1682 + {
  1683 + var visitCount = Convert.ToInt32(item.visit_count);
  1684 +
  1685 + if (visitCountDict.ContainsKey(visitCount))
  1686 + {
  1687 + visitCountDict[visitCount]++;
  1688 + }
  1689 + else
  1690 + {
  1691 + visitCountDict[visitCount] = 1;
  1692 + }
  1693 + }
  1694 +
  1695 + // 构建结果列表,按次数排序
  1696 + var resultList = visitCountDict
  1697 + .OrderBy(x => x.Key)
  1698 + .Select(x => new CustomerVisitFrequencyOutput
  1699 + {
  1700 + VisitCount = x.Key,
  1701 + CustomerCount = x.Value
  1702 + })
  1703 + .ToList();
  1704 +
  1705 + return resultList;
  1706 + }
  1707 + catch (Exception ex)
  1708 + {
  1709 + _logger.LogError(ex, $"获取客户到店次数统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  1710 + throw NCCException.Oh($"获取客户到店次数统计数据失败: {ex.Message}");
  1711 + }
  1712 + }
  1713 +
  1714 + #endregion
  1715 +
  1716 + #region 健康师统计数据
  1717 +
  1718 + /// <summary>
  1719 + /// 获取健康师统计数据
  1720 + /// </summary>
  1721 + /// <remarks>
  1722 + /// 统计指定时间范围内健康师的开单业绩、消耗业绩、退单业绩及项目数
  1723 + ///
  1724 + /// 示例请求:
  1725 + /// ```json
  1726 + /// {
  1727 + /// "startTime": "2025-10-01",
  1728 + /// "endTime": "2025-10-31",
  1729 + /// "storeIds": ["门店ID1", "门店ID2"],
  1730 + /// "healthCoachIds": ["健康师ID1", "健康师ID2"]
  1731 + /// }
  1732 + /// ```
  1733 + ///
  1734 + /// 参数说明:
  1735 + /// - startTime: 开始时间(可选,默认为当月1号)
  1736 + /// - endTime: 结束时间(可选,默认为当前时间)
  1737 + /// - storeIds: 门店ID列表(可选)
  1738 + /// - healthCoachIds: 健康师ID列表(可选)
  1739 + ///
  1740 + /// 返回说明:
  1741 + /// - HealthCoachId: 健康师ID
  1742 + /// - HealthCoachName: 健康师姓名
  1743 + /// - BillingPerformance: 开单业绩
  1744 + /// - ConsumePerformance: 消耗业绩
  1745 + /// - RefundPerformance: 退单业绩
  1746 + /// - BillingProjectCount: 开单项目数
  1747 + /// - ConsumeProjectCount: 消耗项目数
  1748 + /// - RefundProjectCount: 退单项目数
  1749 + /// </remarks>
  1750 + /// <param name="input">查询参数</param>
  1751 + /// <returns>健康师统计数据列表</returns>
  1752 + /// <response code="200">成功返回统计数据</response>
  1753 + /// <response code="400">参数错误</response>
  1754 + /// <response code="500">服务器错误</response>
  1755 + [HttpPost("get-health-coach-statistics")]
  1756 + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachStatistics(HealthCoachStatisticsInput input)
  1757 + {
  1758 + try
  1759 + {
  1760 + // 设置默认时间范围(如果未提供,默认为当月)
  1761 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  1762 + var endTime = input.EndTime ?? DateTime.Now;
  1763 +
  1764 + // 健康师数据字典
  1765 + var healthCoachDict = new Dictionary<string, HealthCoachStatisticsOutput>();
  1766 +
  1767 + // 1. 统计开单业绩和开单项目数
  1768 + var billingSql = $@"
  1769 + SELECT
  1770 + jks.jks as health_coach_id,
  1771 + jks.jksxm as health_coach_name,
  1772 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance,
  1773 + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count
  1774 + FROM lq_kd_jksyj jks
  1775 + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id
  1776 + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  1777 + WHERE jks.F_IsEffective = 1
  1778 + AND kdjlb.F_IsEffective = 1
  1779 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  1780 + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
  1781 +
  1782 + if (input.StoreIds != null && input.StoreIds.Any())
  1783 + {
  1784 + var storeIdsStr = string.Join("','", input.StoreIds);
  1785 + billingSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')";
  1786 + }
  1787 +
  1788 + if (input.HealthCoachIds != null && input.HealthCoachIds.Any())
  1789 + {
  1790 + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds);
  1791 + billingSql += $" AND jks.jks IN ('{healthCoachIdsStr}')";
  1792 + }
  1793 +
  1794 + billingSql += " GROUP BY jks.jks, jks.jksxm";
  1795 +
  1796 + var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
  1797 +
  1798 + foreach (var item in billingData)
  1799 + {
  1800 + var healthCoachId = item.health_coach_id?.ToString();
  1801 + if (!string.IsNullOrEmpty(healthCoachId))
  1802 + {
  1803 + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput
  1804 + {
  1805 + HealthCoachId = healthCoachId,
  1806 + HealthCoachName = item.health_coach_name?.ToString() ?? "未知",
  1807 + BillingPerformance = Convert.ToDecimal(item.billing_performance ?? 0),
  1808 + BillingProjectCount = Convert.ToInt32(item.billing_project_count ?? 0),
  1809 + ConsumePerformance = 0,
  1810 + RefundPerformance = 0,
  1811 + ConsumeProjectCount = 0,
  1812 + RefundProjectCount = 0
  1813 + };
  1814 + }
  1815 + }
  1816 +
  1817 + // 2. 统计消耗业绩和消耗项目数
  1818 + var consumeSql = $@"
  1819 + SELECT
  1820 + jks.jks as health_coach_id,
  1821 + jks.jksxm as health_coach_name,
  1822 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance,
  1823 + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count
  1824 + FROM lq_xh_jksyj jks
  1825 + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id
  1826 + WHERE jks.F_IsEffective = 1
  1827 + AND hyhk.F_IsEffective = 1
  1828 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  1829 + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
  1830 +
  1831 + if (input.StoreIds != null && input.StoreIds.Any())
  1832 + {
  1833 + var storeIdsStr = string.Join("','", input.StoreIds);
  1834 + consumeSql += $" AND hyhk.md IN ('{storeIdsStr}')";
  1835 + }
  1836 +
  1837 + if (input.HealthCoachIds != null && input.HealthCoachIds.Any())
  1838 + {
  1839 + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds);
  1840 + consumeSql += $" AND jks.jks IN ('{healthCoachIdsStr}')";
  1841 + }
  1842 +
  1843 + consumeSql += " GROUP BY jks.jks, jks.jksxm";
  1844 +
  1845 + var consumeData = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql);
  1846 +
  1847 + foreach (var item in consumeData)
  1848 + {
  1849 + var healthCoachId = item.health_coach_id?.ToString();
  1850 + if (!string.IsNullOrEmpty(healthCoachId))
  1851 + {
  1852 + if (healthCoachDict.ContainsKey(healthCoachId))
  1853 + {
  1854 + healthCoachDict[healthCoachId].ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0);
  1855 + healthCoachDict[healthCoachId].ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0);
  1856 + }
  1857 + else
  1858 + {
  1859 + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput
  1860 + {
  1861 + HealthCoachId = healthCoachId,
  1862 + HealthCoachName = item.health_coach_name?.ToString() ?? "未知",
  1863 + BillingPerformance = 0,
  1864 + ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0),
  1865 + RefundPerformance = 0,
  1866 + BillingProjectCount = 0,
  1867 + ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0),
  1868 + RefundProjectCount = 0
  1869 + };
  1870 + }
  1871 + }
  1872 + }
  1873 +
  1874 + // 3. 统计退单业绩和退单项目数
  1875 + var refundSql = $@"
  1876 + SELECT
  1877 + jks.jks as health_coach_id,
  1878 + jks.jksxm as health_coach_name,
  1879 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance,
  1880 + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count
  1881 + FROM lq_hytk_jksyj jks
  1882 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  1883 + WHERE jks.F_IsEffective = 1
  1884 + AND hytk.F_IsEffective = 1
  1885 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  1886 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'";
  1887 +
  1888 + if (input.StoreIds != null && input.StoreIds.Any())
  1889 + {
  1890 + var storeIdsStr = string.Join("','", input.StoreIds);
  1891 + refundSql += $" AND hytk.md IN ('{storeIdsStr}')";
  1892 + }
  1893 +
  1894 + if (input.HealthCoachIds != null && input.HealthCoachIds.Any())
  1895 + {
  1896 + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds);
  1897 + refundSql += $" AND jks.jks IN ('{healthCoachIdsStr}')";
  1898 + }
  1899 +
  1900 + refundSql += " GROUP BY jks.jks, jks.jksxm";
  1901 +
  1902 + var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
  1903 +
  1904 + foreach (var item in refundData)
  1905 + {
  1906 + var healthCoachId = item.health_coach_id?.ToString();
  1907 + if (!string.IsNullOrEmpty(healthCoachId))
  1908 + {
  1909 + if (healthCoachDict.ContainsKey(healthCoachId))
  1910 + {
  1911 + healthCoachDict[healthCoachId].RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0);
  1912 + healthCoachDict[healthCoachId].RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0);
  1913 + }
  1914 + else
  1915 + {
  1916 + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput
  1917 + {
  1918 + HealthCoachId = healthCoachId,
  1919 + HealthCoachName = item.health_coach_name?.ToString() ?? "未知",
  1920 + BillingPerformance = 0,
  1921 + ConsumePerformance = 0,
  1922 + RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0),
  1923 + BillingProjectCount = 0,
  1924 + ConsumeProjectCount = 0,
  1925 + RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
  1926 + };
  1927 + }
  1928 + }
  1929 + }
  1930 +
  1931 + // 返回结果,按健康师姓名排序
  1932 + var resultList = healthCoachDict.Values
  1933 + .OrderBy(x => x.HealthCoachName)
  1934 + .ToList();
  1935 +
  1936 + return resultList;
  1937 + }
  1938 + catch (Exception ex)
  1939 + {
  1940 + _logger.LogError(ex, $"获取健康师统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  1941 + throw NCCException.Oh($"获取健康师统计数据失败: {ex.Message}");
  1942 + }
  1943 + }
  1944 +
  1945 +
  1946 + /// <summary>
  1947 + /// 获取健康师开单业绩排行榜
  1948 + /// </summary>
  1949 + /// <remarks>
  1950 + /// 获取按开单业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据
  1951 + ///
  1952 + /// 示例请求:
  1953 + /// ```json
  1954 + /// {
  1955 + /// "startTime": "2025-10-01",
  1956 + /// "endTime": "2025-10-31",
  1957 + /// "storeIds": ["门店ID1", "门店ID2"]
  1958 + /// }
  1959 + /// ```
  1960 + /// </remarks>
  1961 + /// <param name="input">查询参数</param>
  1962 + /// <returns>开单业绩排行榜(前20名)</returns>
  1963 + /// <response code="200">成功返回排行榜数据</response>
  1964 + /// <response code="400">参数错误</response>
  1965 + /// <response code="500">服务器错误</response>
  1966 + [HttpPost("get-health-coach-billing-ranking")]
  1967 + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachBillingRanking(HealthCoachStatisticsInput input)
  1968 + {
  1969 + try
  1970 + {
  1971 + // 设置默认时间范围(如果未提供,默认为当月)
  1972 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  1973 + var endTime = input.EndTime ?? DateTime.Now;
  1974 +
  1975 + // 第一步:获取开单业绩排行榜前20名健康师(使用jkszh作为ID)
  1976 + var rankingIdsSql = $@"
  1977 + SELECT
  1978 + jks.jkszh as health_coach_id,
  1979 + jks.jksxm as health_coach_name
  1980 + FROM lq_kd_jksyj jks
  1981 + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id
  1982 + WHERE jks.F_IsEffective = 1
  1983 + AND kdjlb.F_IsEffective = 1
  1984 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  1985 + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  1986 + AND jks.jkszh IS NOT NULL
  1987 + AND jks.jkszh != ''";
  1988 +
  1989 + if (input.StoreIds != null && input.StoreIds.Any())
  1990 + {
  1991 + var storeIdsStr = string.Join("','", input.StoreIds);
  1992 + rankingIdsSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')";
  1993 + }
  1994 +
  1995 + rankingIdsSql += @"
  1996 + GROUP BY jks.jkszh, jks.jksxm
  1997 + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC
  1998 + LIMIT 20";
  1999 +
  2000 + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql))
  2001 + .Select(item => new
  2002 + {
  2003 + Id = item.health_coach_id?.ToString(),
  2004 + Name = item.health_coach_name?.ToString()
  2005 + })
  2006 + .Where(x => !string.IsNullOrEmpty(x.Id))
  2007 + .ToList();
  2008 +
  2009 + var rankingIds = rankingData.Select(x => x.Id).ToList();
  2010 + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知");
  2011 +
  2012 + if (!rankingIds.Any())
  2013 + {
  2014 + return new List<HealthCoachStatisticsOutput>();
  2015 + }
  2016 +
  2017 + // 第二步:为这些健康师查询完整的三个业绩数据
  2018 + var healthCoachIdsStr = string.Join("','", rankingIds);
  2019 +
  2020 + // 2.1 查询开单业绩数据(使用jkszh作为ID)
  2021 + var billingDataSql = $@"
  2022 + SELECT
  2023 + jks.jkszh as health_coach_id,
  2024 + jks.jksxm as health_coach_name,
  2025 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance,
  2026 + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count
  2027 + FROM lq_kd_jksyj jks
  2028 + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id
  2029 + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  2030 + WHERE jks.F_IsEffective = 1
  2031 + AND kdjlb.F_IsEffective = 1
  2032 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2033 + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2034 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2035 +
  2036 + if (input.StoreIds != null && input.StoreIds.Any())
  2037 + {
  2038 + var storeIdsStr = string.Join("','", input.StoreIds);
  2039 + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')";
  2040 + }
  2041 +
  2042 + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2043 +
  2044 + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql))
  2045 + .ToDictionary(
  2046 + item => item.health_coach_id?.ToString(),
  2047 + item => new
  2048 + {
  2049 + Name = item.health_coach_name?.ToString() ?? "未知",
  2050 + Performance = Convert.ToDecimal(item.billing_performance ?? 0),
  2051 + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0)
  2052 + }
  2053 + );
  2054 +
  2055 + // 2.2 查询耗卡业绩数据(使用jkszh作为ID)
  2056 + var consumeDataSql = $@"
  2057 + SELECT
  2058 + jks.jkszh as health_coach_id,
  2059 + jks.jksxm as health_coach_name,
  2060 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance,
  2061 + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count
  2062 + FROM lq_xh_jksyj jks
  2063 + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id
  2064 + WHERE jks.F_IsEffective = 1
  2065 + AND hyhk.F_IsEffective = 1
  2066 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2067 + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2068 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2069 +
  2070 + if (input.StoreIds != null && input.StoreIds.Any())
  2071 + {
  2072 + var storeIdsStr = string.Join("','", input.StoreIds);
  2073 + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')";
  2074 + }
  2075 +
  2076 + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2077 +
  2078 + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql))
  2079 + .ToDictionary(
  2080 + item => item.health_coach_id?.ToString(),
  2081 + item => new
  2082 + {
  2083 + Name = item.health_coach_name?.ToString() ?? "未知",
  2084 + Performance = Convert.ToDecimal(item.consume_performance ?? 0),
  2085 + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0)
  2086 + }
  2087 + );
  2088 +
  2089 + // 2.3 查询退卡业绩数据(使用jkszh作为ID)
  2090 + var refundDataSql = $@"
  2091 + SELECT
  2092 + jks.jkszh as health_coach_id,
  2093 + jks.jksxm as health_coach_name,
  2094 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance,
  2095 + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count
  2096 + FROM lq_hytk_jksyj jks
  2097 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  2098 + WHERE jks.F_IsEffective = 1
  2099 + AND hytk.F_IsEffective = 1
  2100 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2101 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2102 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2103 +
  2104 + if (input.StoreIds != null && input.StoreIds.Any())
  2105 + {
  2106 + var storeIdsStr = string.Join("','", input.StoreIds);
  2107 + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')";
  2108 + }
  2109 +
  2110 + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2111 +
  2112 + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql))
  2113 + .ToDictionary(
  2114 + item => item.health_coach_id?.ToString(),
  2115 + item => new
  2116 + {
  2117 + Name = item.health_coach_name?.ToString() ?? "未知",
  2118 + Performance = Convert.ToDecimal(item.refund_performance ?? 0),
  2119 + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
  2120 + }
  2121 + );
  2122 +
  2123 + // 第三步:合并数据,按原始排序构建排行榜
  2124 + var ranking = rankingIds
  2125 + .Select(id =>
  2126 + {
  2127 + var billing = billingData.ContainsKey(id) ? billingData[id] : null;
  2128 + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null;
  2129 + var refund = refundData.ContainsKey(id) ? refundData[id] : null;
  2130 +
  2131 + return new HealthCoachStatisticsOutput
  2132 + {
  2133 + HealthCoachId = id,
  2134 + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"),
  2135 + BillingPerformance = billing?.Performance ?? 0,
  2136 + ConsumePerformance = consume?.Performance ?? 0,
  2137 + RefundPerformance = refund?.Performance ?? 0,
  2138 + BillingProjectCount = billing?.ProjectCount ?? 0,
  2139 + ConsumeProjectCount = consume?.ProjectCount ?? 0,
  2140 + RefundProjectCount = refund?.ProjectCount ?? 0
  2141 + };
  2142 + })
  2143 + .ToList();
  2144 +
  2145 + return ranking;
  2146 + }
  2147 + catch (Exception ex)
  2148 + {
  2149 + _logger.LogError(ex, $"获取健康师开单业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  2150 + throw NCCException.Oh($"获取健康师开单业绩排行榜失败: {ex.Message}");
  2151 + }
  2152 + }
  2153 +
  2154 + /// <summary>
  2155 + /// 获取健康师耗卡业绩排行榜
  2156 + /// </summary>
  2157 + /// <remarks>
  2158 + /// 获取按耗卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据
  2159 + ///
  2160 + /// 示例请求:
  2161 + /// ```json
  2162 + /// {
  2163 + /// "startTime": "2025-10-01",
  2164 + /// "endTime": "2025-10-31",
  2165 + /// "storeIds": ["门店ID1", "门店ID2"]
  2166 + /// }
  2167 + /// ```
  2168 + /// </remarks>
  2169 + /// <param name="input">查询参数</param>
  2170 + /// <returns>耗卡业绩排行榜(前20名)</returns>
  2171 + /// <response code="200">成功返回排行榜数据</response>
  2172 + /// <response code="400">参数错误</response>
  2173 + /// <response code="500">服务器错误</response>
  2174 + [HttpPost("get-health-coach-consume-ranking")]
  2175 + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachConsumeRanking(HealthCoachStatisticsInput input)
  2176 + {
  2177 + try
  2178 + {
  2179 + // 设置默认时间范围(如果未提供,默认为当月)
  2180 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  2181 + var endTime = input.EndTime ?? DateTime.Now;
  2182 +
  2183 + // 第一步:获取耗卡业绩排行榜前20名健康师(使用jkszh作为ID)
  2184 + var rankingIdsSql = $@"
  2185 + SELECT
  2186 + jks.jkszh as health_coach_id,
  2187 + jks.jksxm as health_coach_name
  2188 + FROM lq_xh_jksyj jks
  2189 + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id
  2190 + WHERE jks.F_IsEffective = 1
  2191 + AND hyhk.F_IsEffective = 1
  2192 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2193 + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2194 + AND jks.jkszh IS NOT NULL
  2195 + AND jks.jkszh != ''";
  2196 +
  2197 + if (input.StoreIds != null && input.StoreIds.Any())
  2198 + {
  2199 + var storeIdsStr = string.Join("','", input.StoreIds);
  2200 + rankingIdsSql += $" AND hyhk.md IN ('{storeIdsStr}')";
  2201 + }
  2202 +
  2203 + rankingIdsSql += @"
  2204 + GROUP BY jks.jkszh, jks.jksxm
  2205 + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC
  2206 + LIMIT 20";
  2207 +
  2208 + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql))
  2209 + .Select(item => new
  2210 + {
  2211 + Id = item.health_coach_id?.ToString(),
  2212 + Name = item.health_coach_name?.ToString()
  2213 + })
  2214 + .Where(x => !string.IsNullOrEmpty(x.Id))
  2215 + .ToList();
  2216 +
  2217 + var rankingIds = rankingData.Select(x => x.Id).ToList();
  2218 + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知");
  2219 +
  2220 + if (!rankingIds.Any())
  2221 + {
  2222 + return new List<HealthCoachStatisticsOutput>();
  2223 + }
  2224 +
  2225 + // 第二步:为这些健康师查询完整的三个业绩数据
  2226 + var healthCoachIdsStr = string.Join("','", rankingIds);
  2227 +
  2228 + // 2.1 查询开单业绩数据(使用jkszh作为ID)
  2229 + var billingDataSql = $@"
  2230 + SELECT
  2231 + jks.jkszh as health_coach_id,
  2232 + jks.jksxm as health_coach_name,
  2233 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance,
  2234 + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count
  2235 + FROM lq_kd_jksyj jks
  2236 + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id
  2237 + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  2238 + WHERE jks.F_IsEffective = 1
  2239 + AND kdjlb.F_IsEffective = 1
  2240 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2241 + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2242 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2243 +
  2244 + if (input.StoreIds != null && input.StoreIds.Any())
  2245 + {
  2246 + var storeIdsStr = string.Join("','", input.StoreIds);
  2247 + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')";
  2248 + }
  2249 +
  2250 + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2251 +
  2252 + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql))
  2253 + .ToDictionary(
  2254 + item => item.health_coach_id?.ToString(),
  2255 + item => new
  2256 + {
  2257 + Name = item.health_coach_name?.ToString() ?? "未知",
  2258 + Performance = Convert.ToDecimal(item.billing_performance ?? 0),
  2259 + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0)
  2260 + }
  2261 + );
  2262 +
  2263 + // 2.2 查询耗卡业绩数据(使用jkszh作为ID)
  2264 + var consumeDataSql = $@"
  2265 + SELECT
  2266 + jks.jkszh as health_coach_id,
  2267 + jks.jksxm as health_coach_name,
  2268 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance,
  2269 + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count
  2270 + FROM lq_xh_jksyj jks
  2271 + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id
  2272 + WHERE jks.F_IsEffective = 1
  2273 + AND hyhk.F_IsEffective = 1
  2274 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2275 + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2276 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2277 +
  2278 + if (input.StoreIds != null && input.StoreIds.Any())
  2279 + {
  2280 + var storeIdsStr = string.Join("','", input.StoreIds);
  2281 + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')";
  2282 + }
  2283 +
  2284 + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2285 +
  2286 + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql))
  2287 + .ToDictionary(
  2288 + item => item.health_coach_id?.ToString(),
  2289 + item => new
  2290 + {
  2291 + Name = item.health_coach_name?.ToString() ?? "未知",
  2292 + Performance = Convert.ToDecimal(item.consume_performance ?? 0),
  2293 + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0)
  2294 + }
  2295 + );
  2296 +
  2297 + // 2.3 查询退卡业绩数据(使用jkszh作为ID)
  2298 + var refundDataSql = $@"
  2299 + SELECT
  2300 + jks.jkszh as health_coach_id,
  2301 + jks.jksxm as health_coach_name,
  2302 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance,
  2303 + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count
  2304 + FROM lq_hytk_jksyj jks
  2305 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  2306 + WHERE jks.F_IsEffective = 1
  2307 + AND hytk.F_IsEffective = 1
  2308 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2309 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2310 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2311 +
  2312 + if (input.StoreIds != null && input.StoreIds.Any())
  2313 + {
  2314 + var storeIdsStr = string.Join("','", input.StoreIds);
  2315 + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')";
  2316 + }
  2317 +
  2318 + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2319 +
  2320 + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql))
  2321 + .ToDictionary(
  2322 + item => item.health_coach_id?.ToString(),
  2323 + item => new
  2324 + {
  2325 + Name = item.health_coach_name?.ToString() ?? "未知",
  2326 + Performance = Convert.ToDecimal(item.refund_performance ?? 0),
  2327 + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
  2328 + }
  2329 + );
  2330 +
  2331 + // 第三步:合并数据,按原始排序构建排行榜
  2332 + var ranking = rankingIds
  2333 + .Select(id =>
  2334 + {
  2335 + var billing = billingData.ContainsKey(id) ? billingData[id] : null;
  2336 + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null;
  2337 + var refund = refundData.ContainsKey(id) ? refundData[id] : null;
  2338 +
  2339 + return new HealthCoachStatisticsOutput
  2340 + {
  2341 + HealthCoachId = id,
  2342 + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"),
  2343 + BillingPerformance = billing?.Performance ?? 0,
  2344 + ConsumePerformance = consume?.Performance ?? 0,
  2345 + RefundPerformance = refund?.Performance ?? 0,
  2346 + BillingProjectCount = billing?.ProjectCount ?? 0,
  2347 + ConsumeProjectCount = consume?.ProjectCount ?? 0,
  2348 + RefundProjectCount = refund?.ProjectCount ?? 0
  2349 + };
  2350 + })
  2351 + .ToList();
  2352 +
  2353 + return ranking;
  2354 + }
  2355 + catch (Exception ex)
  2356 + {
  2357 + _logger.LogError(ex, $"获取健康师耗卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  2358 + throw NCCException.Oh($"获取健康师耗卡业绩排行榜失败: {ex.Message}");
  2359 + }
  2360 + }
  2361 +
  2362 + /// <summary>
  2363 + /// 获取健康师退卡业绩排行榜
  2364 + /// </summary>
  2365 + /// <remarks>
  2366 + /// 获取按退卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据
  2367 + ///
  2368 + /// 示例请求:
  2369 + /// ```json
  2370 + /// {
  2371 + /// "startTime": "2025-10-01",
  2372 + /// "endTime": "2025-10-31",
  2373 + /// "storeIds": ["门店ID1", "门店ID2"]
  2374 + /// }
  2375 + /// ```
  2376 + /// </remarks>
  2377 + /// <param name="input">查询参数</param>
  2378 + /// <returns>退卡业绩排行榜(前20名)</returns>
  2379 + /// <response code="200">成功返回排行榜数据</response>
  2380 + /// <response code="400">参数错误</response>
  2381 + /// <response code="500">服务器错误</response>
  2382 + [HttpPost("get-health-coach-refund-ranking")]
  2383 + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachRefundRanking(HealthCoachStatisticsInput input)
  2384 + {
  2385 + try
  2386 + {
  2387 + // 设置默认时间范围(如果未提供,默认为当月)
  2388 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  2389 + var endTime = input.EndTime ?? DateTime.Now;
  2390 +
  2391 + // 第一步:获取退卡业绩排行榜前20名健康师(使用jkszh作为ID)
  2392 + var rankingIdsSql = $@"
  2393 + SELECT
  2394 + jks.jkszh as health_coach_id,
  2395 + jks.jksxm as health_coach_name
  2396 + FROM lq_hytk_jksyj jks
  2397 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  2398 + WHERE jks.F_IsEffective = 1
  2399 + AND hytk.F_IsEffective = 1
  2400 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2401 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2402 + AND jks.jkszh IS NOT NULL
  2403 + AND jks.jkszh != ''";
  2404 +
  2405 + if (input.StoreIds != null && input.StoreIds.Any())
  2406 + {
  2407 + var storeIdsStr = string.Join("','", input.StoreIds);
  2408 + rankingIdsSql += $" AND hytk.md IN ('{storeIdsStr}')";
  2409 + }
  2410 +
  2411 + rankingIdsSql += @"
  2412 + GROUP BY jks.jkszh, jks.jksxm
  2413 + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC
  2414 + LIMIT 20";
  2415 +
  2416 + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql))
  2417 + .Select(item => new
  2418 + {
  2419 + Id = item.health_coach_id?.ToString(),
  2420 + Name = item.health_coach_name?.ToString()
  2421 + })
  2422 + .Where(x => !string.IsNullOrEmpty(x.Id))
  2423 + .ToList();
  2424 +
  2425 + var rankingIds = rankingData.Select(x => x.Id).ToList();
  2426 + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知");
  2427 +
  2428 + if (!rankingIds.Any())
  2429 + {
  2430 + return new List<HealthCoachStatisticsOutput>();
  2431 + }
  2432 +
  2433 + // 第二步:为这些健康师查询完整的三个业绩数据
  2434 + var healthCoachIdsStr = string.Join("','", rankingIds);
  2435 +
  2436 + // 2.1 查询开单业绩数据(使用jkszh作为ID)
  2437 + var billingDataSql = $@"
  2438 + SELECT
  2439 + jks.jkszh as health_coach_id,
  2440 + jks.jksxm as health_coach_name,
  2441 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance,
  2442 + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count
  2443 + FROM lq_kd_jksyj jks
  2444 + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id
  2445 + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  2446 + WHERE jks.F_IsEffective = 1
  2447 + AND kdjlb.F_IsEffective = 1
  2448 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2449 + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2450 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2451 +
  2452 + if (input.StoreIds != null && input.StoreIds.Any())
  2453 + {
  2454 + var storeIdsStr = string.Join("','", input.StoreIds);
  2455 + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')";
  2456 + }
  2457 +
  2458 + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2459 +
  2460 + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql))
  2461 + .ToDictionary(
  2462 + item => item.health_coach_id?.ToString(),
  2463 + item => new
  2464 + {
  2465 + Name = item.health_coach_name?.ToString() ?? "未知",
  2466 + Performance = Convert.ToDecimal(item.billing_performance ?? 0),
  2467 + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0)
  2468 + }
  2469 + );
  2470 +
  2471 + // 2.2 查询耗卡业绩数据(使用jkszh作为ID)
  2472 + var consumeDataSql = $@"
  2473 + SELECT
  2474 + jks.jkszh as health_coach_id,
  2475 + jks.jksxm as health_coach_name,
  2476 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance,
  2477 + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count
  2478 + FROM lq_xh_jksyj jks
  2479 + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id
  2480 + WHERE jks.F_IsEffective = 1
  2481 + AND hyhk.F_IsEffective = 1
  2482 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2483 + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2484 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2485 +
  2486 + if (input.StoreIds != null && input.StoreIds.Any())
  2487 + {
  2488 + var storeIdsStr = string.Join("','", input.StoreIds);
  2489 + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')";
  2490 + }
  2491 +
  2492 + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2493 +
  2494 + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql))
  2495 + .ToDictionary(
  2496 + item => item.health_coach_id?.ToString(),
  2497 + item => new
  2498 + {
  2499 + Name = item.health_coach_name?.ToString() ?? "未知",
  2500 + Performance = Convert.ToDecimal(item.consume_performance ?? 0),
  2501 + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0)
  2502 + }
  2503 + );
  2504 +
  2505 + // 2.3 查询退卡业绩数据(使用jkszh作为ID)
  2506 + var refundDataSql = $@"
  2507 + SELECT
  2508 + jks.jkszh as health_coach_id,
  2509 + jks.jksxm as health_coach_name,
  2510 + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance,
  2511 + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count
  2512 + FROM lq_hytk_jksyj jks
  2513 + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id
  2514 + WHERE jks.F_IsEffective = 1
  2515 + AND hytk.F_IsEffective = 1
  2516 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}'
  2517 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2518 + AND jks.jkszh IN ('{healthCoachIdsStr}')";
  2519 +
  2520 + if (input.StoreIds != null && input.StoreIds.Any())
  2521 + {
  2522 + var storeIdsStr = string.Join("','", input.StoreIds);
  2523 + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')";
  2524 + }
  2525 +
  2526 + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm";
  2527 +
  2528 + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql))
  2529 + .ToDictionary(
  2530 + item => item.health_coach_id?.ToString(),
  2531 + item => new
  2532 + {
  2533 + Name = item.health_coach_name?.ToString() ?? "未知",
  2534 + Performance = Convert.ToDecimal(item.refund_performance ?? 0),
  2535 + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0)
  2536 + }
  2537 + );
  2538 +
  2539 + // 第三步:合并数据,按原始排序构建排行榜
  2540 + var ranking = rankingIds
  2541 + .Select(id =>
  2542 + {
  2543 + var billing = billingData.ContainsKey(id) ? billingData[id] : null;
  2544 + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null;
  2545 + var refund = refundData.ContainsKey(id) ? refundData[id] : null;
  2546 +
  2547 + return new HealthCoachStatisticsOutput
  2548 + {
  2549 + HealthCoachId = id,
  2550 + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"),
  2551 + BillingPerformance = billing?.Performance ?? 0,
  2552 + ConsumePerformance = consume?.Performance ?? 0,
  2553 + RefundPerformance = refund?.Performance ?? 0,
  2554 + BillingProjectCount = billing?.ProjectCount ?? 0,
  2555 + ConsumeProjectCount = consume?.ProjectCount ?? 0,
  2556 + RefundProjectCount = refund?.ProjectCount ?? 0
  2557 + };
  2558 + })
  2559 + .ToList();
  2560 +
  2561 + return ranking;
  2562 + }
  2563 + catch (Exception ex)
  2564 + {
  2565 + _logger.LogError(ex, $"获取健康师退卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  2566 + throw NCCException.Oh($"获取健康师退卡业绩排行榜失败: {ex.Message}");
  2567 + }
  2568 + }
  2569 +
  2570 + #endregion
  2571 +
  2572 + #region 门店剩余权益统计
  2573 +
  2574 + /// <summary>
  2575 + /// 获取门店剩余权益统计
  2576 + /// </summary>
  2577 + /// <remarks>
  2578 + /// 统计指定时间范围内各门店的剩余权益数据,包括剩余权益累计金额和剩余权益累计人数
  2579 + ///
  2580 + /// 剩余权益累计金额 = 开单总金额 - 消耗总金额 - 退卡总金额
  2581 + /// 剩余权益累计人数 = 有剩余权益的会员数量(开单品项总次数 - 消耗品项总次数 - 退卡品项总次数 > 0)
  2582 + ///
  2583 + /// 示例请求:
  2584 + /// ```json
  2585 + /// {
  2586 + /// "startTime": "2025-10-01",
  2587 + /// "endTime": "2025-10-31",
  2588 + /// "storeIds": ["门店ID1", "门店ID2"]
  2589 + /// }
  2590 + /// ```
  2591 + ///
  2592 + /// 参数说明:
  2593 + /// - startTime: 开始时间(可选,默认为当月1号)
  2594 + /// - endTime: 结束时间(可选,默认为当前时间)
  2595 + /// - storeIds: 门店ID列表(可选,不传则统计所有门店)
  2596 + ///
  2597 + /// 返回字段说明:
  2598 + /// - StoreId: 门店ID
  2599 + /// - StoreName: 门店名称
  2600 + /// - RemainingRightsAmount: 剩余权益累计金额
  2601 + /// - RemainingRightsPersonCount: 剩余权益累计人数
  2602 + /// </remarks>
  2603 + /// <param name="input">查询参数</param>
  2604 + /// <returns>门店剩余权益统计数据列表</returns>
  2605 + /// <response code="200">成功返回统计数据</response>
  2606 + /// <response code="400">参数错误</response>
  2607 + /// <response code="500">服务器错误</response>
  2608 + [HttpPost("get-store-remaining-rights")]
  2609 + public async Task<List<StoreRemainingRightsOutput>> GetStoreRemainingRights(StoreRemainingRightsInput input)
  2610 + {
  2611 + try
  2612 + {
  2613 + // 设置默认时间范围(如果未提供,默认为当月)
  2614 + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
  2615 + var endTime = input.EndTime ?? DateTime.Now;
  2616 + // 第一步:获取门店基础信息
  2617 + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1";
  2618 + if (input.StoreIds != null && input.StoreIds.Any())
  2619 + {
  2620 + var storeIdsStr = string.Join("','", input.StoreIds);
  2621 + storeSql += $" AND F_Id IN ('{storeIdsStr}')";
  2622 + }
  2623 + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql);
  2624 + // 构建门店字典
  2625 + var storeDict = new Dictionary<string, string>();
  2626 + foreach (var store in stores)
  2627 + {
  2628 + var storeId = store.F_Id?.ToString();
  2629 + if (!string.IsNullOrEmpty(storeId))
  2630 + {
  2631 + storeDict[storeId] = store.dm?.ToString() ?? "未知门店";
  2632 + }
  2633 + }
  2634 +
  2635 + if (!storeDict.Any())
  2636 + {
  2637 + return new List<StoreRemainingRightsOutput>();
  2638 + }
  2639 +
  2640 + var allStoreIds = storeDict.Keys.ToList();
  2641 + var storeIdsStrForSql = string.Join("','", allStoreIds);
  2642 +
  2643 + // 第二步:统计开单总金额(按门店)
  2644 + var billingAmountSql = $@"
  2645 + SELECT
  2646 + kd.djmd as store_id,
  2647 + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount
  2648 + FROM lq_kd_kdjlb kd
  2649 + WHERE kd.F_IsEffective = 1
  2650 + AND kd.djmd IN ('{storeIdsStrForSql}')
  2651 + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2652 + GROUP BY kd.djmd";
  2653 +
  2654 + var billingAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(billingAmountSql);
  2655 + var billingAmountDict = new Dictionary<string, decimal>();
  2656 + foreach (var item in billingAmountResults)
  2657 + {
  2658 + var storeId = item.store_id?.ToString();
  2659 + if (!string.IsNullOrEmpty(storeId))
  2660 + {
  2661 + billingAmountDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0);
  2662 + }
  2663 + }
  2664 +
  2665 + // 第三步:统计消耗总金额(按门店)
  2666 + var consumeAmountSql = $@"
  2667 + SELECT
  2668 + xh.md as store_id,
  2669 + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount
  2670 + FROM lq_xh_hyhk xh
  2671 + WHERE xh.F_IsEffective = 1
  2672 + AND xh.md IN ('{storeIdsStrForSql}')
  2673 + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2674 + GROUP BY xh.md";
  2675 +
  2676 + var consumeAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeAmountSql);
  2677 + var consumeAmountDict = new Dictionary<string, decimal>();
  2678 + foreach (var item in consumeAmountResults)
  2679 + {
  2680 + var storeId = item.store_id?.ToString();
  2681 + if (!string.IsNullOrEmpty(storeId))
  2682 + {
  2683 + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0);
  2684 + }
  2685 + }
  2686 +
  2687 + // 第四步:统计退卡总金额(按门店)
  2688 + var refundAmountSql = $@"
  2689 + SELECT
  2690 + hytk.md as store_id,
  2691 + COALESCE(SUM(CAST(hytk.tkje AS DECIMAL(18,2))), 0) as refund_amount
  2692 + FROM lq_hytk_hytk hytk
  2693 + WHERE hytk.F_IsEffective = 1
  2694 + AND hytk.md IN ('{storeIdsStrForSql}')
  2695 + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2696 + GROUP BY hytk.md";
  2697 +
  2698 + var refundAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(refundAmountSql);
  2699 + var refundAmountDict = new Dictionary<string, decimal>();
  2700 + foreach (var item in refundAmountResults)
  2701 + {
  2702 + var storeId = item.store_id?.ToString();
  2703 + if (!string.IsNullOrEmpty(storeId))
  2704 + {
  2705 + refundAmountDict[storeId] = Convert.ToDecimal(item.refund_amount ?? 0);
  2706 + }
  2707 + }
  2708 +
  2709 + // 第五步:优化策略 - 先获取有开单的会员,然后只查询这些会员的消耗和退卡数据
  2710 + // 5.1 首先获取有开单记录的会员ID(按门店分组)
  2711 + var billingMembersSql = $@"
  2712 + SELECT DISTINCT
  2713 + kd.djmd as store_id,
  2714 + px.F_MemberId as member_id
  2715 + FROM lq_kd_pxmx px
  2716 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  2717 + WHERE px.F_IsEffective = 1
  2718 + AND kd.F_IsEffective = 1
  2719 + AND kd.djmd IN ('{storeIdsStrForSql}')
  2720 + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2721 + AND px.F_MemberId IS NOT NULL
  2722 + AND px.F_MemberId != ''";
  2723 +
  2724 + var billingMembersResults = await _db.Ado.SqlQueryAsync<dynamic>(billingMembersSql);
  2725 +
  2726 + // 按门店分组会员ID
  2727 + var storeMemberDict = new Dictionary<string, HashSet<string>>();
  2728 + foreach (var item in billingMembersResults)
  2729 + {
  2730 + var storeId = item.store_id?.ToString();
  2731 + var memberId = item.member_id?.ToString();
  2732 + if (!string.IsNullOrEmpty(storeId) && !string.IsNullOrEmpty(memberId))
  2733 + {
  2734 + if (!storeMemberDict.ContainsKey(storeId))
  2735 + {
  2736 + storeMemberDict[storeId] = new HashSet<string>();
  2737 + }
  2738 + storeMemberDict[storeId].Add(memberId);
  2739 + }
  2740 + }
  2741 +
  2742 + // 如果没有开单会员,直接返回
  2743 + if (!storeMemberDict.Any())
  2744 + {
  2745 + var emptyResultList = new List<StoreRemainingRightsOutput>();
  2746 + foreach (var kvp in storeDict)
  2747 + {
  2748 + var storeId = kvp.Key;
  2749 + var storeName = kvp.Value;
  2750 + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0;
  2751 + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0;
  2752 + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0;
  2753 + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount;
  2754 +
  2755 + emptyResultList.Add(new StoreRemainingRightsOutput
  2756 + {
  2757 + StoreId = storeId,
  2758 + StoreName = storeName,
  2759 + RemainingRightsAmount = remainingRightsAmount,
  2760 + RemainingRightsPersonCount = 0
  2761 + });
  2762 + }
  2763 + return emptyResultList.OrderByDescending(x => x.RemainingRightsAmount).ToList();
  2764 + }
  2765 +
  2766 + // 5.2 分别查询这些会员的开单、消耗、退卡项目数(使用IN子句,大幅减少查询范围)
  2767 + var remainingPersonCountDict = new Dictionary<string, int>();
  2768 +
  2769 + foreach (var storeKvp in storeMemberDict)
  2770 + {
  2771 + var storeId = storeKvp.Key;
  2772 + var memberIds = storeKvp.Value.ToList();
  2773 +
  2774 + if (!memberIds.Any())
  2775 + {
  2776 + remainingPersonCountDict[storeId] = 0;
  2777 + continue;
  2778 + }
  2779 +
  2780 + // 分批处理会员ID(避免IN子句过长)
  2781 + var batchSize = 500;
  2782 + var remainingMembers = new HashSet<string>();
  2783 +
  2784 + for (int i = 0; i < memberIds.Count; i += batchSize)
  2785 + {
  2786 + var batchMemberIds = memberIds.Skip(i).Take(batchSize).ToList();
  2787 + var memberIdsStr = string.Join("','", batchMemberIds);
  2788 +
  2789 + // 查询这批会员的开单品项数
  2790 + var billingBatchSql = $@"
  2791 + SELECT
  2792 + px.F_MemberId as member_id,
  2793 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_count
  2794 + FROM lq_kd_pxmx px
  2795 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  2796 + WHERE px.F_IsEffective = 1
  2797 + AND kd.F_IsEffective = 1
  2798 + AND kd.djmd = '{storeId}'
  2799 + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2800 + AND px.F_MemberId IN ('{memberIdsStr}')
  2801 + GROUP BY px.F_MemberId";
  2802 +
  2803 + // 查询这批会员的消耗品项数
  2804 + var consumeBatchSql = $@"
  2805 + SELECT
  2806 + xhpx.F_MemberId as member_id,
  2807 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_count
  2808 + FROM lq_xh_pxmx xhpx
  2809 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  2810 + WHERE xhpx.F_IsEffective = 1
  2811 + AND xh.F_IsEffective = 1
  2812 + AND xh.md = '{storeId}'
  2813 + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2814 + AND xhpx.F_MemberId IN ('{memberIdsStr}')
  2815 + GROUP BY xhpx.F_MemberId";
  2816 +
  2817 + // 查询这批会员的退卡品项数
  2818 + var refundBatchSql = $@"
  2819 + SELECT
  2820 + px.F_MemberId as member_id,
  2821 + SUM(CAST(hytkmx.F_ProjectNumber AS DECIMAL(18,2))) as refund_count
  2822 + FROM lq_hytk_mx hytkmx
  2823 + INNER JOIN (
  2824 + SELECT F_Id
  2825 + FROM lq_hytk_hytk
  2826 + WHERE F_IsEffective = 1
  2827 + AND md = '{storeId}'
  2828 + AND tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'
  2829 + ) hytk ON hytkmx.F_RefundInfoId = hytk.F_Id
  2830 + INNER JOIN lq_kd_pxmx px ON hytkmx.F_BillingItemId = px.F_Id AND px.F_IsEffective = 1
  2831 + WHERE hytkmx.F_IsEffective = 1
  2832 + AND px.F_MemberId IN ('{memberIdsStr}')
  2833 + GROUP BY px.F_MemberId";
  2834 +
  2835 + // 并行查询三个数据
  2836 + var billingTask = _db.Ado.SqlQueryAsync<dynamic>(billingBatchSql);
  2837 + var consumeTask = _db.Ado.SqlQueryAsync<dynamic>(consumeBatchSql);
  2838 + var refundTask = _db.Ado.SqlQueryAsync<dynamic>(refundBatchSql);
  2839 +
  2840 + await Task.WhenAll(billingTask, consumeTask, refundTask);
  2841 +
  2842 + var billingData = (await billingTask).ToDictionary(
  2843 + x => x.member_id?.ToString(),
  2844 + x => Convert.ToDecimal(x.billing_count ?? 0)
  2845 + );
  2846 + var consumeData = (await consumeTask).ToDictionary(
  2847 + x => x.member_id?.ToString(),
  2848 + x => Convert.ToDecimal(x.consume_count ?? 0)
  2849 + );
  2850 + var refundData = (await refundTask).ToDictionary(
  2851 + x => x.member_id?.ToString(),
  2852 + x => Convert.ToDecimal(x.refund_count ?? 0)
  2853 + );
  2854 +
  2855 + // 计算这批会员的剩余次数
  2856 + foreach (var memberId in batchMemberIds)
  2857 + {
  2858 + var billingCount = billingData.ContainsKey(memberId) ? billingData[memberId] : 0;
  2859 + var consumeCount = consumeData.ContainsKey(memberId) ? consumeData[memberId] : 0;
  2860 + var refundCount = refundData.ContainsKey(memberId) ? refundData[memberId] : 0;
  2861 + var remainingCount = billingCount - consumeCount - refundCount;
  2862 +
  2863 + if (remainingCount > 0)
  2864 + {
  2865 + remainingMembers.Add(memberId);
  2866 + }
  2867 + }
  2868 + }
  2869 +
  2870 + remainingPersonCountDict[storeId] = remainingMembers.Count;
  2871 + }
  2872 +
  2873 + // 第六步:合并数据并计算剩余权益
  2874 + var resultList = new List<StoreRemainingRightsOutput>();
  2875 +
  2876 + foreach (var kvp in storeDict)
  2877 + {
  2878 + var storeId = kvp.Key;
  2879 + var storeName = kvp.Value;
  2880 +
  2881 + // 计算剩余权益金额
  2882 + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0;
  2883 + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0;
  2884 + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0;
  2885 + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount;
  2886 +
  2887 + // 获取剩余权益人数(已在SQL中计算)
  2888 + var remainingPersonCount = remainingPersonCountDict.ContainsKey(storeId) ? remainingPersonCountDict[storeId] : 0;
  2889 +
  2890 + resultList.Add(new StoreRemainingRightsOutput
  2891 + {
  2892 + StoreId = storeId,
  2893 + StoreName = storeName,
  2894 + RemainingRightsAmount = remainingRightsAmount,
  2895 + RemainingRightsPersonCount = remainingPersonCount
  2896 + });
  2897 + }
  2898 +
  2899 + // 按剩余权益金额排序
  2900 + resultList = resultList.OrderByDescending(x => x.RemainingRightsAmount).ToList();
  2901 +
  2902 + return resultList;
  2903 + }
  2904 + catch (Exception ex)
  2905 + {
  2906 + _logger.LogError(ex, $"获取门店剩余权益统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}");
  2907 + throw NCCException.Oh($"获取门店剩余权益统计数据失败: {ex.Message}");
  2908 + }
  2909 + }
  2910 +
  2911 + #endregion
1328 2912 }
1329 2913 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -971,7 +971,7 @@ namespace NCC.Extend.LqStatistics
971 971 + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n"
972 972 + $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n"
973 973 + $"完成率:{monthlyStats.CompletionRate:F2}%\n\n"
974   - + $"http://lvqian.antissoft.com/html/dailyReportnew.html";
  974 + + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html";
975 975  
976 976 var result = await _weChatBotService.SendTextMessage(messageContent);
977 977 return new
... ... @@ -2586,7 +2586,7 @@ namespace NCC.Extend.LqStatistics
2586 2586 LEFT JOIN (
2587 2587 SELECT
2588 2588 hytk.md as F_StoreId,
2589   - COALESCE(SUM(hytk.tkje), 0) as F_RefundAmount,
  2589 + COALESCE(SUM(hytk.F_ActualRefundAmount), 0) as F_RefundAmount,
2590 2590 COUNT(DISTINCT hytk.F_Id) as F_RefundCount
2591 2591 FROM lq_hytk_hytk hytk
2592 2592 WHERE hytk.F_IsEffective = 1
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
... ... @@ -972,7 +972,8 @@ namespace NCC.Extend.LqTkjlb
972 972 /// {
973 973 /// "startTime": "2025-10-01",
974 974 /// "endTime": "2025-10-31",
975   - /// "eventId": "活动ID"
  975 + /// "eventId": "活动ID",
  976 + /// "storeId": ["门店ID1", "门店ID2"]
976 977 /// }
977 978 /// ```
978 979 ///
... ... @@ -980,6 +981,7 @@ namespace NCC.Extend.LqTkjlb
980 981 /// - startTime: 开始时间(可选)
981 982 /// - endTime: 结束时间(可选)
982 983 /// - eventId: 活动ID(可选)
  984 + /// - storeId: 门店ID数组(可选,可传入多个门店ID进行筛选)
983 985 ///
984 986 /// 返回字段说明:
985 987 /// - TkCount: 拓客人数
... ... @@ -1024,11 +1026,18 @@ namespace NCC.Extend.LqTkjlb
1024 1026 eventFilter = $"AND tk.F_EventId = '{input.EventId}'";
1025 1027 }
1026 1028  
  1029 + string storeFilter = "";
  1030 + if (input.StoreId != null && input.StoreId.Any() && input.StoreId.Any(s => !string.IsNullOrWhiteSpace(s)))
  1031 + {
  1032 + var storeIdsStr = string.Join("','", input.StoreId.Where(s => !string.IsNullOrWhiteSpace(s)));
  1033 + storeFilter = $"AND tk.F_StoreId IN ('{storeIdsStr}')";
  1034 + }
  1035 +
1027 1036 // 第一步:获取拓客人数(去重会员ID)
1028 1037 var tkSql = $@"
1029 1038 SELECT COUNT(DISTINCT tk.F_MemberId) as tk_count
1030 1039 FROM lq_tkjlb tk
1031   - WHERE 1=1 {timeFilter} {eventFilter}";
  1040 + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}";
1032 1041  
1033 1042 var tkResult = await _db.Ado.SqlQueryAsync<dynamic>(tkSql);
1034 1043 var tkCount = Convert.ToInt32(tkResult?.FirstOrDefault()?.tk_count ?? 0);
... ... @@ -1039,7 +1048,7 @@ namespace NCC.Extend.LqTkjlb
1039 1048 FROM lq_tkjlb tk
1040 1049 INNER JOIN lq_yaoyjl yy ON tk.F_MemberId = yy.yykh
1041 1050 AND yy.F_StoreId = tk.F_StoreId
1042   - WHERE 1=1 {timeFilter} {eventFilter}";
  1051 + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}";
1043 1052  
1044 1053 var yaoyResult = await _db.Ado.SqlQueryAsync<dynamic>(yaoySql);
1045 1054 var yaoyCount = Convert.ToInt32(yaoyResult?.FirstOrDefault()?.yaoy_count ?? 0);
... ... @@ -1050,7 +1059,7 @@ namespace NCC.Extend.LqTkjlb
1050 1059 FROM lq_tkjlb tk
1051 1060 INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk
1052 1061 AND yyjl.F_Status = '已确认'
1053   - WHERE 1=1 {timeFilter} {eventFilter}";
  1062 + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}";
1054 1063  
1055 1064 var yyResult = await _db.Ado.SqlQueryAsync<dynamic>(yySql);
1056 1065 var yyCount = Convert.ToInt32(yyResult?.FirstOrDefault()?.yy_count ?? 0);
... ... @@ -1062,7 +1071,7 @@ namespace NCC.Extend.LqTkjlb
1062 1071 INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk
1063 1072 AND yyjl.F_Status = '已确认'
1064 1073 WHERE yyjl.yysj <= NOW()
1065   - AND 1=1 {timeFilter} {eventFilter}";
  1074 + AND 1=1 {timeFilter} {eventFilter} {storeFilter}";
1066 1075  
1067 1076 var ddResult = await _db.Ado.SqlQueryAsync<dynamic>(ddSql);
1068 1077 var ddCount = Convert.ToInt32(ddResult?.FirstOrDefault()?.dd_count ?? 0);
... ... @@ -1075,7 +1084,7 @@ namespace NCC.Extend.LqTkjlb
1075 1084 FROM lq_tkjlb tk
1076 1085 INNER JOIN lq_kd_kdjlb kd ON tk.F_MemberId = kd.kdhy
1077 1086 AND kd.F_IsEffective = 1
1078   - WHERE 1=1 {timeFilter} {eventFilter}";
  1087 + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}";
1079 1088  
1080 1089 var kdResult = await _db.Ado.SqlQueryAsync<dynamic>(kdSql);
1081 1090 var kdCount = Convert.ToInt32(kdResult?.FirstOrDefault()?.kd_count ?? 0);
... ... @@ -1089,7 +1098,7 @@ namespace NCC.Extend.LqTkjlb
1089 1098 FROM lq_tkjlb tk
1090 1099 INNER JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy
1091 1100 AND xh.F_IsEffective = 1
1092   - WHERE 1=1 {timeFilter} {eventFilter}";
  1101 + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}";
1093 1102  
1094 1103 var xfResult = await _db.Ado.SqlQueryAsync<dynamic>(xfSql);
1095 1104 var xfCount = Convert.ToInt32(xfResult?.FirstOrDefault()?.xf_count ?? 0);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs
... ... @@ -18,6 +18,7 @@ using NCC.DataEncryption;
18 18 using NCC.Dependency;
19 19 using NCC.DynamicApiController;
20 20 using NCC.Extend.Entitys.Dto.LqYcsdJsj;
  21 +using NCC.Extend.Entitys.Dto.LqJinsanjiaoUser;
21 22 using NCC.Extend.Entitys.lq_jinsanjiao_user;
22 23 using NCC.Extend.Entitys.lq_kd_jksyj;
23 24 using NCC.Extend.Entitys.lq_mdxx;
... ... @@ -83,7 +84,7 @@ namespace NCC.Extend.LqYcsdJsj
83 84  
84 85 // 获取成员信息
85 86 var members = await _db.Queryable<NCC.Extend.Entitys.lq_jinsanjiao_user.LqJinsanjiaoUserEntity>()
86   - .Where(x => x.JsjId == id && x.Status == "ACTIVE")
  87 + .Where(x => x.JsjId == id && x.Status == "ACTIVE" && x.DeleteMark == 0)
87 88 .OrderBy(x => x.SortOrder)
88 89 .Select(x => new
89 90 {
... ... @@ -271,6 +272,45 @@ namespace NCC.Extend.LqYcsdJsj
271 272 }
272 273 #endregion
273 274  
  275 + #region 标记删除用户金三角关联信息
  276 + /// <summary>
  277 + /// 标记删除用户金三角关联信息
  278 + /// </summary>
  279 + /// <remarks>
  280 + /// 根据金三角用户关系ID,将绑定关系的删除标记设置为已删除
  281 + ///
  282 + /// 示例请求:
  283 + /// ```json
  284 + /// {
  285 + /// "id": "关系ID"
  286 + /// }
  287 + /// ```
  288 + /// </remarks>
  289 + /// <param name="input">参数</param>
  290 + /// <returns></returns>
  291 + /// <response code="200">成功删除</response>
  292 + /// <response code="400">参数错误</response>
  293 + /// <response code="500">服务器错误</response>
  294 + [HttpPost("Actions/DeleteJsjUserRelation")]
  295 + public async Task DeleteJsjUserRelation([FromBody] LqJinsanjiaoUserDeleteInput input)
  296 + {
  297 + if (string.IsNullOrEmpty(input.Id))
  298 + throw NCCException.Oh(ErrorCode.COM1000, "ID不能为空");
  299 +
  300 + var isOk = await _db.Updateable<LqJinsanjiaoUserEntity>()
  301 + .SetColumns(x => new LqJinsanjiaoUserEntity
  302 + {
  303 + DeleteMark = 1,
  304 + Status = "INACTIVE"
  305 + })
  306 + .Where(x => x.Id == input.Id && x.DeleteMark == 0)
  307 + .ExecuteCommandAsync();
  308 +
  309 + if (!(isOk > 0))
  310 + throw NCCException.Oh(ErrorCode.COM1000, "未找到要删除的记录");
  311 + }
  312 + #endregion
  313 +
274 314 #region 新建金三角
275 315 /// <summary>
276 316 /// 新建金三角
... ...
优化GetStoreRemainingRights性能索引.sql 0 → 100644
  1 +-- ============================================
  2 +-- 优化 GetStoreRemainingRights 方法性能的索引
  3 +-- ============================================
  4 +-- 说明:这些索引专门为统计门店剩余权益功能优化
  5 +-- 执行前请检查索引是否已存在,避免重复创建
  6 +
  7 +-- ============================================
  8 +-- 1. lq_kd_pxmx (开单品项明细表) 索引
  9 +-- ============================================
  10 +-- 用于:查询开单品项总数、按会员分组统计、JOIN关联
  11 +-- 查询条件:F_IsEffective, F_MemberId, glkdbh
  12 +
  13 +-- 索引1:用于F_MemberId IN查询和JOIN
  14 +-- MySQL 5.7以下版本请先检查索引是否存在,或直接执行(如果已存在会报错,可忽略)
  15 +CREATE INDEX idx_kd_pxmx_member_effective_glkdbh
  16 +ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh);
  17 +
  18 +-- 索引2:用于JOIN和按会员分组(补充索引,覆盖JOIN场景)
  19 +CREATE INDEX idx_kd_pxmx_glkdbh_member_effective
  20 +ON lq_kd_pxmx(glkdbh, F_MemberId, F_IsEffective);
  21 +
  22 +-- ============================================
  23 +-- 2. lq_kd_kdjlb (开单记录表) 索引
  24 +-- ============================================
  25 +-- 用于:按门店和时间范围过滤、JOIN关联
  26 +-- 查询条件:F_IsEffective, djmd, kdrq
  27 +
  28 +-- 索引1:用于门店+时间范围查询(最常用)
  29 +CREATE INDEX idx_kd_kdjlb_store_date_effective
  30 +ON lq_kd_kdjlb(djmd, kdrq, F_IsEffective);
  31 +
  32 +-- 索引2:用于JOIN(补充,F_Id是主键可能不需要)
  33 +-- 如果F_Id是主键,则不需要此索引
  34 +-- CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_id_effective
  35 +-- ON lq_kd_kdjlb(F_Id, F_IsEffective);
  36 +
  37 +-- ============================================
  38 +-- 3. lq_xh_pxmx (消耗品项明细表) 索引
  39 +-- ============================================
  40 +-- 用于:查询消耗品项总数、按会员分组统计、JOIN关联
  41 +-- 查询条件:F_IsEffective, F_MemberId, F_ConsumeInfoId
  42 +
  43 +-- 索引1:用于F_MemberId IN查询和JOIN
  44 +CREATE INDEX idx_xh_pxmx_member_effective_consume
  45 +ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId);
  46 +
  47 +-- 索引2:用于JOIN场景(补充索引)
  48 +CREATE INDEX idx_xh_pxmx_consume_member_effective
  49 +ON lq_xh_pxmx(F_ConsumeInfoId, F_MemberId, F_IsEffective);
  50 +
  51 +-- ============================================
  52 +-- 4. lq_xh_hyhk (消耗会员耗卡表) 索引
  53 +-- ============================================
  54 +-- 用于:按门店和时间范围过滤、JOIN关联
  55 +-- 查询条件:F_IsEffective, md, hksj
  56 +
  57 +-- 索引1:用于门店+时间范围查询(最常用)
  58 +CREATE INDEX idx_xh_hyhk_store_date_effective
  59 +ON lq_xh_hyhk(md, hksj, F_IsEffective);
  60 +
  61 +-- 索引2:用于JOIN(F_Id是主键则不需要)
  62 +-- CREATE INDEX IF NOT EXISTS idx_xh_hyhk_id_effective
  63 +-- ON lq_xh_hyhk(F_Id, F_IsEffective);
  64 +
  65 +-- ============================================
  66 +-- 5. lq_hytk_hytk (退卡记录表) 索引
  67 +-- ============================================
  68 +-- 用于:按门店和时间范围过滤、JOIN关联
  69 +-- 查询条件:F_IsEffective, md, tksj
  70 +
  71 +-- 索引1:用于门店+时间范围查询(最常用)
  72 +CREATE INDEX idx_hytk_hytk_store_date_effective
  73 +ON lq_hytk_hytk(md, tksj, F_IsEffective);
  74 +
  75 +-- 索引2:用于JOIN(F_Id是主键则不需要)
  76 +-- CREATE INDEX IF NOT EXISTS idx_hytk_hytk_id_effective
  77 +-- ON lq_hytk_hytk(F_Id, F_IsEffective);
  78 +
  79 +-- ============================================
  80 +-- 6. lq_hytk_mx (退卡明细表) 索引
  81 +-- ============================================
  82 +-- 用于:JOIN关联、按会员分组统计
  83 +-- 查询条件:F_IsEffective, F_RefundInfoId, F_BillingItemId
  84 +
  85 +-- 索引1:用于JOIN lq_hytk_hytk
  86 +CREATE INDEX idx_hytk_mx_refund_effective
  87 +ON lq_hytk_mx(F_RefundInfoId, F_IsEffective);
  88 +
  89 +-- 索引2:用于JOIN lq_kd_pxmx
  90 +CREATE INDEX idx_hytk_mx_billing_effective
  91 +ON lq_hytk_mx(F_BillingItemId, F_IsEffective);
  92 +
  93 +-- ============================================
  94 +-- 7. 复合索引(补充优化)
  95 +-- ============================================
  96 +-- 如果需要进一步优化,可以考虑以下复合索引
  97 +
  98 +-- lq_kd_pxmx: 覆盖查询(包含F_ProjectNumber,避免回表)
  99 +-- 注意:此索引较大,请根据数据量决定是否创建
  100 +-- CREATE INDEX IF NOT EXISTS idx_kd_pxmx_cover
  101 +-- ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh, F_ProjectNumber);
  102 +
  103 +-- lq_xh_pxmx: 覆盖查询(包含F_ProjectNumber)
  104 +-- CREATE INDEX IF NOT EXISTS idx_xh_pxmx_cover
  105 +-- ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId, F_ProjectNumber);
  106 +
  107 +-- ============================================
  108 +-- 索引创建前的检查(避免重复创建)
  109 +-- ============================================
  110 +-- 执行以下SQL检查索引是否已存在:
  111 +
  112 +-- SELECT
  113 +-- TABLE_NAME,
  114 +-- INDEX_NAME,
  115 +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS
  116 +-- FROM INFORMATION_SCHEMA.STATISTICS
  117 +-- WHERE TABLE_SCHEMA = DATABASE()
  118 +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx')
  119 +-- AND INDEX_NAME LIKE 'idx_%'
  120 +-- GROUP BY TABLE_NAME, INDEX_NAME
  121 +-- ORDER BY TABLE_NAME, INDEX_NAME;
  122 +
  123 +-- ============================================
  124 +-- 索引创建后的验证
  125 +-- ============================================
  126 +-- 执行以下SQL验证索引是否创建成功:
  127 +
  128 +-- SELECT
  129 +-- TABLE_NAME,
  130 +-- INDEX_NAME,
  131 +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS,
  132 +-- NON_UNIQUE,
  133 +-- CARDINALITY
  134 +-- FROM INFORMATION_SCHEMA.STATISTICS
  135 +-- WHERE TABLE_SCHEMA = DATABASE()
  136 +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx')
  137 +-- AND INDEX_NAME LIKE 'idx_%'
  138 +-- GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE, CARDINALITY
  139 +-- ORDER BY TABLE_NAME, INDEX_NAME;
  140 +
  141 +-- ============================================
  142 +-- 性能测试建议
  143 +-- ============================================
  144 +-- 1. 创建索引前,记录查询时间
  145 +-- 2. 创建索引后,使用相同参数重新测试
  146 +-- 3. 使用 EXPLAIN 分析查询计划,确认索引被使用
  147 +-- 4. 如果索引未被使用,检查WHERE条件顺序是否匹配索引列顺序
  148 +
  149 +-- ============================================
  150 +-- 注意事项
  151 +-- ============================================
  152 +-- 1. 索引会占用存储空间,请定期监控
  153 +-- 2. 索引会影响INSERT/UPDATE性能,但查询性能提升显著
  154 +-- 3. 如果表数据量很大,创建索引可能需要较长时间
  155 +-- 4. 建议在业务低峰期执行索引创建
  156 +-- 5. MySQL不同版本的兼容性:
  157 +-- - MySQL 8.0+ 支持 CREATE INDEX IF NOT EXISTS
  158 +-- - MySQL 5.7及以下不支持 IF NOT EXISTS,需要先检查索引是否存在
  159 +-- 本文件已移除 IF NOT EXISTS,如索引已存在会报错,可安全忽略
  160 +-- 6. 如果报错 "Duplicate key name",说明索引已存在,可以继续执行后面的语句
  161 +
... ...
创建会员开单耗卡项目数视图.sql 0 → 100644
  1 +-- ============================================
  2 +-- 创建会员开单耗卡项目数视图
  3 +-- ============================================
  4 +-- 说明:创建视图可以提升查询效率,避免每次都执行复杂的JOIN和聚合操作
  5 +-- 视图会预先聚合好数据,查询时直接使用
  6 +
  7 +-- ============================================
  8 +-- 方案1:基础聚合视图(推荐)
  9 +-- ============================================
  10 +-- 优点:查询简单、性能好、可以灵活按时间范围查询
  11 +-- 缺点:仍然需要每次查询时过滤时间范围
  12 +
  13 +-- 删除已存在的视图(如果存在)
  14 +DROP VIEW IF EXISTS v_member_billing_consume_project;
  15 +
  16 +-- 创建视图:会员开单项目数汇总(不限制时间,包含所有数据)
  17 +CREATE VIEW v_member_billing_project AS
  18 +SELECT
  19 + px.F_MemberId as member_id,
  20 + kd.djmd as store_id,
  21 + kd.kdrq as billing_date,
  22 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count
  23 +FROM lq_kd_pxmx px
  24 +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  25 +WHERE px.F_IsEffective = 1
  26 + AND kd.F_IsEffective = 1
  27 + AND px.F_MemberId IS NOT NULL
  28 + AND px.F_MemberId != ''
  29 +GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq);
  30 +
  31 +-- 创建视图:会员耗卡项目数汇总(不限制时间,包含所有数据)
  32 +CREATE VIEW v_member_consume_project AS
  33 +SELECT
  34 + xhpx.F_MemberId as member_id,
  35 + xh.md as store_id,
  36 + xh.hksj as consume_date,
  37 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  38 +FROM lq_xh_pxmx xhpx
  39 +INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  40 +WHERE xhpx.F_IsEffective = 1
  41 + AND xh.F_IsEffective = 1
  42 + AND xhpx.F_MemberId IS NOT NULL
  43 + AND xhpx.F_MemberId != ''
  44 +GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj);
  45 +
  46 +-- 创建组合视图:会员开单耗卡项目数(按日汇总)
  47 +CREATE VIEW v_member_billing_consume_daily AS
  48 +SELECT
  49 + COALESCE(billing.member_id, consume.member_id) as member_id,
  50 + COALESCE(billing.store_id, consume.store_id) as store_id,
  51 + COALESCE(billing.billing_date, consume.consume_date) as statistics_date,
  52 + COALESCE(billing.billing_project_count, 0) as billing_project_count,
  53 + COALESCE(consume.consume_project_count, 0) as consume_project_count,
  54 + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count
  55 +FROM v_member_billing_project billing
  56 +FULL OUTER JOIN v_member_consume_project consume
  57 + ON billing.member_id = consume.member_id
  58 + AND billing.store_id = consume.store_id
  59 + AND DATE(billing.billing_date) = DATE(consume.consume_date);
  60 +
  61 +-- ============================================
  62 +-- 方案2:使用UNION ALL的视图(兼容MySQL,推荐)
  63 +-- ============================================
  64 +-- 优点:兼容MySQL所有版本、查询简单、性能好
  65 +
  66 +DROP VIEW IF EXISTS v_member_billing_consume_project;
  67 +
  68 +CREATE VIEW v_member_billing_consume_project AS
  69 +SELECT
  70 + member_id,
  71 + store_id,
  72 + statistics_date,
  73 + SUM(billing_project_count) as billing_project_count,
  74 + SUM(consume_project_count) as consume_project_count,
  75 + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count
  76 +FROM (
  77 + -- 开单项目数(按会员、门店、日期分组)
  78 + SELECT
  79 + px.F_MemberId as member_id,
  80 + kd.djmd as store_id,
  81 + DATE(kd.kdrq) as statistics_date,
  82 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count,
  83 + 0 as consume_project_count
  84 + FROM lq_kd_pxmx px
  85 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  86 + WHERE px.F_IsEffective = 1
  87 + AND kd.F_IsEffective = 1
  88 + AND px.F_MemberId IS NOT NULL
  89 + AND px.F_MemberId != ''
  90 + GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq)
  91 +
  92 + UNION ALL
  93 +
  94 + -- 耗卡项目数(按会员、门店、日期分组)
  95 + SELECT
  96 + xhpx.F_MemberId as member_id,
  97 + xh.md as store_id,
  98 + DATE(xh.hksj) as statistics_date,
  99 + 0 as billing_project_count,
  100 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  101 + FROM lq_xh_pxmx xhpx
  102 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  103 + WHERE xhpx.F_IsEffective = 1
  104 + AND xh.F_IsEffective = 1
  105 + AND xhpx.F_MemberId IS NOT NULL
  106 + AND xhpx.F_MemberId != ''
  107 + GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj)
  108 +) all_data
  109 +GROUP BY member_id, store_id, statistics_date;
  110 +
  111 +-- ============================================
  112 +-- 方案3:按会员汇总的总视图(不按日期,累计汇总)
  113 +-- ============================================
  114 +-- 优点:查询最简单、性能最好(适合查询总量)
  115 +-- 缺点:无法按时间范围查询
  116 +
  117 +DROP VIEW IF EXISTS v_member_billing_consume_total;
  118 +
  119 +CREATE VIEW v_member_billing_consume_total AS
  120 +SELECT
  121 + member_id,
  122 + SUM(billing_project_count) as total_billing_project_count,
  123 + SUM(consume_project_count) as total_consume_project_count,
  124 + SUM(billing_project_count) - SUM(consume_project_count) as total_remaining_project_count,
  125 + COUNT(DISTINCT store_id) as store_count
  126 +FROM (
  127 + -- 开单项目数(按会员、门店汇总)
  128 + SELECT
  129 + px.F_MemberId as member_id,
  130 + kd.djmd as store_id,
  131 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count,
  132 + 0 as consume_project_count
  133 + FROM lq_kd_pxmx px
  134 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  135 + WHERE px.F_IsEffective = 1
  136 + AND kd.F_IsEffective = 1
  137 + AND px.F_MemberId IS NOT NULL
  138 + AND px.F_MemberId != ''
  139 + GROUP BY px.F_MemberId, kd.djmd
  140 +
  141 + UNION ALL
  142 +
  143 + -- 耗卡项目数(按会员、门店汇总)
  144 + SELECT
  145 + xhpx.F_MemberId as member_id,
  146 + xh.md as store_id,
  147 + 0 as billing_project_count,
  148 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  149 + FROM lq_xh_pxmx xhpx
  150 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  151 + WHERE xhpx.F_IsEffective = 1
  152 + AND xh.F_IsEffective = 1
  153 + AND xhpx.F_MemberId IS NOT NULL
  154 + AND xhpx.F_MemberId != ''
  155 + GROUP BY xhpx.F_MemberId, xh.md
  156 +) all_data
  157 +GROUP BY member_id;
  158 +
  159 +
  160 +-- ============================================
  161 +-- 使用视图的查询示例
  162 +-- ============================================
  163 +
  164 +-- 示例1:查询所有会员的开单耗卡项目数(按会员汇总,不限制时间)
  165 +SELECT
  166 + member_id,
  167 + SUM(billing_project_count) as billing_project_count,
  168 + SUM(consume_project_count) as consume_project_count,
  169 + SUM(remaining_project_count) as remaining_project_count
  170 +FROM v_member_billing_consume_project
  171 +GROUP BY member_id
  172 +ORDER BY billing_project_count DESC;
  173 +
  174 +-- 示例2:查询指定时间范围内的数据
  175 +SELECT
  176 + member_id,
  177 + SUM(billing_project_count) as billing_project_count,
  178 + SUM(consume_project_count) as consume_project_count,
  179 + SUM(remaining_project_count) as remaining_project_count
  180 +FROM v_member_billing_consume_project
  181 +WHERE statistics_date >= '2025-10-01'
  182 + AND statistics_date <= '2025-10-30'
  183 +GROUP BY member_id
  184 +ORDER BY billing_project_count DESC;
  185 +
  186 +-- 示例3:查询指定门店的数据
  187 +SELECT
  188 + member_id,
  189 + SUM(billing_project_count) as billing_project_count,
  190 + SUM(consume_project_count) as consume_project_count,
  191 + SUM(remaining_project_count) as remaining_project_count
  192 +FROM v_member_billing_consume_project
  193 +WHERE store_id = '1649328471923847169'
  194 + AND statistics_date >= '2025-10-01'
  195 + AND statistics_date <= '2025-10-30'
  196 +GROUP BY member_id
  197 +ORDER BY billing_project_count DESC;
  198 +
  199 +-- 示例4:使用总视图(最简单的查询,累计所有数据)
  200 +SELECT
  201 + member_id,
  202 + total_billing_project_count,
  203 + total_consume_project_count,
  204 + total_remaining_project_count,
  205 + store_count
  206 +FROM v_member_billing_consume_total
  207 +ORDER BY total_billing_project_count DESC
  208 +LIMIT 100;
  209 +
  210 +
  211 +-- ============================================
  212 +-- 视图性能优化建议
  213 +-- ============================================
  214 +-- 1. 确保基础表已创建索引(参考优化GetStoreRemainingRights性能索引.sql)
  215 +-- 2. 如果数据量很大,可以考虑:
  216 +-- a. 创建物化视图(但MySQL不支持,需要定期刷新汇总表)
  217 +-- b. 创建汇总表,定期更新(推荐)
  218 +-- 3. 定期分析视图性能:
  219 +-- EXPLAIN SELECT * FROM v_member_billing_consume_project WHERE statistics_date >= '2025-10-01';
  220 +
  221 +-- ============================================
  222 +-- 视图维护
  223 +-- ============================================
  224 +-- 查看视图定义
  225 +-- SHOW CREATE VIEW v_member_billing_consume_project;
  226 +
  227 +-- 查看所有视图
  228 +-- SELECT TABLE_NAME
  229 +-- FROM INFORMATION_SCHEMA.VIEWS
  230 +-- WHERE TABLE_SCHEMA = DATABASE();
  231 +
  232 +-- 删除视图
  233 +-- DROP VIEW IF EXISTS v_member_billing_consume_project;
  234 +-- DROP VIEW IF EXISTS v_member_billing_consume_total;
  235 +
... ...
数据库说明.md
... ... @@ -90,7 +90,20 @@
90 90 - `lq_xh_jksyj.F_kdpxid` ↔ `lq_xh_pxmx.F_Id` (健康师业绩关联品项明细)
91 91 - `lq_xh_kjbsyj.F_hkpxid` ↔ `lq_xh_pxmx.F_Id` (科技部老师业绩关联品项明细)
92 92  
93   -### 5. 业绩统计关系
  93 +### 5. 退卡业务关系
  94 +- **退卡记录**: `lq_hytk_hytk` (退卡记录表)
  95 +- **退卡品项明细**: `lq_hytk_mx` (退卡品项明细)
  96 +- **退卡健康师业绩**: `lq_hytk_jksyj` (退卡健康师业绩)
  97 +- **退卡科技部老师业绩**: `lq_hytk_kjbsyj` (退卡科技部老师业绩)
  98 +- **关联字段**:
  99 + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_mx.F_RefundInfoId` (退卡记录关联品项明细)
  100 + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_jksyj.gltkbh` (退卡记录关联健康师业绩)
  101 + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_kjbsyj.gltkbh` (退卡记录关联科技部老师业绩)
  102 + - `lq_hytk_mx.F_BillingItemId` ↔ `lq_kd_pxmx.F_Id` (退卡明细关联开单品项明细)
  103 + - `lq_hytk_mx.F_MemberId` ↔ `lq_khxx.F_Id` (退卡明细关联会员,通过会员ID)
  104 + - `lq_hytk_hytk.hy` ↔ `lq_khxx.F_Id` (退卡记录关联会员)
  105 +
  106 +### 6. 业绩统计关系
94 107 - **业绩明细**: `lq_yjmxb` (业绩统计表)
95 108 - **关联字段**:
96 109 - `lq_yjmxb.jks` ↔ `BASE_USER.F_REALNAME` (健康师姓名)
... ...
查询所有会员开单耗卡项目数.sql 0 → 100644
  1 +-- ============================================
  2 +-- 查询所有会员的开单项目数和耗卡项目数
  3 +-- ============================================
  4 +-- 说明:统计每个会员在指定时间范围内的开单项目总数和耗卡项目总数
  5 +
  6 +-- ============================================
  7 +-- 版本1:基础查询(不限制时间范围)
  8 +-- ============================================
  9 +SELECT
  10 + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id,
  11 + COALESCE(billing.billing_project_count, 0) as billing_project_count,
  12 + COALESCE(consume.consume_project_count, 0) as consume_project_count,
  13 + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count
  14 +FROM (
  15 + -- 开单项目数(按会员分组)
  16 + SELECT
  17 + px.F_MemberId,
  18 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count
  19 + FROM lq_kd_pxmx px
  20 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  21 + WHERE px.F_IsEffective = 1
  22 + AND kd.F_IsEffective = 1
  23 + AND px.F_MemberId IS NOT NULL
  24 + AND px.F_MemberId != ''
  25 + GROUP BY px.F_MemberId
  26 +) billing
  27 +FULL OUTER JOIN (
  28 + -- 耗卡项目数(按会员分组)
  29 + SELECT
  30 + xhpx.F_MemberId,
  31 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  32 + FROM lq_xh_pxmx xhpx
  33 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  34 + WHERE xhpx.F_IsEffective = 1
  35 + AND xh.F_IsEffective = 1
  36 + AND xhpx.F_MemberId IS NOT NULL
  37 + AND xhpx.F_MemberId != ''
  38 + GROUP BY xhpx.F_MemberId
  39 +) consume ON billing.F_MemberId = consume.F_MemberId
  40 +ORDER BY billing_project_count DESC;
  41 +
  42 +
  43 +-- ============================================
  44 +-- 版本2:带时间范围限制(推荐使用)
  45 +-- ============================================
  46 +-- 使用方法:修改下面的时间范围
  47 +SELECT
  48 + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id,
  49 + COALESCE(billing.billing_project_count, 0) as billing_project_count,
  50 + COALESCE(consume.consume_project_count, 0) as consume_project_count,
  51 + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count
  52 +FROM (
  53 + -- 开单项目数(按会员分组)
  54 + SELECT
  55 + px.F_MemberId,
  56 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count
  57 + FROM lq_kd_pxmx px
  58 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  59 + WHERE px.F_IsEffective = 1
  60 + AND kd.F_IsEffective = 1
  61 + AND px.F_MemberId IS NOT NULL
  62 + AND px.F_MemberId != ''
  63 + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  64 + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  65 + GROUP BY px.F_MemberId
  66 +) billing
  67 +FULL OUTER JOIN (
  68 + -- 耗卡项目数(按会员分组)
  69 + SELECT
  70 + xhpx.F_MemberId,
  71 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  72 + FROM lq_xh_pxmx xhpx
  73 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  74 + WHERE xhpx.F_IsEffective = 1
  75 + AND xh.F_IsEffective = 1
  76 + AND xhpx.F_MemberId IS NOT NULL
  77 + AND xhpx.F_MemberId != ''
  78 + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  79 + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  80 + GROUP BY xhpx.F_MemberId
  81 +) consume ON billing.F_MemberId = consume.F_MemberId
  82 +ORDER BY billing_project_count DESC;
  83 +
  84 +
  85 +-- ============================================
  86 +-- 版本3:兼容MySQL的写法(MySQL不支持FULL OUTER JOIN)
  87 +-- ============================================
  88 +SELECT
  89 + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id,
  90 + COALESCE(billing.billing_project_count, 0) as billing_project_count,
  91 + COALESCE(consume.consume_project_count, 0) as consume_project_count,
  92 + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count
  93 +FROM (
  94 + -- 开单项目数(按会员分组)
  95 + SELECT
  96 + px.F_MemberId,
  97 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count
  98 + FROM lq_kd_pxmx px
  99 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  100 + WHERE px.F_IsEffective = 1
  101 + AND kd.F_IsEffective = 1
  102 + AND px.F_MemberId IS NOT NULL
  103 + AND px.F_MemberId != ''
  104 + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  105 + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  106 + GROUP BY px.F_MemberId
  107 +) billing
  108 +LEFT JOIN (
  109 + -- 耗卡项目数(按会员分组)
  110 + SELECT
  111 + xhpx.F_MemberId,
  112 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  113 + FROM lq_xh_pxmx xhpx
  114 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  115 + WHERE xhpx.F_IsEffective = 1
  116 + AND xh.F_IsEffective = 1
  117 + AND xhpx.F_MemberId IS NOT NULL
  118 + AND xhpx.F_MemberId != ''
  119 + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  120 + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  121 + GROUP BY xhpx.F_MemberId
  122 +) consume ON billing.F_MemberId = consume.F_MemberId
  123 +
  124 +UNION
  125 +
  126 +SELECT
  127 + consume.F_MemberId as member_id,
  128 + 0 as billing_project_count,
  129 + consume.consume_project_count,
  130 + (0 - consume.consume_project_count) as remaining_project_count
  131 +FROM (
  132 + -- 耗卡项目数(按会员分组)
  133 + SELECT
  134 + xhpx.F_MemberId,
  135 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  136 + FROM lq_xh_pxmx xhpx
  137 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  138 + WHERE xhpx.F_IsEffective = 1
  139 + AND xh.F_IsEffective = 1
  140 + AND xhpx.F_MemberId IS NOT NULL
  141 + AND xhpx.F_MemberId != ''
  142 + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  143 + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  144 + GROUP BY xhpx.F_MemberId
  145 +) consume
  146 +LEFT JOIN (
  147 + -- 开单项目数(按会员分组)
  148 + SELECT
  149 + px.F_MemberId,
  150 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count
  151 + FROM lq_kd_pxmx px
  152 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  153 + WHERE px.F_IsEffective = 1
  154 + AND kd.F_IsEffective = 1
  155 + AND px.F_MemberId IS NOT NULL
  156 + AND px.F_MemberId != ''
  157 + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  158 + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  159 + GROUP BY px.F_MemberId
  160 +) billing ON consume.F_MemberId = billing.F_MemberId
  161 +WHERE billing.F_MemberId IS NULL
  162 +
  163 +ORDER BY billing_project_count DESC;
  164 +
  165 +
  166 +-- ============================================
  167 +-- 版本4:最简单版本(推荐,使用UNION ALL + GROUP BY实现FULL OUTER JOIN效果)
  168 +-- ============================================
  169 +SELECT
  170 + member_id,
  171 + SUM(billing_project_count) as billing_project_count,
  172 + SUM(consume_project_count) as consume_project_count,
  173 + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count
  174 +FROM (
  175 + -- 开单项目数
  176 + SELECT
  177 + px.F_MemberId as member_id,
  178 + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count,
  179 + 0 as consume_project_count
  180 + FROM lq_kd_pxmx px
  181 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  182 + WHERE px.F_IsEffective = 1
  183 + AND kd.F_IsEffective = 1
  184 + AND px.F_MemberId IS NOT NULL
  185 + AND px.F_MemberId != ''
  186 + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  187 + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  188 + GROUP BY px.F_MemberId
  189 +
  190 + UNION ALL
  191 +
  192 + -- 耗卡项目数
  193 + SELECT
  194 + xhpx.F_MemberId as member_id,
  195 + 0 as billing_project_count,
  196 + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count
  197 + FROM lq_xh_pxmx xhpx
  198 + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id
  199 + WHERE xhpx.F_IsEffective = 1
  200 + AND xh.F_IsEffective = 1
  201 + AND xhpx.F_MemberId IS NOT NULL
  202 + AND xhpx.F_MemberId != ''
  203 + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里)
  204 + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里)
  205 + GROUP BY xhpx.F_MemberId
  206 +) all_data
  207 +GROUP BY member_id
  208 +ORDER BY billing_project_count DESC;
  209 +
... ...
添加lq_hytk_mx表会员ID字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 为 lq_hytk_mx 表添加会员ID字段
  3 +-- ============================================
  4 +
  5 +-- 1. 添加会员ID字段
  6 +ALTER TABLE lq_hytk_mx
  7 +ADD COLUMN F_MemberId VARCHAR(50) NULL COMMENT '会员id' AFTER F_BillingItemId;
  8 +
  9 +-- 2. 创建索引以优化查询性能
  10 +CREATE INDEX idx_hytk_mx_member_id ON lq_hytk_mx(F_MemberId);
  11 +
  12 +-- 3. 根据退卡信息表填充现有数据的会员ID(通过 F_RefundInfoId 关联)
  13 +UPDATE lq_hytk_mx mx
  14 +INNER JOIN lq_hytk_hytk hytk ON mx.F_RefundInfoId = hytk.F_Id
  15 +SET mx.F_MemberId = hytk.hy
  16 +WHERE mx.F_MemberId IS NULL
  17 + AND hytk.hy IS NOT NULL
  18 + AND hytk.hy != '';
  19 +
  20 +-- 4. 根据开单品项明细表填充会员ID(如果退卡信息表中没有,则从开单品项明细表获取)
  21 +UPDATE lq_hytk_mx mx
  22 +INNER JOIN lq_kd_pxmx pxmx ON mx.F_BillingItemId = pxmx.F_Id
  23 +SET mx.F_MemberId = pxmx.F_MemberId
  24 +WHERE mx.F_MemberId IS NULL
  25 + AND pxmx.F_MemberId IS NOT NULL
  26 + AND pxmx.F_MemberId != '';
  27 +
... ...