Commit 28b8ebfb1de2cbf689c3ee9bd41ac7505da0ca97

Authored by “wangming”
1 parent 9ec80591

feat: 添加业绩表品项分类和品项ID字段,新增统计报表接口

- 为6个业绩表添加品项分类(F_ItemCategory)和品项ID(F_ItemId)字段
- 创建同步SQL脚本,从品项明细表同步数据到业绩表
- 新增线索池客户统计报表接口
- 新增门店统计报表接口
- 新增会员升单统计报表接口(前4单中是否有升医美、升科美、升生美)
Showing 18 changed files with 1238 additions and 7 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  4 +{
  5 + /// <summary>
  6 + /// 线索池客户统计报表输出
  7 + /// </summary>
  8 + public class LeadCustomerStatisticsListOutput
  9 + {
  10 + /// <summary>
  11 + /// 线索池客户(拓客编号)
  12 + /// </summary>
  13 + public string LeadCustomerId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 客户姓名
  17 + /// </summary>
  18 + public string CustomerName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 拓客时间
  22 + /// </summary>
  23 + public DateTime? ExpansionTime { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 是否邀约(是/否)
  27 + /// </summary>
  28 + public string HasInvite { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 是否预约(是/否)
  32 + /// </summary>
  33 + public string HasAppointment { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 是否有消耗(是/否)
  37 + /// </summary>
  38 + public string HasConsume { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 是否开单(是/否)
  42 + /// </summary>
  43 + public string HasBilling { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 未开单原因
  47 + /// </summary>
  48 + public string NoBillingReason { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 开卡金额
  52 + /// </summary>
  53 + public decimal BillingAmount { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 开卡卡项(多个卡项用顿号分隔)
  57 + /// </summary>
  58 + public string BillingItems { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 实际预约记录数(不管是否通过邀约产生)
  62 + /// </summary>
  63 + public int ActualAppointmentCount { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 实际消耗记录数(不管是否通过预约产生)
  67 + /// </summary>
  68 + public int ActualConsumeCount { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 实际开单记录数(不管是否通过预约产生)
  72 + /// </summary>
  73 + public int ActualBillingCount { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 问题分析说明
  77 + /// </summary>
  78 + public string Analysis { get; set; }
  79 + }
  80 +}
  81 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListQueryInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.ComponentModel.DataAnnotations;
  4 +
  5 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  6 +{
  7 + /// <summary>
  8 + /// 线索池客户统计报表查询输入
  9 + /// </summary>
  10 + public class LeadCustomerStatisticsListQueryInput
  11 + {
  12 + /// <summary>
  13 + /// 页码
  14 + /// </summary>
  15 + [Required]
  16 + public int PageIndex { get; set; } = 1;
  17 +
  18 + /// <summary>
  19 + /// 页大小
  20 + /// </summary>
  21 + [Required]
  22 + public int PageSize { get; set; } = 20;
  23 +
  24 + /// <summary>
  25 + /// 开始时间(拓客时间范围)
  26 + /// </summary>
  27 + public DateTime? StartTime { get; set; }
  28 +
  29 + /// <summary>
  30 + /// 结束时间(拓客时间范围)
  31 + /// </summary>
  32 + public DateTime? EndTime { get; set; }
  33 +
  34 + /// <summary>
  35 + /// 门店ID列表(可以多个门店)
  36 + /// </summary>
  37 + public List<string> StoreIds { get; set; }
  38 +
  39 + /// <summary>
  40 + /// 拓客活动ID
  41 + /// </summary>
  42 + public string EventId { get; set; }
  43 + }
  44 +}
  45 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  2 +{
  3 + /// <summary>
  4 + /// 会员升单统计输出
  5 + /// </summary>
  6 + public class MemberUpgradeStatisticsListOutput
  7 + {
  8 + /// <summary>
  9 + /// 会员ID
  10 + /// </summary>
  11 + public string MemberId { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 会员姓名
  15 + /// </summary>
  16 + public string MemberName { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 会员手机号
  20 + /// </summary>
  21 + public string MemberPhone { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 前4单中是否有升医美(是/否)
  25 + /// </summary>
  26 + public string HasUpgradeMedicalBeauty { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 前4单中是否有升科美(是/否)
  30 + /// </summary>
  31 + public string HasUpgradeTechBeauty { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 前4单中是否有升生美(是/否)
  35 + /// </summary>
  36 + public string HasUpgradeLifeBeauty { get; set; }
  37 + }
  38 +}
  39 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListQueryInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  5 +{
  6 + /// <summary>
  7 + /// 会员升单统计查询输入
  8 + /// </summary>
  9 + public class MemberUpgradeStatisticsListQueryInput
  10 + {
  11 + /// <summary>
  12 + /// 页码
  13 + /// </summary>
  14 + public int PageIndex { get; set; } = 1;
  15 +
  16 + /// <summary>
  17 + /// 每页数量
  18 + /// </summary>
  19 + public int PageSize { get; set; } = 20;
  20 +
  21 + /// <summary>
  22 + /// 会员ID列表(可选,不传则查询所有会员)
  23 + /// </summary>
  24 + public List<string> MemberIds { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 是否升医美(true-是,false-否,null-不筛选)
  28 + /// </summary>
  29 + public bool? HasUpgradeMedicalBeauty { get; set; }
  30 +
  31 + /// <summary>
  32 + /// 是否升科美(true-是,false-否,null-不筛选)
  33 + /// </summary>
  34 + public bool? HasUpgradeTechBeauty { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 是否升生美(true-是,false-否,null-不筛选)
  38 + /// </summary>
  39 + public bool? HasUpgradeLifeBeauty { get; set; }
  40 + }
  41 +}
  42 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  2 +{
  3 + /// <summary>
  4 + /// 门店统计报表输出
  5 + /// </summary>
  6 + public class StoreStatisticsListOutput
  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 TotalCount { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 拓客人数(去重的会员数)
  25 + /// </summary>
  26 + public int TkMemberCount { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 邀约数(通过拓客编号关联的邀约记录数)
  30 + /// </summary>
  31 + public int InviteCount { get; set; }
  32 +
  33 + /// <summary>
  34 + /// 预约数(通过邀约ID关联的预约记录数)
  35 + /// </summary>
  36 + public int AppointmentCount { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 耗卡数(通过预约ID关联的耗卡记录数)
  40 + /// </summary>
  41 + public int ConsumeCount { get; set; }
  42 +
  43 + /// <summary>
  44 + /// 开单数(通过预约ID关联的开单记录数)
  45 + /// </summary>
  46 + public int BillingCount { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 开单金额(通过预约ID关联的开单记录金额汇总)
  50 + /// </summary>
  51 + public decimal BillingAmount { get; set; }
  52 + }
  53 +}
  54 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListQueryInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.ComponentModel.DataAnnotations;
  4 +
  5 +namespace NCC.Extend.Entitys.Dto.LqStatistics
  6 +{
  7 + /// <summary>
  8 + /// 门店统计报表查询输入
  9 + /// </summary>
  10 + public class StoreStatisticsListQueryInput
  11 + {
  12 + /// <summary>
  13 + /// 开始时间(拓客时间范围)
  14 + /// </summary>
  15 + public DateTime? StartTime { get; set; }
  16 +
  17 + /// <summary>
  18 + /// 结束时间(拓客时间范围)
  19 + /// </summary>
  20 + public DateTime? EndTime { get; set; }
  21 +
  22 + /// <summary>
  23 + /// 门店ID列表(可以多个门店)
  24 + /// </summary>
  25 + public List<string> StoreIds { get; set; }
  26 +
  27 + /// <summary>
  28 + /// 拓客活动ID(可选,不传则查询所有活动)
  29 + /// </summary>
  30 + public string EventId { get; set; }
  31 + }
  32 +}
  33 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_jksyj/LqHytkJksyjEntity.cs
... ... @@ -113,5 +113,17 @@ namespace NCC.Extend.Entitys.lq_hytk_jksyj
113 113 /// </summary>
114 114 [SugarColumn(ColumnName = "F_IsEffective")]
115 115 public int IsEffective { get; set; } = StatusEnum.有效.GetHashCode();
  116 +
  117 + /// <summary>
  118 + /// 品项分类
  119 + /// </summary>
  120 + [SugarColumn(ColumnName = "F_ItemCategory")]
  121 + public string ItemCategory { get; set; }
  122 +
  123 + /// <summary>
  124 + /// 品项ID
  125 + /// </summary>
  126 + [SugarColumn(ColumnName = "F_ItemId")]
  127 + public string ItemId { get; set; }
116 128 }
117 129 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_kjbsyj/LqHytkKjbsyjEntity.cs
... ... @@ -108,5 +108,17 @@ namespace NCC.Extend.Entitys.lq_hytk_kjbsyj
108 108 /// </summary>
109 109 [SugarColumn(ColumnName = "F_IsEffective")]
110 110 public int IsEffective { get; set; } = StatusEnum.有效.GetHashCode();
  111 +
  112 + /// <summary>
  113 + /// 品项分类
  114 + /// </summary>
  115 + [SugarColumn(ColumnName = "F_ItemCategory")]
  116 + public string ItemCategory { get; set; }
  117 +
  118 + /// <summary>
  119 + /// 品项ID
  120 + /// </summary>
  121 + [SugarColumn(ColumnName = "F_ItemId")]
  122 + public string ItemId { get; set; }
111 123 }
112 124 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_jksyj/LqKdJksyjEntity.cs
... ... @@ -76,5 +76,17 @@ namespace NCC.Extend.Entitys.lq_kd_jksyj
76 76 /// </summary>
77 77 [SugarColumn(ColumnName = "F_ActivityId")]
78 78 public string ActivityId { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 品项分类
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_ItemCategory")]
  84 + public string ItemCategory { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 品项ID
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_ItemId")]
  90 + public string ItemId { get; set; }
79 91 }
80 92 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_kjbsyj/LqKdKjbsyjEntity.cs
... ... @@ -76,5 +76,17 @@ namespace NCC.Extend.Entitys.lq_kd_kjbsyj
76 76 /// </summary>
77 77 [SugarColumn(ColumnName = "F_ActivityId")]
78 78 public string ActivityId { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 品项分类
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_ItemCategory")]
  84 + public string ItemCategory { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 品项ID
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_ItemId")]
  90 + public string ItemId { get; set; }
79 91 }
80 92 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_jksyj/LqXhJksyjEntity.cs
... ... @@ -124,5 +124,17 @@ namespace NCC.Extend.Entitys.lq_xh_jksyj
124 124 /// </summary>
125 125 [SugarColumn(ColumnName = "F_AccompaniedProjectNumber")]
126 126 public decimal? AccompaniedProjectNumber { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 品项分类
  130 + /// </summary>
  131 + [SugarColumn(ColumnName = "F_ItemCategory")]
  132 + public string ItemCategory { get; set; }
  133 +
  134 + /// <summary>
  135 + /// 品项ID
  136 + /// </summary>
  137 + [SugarColumn(ColumnName = "F_ItemId")]
  138 + public string ItemId { get; set; }
127 139 }
128 140 }
129 141 \ No newline at end of file
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_kjbsyj/LqXhKjbsyjEntity.cs
... ... @@ -106,5 +106,17 @@ namespace NCC.Extend.Entitys.lq_xh_kjbsyj
106 106 /// </summary>
107 107 [SugarColumn(ColumnName = "F_IsEffective")]
108 108 public int? IsEffective { get; set; } = 1;
  109 +
  110 + /// <summary>
  111 + /// 品项分类
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_ItemCategory")]
  114 + public string ItemCategory { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 品项ID
  118 + /// </summary>
  119 + [SugarColumn(ColumnName = "F_ItemId")]
  120 + public string ItemId { get; set; }
109 121 }
110 122 }
111 123 \ No newline at end of file
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -446,11 +446,9 @@ namespace NCC.Extend
446 446 }
447 447  
448 448 // 查询该批次的所有使用记录
449   - var usageRecords = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>(
450   - (u, product) => u.ProductId == product.Id)
451   - .LeftJoin<LqMdxxEntity>((u, product, store) => u.StoreId == store.Id)
452   - .Where((u, product, store) => u.UsageBatchId == batchId)
453   - .Select((u, product, store) => new LqInventoryUsageListOutput
  449 + var usageRecords = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>((u, product) => u.ProductId == product.Id)
  450 + .Where((u, product) => u.UsageBatchId == batchId)
  451 + .Select((u, product) => new LqInventoryUsageListOutput
454 452 {
455 453 id = u.Id,
456 454 productId = u.ProductId,
... ... @@ -458,7 +456,7 @@ namespace NCC.Extend
458 456 productCategory = product.ProductCategory,
459 457 productPrice = product.Price,
460 458 storeId = u.StoreId,
461   - storeName = store.Dm,
  459 + storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(store => store.Id == u.StoreId).Select(store => store.Dm),
462 460 usageTime = u.UsageTime,
463 461 usageQuantity = u.UsageQuantity,
464 462 relatedConsumeId = u.RelatedConsumeId,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -1595,7 +1595,7 @@ namespace NCC.Extend.LqKhxx
1595 1595 }
1596 1596  
1597 1597 /// <summary>
1598   - /// 批量更新所有会员信息(高性能版:使用SQL批量更新)
  1598 + /// 批量更新所有会员信息(高性能版:使用SQL批量更新)【通过定时任务去执行,每天晚上执行一次】
1599 1599 /// </summary>
1600 1600 /// <returns></returns>
1601 1601 [HttpPost("BatchUpdateMemberInfo")]
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -53,6 +53,9 @@ using SqlSugar;
53 53 using Yitter.IdGenerator;
54 54 using NCC.Extend.Entitys.lq_kd_pxmx;
55 55 using NCC.Extend.Entitys.lq_khxx;
  56 +using NCC.Extend.Entitys.lq_tkjlb;
  57 +using NCC.Extend.Entitys.lq_yaoyjl;
  58 +using NCC.Extend.Entitys.lq_yyjl;
56 59  
57 60 namespace NCC.Extend.LqStatistics
58 61 {
... ... @@ -3949,6 +3952,754 @@ namespace NCC.Extend.LqStatistics
3949 3952 }
3950 3953 #endregion
3951 3954  
  3955 + #region 线索池客户统计报表
  3956 + /// <summary>
  3957 + /// 获取线索池客户统计报表
  3958 + /// </summary>
  3959 + /// <remarks>
  3960 + /// 根据拓客记录统计线索池客户的邀约、预约、消耗、开单等信息
  3961 + ///
  3962 + /// 业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗
  3963 + ///
  3964 + /// 示例请求:
  3965 + /// ```json
  3966 + /// {
  3967 + /// "pageIndex": 1,
  3968 + /// "pageSize": 20,
  3969 + /// "startTime": "2025-10-01T00:00:00",
  3970 + /// "endTime": "2025-10-31T23:59:59",
  3971 + /// "storeIds": ["store1", "store2"],
  3972 + /// "eventId": "event123"
  3973 + /// }
  3974 + /// ```
  3975 + ///
  3976 + /// 参数说明:
  3977 + /// - pageIndex: 页码,从1开始
  3978 + /// - pageSize: 每页数量
  3979 + /// - startTime: 拓客时间范围开始时间
  3980 + /// - endTime: 拓客时间范围结束时间
  3981 + /// - storeIds: 门店ID列表,可传多个
  3982 + /// - eventId: 拓客活动ID
  3983 + ///
  3984 + /// 返回数据说明:
  3985 + /// - LeadCustomerId: 线索池客户(拓客编号)
  3986 + /// - CustomerName: 客户姓名
  3987 + /// - ExpansionTime: 拓客时间
  3988 + /// - HasInvite: 是否邀约(是/否),通过拓客编号关联邀约表
  3989 + /// - HasAppointment: 是否预约(是/否),只统计通过邀约产生的预约(预约表的F_InviteId关联邀约表)
  3990 + /// - HasConsume: 是否有消耗(是/否),只统计通过预约产生的耗卡(耗卡表的F_AppointmentId关联预约表)
  3991 + /// - HasBilling: 是否开单(是/否),只统计通过预约产生的开单(开单表的F_AppointmentId关联预约表)
  3992 + /// - NoBillingReason: 未开单原因,从预约记录的F_NoDealRemark字段获取
  3993 + /// - BillingAmount: 开卡金额,汇总通过预约产生的开单记录的整单业绩(zdyj)
  3994 + /// - BillingItems: 开卡卡项,汇总通过预约产生的开单品项名称,多个用顿号分隔
  3995 + /// - ActualAppointmentCount: 实际预约记录数(不管是否通过邀约产生),用于问题分析
  3996 + /// - ActualConsumeCount: 实际消耗记录数(不管是否通过预约产生),用于问题分析
  3997 + /// - ActualBillingCount: 实际开单记录数(不管是否通过预约产生),用于问题分析
  3998 + /// - Analysis: 问题分析说明,自动分析数据异常情况,如:有预约记录但未通过邀约产生、有消耗记录但未通过预约产生等
  3999 + ///
  4000 + /// 返回示例:
  4001 + /// ```json
  4002 + /// {
  4003 + /// "list": [
  4004 + /// {
  4005 + /// "LeadCustomerId": "751248448816153862",
  4006 + /// "CustomerName": "王女士",
  4007 + /// "ExpansionTime": "2025-10-24T03:33:10.000Z",
  4008 + /// "HasInvite": "否",
  4009 + /// "HasAppointment": "否",
  4010 + /// "HasConsume": "否",
  4011 + /// "HasBilling": "否",
  4012 + /// "NoBillingReason": null,
  4013 + /// "BillingAmount": 0,
  4014 + /// "BillingItems": null,
  4015 + /// "ActualAppointmentCount": 3,
  4016 + /// "ActualConsumeCount": 4,
  4017 + /// "ActualBillingCount": 5,
  4018 + /// "Analysis": "有3条预约记录,但未通过邀约产生(F_InviteId为null);有4条消耗记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生);有5条开单记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)"
  4019 + /// }
  4020 + /// ],
  4021 + /// "pagination": {
  4022 + /// "pageIndex": 1,
  4023 + /// "pageSize": 20,
  4024 + /// "total": 1511
  4025 + /// }
  4026 + /// }
  4027 + /// ```
  4028 + /// </remarks>
  4029 + /// <param name="input">查询条件</param>
  4030 + /// <returns>线索池客户统计报表列表,包含统计数据和问题分析</returns>
  4031 + /// <response code="200">查询成功,返回统计报表列表和分页信息</response>
  4032 + /// <response code="400">参数错误</response>
  4033 + /// <response code="500">服务器内部错误</response>
  4034 + [HttpPost("get-lead-customer-statistics-list")]
  4035 + public async Task<dynamic> GetLeadCustomerStatisticsList([FromBody] LeadCustomerStatisticsListQueryInput input)
  4036 + {
  4037 + try
  4038 + {
  4039 + // 构建WHERE条件
  4040 + var whereConditions = new List<string>();
  4041 + var parameters = new List<SugarParameter>();
  4042 +
  4043 + if (input.StartTime.HasValue)
  4044 + {
  4045 + whereConditions.Add("tk.F_ExpansionTime >= @StartTime");
  4046 + parameters.Add(new SugarParameter("@StartTime", input.StartTime.Value));
  4047 + }
  4048 +
  4049 + if (input.EndTime.HasValue)
  4050 + {
  4051 + whereConditions.Add("tk.F_ExpansionTime <= @EndTime");
  4052 + parameters.Add(new SugarParameter("@EndTime", input.EndTime.Value));
  4053 + }
  4054 +
  4055 + if (input.StoreIds != null && input.StoreIds.Any())
  4056 + {
  4057 + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}"));
  4058 + whereConditions.Add($"tk.F_StoreId IN ({storeIdParams})");
  4059 + for (int i = 0; i < input.StoreIds.Count; i++)
  4060 + {
  4061 + parameters.Add(new SugarParameter($"@StoreId{i}", input.StoreIds[i]));
  4062 + }
  4063 + }
  4064 +
  4065 + if (!string.IsNullOrEmpty(input.EventId))
  4066 + {
  4067 + whereConditions.Add("tk.F_EventId = @EventId");
  4068 + parameters.Add(new SugarParameter("@EventId", input.EventId));
  4069 + }
  4070 +
  4071 + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : "";
  4072 +
  4073 + // 使用子查询优化性能,避免复杂的JOIN和GROUP BY
  4074 + var sql = $@"
  4075 + SELECT
  4076 + tk.F_Id as LeadCustomerId,
  4077 + tk.F_CustomerName as CustomerName,
  4078 + tk.F_ExpansionTime as ExpansionTime,
  4079 + -- 是否邀约:通过拓客编号关联
  4080 + CASE WHEN yaoy_stats.has_invite = 1 THEN '是' ELSE '否' END as HasInvite,
  4081 + -- 是否预约:通过邀约ID关联(只统计通过邀约产生的预约)
  4082 + CASE WHEN yy_stats.has_appointment = 1 THEN '是' ELSE '否' END as HasAppointment,
  4083 + -- 是否有消耗:通过预约ID关联(只统计通过预约产生的耗卡)
  4084 + CASE WHEN xh_stats.has_consume = 1 THEN '是' ELSE '否' END as HasConsume,
  4085 + -- 是否开单:通过预约ID关联(只统计通过预约产生的开单)
  4086 + CASE WHEN kd_stats.has_billing = 1 THEN '是' ELSE '否' END as HasBilling,
  4087 + -- 未开单原因:从预约记录中获取(只取通过邀约产生的预约)
  4088 + yy_stats.no_billing_reason as NoBillingReason,
  4089 + -- 开卡金额:汇总通过预约产生的开单记录
  4090 + COALESCE(kd_stats.billing_amount, 0) as BillingAmount,
  4091 + -- 开卡卡项:汇总通过预约产生的开单品项
  4092 + kd_stats.billing_items as BillingItems,
  4093 + -- 实际预约记录数(不管是否通过邀约产生)
  4094 + COALESCE(yy_actual.count, 0) as ActualAppointmentCount,
  4095 + -- 实际消耗记录数(不管是否通过预约产生)
  4096 + COALESCE(xh_actual.count, 0) as ActualConsumeCount,
  4097 + -- 实际开单记录数(不管是否通过预约产生)
  4098 + COALESCE(kd_actual.count, 0) as ActualBillingCount
  4099 + FROM lq_tkjlb tk
  4100 + -- 邀约统计子查询
  4101 + LEFT JOIN (
  4102 + SELECT
  4103 + yaoy.tkbh as tk_id,
  4104 + 1 as has_invite
  4105 + FROM lq_yaoyjl yaoy
  4106 + GROUP BY yaoy.tkbh
  4107 + ) yaoy_stats ON yaoy_stats.tk_id = tk.F_Id
  4108 + -- 预约统计子查询(只统计通过邀约产生的预约)
  4109 + LEFT JOIN (
  4110 + SELECT
  4111 + tk_inner.F_MemberId as member_id,
  4112 + 1 as has_appointment,
  4113 + MAX(yy.F_NoDealRemark) as no_billing_reason
  4114 + FROM lq_tkjlb tk_inner
  4115 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id
  4116 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4117 + GROUP BY tk_inner.F_MemberId
  4118 + ) yy_stats ON yy_stats.member_id = tk.F_MemberId
  4119 + -- 消耗统计子查询(只统计通过预约产生的耗卡)
  4120 + LEFT JOIN (
  4121 + SELECT
  4122 + tk_inner.F_MemberId as member_id,
  4123 + 1 as has_consume
  4124 + FROM lq_tkjlb tk_inner
  4125 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id
  4126 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4127 + INNER JOIN lq_xh_hyhk xh ON xh.F_AppointmentId = yy.F_Id AND xh.F_IsEffective = 1
  4128 + GROUP BY tk_inner.F_MemberId
  4129 + ) xh_stats ON xh_stats.member_id = tk.F_MemberId
  4130 + -- 开单统计子查询(只统计通过预约产生的开单,包含金额和品项)
  4131 + LEFT JOIN (
  4132 + SELECT
  4133 + tk_inner.F_MemberId as member_id,
  4134 + 1 as has_billing,
  4135 + SUM(kd.zdyj) as billing_amount,
  4136 + GROUP_CONCAT(DISTINCT kdpx.pxmc SEPARATOR '、') as billing_items
  4137 + FROM lq_tkjlb tk_inner
  4138 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id
  4139 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4140 + INNER JOIN lq_kd_kdjlb kd ON kd.F_AppointmentId = yy.F_Id AND kd.F_IsEffective = 1
  4141 + LEFT JOIN lq_kd_pxmx kdpx ON kdpx.glkdbh = kd.F_Id AND kdpx.F_IsEffective = 1
  4142 + GROUP BY tk_inner.F_MemberId
  4143 + ) kd_stats ON kd_stats.member_id = tk.F_MemberId
  4144 + -- 实际预约记录数统计(不管是否通过邀约产生)
  4145 + LEFT JOIN (
  4146 + SELECT
  4147 + yy.gk as member_id,
  4148 + COUNT(*) as count
  4149 + FROM lq_yyjl yy
  4150 + GROUP BY yy.gk
  4151 + ) yy_actual ON yy_actual.member_id = tk.F_MemberId
  4152 + -- 实际消耗记录数统计(不管是否通过预约产生)
  4153 + LEFT JOIN (
  4154 + SELECT
  4155 + xh.hy as member_id,
  4156 + COUNT(*) as count
  4157 + FROM lq_xh_hyhk xh
  4158 + WHERE xh.F_IsEffective = 1
  4159 + GROUP BY xh.hy
  4160 + ) xh_actual ON xh_actual.member_id = tk.F_MemberId
  4161 + -- 实际开单记录数统计(不管是否通过预约产生)
  4162 + LEFT JOIN (
  4163 + SELECT
  4164 + kd.kdhy as member_id,
  4165 + COUNT(*) as count
  4166 + FROM lq_kd_kdjlb kd
  4167 + WHERE kd.F_IsEffective = 1
  4168 + GROUP BY kd.kdhy
  4169 + ) kd_actual ON kd_actual.member_id = tk.F_MemberId
  4170 + {whereClause}
  4171 + ORDER BY tk.F_ExpansionTime DESC
  4172 + LIMIT @PageSize OFFSET @Offset";
  4173 +
  4174 + parameters.Add(new SugarParameter("@PageSize", input.PageSize));
  4175 + parameters.Add(new SugarParameter("@Offset", (input.PageIndex - 1) * input.PageSize));
  4176 +
  4177 + // 查询总数
  4178 + var countSql = $@"
  4179 + SELECT COUNT(*)
  4180 + FROM lq_tkjlb tk
  4181 + {whereClause}";
  4182 +
  4183 + var countParameters = parameters.Where(p => p.ParameterName != "@PageSize" && p.ParameterName != "@Offset").ToList();
  4184 + var totalCount = await _db.Ado.GetIntAsync(countSql, countParameters);
  4185 +
  4186 + // 执行查询
  4187 + var result = await _db.Ado.SqlQueryAsync<LeadCustomerStatisticsListOutput>(sql, parameters);
  4188 +
  4189 + // 生成问题分析说明
  4190 + foreach (var item in result)
  4191 + {
  4192 + var analysisList = new List<string>();
  4193 +
  4194 + if (item.HasInvite == "否" && item.HasAppointment == "否" && item.ActualAppointmentCount > 0)
  4195 + {
  4196 + analysisList.Add($"有{item.ActualAppointmentCount}条预约记录,但未通过邀约产生(F_InviteId为null)");
  4197 + }
  4198 +
  4199 + if (item.HasAppointment == "否" && item.HasConsume == "否" && item.ActualConsumeCount > 0)
  4200 + {
  4201 + analysisList.Add($"有{item.ActualConsumeCount}条消耗记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)");
  4202 + }
  4203 +
  4204 + if (item.HasAppointment == "否" && item.HasBilling == "否" && item.ActualBillingCount > 0)
  4205 + {
  4206 + analysisList.Add($"有{item.ActualBillingCount}条开单记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)");
  4207 + }
  4208 +
  4209 + if (item.HasInvite == "是" && item.HasAppointment == "否" && item.ActualAppointmentCount > 0)
  4210 + {
  4211 + analysisList.Add($"有邀约记录,有{item.ActualAppointmentCount}条预约记录,但预约记录的F_InviteId未关联到邀约记录");
  4212 + }
  4213 +
  4214 + if (item.HasAppointment == "是" && item.HasConsume == "否" && item.ActualConsumeCount > 0)
  4215 + {
  4216 + analysisList.Add($"有预约记录,有{item.ActualConsumeCount}条消耗记录,但消耗记录的F_AppointmentId未关联到预约记录");
  4217 + }
  4218 +
  4219 + if (item.HasAppointment == "是" && item.HasBilling == "否" && item.ActualBillingCount > 0)
  4220 + {
  4221 + analysisList.Add($"有预约记录,有{item.ActualBillingCount}条开单记录,但开单记录的F_AppointmentId未关联到预约记录");
  4222 + }
  4223 +
  4224 + if (analysisList.Count == 0)
  4225 + {
  4226 + item.Analysis = "数据正常,符合业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗";
  4227 + }
  4228 + else
  4229 + {
  4230 + item.Analysis = string.Join(";", analysisList);
  4231 + }
  4232 + }
  4233 +
  4234 + return new
  4235 + {
  4236 + list = result,
  4237 + pagination = new
  4238 + {
  4239 + pageIndex = input.PageIndex,
  4240 + pageSize = input.PageSize,
  4241 + total = totalCount
  4242 + }
  4243 + };
  4244 + }
  4245 + catch (Exception ex)
  4246 + {
  4247 + _logger.LogError(ex, "获取线索池客户统计报表失败");
  4248 + throw NCCException.Oh($"获取线索池客户统计报表失败:{ex.Message}");
  4249 + }
  4250 + }
  4251 + #endregion
  4252 +
  4253 + #region 门店统计报表
  4254 + /// <summary>
  4255 + /// 获取门店统计报表
  4256 + /// </summary>
  4257 + /// <remarks>
  4258 + /// 按门店统计拓客、邀约、预约、消耗、开单等数据
  4259 + ///
  4260 + /// 业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗
  4261 + ///
  4262 + /// 示例请求:
  4263 + /// ```json
  4264 + /// {
  4265 + /// "startTime": "2025-10-01T00:00:00",
  4266 + /// "endTime": "2025-10-31T23:59:59",
  4267 + /// "storeIds": ["store1", "store2"],
  4268 + /// "eventId": "event123"
  4269 + /// }
  4270 + /// ```
  4271 + ///
  4272 + /// 参数说明:
  4273 + /// - startTime: 拓客时间范围开始时间
  4274 + /// - endTime: 拓客时间范围结束时间
  4275 + /// - storeIds: 门店ID列表,可传多个
  4276 + /// - eventId: 拓客活动ID
  4277 + ///
  4278 + /// 返回数据说明:
  4279 + /// - StoreId: 门店ID
  4280 + /// - StoreName: 门店名称
  4281 + /// - TotalCount: 总人数(从客户信息表按归属门店统计)
  4282 + /// - TkMemberCount: 拓客人数(拓客记录数,不去重)
  4283 + /// - InviteCount: 邀约数(通过拓客编号关联的邀约记录数)
  4284 + /// - AppointmentCount: 预约数(通过邀约ID关联的预约记录数,只统计通过邀约产生的预约)
  4285 + /// - ConsumeCount: 耗卡数(通过预约ID关联的耗卡记录数,只统计通过预约产生的耗卡)
  4286 + /// - BillingCount: 开单数(通过预约ID关联的开单记录数,只统计通过预约产生的开单)
  4287 + /// - BillingAmount: 开单金额(通过预约ID关联的开单记录金额汇总)
  4288 + ///
  4289 + /// 返回示例:
  4290 + /// ```json
  4291 + /// {
  4292 + /// "list": [
  4293 + /// {
  4294 + /// "StoreId": "1649328471923847169",
  4295 + /// "StoreName": "绿纤紫荆店",
  4296 + /// "TotalCount": 119,
  4297 + /// "TkMemberCount": 117,
  4298 + /// "InviteCount": 4,
  4299 + /// "AppointmentCount": 2,
  4300 + /// "ConsumeCount": 1,
  4301 + /// "BillingCount": 1,
  4302 + /// "BillingAmount": 199.00
  4303 + /// }
  4304 + /// ]
  4305 + /// }
  4306 + /// ```
  4307 + /// </remarks>
  4308 + /// <param name="input">查询条件</param>
  4309 + /// <returns>门店统计报表列表</returns>
  4310 + /// <response code="200">查询成功,返回门店统计报表列表</response>
  4311 + /// <response code="400">参数错误</response>
  4312 + /// <response code="500">服务器内部错误</response>
  4313 + [HttpPost("get-store-statistics-list")]
  4314 + public async Task<dynamic> GetStoreStatisticsList([FromBody] StoreStatisticsListQueryInput input)
  4315 + {
  4316 + try
  4317 + {
  4318 + // 构建WHERE条件(带表别名,用于子查询)
  4319 + var whereConditions = new List<string>();
  4320 + // 构建WHERE条件(不带表别名,用于UNION的SELECT)
  4321 + var whereConditionsNoAlias = new List<string>();
  4322 + var parameters = new List<SugarParameter>();
  4323 +
  4324 + if (input.StartTime.HasValue)
  4325 + {
  4326 + whereConditions.Add("tk.F_ExpansionTime >= @StartTime");
  4327 + whereConditionsNoAlias.Add("F_ExpansionTime >= @StartTime");
  4328 + parameters.Add(new SugarParameter("@StartTime", input.StartTime.Value));
  4329 + }
  4330 +
  4331 + if (input.EndTime.HasValue)
  4332 + {
  4333 + whereConditions.Add("tk.F_ExpansionTime <= @EndTime");
  4334 + whereConditionsNoAlias.Add("F_ExpansionTime <= @EndTime");
  4335 + parameters.Add(new SugarParameter("@EndTime", input.EndTime.Value));
  4336 + }
  4337 +
  4338 + if (input.StoreIds != null && input.StoreIds.Any())
  4339 + {
  4340 + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}"));
  4341 + whereConditions.Add($"tk.F_StoreId IN ({storeIdParams})");
  4342 + whereConditionsNoAlias.Add($"F_StoreId IN ({storeIdParams})");
  4343 + for (int i = 0; i < input.StoreIds.Count; i++)
  4344 + {
  4345 + parameters.Add(new SugarParameter($"@StoreId{i}", input.StoreIds[i]));
  4346 + }
  4347 + }
  4348 +
  4349 + if (!string.IsNullOrEmpty(input.EventId))
  4350 + {
  4351 + whereConditions.Add("tk.F_EventId = @EventId");
  4352 + whereConditionsNoAlias.Add("F_EventId = @EventId");
  4353 + parameters.Add(new SugarParameter("@EventId", input.EventId));
  4354 + }
  4355 +
  4356 + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : "";
  4357 + var whereClauseNoAlias = whereConditionsNoAlias.Any() ? "WHERE " + string.Join(" AND ", whereConditionsNoAlias) : "";
  4358 +
  4359 + // 构建门店筛选条件(用于客户信息表查询)
  4360 + var khWhereConditions = new List<string>();
  4361 + var khWhereConditionsNoAlias = new List<string>();
  4362 + if (input.StoreIds != null && input.StoreIds.Any())
  4363 + {
  4364 + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}"));
  4365 + khWhereConditions.Add($"kh.gsmd IN ({storeIdParams})");
  4366 + khWhereConditionsNoAlias.Add($"gsmd IN ({storeIdParams})");
  4367 + }
  4368 +
  4369 + var khWhereClause = khWhereConditions.Any() ? "WHERE " + string.Join(" AND ", khWhereConditions) : "WHERE kh.gsmd IS NOT NULL";
  4370 + var khWhereClauseNoAlias = khWhereConditionsNoAlias.Any() ? "WHERE " + string.Join(" AND ", khWhereConditionsNoAlias) : "WHERE gsmd IS NOT NULL";
  4371 +
  4372 + // 使用子查询优化性能,避免复杂的JOIN
  4373 + var sql = $@"
  4374 + SELECT
  4375 + COALESCE(total_stats.StoreId, tk_stats.StoreId, yaoy_stats.StoreId, yy_stats.StoreId, xh_stats.StoreId, kd_stats.StoreId) as StoreId,
  4376 + COALESCE(md.dm, '') as StoreName,
  4377 + COALESCE(total_stats.TotalCount, 0) as TotalCount,
  4378 + COALESCE(tk_stats.TkMemberCount, 0) as TkMemberCount,
  4379 + COALESCE(yaoy_stats.InviteCount, 0) as InviteCount,
  4380 + COALESCE(yy_stats.AppointmentCount, 0) as AppointmentCount,
  4381 + COALESCE(xh_stats.ConsumeCount, 0) as ConsumeCount,
  4382 + COALESCE(kd_stats.BillingCount, 0) as BillingCount,
  4383 + COALESCE(kd_stats.BillingAmount, 0) as BillingAmount
  4384 + FROM (
  4385 + SELECT DISTINCT StoreId FROM (
  4386 + SELECT gsmd as StoreId FROM lq_khxx {khWhereClauseNoAlias}
  4387 + UNION
  4388 + SELECT F_StoreId as StoreId FROM lq_tkjlb {whereClauseNoAlias}
  4389 + ) as all_stores
  4390 + ) as stores
  4391 + LEFT JOIN lq_mdxx md ON md.F_Id = stores.StoreId
  4392 + -- 总人数统计(从客户信息表按归属门店统计)
  4393 + LEFT JOIN (
  4394 + SELECT
  4395 + kh.gsmd as StoreId,
  4396 + COUNT(*) as TotalCount
  4397 + FROM lq_khxx kh
  4398 + {khWhereClause}
  4399 + GROUP BY kh.gsmd
  4400 + ) total_stats ON total_stats.StoreId = stores.StoreId
  4401 + -- 拓客人数统计(不用去重)
  4402 + LEFT JOIN (
  4403 + SELECT
  4404 + tk.F_StoreId as StoreId,
  4405 + COUNT(tk.F_MemberId) as TkMemberCount
  4406 + FROM lq_tkjlb tk
  4407 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)}
  4408 + GROUP BY tk.F_StoreId
  4409 + ) tk_stats ON tk_stats.StoreId = stores.StoreId
  4410 + -- 邀约数统计
  4411 + LEFT JOIN (
  4412 + SELECT
  4413 + tk.F_StoreId as StoreId,
  4414 + COUNT(DISTINCT yaoy.F_Id) as InviteCount
  4415 + FROM lq_tkjlb tk
  4416 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id
  4417 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)}
  4418 + GROUP BY tk.F_StoreId
  4419 + ) yaoy_stats ON yaoy_stats.StoreId = stores.StoreId
  4420 + -- 预约数统计(通过邀约ID关联)
  4421 + LEFT JOIN (
  4422 + SELECT
  4423 + tk.F_StoreId as StoreId,
  4424 + COUNT(DISTINCT yy.F_Id) as AppointmentCount
  4425 + FROM lq_tkjlb tk
  4426 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id
  4427 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4428 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)}
  4429 + GROUP BY tk.F_StoreId
  4430 + ) yy_stats ON yy_stats.StoreId = stores.StoreId
  4431 + -- 耗卡数统计(通过预约ID关联)
  4432 + LEFT JOIN (
  4433 + SELECT
  4434 + tk.F_StoreId as StoreId,
  4435 + COUNT(DISTINCT xh.F_Id) as ConsumeCount
  4436 + FROM lq_tkjlb tk
  4437 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id
  4438 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4439 + INNER JOIN lq_xh_hyhk xh ON xh.F_AppointmentId = yy.F_Id AND xh.F_IsEffective = 1
  4440 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)}
  4441 + GROUP BY tk.F_StoreId
  4442 + ) xh_stats ON xh_stats.StoreId = stores.StoreId
  4443 + -- 开单数和开单金额统计(通过预约ID关联)
  4444 + LEFT JOIN (
  4445 + SELECT
  4446 + tk.F_StoreId as StoreId,
  4447 + COUNT(DISTINCT kd.F_Id) as BillingCount,
  4448 + SUM(kd.zdyj) as BillingAmount
  4449 + FROM lq_tkjlb tk
  4450 + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id
  4451 + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id
  4452 + INNER JOIN lq_kd_kdjlb kd ON kd.F_AppointmentId = yy.F_Id AND kd.F_IsEffective = 1
  4453 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)}
  4454 + GROUP BY tk.F_StoreId
  4455 + ) kd_stats ON kd_stats.StoreId = stores.StoreId
  4456 + WHERE stores.StoreId IS NOT NULL
  4457 + ORDER BY stores.StoreId";
  4458 +
  4459 + // 执行查询
  4460 + var result = await _db.Ado.SqlQueryAsync<StoreStatisticsListOutput>(sql, parameters);
  4461 +
  4462 + return new
  4463 + {
  4464 + list = result
  4465 + };
  4466 + }
  4467 + catch (Exception ex)
  4468 + {
  4469 + _logger.LogError(ex, "获取门店统计报表失败");
  4470 + throw NCCException.Oh($"获取门店统计报表失败:{ex.Message}");
  4471 + }
  4472 + }
  4473 + #endregion
  4474 +
  4475 + #region 会员升单统计
  4476 + /// <summary>
  4477 + /// 获取会员升单统计(前4单中是否有升医美、升科美、升生美)
  4478 + /// </summary>
  4479 + /// <remarks>
  4480 + /// 统计每个会员的前4单开单记录中是否有升医美、升科美、升生美
  4481 + ///
  4482 + /// 示例请求:
  4483 + /// ```json
  4484 + /// {
  4485 + /// "pageIndex": 1,
  4486 + /// "pageSize": 20,
  4487 + /// "memberIds": ["member1", "member2"],
  4488 + /// "hasUpgradeMedicalBeauty": true,
  4489 + /// "hasUpgradeTechBeauty": false,
  4490 + /// "hasUpgradeLifeBeauty": null
  4491 + /// }
  4492 + /// ```
  4493 + ///
  4494 + /// 参数说明:
  4495 + /// - pageIndex: 页码,从1开始
  4496 + /// - pageSize: 每页数量
  4497 + /// - memberIds: 会员ID列表(可选,不传则查询所有会员)
  4498 + /// - hasUpgradeMedicalBeauty: 是否升医美(true-是,false-否,null-不筛选)
  4499 + /// - hasUpgradeTechBeauty: 是否升科美(true-是,false-否,null-不筛选)
  4500 + /// - hasUpgradeLifeBeauty: 是否升生美(true-是,false-否,null-不筛选)
  4501 + ///
  4502 + /// 返回数据说明:
  4503 + /// - MemberId: 会员ID
  4504 + /// - MemberName: 会员姓名
  4505 + /// - MemberPhone: 会员手机号
  4506 + /// - HasUpgradeMedicalBeauty: 前4单中是否有升医美(是/否)
  4507 + /// - HasUpgradeTechBeauty: 前4单中是否有升科美(是/否)
  4508 + /// - HasUpgradeLifeBeauty: 前4单中是否有升生美(是/否)
  4509 + ///
  4510 + /// 返回示例:
  4511 + /// ```json
  4512 + /// {
  4513 + /// "list": [
  4514 + /// {
  4515 + /// "MemberId": "744326092097062149",
  4516 + /// "MemberName": "张女士",
  4517 + /// "MemberPhone": "13800138000",
  4518 + /// "HasUpgradeMedicalBeauty": "否",
  4519 + /// "HasUpgradeTechBeauty": "否",
  4520 + /// "HasUpgradeLifeBeauty": "否"
  4521 + /// }
  4522 + /// ],
  4523 + /// "pagination": {
  4524 + /// "pageIndex": 1,
  4525 + /// "pageSize": 20,
  4526 + /// "total": 100
  4527 + /// }
  4528 + /// }
  4529 + /// ```
  4530 + /// </remarks>
  4531 + /// <param name="input">查询条件</param>
  4532 + /// <returns>会员升单统计列表</returns>
  4533 + /// <response code="200">查询成功,返回会员升单统计列表</response>
  4534 + /// <response code="400">参数错误</response>
  4535 + /// <response code="500">服务器内部错误</response>
  4536 + [HttpPost("get-member-upgrade-statistics-list")]
  4537 + public async Task<dynamic> GetMemberUpgradeStatisticsList([FromBody] MemberUpgradeStatisticsListQueryInput input)
  4538 + {
  4539 + try
  4540 + {
  4541 + // 构建WHERE条件
  4542 + var whereConditions = new List<string>();
  4543 + var parameters = new List<SugarParameter>();
  4544 +
  4545 + if (input.MemberIds != null && input.MemberIds.Any())
  4546 + {
  4547 + var memberIdParams = string.Join(",", input.MemberIds.Select((_, i) => $"@MemberId{i}"));
  4548 + whereConditions.Add($"kd.kdhy IN ({memberIdParams})");
  4549 + for (int i = 0; i < input.MemberIds.Count; i++)
  4550 + {
  4551 + parameters.Add(new SugarParameter($"@MemberId{i}", input.MemberIds[i]));
  4552 + }
  4553 + }
  4554 +
  4555 + var whereClause = whereConditions.Any() ? "AND " + string.Join(" AND ", whereConditions) : "";
  4556 +
  4557 + // 构建HAVING条件(用于筛选升单条件)
  4558 + var havingConditions = new List<string>();
  4559 + if (input.HasUpgradeMedicalBeauty.HasValue)
  4560 + {
  4561 + if (input.HasUpgradeMedicalBeauty.Value)
  4562 + {
  4563 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1");
  4564 + }
  4565 + else
  4566 + {
  4567 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 0");
  4568 + }
  4569 + }
  4570 + if (input.HasUpgradeTechBeauty.HasValue)
  4571 + {
  4572 + if (input.HasUpgradeTechBeauty.Value)
  4573 + {
  4574 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1");
  4575 + }
  4576 + else
  4577 + {
  4578 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 0");
  4579 + }
  4580 + }
  4581 + if (input.HasUpgradeLifeBeauty.HasValue)
  4582 + {
  4583 + if (input.HasUpgradeLifeBeauty.Value)
  4584 + {
  4585 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1");
  4586 + }
  4587 + else
  4588 + {
  4589 + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 0");
  4590 + }
  4591 + }
  4592 +
  4593 + var havingClause = havingConditions.Any() ? "HAVING " + string.Join(" AND ", havingConditions) : "";
  4594 +
  4595 + // 分页参数
  4596 + var offset = (input.PageIndex - 1) * input.PageSize;
  4597 + parameters.Add(new SugarParameter("@PageSize", input.PageSize));
  4598 + parameters.Add(new SugarParameter("@Offset", offset));
  4599 +
  4600 + // 查询每个会员的前4单中是否有升医美、升科美、升生美
  4601 + var sql = $@"
  4602 + SELECT
  4603 + kd.kdhy as MemberId,
  4604 + kh.khmc as MemberName,
  4605 + kh.sjh as MemberPhone,
  4606 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeMedicalBeauty,
  4607 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeTechBeauty,
  4608 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeLifeBeauty
  4609 + FROM (
  4610 + SELECT
  4611 + kd.kdhy,
  4612 + kd.F_Id,
  4613 + kd.kdrq,
  4614 + kd.F_CreateTime,
  4615 + kd.F_UpgradeMedicalBeauty,
  4616 + kd.F_UpgradeTechBeauty,
  4617 + kd.F_UpgradeLifeBeauty
  4618 + FROM lq_kd_kdjlb kd
  4619 + WHERE kd.F_IsEffective = 1
  4620 + AND kd.kdhy IS NOT NULL
  4621 + {whereClause}
  4622 + AND (
  4623 + SELECT COUNT(*)
  4624 + FROM lq_kd_kdjlb kd2
  4625 + WHERE kd2.kdhy = kd.kdhy
  4626 + AND kd2.F_IsEffective = 1
  4627 + AND (
  4628 + kd2.kdrq > kd.kdrq
  4629 + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime > kd.F_CreateTime)
  4630 + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime = kd.F_CreateTime AND kd2.F_Id > kd.F_Id)
  4631 + )
  4632 + ) < 4
  4633 + ) kd
  4634 + LEFT JOIN lq_khxx kh ON kh.F_Id = kd.kdhy
  4635 + GROUP BY kd.kdhy, kh.khmc, kh.sjh
  4636 + {havingClause}
  4637 + ORDER BY kd.kdhy
  4638 + LIMIT @PageSize OFFSET @Offset";
  4639 +
  4640 + // 查询总数(需要应用相同的HAVING条件)
  4641 + var countSql = $@"
  4642 + SELECT COUNT(*)
  4643 + FROM (
  4644 + SELECT
  4645 + kd.kdhy,
  4646 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeMedicalBeauty,
  4647 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeTechBeauty,
  4648 + CASE WHEN MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeLifeBeauty
  4649 + FROM (
  4650 + SELECT
  4651 + kd.kdhy,
  4652 + kd.F_Id,
  4653 + kd.kdrq,
  4654 + kd.F_CreateTime,
  4655 + kd.F_UpgradeMedicalBeauty,
  4656 + kd.F_UpgradeTechBeauty,
  4657 + kd.F_UpgradeLifeBeauty
  4658 + FROM lq_kd_kdjlb kd
  4659 + WHERE kd.F_IsEffective = 1
  4660 + AND kd.kdhy IS NOT NULL
  4661 + {whereClause}
  4662 + AND (
  4663 + SELECT COUNT(*)
  4664 + FROM lq_kd_kdjlb kd2
  4665 + WHERE kd2.kdhy = kd.kdhy
  4666 + AND kd2.F_IsEffective = 1
  4667 + AND (
  4668 + kd2.kdrq > kd.kdrq
  4669 + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime > kd.F_CreateTime)
  4670 + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime = kd.F_CreateTime AND kd2.F_Id > kd.F_Id)
  4671 + )
  4672 + ) < 4
  4673 + ) kd
  4674 + GROUP BY kd.kdhy
  4675 + {havingClause}
  4676 + ) as filtered_results";
  4677 +
  4678 + var countParameters = parameters.Where(p => p.ParameterName != "@PageSize" && p.ParameterName != "@Offset").ToList();
  4679 + var totalCount = await _db.Ado.GetIntAsync(countSql, countParameters);
  4680 +
  4681 + // 执行查询
  4682 + var result = await _db.Ado.SqlQueryAsync<MemberUpgradeStatisticsListOutput>(sql, parameters);
  4683 +
  4684 + return new
  4685 + {
  4686 + list = result,
  4687 + pagination = new
  4688 + {
  4689 + pageIndex = input.PageIndex,
  4690 + pageSize = input.PageSize,
  4691 + total = totalCount
  4692 + }
  4693 + };
  4694 + }
  4695 + catch (Exception ex)
  4696 + {
  4697 + _logger.LogError(ex, "获取会员升单统计失败");
  4698 + throw NCCException.Oh($"获取会员升单统计失败:{ex.Message}");
  4699 + }
  4700 + }
  4701 + #endregion
  4702 +
3952 4703  
3953 4704  
3954 4705 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreConsumableInventoryService.cs
1 1 using System;
  2 +using System.Collections.Generic;
2 3 using System.Linq;
3 4 using System.Threading.Tasks;
4 5 using Microsoft.AspNetCore.Mvc;
5 6 using Microsoft.Extensions.Logging;
6 7 using NCC.Common.Core.Manager;
7 8 using NCC.Common.Enum;
  9 +using NCC.Common.Extension;
8 10 using NCC.Common.Filter;
9 11 using NCC.Dependency;
10 12 using NCC.DynamicApiController;
  13 +using NCC.Extend.Entitys.Dto.Common;
11 14 using NCC.Extend.Entitys.Dto.LqStoreConsumableInventory;
12 15 using NCC.Extend.Entitys.Enum;
13 16 using NCC.Extend.Entitys.lq_mdxx;
... ... @@ -367,6 +370,26 @@ namespace NCC.Extend
367 370 }
368 371 }
369 372 #endregion
  373 +
  374 +
  375 + #region 获取消耗品产品类型枚举内容
  376 + /// <summary>
  377 + /// 获取消耗品产品类型枚举内容
  378 + /// </summary>
  379 + /// <returns>消耗品产品类型枚举列表</returns>
  380 + [HttpGet("consumable-product-type")]
  381 + public List<EnumOutput> GetConsumableProductTypeSelector()
  382 + {
  383 + return Enum.GetValues<ConsumableProductTypeEnum>()
  384 + .Select(e => new EnumOutput
  385 + {
  386 + Value = (int)e,
  387 + Name = e.ToString(),
  388 + Description = e.GetDescription(),
  389 + })
  390 + .ToList();
  391 + }
  392 + #endregion
370 393 }
371 394 }
372 395  
... ...
sql/同步健康师业绩表品项分类和品项ID.sql 0 → 100644
  1 +-- 同步健康师业绩表和科技老师业绩表中的品项分类和品项ID字段
  2 +-- 数据来源:通过关联的品项明细表获取
  3 +
  4 +-- ============================================
  5 +-- 健康师业绩表同步
  6 +-- ============================================
  7 +
  8 +-- 1. 开单健康师业绩表:从开单品项明细表(lq_kd_pxmx)同步
  9 +UPDATE lq_kd_jksyj kd
  10 +INNER JOIN lq_kd_pxmx px ON px.F_Id = kd.F_kdpxid
  11 +SET
  12 + kd.F_ItemCategory = px.F_ItemCategory,
  13 + kd.F_ItemId = px.px
  14 +WHERE kd.F_kdpxid IS NOT NULL;
  15 +
  16 +-- 2. 耗卡健康师业绩表:从耗卡品项明细表(lq_xh_pxmx)同步
  17 +UPDATE lq_xh_jksyj xh
  18 +INNER JOIN lq_xh_pxmx px ON px.F_Id = xh.F_kdpxid
  19 +SET
  20 + xh.F_ItemCategory = px.F_ItemCategory,
  21 + xh.F_ItemId = px.px
  22 +WHERE xh.F_kdpxid IS NOT NULL;
  23 +
  24 +-- 3. 退卡健康师业绩表:从退卡品项明细表(lq_hytk_mx)同步
  25 +-- 注意:F_CardReturn 关联到 lq_hytk_mx.F_Id,F_tkpxid 是项目资料表ID(品项ID)
  26 +UPDATE lq_hytk_jksyj tk
  27 +INNER JOIN lq_hytk_mx mx ON mx.F_Id = tk.F_CardReturn
  28 +SET
  29 + tk.F_ItemCategory = mx.F_ItemCategory,
  30 + tk.F_ItemId = mx.px
  31 +WHERE tk.F_CardReturn IS NOT NULL;
  32 +
  33 +-- ============================================
  34 +-- 科技老师业绩表同步
  35 +-- ============================================
  36 +
  37 +-- 4. 开单科技老师业绩表:从开单品项明细表(lq_kd_pxmx)同步
  38 +UPDATE lq_kd_kjbsyj kd
  39 +INNER JOIN lq_kd_pxmx px ON px.F_Id = kd.F_kdpxid
  40 +SET
  41 + kd.F_ItemCategory = px.F_ItemCategory,
  42 + kd.F_ItemId = px.px
  43 +WHERE kd.F_kdpxid IS NOT NULL;
  44 +
  45 +-- 5. 耗卡科技老师业绩表:从耗卡品项明细表(lq_xh_pxmx)同步
  46 +UPDATE lq_xh_kjbsyj xh
  47 +INNER JOIN lq_xh_pxmx px ON px.F_Id = xh.F_hkpxid
  48 +SET
  49 + xh.F_ItemCategory = px.F_ItemCategory,
  50 + xh.F_ItemId = px.px
  51 +WHERE xh.F_hkpxid IS NOT NULL;
  52 +
  53 +-- 6. 退卡科技老师业绩表:从退卡品项明细表(lq_hytk_mx)同步
  54 +-- 注意:F_CardReturn 关联到 lq_hytk_mx.F_Id,F_tkpxid 是项目资料表ID(品项ID)
  55 +UPDATE lq_hytk_kjbsyj tk
  56 +INNER JOIN lq_hytk_mx mx ON mx.F_Id = tk.F_CardReturn
  57 +SET
  58 + tk.F_ItemCategory = mx.F_ItemCategory,
  59 + tk.F_ItemId = mx.px
  60 +WHERE tk.F_CardReturn IS NOT NULL;
  61 +
... ...
sql/添加业绩表品项分类字段.sql 0 → 100644
  1 +-- 为6个业绩表添加品项分类字段和品项ID字段
  2 +
  3 +-- 1. 开单健康师业绩表
  4 +ALTER TABLE `lq_kd_jksyj`
  5 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_ActivityId`,
  6 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  7 +
  8 +-- 2. 开单科技老师业绩表
  9 +ALTER TABLE `lq_kd_kjbsyj`
  10 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_ActivityId`,
  11 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  12 +
  13 +-- 3. 耗卡健康师业绩表
  14 +ALTER TABLE `lq_xh_jksyj`
  15 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_AccompaniedProjectNumber`,
  16 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  17 +
  18 +-- 4. 耗卡科技老师业绩表
  19 +ALTER TABLE `lq_xh_kjbsyj`
  20 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`,
  21 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  22 +
  23 +-- 5. 退卡健康师业绩表
  24 +ALTER TABLE `lq_hytk_jksyj`
  25 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`,
  26 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  27 +
  28 +-- 6. 退卡科技老师业绩表
  29 +ALTER TABLE `lq_hytk_kjbsyj`
  30 +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`,
  31 +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`;
  32 +
... ...