Commit 887a79d6320132f8e4cf830613dd489f1c6cf100

Authored by 李宇
2 parents 1138c09e 28b8ebfb

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

Showing 33 changed files with 1872 additions and 231 deletions
netcore/src/Infrastructure/NCC/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs
... ... @@ -152,6 +152,9 @@ namespace NCC.SpecificationDocument
152 152 //使得 Swagger 能够正确地显示 Enum 的对应关系
153 153 if (_specificationDocumentSettings.EnableEnumSchemaFilter == true) swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
154 154  
  155 + // 使得 Swagger 能够显示实体类的描述(从 XML 注释中读取)
  156 + swaggerGenOptions.SchemaFilter<EntityDescriptionSchemaFilter>();
  157 +
155 158 // 支持控制器排序操作
156 159 if (_specificationDocumentSettings.EnableTagsOrderDocumentFilter == true) swaggerGenOptions.DocumentFilter<TagsOrderDocumentFilter>();
157 160  
... ...
netcore/src/Infrastructure/NCC/SpecificationDocument/Filters/EntityDescriptionSchemaFilter.cs 0 → 100644
  1 +using NCC.Dependency;
  2 +using NCC.ConfigurableOptions;
  3 +using Microsoft.OpenApi.Models;
  4 +using Swashbuckle.AspNetCore.SwaggerGen;
  5 +using System;
  6 +using System.Collections.Generic;
  7 +using System.IO;
  8 +using System.Linq;
  9 +using System.Xml.Linq;
  10 +
  11 +namespace NCC.SpecificationDocument
  12 +{
  13 + /// <summary>
  14 + /// 修正 规范化文档实体类描述提示
  15 + /// </summary>
  16 + [SuppressSniffer]
  17 + public class EntityDescriptionSchemaFilter : ISchemaFilter
  18 + {
  19 + /// <summary>
  20 + /// 实现过滤器方法
  21 + /// </summary>
  22 + /// <param name="schema">OpenAPI Schema</param>
  23 + /// <param name="context">Schema 过滤器上下文</param>
  24 + public void Apply(OpenApiSchema schema, SchemaFilterContext context)
  25 + {
  26 + var type = context.Type;
  27 +
  28 + // 只处理项目程序集中的类型
  29 + if (!App.Assemblies.Contains(type.Assembly))
  30 + return;
  31 +
  32 + // 尝试从 XML 注释中获取类型描述
  33 + try
  34 + {
  35 + // 直接使用程序集名称查找 XML 文件
  36 + var assemblyName = type.Assembly.GetName().Name;
  37 + var xmlFileName = $"{assemblyName}.xml";
  38 + var xmlFilePath = Path.Combine(AppContext.BaseDirectory, xmlFileName);
  39 +
  40 + if (File.Exists(xmlFilePath))
  41 + {
  42 + try
  43 + {
  44 + var xmlDoc = XDocument.Load(xmlFilePath);
  45 + var memberName = $"T:{type.FullName}";
  46 + var memberElement = xmlDoc.Descendants("member")
  47 + .FirstOrDefault(m => m.Attribute("name")?.Value == memberName);
  48 +
  49 + if (memberElement != null)
  50 + {
  51 + var summaryElement = memberElement.Element("summary");
  52 + if (summaryElement != null)
  53 + {
  54 + var description = summaryElement.Value.Trim();
  55 + if (!string.IsNullOrEmpty(description))
  56 + {
  57 + schema.Description = description;
  58 + return;
  59 + }
  60 + }
  61 + }
  62 + }
  63 + catch
  64 + {
  65 + // 忽略 XML 解析错误
  66 + }
  67 + }
  68 +
  69 + // 如果直接查找失败,尝试从配置的 XML 注释列表中查找
  70 + var specificationDocumentSettings = App.GetOptions<SpecificationDocumentSettingsOptions>();
  71 + var xmlComments = specificationDocumentSettings?.XmlComments;
  72 + if (xmlComments != null && xmlComments.Any())
  73 + {
  74 + foreach (var xmlComment in xmlComments)
  75 + {
  76 + var assemblyXmlName = xmlComment.EndsWith(".xml") ? xmlComment : $"{xmlComment}.xml";
  77 + var assemblyXmlPath = Path.Combine(AppContext.BaseDirectory, assemblyXmlName);
  78 +
  79 + if (File.Exists(assemblyXmlPath) && assemblyXmlPath != xmlFilePath)
  80 + {
  81 + try
  82 + {
  83 + var xmlDoc = XDocument.Load(assemblyXmlPath);
  84 + var memberName = $"T:{type.FullName}";
  85 + var memberElement = xmlDoc.Descendants("member")
  86 + .FirstOrDefault(m => m.Attribute("name")?.Value == memberName);
  87 +
  88 + if (memberElement != null)
  89 + {
  90 + var summaryElement = memberElement.Element("summary");
  91 + if (summaryElement != null)
  92 + {
  93 + var description = summaryElement.Value.Trim();
  94 + if (!string.IsNullOrEmpty(description))
  95 + {
  96 + schema.Description = description;
  97 + break;
  98 + }
  99 + }
  100 + }
  101 + }
  102 + catch
  103 + {
  104 + // 忽略 XML 解析错误
  105 + }
  106 + }
  107 + }
  108 + }
  109 + }
  110 + catch
  111 + {
  112 + // 忽略配置获取错误
  113 + }
  114 + }
  115 + }
  116 +}
  117 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs
... ... @@ -66,14 +66,24 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
66 66 public decimal consumeAmount { get; set; }
67 67  
68 68 /// <summary>
69   - /// 人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重
  69 + /// 有效人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重,F_HasBilling=1,支持小数
70 70 /// </summary>
71   - public int headCount { get; set; }
  71 + public decimal headCount { get; set; }
72 72  
73 73 /// <summary>
74   - /// 人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,同一客户不同天算多次
  74 + /// 有效人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,F_HasBilling=1,同一客户不同天算多次,支持小数
75 75 /// </summary>
76   - public int personCount { get; set; }
  76 + public decimal personCount { get; set; }
  77 +
  78 + /// <summary>
  79 + /// 无效人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重,F_HasBilling=0,支持小数)
  80 + /// </summary>
  81 + public decimal invalidHeadCount { get; set; }
  82 +
  83 + /// <summary>
  84 + /// 无效人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,F_HasBilling=0,同一客户不同天算多次,支持小数)
  85 + /// </summary>
  86 + public decimal invalidPersonCount { get; set; }
77 87  
78 88 /// <summary>
79 89 /// 消耗项目数 - 统计该健康师在指定时间周期内消耗的项目总次数
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs
1 1 using System;
2 2 using System.ComponentModel.DataAnnotations;
  3 +using NCC.Dependency;
3 4  
4 5 namespace NCC.Extend.Entitys.Dto.LqProduct
5 6 {
... ...
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.Entitys/NCC.Extend.Entitys.csproj
... ... @@ -2,9 +2,12 @@
2 2 <PropertyGroup>
3 3 <TargetFramework>net6.0</TargetFramework>
4 4 </PropertyGroup>
5   - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
  5 + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  6 + <DocumentationFile>bin\Debug\$(AssemblyName).xml</DocumentationFile>
  7 + </PropertyGroup>
  8 + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
6 9 <OutputPath />
7   - <DocumentationFile>bin\Release\NCC.Extend.Entitys.xml</DocumentationFile>
  10 + <DocumentationFile>bin\Release\$(AssemblyName).xml</DocumentationFile>
8 11 </PropertyGroup>
9 12 <ItemGroup>
10 13 <ProjectReference Include="..\..\..\Infrastructure\NCC.Expand.Thirdparty\NCC.Expand.Thirdparty.csproj" />
... ...
netcore/src/Modularity/Extend/NCC.Extend.Interfaces/NCC.Extend.Interfaces.csproj
... ... @@ -3,6 +3,12 @@
3 3 <PropertyGroup>
4 4 <TargetFramework>net6.0</TargetFramework>
5 5 </PropertyGroup>
  6 + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  7 + <DocumentationFile>bin\Debug\$(AssemblyName).xml</DocumentationFile>
  8 + </PropertyGroup>
  9 + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  10 + <DocumentationFile>bin\Release\$(AssemblyName).xml</DocumentationFile>
  11 + </PropertyGroup>
6 12  
7 13 <ItemGroup>
8 14 <ProjectReference Include="..\NCC.Extend.Entitys\NCC.Extend.Entitys.csproj" />
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
... ... @@ -399,9 +399,7 @@ namespace NCC.Extend
399 399 throw NCCException.Oh("库存ID不能为空");
400 400 }
401 401  
402   - var inventory = await _db.Queryable<LqInventoryEntity>()
403   - .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode())
404   - .FirstAsync();
  402 + var inventory = await _db.Queryable<LqInventoryEntity>().Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync();
405 403  
406 404 if (inventory == null)
407 405 {
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -166,71 +166,59 @@ namespace NCC.Extend
166 166 }
167 167  
168 168 // 生成批次ID(如果未提供)
169   - var batchId = string.IsNullOrWhiteSpace(input.BatchId)
170   - ? YitIdHelper.NextId().ToString()
171   - : input.BatchId;
172   -
  169 + var batchId = string.IsNullOrWhiteSpace(input.BatchId) ? YitIdHelper.NextId().ToString() : input.BatchId;
173 170 var successIds = new List<string>();
174   - var failItems = new List<BatchCreateFailItem>();
175 171  
176   - _db.Ado.BeginTran();
  172 + // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚)
  173 + var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList();
177 174  
178   - try
179   - {
180   - // 按产品ID分组,批量验证库存
181   - var productGroups = input.UsageItems
182   - .Select((item, index) => new { Item = item, Index = index })
183   - .GroupBy(x => x.Item.ProductId)
184   - .ToList();
185   -
186   - // 计算每个产品的总需求并检查库存
187   - foreach (var productGroup in productGroups)
188   - {
189   - var productId = productGroup.Key;
190   - var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity);
  175 + // 获取所有需要检查的产品ID
  176 + var productIds = productGroups.Select(x => x.Key).Distinct().ToList();
191 177  
192   - // 计算该产品的总库存数量
193   - var totalInventory = await _db.Queryable<LqInventoryEntity>()
194   - .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
195   - .SumAsync(x => (int?)x.Quantity) ?? 0;
  178 + // 批量查询所有产品的库存信息(总库存)
  179 + var inventoryList = await _db.Queryable<LqInventoryEntity>().Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()).GroupBy(x => x.ProductId).Select(x => new { ProductId = x.ProductId, TotalInventory = SqlFunc.AggregateSum(x.Quantity) }).ToListAsync();
  180 + var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory));
196 181  
197   - // 计算该产品的已使用数量
198   - var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
199   - .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
200   - .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  182 + // 批量查询所有产品的已使用数量
  183 + var usageList = await _db.Queryable<LqInventoryUsageEntity>().Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()).GroupBy(x => x.ProductId).Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum(x.UsageQuantity) }).ToListAsync();
  184 + var usageMap = usageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage));
201 185  
202   - // 计算可用库存
203   - var availableInventory = totalInventory - totalUsage;
  186 + // 批量查询所有产品的名称
  187 + var productDict = await _db.Queryable<LqProductEntity>()
  188 + .Where(x => productIds.Contains(x.Id))
  189 + .Select(x => new { x.Id, x.ProductName })
  190 + .ToListAsync();
  191 + var productNameMap = productDict.ToDictionary(x => x.Id, x => x.ProductName ?? "未知产品");
204 192  
205   - // 检查库存是否足够
206   - if (availableInventory < totalRequired)
207   - {
208   - var failIndices = productGroup.Select(x => x.Index).ToList();
209   -
210   - foreach (var index in failIndices)
211   - {
212   - failItems.Add(new BatchCreateFailItem
213   - {
214   - Index = index,
215   - ProductId = productId,
216   - Reason = $"库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}"
217   - });
218   - }
219   - }
  193 + // 检查每个产品的库存
  194 + foreach (var productGroup in productGroups)
  195 + {
  196 + var productId = productGroup.Key;
  197 + var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity);
  198 +
  199 + // 从字典中获取库存信息
  200 + var totalInventory = inventoryMap.GetValueOrDefault(productId, 0);
  201 + var totalUsage = usageMap.GetValueOrDefault(productId, 0);
  202 + var availableInventory = totalInventory - totalUsage;
  203 +
  204 + // 检查库存是否足够,如果不足则直接抛出异常
  205 + if (availableInventory < totalRequired)
  206 + {
  207 + var productName = productNameMap.GetValueOrDefault(productId, "未知产品");
  208 + throw NCCException.Oh($"产品【{productName}】(ID: {productId}) 库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}");
220 209 }
  210 + }
  211 +
  212 + _db.Ado.BeginTran();
221 213  
222   - // 创建成功的使用记录
  214 + try
  215 + {
  216 + // 创建使用记录
223 217 var entitiesToInsert = new List<LqInventoryUsageEntity>();
224 218 for (int i = 0; i < input.UsageItems.Count; i++)
225 219 {
226 220 var item = input.UsageItems[i];
227 221  
228   - // 跳过失败项
229   - if (failItems.Any(x => x.Index == i))
230   - {
231   - continue;
232   - }
233   -
234 222 var usageEntity = new LqInventoryUsageEntity
235 223 {
236 224 Id = YitIdHelper.NextId().ToString(),
... ... @@ -265,9 +253,9 @@ namespace NCC.Extend
265 253 {
266 254 BatchId = batchId,
267 255 SuccessCount = successIds.Count,
268   - FailCount = failItems.Count,
  256 + FailCount = 0,
269 257 SuccessIds = successIds,
270   - FailItems = failItems
  258 + FailItems = new List<BatchCreateFailItem>()
271 259 };
272 260 }
273 261 catch
... ... @@ -346,35 +334,34 @@ namespace NCC.Extend
346 334 var sidx = input.sidx == null ? "id" : input.sidx;
347 335  
348 336 // 查询使用记录信息,关联产品表
349   - var data = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>(
350   - (usage, product) => usage.ProductId == product.Id)
351   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
352   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId)
353   - .WhereIF(input.UsageStartTime.HasValue, (usage, product) => usage.UsageTime >= input.UsageStartTime.Value)
354   - .WhereIF(input.UsageEndTime.HasValue, (usage, product) => usage.UsageTime <= input.UsageEndTime.Value)
355   - .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), (usage, product) => usage.RelatedConsumeId == input.RelatedConsumeId)
356   - .WhereIF(!string.IsNullOrWhiteSpace(input.UsageBatchId), (usage, product) => usage.UsageBatchId == input.UsageBatchId)
357   - .WhereIF(input.IsEffective.HasValue, (usage, product) => usage.IsEffective == input.IsEffective.Value)
358   - .Select((usage, product) => new LqInventoryUsageListOutput
  337 + var data = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>((u, product) => u.ProductId == product.Id)
  338 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId)
  339 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId)
  340 + .WhereIF(input.UsageStartTime.HasValue, (u, product) => u.UsageTime >= input.UsageStartTime.Value)
  341 + .WhereIF(input.UsageEndTime.HasValue, (u, product) => u.UsageTime <= input.UsageEndTime.Value)
  342 + .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), (u, product) => u.RelatedConsumeId == input.RelatedConsumeId)
  343 + .WhereIF(!string.IsNullOrWhiteSpace(input.UsageBatchId), (u, product) => u.UsageBatchId == input.UsageBatchId)
  344 + .WhereIF(input.IsEffective.HasValue, (u, product) => u.IsEffective == input.IsEffective.Value)
  345 + .Select((u, product) => new LqInventoryUsageListOutput
359 346 {
360   - id = usage.Id,
361   - productId = usage.ProductId,
  347 + id = u.Id,
  348 + productId = u.ProductId,
362 349 productName = product.ProductName,
363 350 productCategory = product.ProductCategory,
364 351 productPrice = product.Price,
365   - storeId = usage.StoreId,
366   - storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(u => u.Id == usage.StoreId).Select(u => u.Dm),
367   - usageTime = usage.UsageTime,
368   - usageQuantity = usage.UsageQuantity,
369   - relatedConsumeId = usage.RelatedConsumeId,
370   - usageBatchId = usage.UsageBatchId,
371   - createUser = usage.CreateUser,
  352 + storeId = u.StoreId,
  353 + storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(store => store.Id == u.StoreId).Select(store => store.Dm),
  354 + usageTime = u.UsageTime,
  355 + usageQuantity = u.UsageQuantity,
  356 + relatedConsumeId = u.RelatedConsumeId,
  357 + usageBatchId = u.UsageBatchId,
  358 + createUser = u.CreateUser,
372 359 createUserName = "",
373   - createTime = usage.CreateTime,
374   - updateUser = usage.UpdateUser,
  360 + createTime = u.CreateTime,
  361 + updateUser = u.UpdateUser,
375 362 updateUserName = "",
376   - updateTime = usage.UpdateTime,
377   - isEffective = usage.IsEffective
  363 + updateTime = u.UpdateTime,
  364 + isEffective = u.IsEffective
378 365 })
379 366 .MergeTable()
380 367 .OrderBy(sidx + " " + input.sort)
... ... @@ -459,30 +446,28 @@ namespace NCC.Extend
459 446 }
460 447  
461 448 // 查询该批次的所有使用记录
462   - var usageRecords = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>(
463   - (usage, product) => usage.ProductId == product.Id)
464   - .LeftJoin<LqMdxxEntity>((usage, product, store) => usage.StoreId == store.Id)
465   - .Where((usage, product, store) => usage.UsageBatchId == batchId)
466   - .Select((usage, 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
467 452 {
468   - id = usage.Id,
469   - productId = usage.ProductId,
  453 + id = u.Id,
  454 + productId = u.ProductId,
470 455 productName = product.ProductName,
471 456 productCategory = product.ProductCategory,
472 457 productPrice = product.Price,
473   - storeId = usage.StoreId,
474   - storeName = store.Dm,
475   - usageTime = usage.UsageTime,
476   - usageQuantity = usage.UsageQuantity,
477   - relatedConsumeId = usage.RelatedConsumeId,
478   - usageBatchId = usage.UsageBatchId,
479   - createUser = usage.CreateUser,
  458 + storeId = u.StoreId,
  459 + storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(store => store.Id == u.StoreId).Select(store => store.Dm),
  460 + usageTime = u.UsageTime,
  461 + usageQuantity = u.UsageQuantity,
  462 + relatedConsumeId = u.RelatedConsumeId,
  463 + usageBatchId = u.UsageBatchId,
  464 + createUser = u.CreateUser,
480 465 createUserName = "",
481   - createTime = usage.CreateTime,
482   - updateUser = usage.UpdateUser,
  466 + createTime = u.CreateTime,
  467 + updateUser = u.UpdateUser,
483 468 updateUserName = "",
484   - updateTime = usage.UpdateTime,
485   - isEffective = usage.IsEffective
  469 + updateTime = u.UpdateTime,
  470 + isEffective = u.IsEffective
486 471 })
487 472 .MergeTable()
488 473 .OrderBy("createTime")
... ... @@ -563,23 +548,23 @@ namespace NCC.Extend
563 548 try
564 549 {
565 550 var data = await _db.Queryable<LqInventoryUsageEntity>()
566   - .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
567   - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
568   - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
569   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
570   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId)
571   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory)
572   - .GroupBy((usage, product) => new { usage.ProductId, product.ProductName, product.ProductCategory, product.Price })
573   - .Select((usage, product) => new ProductUsageStatisticsOutput
  551 + .LeftJoin<LqProductEntity>((u, product) => u.ProductId == product.Id)
  552 + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime)
  553 + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode())
  554 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId)
  555 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId)
  556 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory)
  557 + .GroupBy((u, product) => new { u.ProductId, product.ProductName, product.ProductCategory, product.Price })
  558 + .Select((u, product) => new ProductUsageStatisticsOutput
574 559 {
575   - ProductId = usage.ProductId,
  560 + ProductId = u.ProductId,
576 561 ProductName = product.ProductName,
577 562 ProductCategory = product.ProductCategory,
578 563 ProductPrice = product.Price,
579   - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity),
580   - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price),
581   - UsageCount = SqlFunc.AggregateCount(usage.Id),
582   - AverageUsageQuantity = SqlFunc.AggregateAvg(usage.UsageQuantity)
  564 + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity),
  565 + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price),
  566 + UsageCount = SqlFunc.AggregateCount(u.Id),
  567 + AverageUsageQuantity = SqlFunc.AggregateAvg(u.UsageQuantity)
583 568 })
584 569 .ToListAsync();
585 570  
... ... @@ -611,22 +596,22 @@ namespace NCC.Extend
611 596 try
612 597 {
613 598 var data = await _db.Queryable<LqInventoryUsageEntity>()
614   - .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
615   - .LeftJoin<LqMdxxEntity>((usage, product, store) => usage.StoreId == store.Id)
616   - .Where((usage, product, store) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
617   - .Where((usage, product, store) => usage.IsEffective == StatusEnum.有效.GetHashCode())
618   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product, store) => usage.ProductId == input.ProductId)
619   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product, store) => usage.StoreId == input.StoreId)
620   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product, store) => product.ProductCategory == input.ProductCategory)
621   - .GroupBy((usage, product, store) => new { usage.StoreId, store.Dm })
622   - .Select((usage, product, store) => new StoreUsageStatisticsOutput
  599 + .LeftJoin<LqProductEntity>((u, product) => u.ProductId == product.Id)
  600 + .LeftJoin<LqMdxxEntity>((u, product, store) => u.StoreId == store.Id)
  601 + .Where((u, product, store) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime)
  602 + .Where((u, product, store) => u.IsEffective == StatusEnum.有效.GetHashCode())
  603 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product, store) => u.ProductId == input.ProductId)
  604 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product, store) => u.StoreId == input.StoreId)
  605 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product, store) => product.ProductCategory == input.ProductCategory)
  606 + .GroupBy((u, product, store) => new { u.StoreId, store.Dm })
  607 + .Select((u, product, store) => new StoreUsageStatisticsOutput
623 608 {
624   - StoreId = usage.StoreId,
  609 + StoreId = u.StoreId,
625 610 StoreName = store.Dm,
626   - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity),
627   - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price),
628   - UsageCount = SqlFunc.AggregateCount(usage.Id),
629   - ProductVarietyCount = SqlFunc.AggregateCount(usage.ProductId)
  611 + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity),
  612 + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price),
  613 + UsageCount = SqlFunc.AggregateCount(u.Id),
  614 + ProductVarietyCount = SqlFunc.AggregateCount(u.ProductId)
630 615 })
631 616 .ToListAsync();
632 617  
... ... @@ -659,18 +644,18 @@ namespace NCC.Extend
659 644 {
660 645 // 先获取基础数据
661 646 var baseData = await _db.Queryable<LqInventoryUsageEntity>()
662   - .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
663   - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
664   - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
665   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
666   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId)
667   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory)
668   - .Select((usage, product) => new
  647 + .LeftJoin<LqProductEntity>((u, product) => u.ProductId == product.Id)
  648 + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime)
  649 + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode())
  650 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId)
  651 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId)
  652 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory)
  653 + .Select((u, product) => new
669 654 {
670   - UsageTime = usage.UsageTime,
671   - UsageQuantity = usage.UsageQuantity,
672   - UsageAmount = usage.UsageQuantity * product.Price,
673   - ProductId = usage.ProductId
  655 + UsageTime = u.UsageTime,
  656 + UsageQuantity = u.UsageQuantity,
  657 + UsageAmount = u.UsageQuantity * product.Price,
  658 + ProductId = u.ProductId
674 659 })
675 660 .ToListAsync();
676 661  
... ... @@ -740,22 +725,22 @@ namespace NCC.Extend
740 725 try
741 726 {
742 727 var data = await _db.Queryable<LqInventoryUsageEntity>()
743   - .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
744   - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
745   - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
746   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
747   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId)
748   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory)
749   - .GroupBy((usage, product) => new { usage.ProductId, product.ProductName, product.ProductCategory })
750   - .Select((usage, product) => new ProductUsageRankingOutput
  728 + .LeftJoin<LqProductEntity>((u, product) => u.ProductId == product.Id)
  729 + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime)
  730 + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode())
  731 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId)
  732 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId)
  733 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory)
  734 + .GroupBy((u, product) => new { u.ProductId, product.ProductName, product.ProductCategory })
  735 + .Select((u, product) => new ProductUsageRankingOutput
751 736 {
752   - ProductId = usage.ProductId,
  737 + ProductId = u.ProductId,
753 738 ProductName = product.ProductName,
754 739 ProductCategory = product.ProductCategory,
755   - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity),
756   - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price),
757   - UsageCount = SqlFunc.AggregateCount(usage.Id),
758   - StoreCount = SqlFunc.AggregateCount(usage.StoreId)
  740 + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity),
  741 + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price),
  742 + UsageCount = SqlFunc.AggregateCount(u.Id),
  743 + StoreCount = SqlFunc.AggregateCount(u.StoreId)
759 744 })
760 745 .OrderBy("TotalUsageQuantity desc")
761 746 .Take(input.RankingCount)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -2549,7 +2549,8 @@ namespace NCC.Extend.LqKdKdjlb
2549 2549 totalPrice = it.TotalPrice,
2550 2550 actualPrice = it.ActualPrice,
2551 2551 projectNumber = it.ProjectNumber,
2552   - remark = it.Remark
  2552 + remark = it.Remark,
  2553 + itemCategory = it.ItemCategory,
2553 2554 })
2554 2555 .ToListAsync();
2555 2556  
... ... @@ -2596,7 +2597,8 @@ namespace NCC.Extend.LqKdKdjlb
2596 2597 totalPrice = x.totalPrice,
2597 2598 actualPrice = x.actualPrice,
2598 2599 projectNumber = x.projectNumber,
2599   - remark = x.remark
  2600 + remark = x.remark,
  2601 + itemCategory = x.itemCategory,
2600 2602 }).ToList(),
2601 2603  
2602 2604 giftedItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "赠送").Select(x => new
... ... @@ -2608,7 +2610,8 @@ namespace NCC.Extend.LqKdKdjlb
2608 2610 totalPrice = x.totalPrice,
2609 2611 actualPrice = x.actualPrice,
2610 2612 projectNumber = x.projectNumber,
2611   - remark = x.remark
  2613 + remark = x.remark,
  2614 + itemCategory = x.itemCategory,
2612 2615 }).ToList(),
2613 2616  
2614 2617 experienceItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "体验").Select(x => new
... ... @@ -2620,7 +2623,8 @@ namespace NCC.Extend.LqKdKdjlb
2620 2623 totalPrice = x.totalPrice,
2621 2624 actualPrice = x.actualPrice,
2622 2625 projectNumber = x.projectNumber,
2623   - remark = x.remark
  2626 + remark = x.remark,
  2627 + itemCategory = x.itemCategory,
2624 2628 }).ToList(),
2625 2629  
2626 2630 // 金额信息
... ... @@ -2670,7 +2674,6 @@ namespace NCC.Extend.LqKdKdjlb
2670 2674 },
2671 2675 message = "获取开单记录汇总信息成功"
2672 2676 };
2673   -
2674 2677 return result;
2675 2678 }
2676 2679 catch (Exception ex)
... ... @@ -3210,8 +3213,10 @@ namespace NCC.Extend.LqKdKdjlb
3210 3213  
3211 3214 -- 消耗相关统计
3212 3215 COALESCE(consume_stats.ConsumeAmount, 0) as ConsumeAmount,
3213   - COALESCE(consume_stats.HeadCount, 0) as HeadCount,
3214   - COALESCE(consume_stats.PersonCount, 0) as PersonCount,
  3216 + CAST(COALESCE(headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as HeadCount,
  3217 + CAST(COALESCE(personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as PersonCount,
  3218 + CAST(COALESCE(invalid_headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as InvalidHeadCount,
  3219 + CAST(COALESCE(invalid_personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as InvalidPersonCount,
3215 3220 CAST(COALESCE(consume_stats.ProjectCount, 0) AS DECIMAL(18,2)) as ProjectCount
3216 3221  
3217 3222 FROM BASE_USER u
... ... @@ -3274,8 +3279,6 @@ namespace NCC.Extend.LqKdKdjlb
3274 3279 SELECT
3275 3280 jksyj.jkszh as EmployeeId,
3276 3281 SUM(jksyj.jksyj) as ConsumeAmount,
3277   - COUNT(DISTINCT hyhk.hy) as HeadCount,
3278   - COUNT(DISTINCT CONCAT(jksyj.jkszh, '_', hyhk.hy, '_', DATE(hyhk.hksj))) as PersonCount,
3279 3282 CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount
3280 3283 FROM lq_xh_jksyj jksyj
3281 3284 INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
... ... @@ -3287,6 +3290,98 @@ namespace NCC.Extend.LqKdKdjlb
3287 3290 GROUP BY jksyj.jkszh
3288 3291 ) consume_stats ON u.F_Id = consume_stats.EmployeeId
3289 3292  
  3293 + -- 有效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=1)
  3294 + LEFT JOIN (
  3295 + SELECT
  3296 + F_PersonId as EmployeeId,
  3297 + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount
  3298 + FROM (
  3299 + SELECT
  3300 + F_PersonId,
  3301 + F_WorkMonth,
  3302 + F_MemberId,
  3303 + F_Quantity
  3304 + FROM lq_person_times_record
  3305 + WHERE F_PersonId IS NOT NULL
  3306 + AND F_IsEffective = 1
  3307 + AND F_PersonType = '健康师'
  3308 + AND F_HasBilling = 1
  3309 + AND F_WorkDate >= DATE(@startTime)
  3310 + AND F_WorkDate <= DATE(@endTime)
  3311 + GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity
  3312 + ) as distinct_headcount
  3313 + GROUP BY F_PersonId
  3314 + ) headcount_stats ON u.F_Id = headcount_stats.EmployeeId
  3315 +
  3316 + -- 有效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=1)
  3317 + LEFT JOIN (
  3318 + SELECT
  3319 + F_PersonId as EmployeeId,
  3320 + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount
  3321 + FROM (
  3322 + SELECT
  3323 + F_PersonId,
  3324 + F_WorkDate,
  3325 + F_MemberId,
  3326 + F_Quantity
  3327 + FROM lq_person_times_record
  3328 + WHERE F_PersonId IS NOT NULL
  3329 + AND F_IsEffective = 1
  3330 + AND F_PersonType = '健康师'
  3331 + AND F_HasBilling = 1
  3332 + AND F_WorkDate >= DATE(@startTime)
  3333 + AND F_WorkDate <= DATE(@endTime)
  3334 + GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity
  3335 + ) as distinct_personcount
  3336 + GROUP BY F_PersonId
  3337 + ) personcount_stats ON u.F_Id = personcount_stats.EmployeeId
  3338 +
  3339 + -- 无效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=0)
  3340 + LEFT JOIN (
  3341 + SELECT
  3342 + F_PersonId as EmployeeId,
  3343 + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount
  3344 + FROM (
  3345 + SELECT
  3346 + F_PersonId,
  3347 + F_WorkMonth,
  3348 + F_MemberId,
  3349 + F_Quantity
  3350 + FROM lq_person_times_record
  3351 + WHERE F_PersonId IS NOT NULL
  3352 + AND F_IsEffective = 1
  3353 + AND F_PersonType = '健康师'
  3354 + AND F_HasBilling = 0
  3355 + AND F_WorkDate >= DATE(@startTime)
  3356 + AND F_WorkDate <= DATE(@endTime)
  3357 + GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity
  3358 + ) as distinct_headcount
  3359 + GROUP BY F_PersonId
  3360 + ) invalid_headcount_stats ON u.F_Id = invalid_headcount_stats.EmployeeId
  3361 +
  3362 + -- 无效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=0)
  3363 + LEFT JOIN (
  3364 + SELECT
  3365 + F_PersonId as EmployeeId,
  3366 + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount
  3367 + FROM (
  3368 + SELECT
  3369 + F_PersonId,
  3370 + F_WorkDate,
  3371 + F_MemberId,
  3372 + F_Quantity
  3373 + FROM lq_person_times_record
  3374 + WHERE F_PersonId IS NOT NULL
  3375 + AND F_IsEffective = 1
  3376 + AND F_PersonType = '健康师'
  3377 + AND F_HasBilling = 0
  3378 + AND F_WorkDate >= DATE(@startTime)
  3379 + AND F_WorkDate <= DATE(@endTime)
  3380 + GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity
  3381 + ) as distinct_personcount
  3382 + GROUP BY F_PersonId
  3383 + ) invalid_personcount_stats ON u.F_Id = invalid_personcount_stats.EmployeeId
  3384 +
3290 3385 WHERE u.F_GW = '健康师'
3291 3386 ";
3292 3387  
... ...
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/LqProductService.cs
... ... @@ -228,7 +228,7 @@ namespace NCC.Extend
228 228 price = x.Price,
229 229 productCategory = x.ProductCategory,
230 230 departmentId = x.DepartmentId,
231   - departmentName = "",
  231 + departmentName = SqlFunc.Subqueryable<OrganizeEntity>().Where(y => y.Id == x.DepartmentId).Select(y => y.FullName),
232 232 standardUnit = x.StandardUnit,
233 233 onShelfStatus = x.OnShelfStatus,
234 234 statisticsCategory = x.StatisticsCategory,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqPurchaseRecordsService.cs
... ... @@ -28,7 +28,7 @@ namespace NCC.Extend.LqPurchaseRecords
28 28 /// <summary>
29 29 /// 购买记录表服务
30 30 /// </summary>
31   - [ApiDescriptionSettings(Tag = "Extend",Name = "LqPurchaseRecords", Order = 200)]
  31 + [ApiDescriptionSettings(Tag = "Extend", Name = "LqPurchaseRecords", Order = 200)]
32 32 [Route("api/Extend/[controller]")]
33 33 public class LqPurchaseRecordsService : ILqPurchaseRecordsService, IDynamicApiController, ITransient
34 34 {
... ... @@ -43,7 +43,7 @@ namespace NCC.Extend.LqPurchaseRecords
43 43 ISqlSugarRepository<LqPurchaseRecordsEntity> lqPurchaseRecordsRepository,
44 44 IUserManager userManager)
45 45 {
46   - _lqPurchaseRecordsRepository = lqPurchaseRecordsRepository;
  46 + _lqPurchaseRecordsRepository = lqPurchaseRecordsRepository;
47 47 _db = _lqPurchaseRecordsRepository.Context;
48 48 _userManager = userManager;
49 49 }
... ... @@ -98,25 +98,25 @@ namespace NCC.Extend.LqPurchaseRecords
98 98 .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0))
99 99 .WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59))
100 100 .WhereIF(!string.IsNullOrEmpty(input.applicationId), p => p.ApplicationId.Contains(input.applicationId))
101   - .Select(it=> new LqPurchaseRecordsListOutput
  101 + .Select(it => new LqPurchaseRecordsListOutput
102 102 {
103 103 id = it.Id,
104   - reimbursementCategoryId=it.ReimbursementCategoryId,
105   - reimbursementCategoryName=it.ReimbursementCategoryName,
106   - unitPrice=it.UnitPrice,
107   - quantity=it.Quantity,
108   - amount=it.Amount,
109   - memo=it.Memo,
110   - purchaseTime=it.PurchaseTime,
111   - createTime=it.CreateTime,
112   - createUser=it.CreateUser,
113   - createUserStoreId=it.CreateUserStoreId,
114   - approveStatus=it.ApproveStatus,
115   - approveUser=it.ApproveUser,
116   - approveTime=it.ApproveTime,
117   - applicationId=it.ApplicationId,
118   - }).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
119   - return PageResult<LqPurchaseRecordsListOutput>.SqlSugarPageResult(data);
  104 + reimbursementCategoryId = it.ReimbursementCategoryId,
  105 + reimbursementCategoryName = it.ReimbursementCategoryName,
  106 + unitPrice = it.UnitPrice,
  107 + quantity = it.Quantity,
  108 + amount = it.Amount,
  109 + memo = it.Memo,
  110 + purchaseTime = it.PurchaseTime,
  111 + createTime = it.CreateTime,
  112 + createUser = it.CreateUser,
  113 + createUserStoreId = it.CreateUserStoreId,
  114 + approveStatus = it.ApproveStatus,
  115 + approveUser = it.ApproveUser,
  116 + approveTime = it.ApproveTime,
  117 + applicationId = it.ApplicationId,
  118 + }).MergeTable().OrderBy(sidx + " " + input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
  119 + return PageResult<LqPurchaseRecordsListOutput>.SqlSugarPageResult(data);
120 120 }
121 121  
122 122 /// <summary>
... ... @@ -171,25 +171,25 @@ namespace NCC.Extend.LqPurchaseRecords
171 171 .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0))
172 172 .WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59))
173 173 .WhereIF(!string.IsNullOrEmpty(input.applicationId), p => p.ApplicationId.Contains(input.applicationId))
174   - .Select(it=> new LqPurchaseRecordsListOutput
  174 + .Select(it => new LqPurchaseRecordsListOutput
175 175 {
176 176 id = it.Id,
177   - reimbursementCategoryId=it.ReimbursementCategoryId,
178   - reimbursementCategoryName=it.ReimbursementCategoryName,
179   - unitPrice=it.UnitPrice,
180   - quantity=it.Quantity,
181   - amount=it.Amount,
182   - memo=it.Memo,
183   - purchaseTime=it.PurchaseTime,
184   - createTime=it.CreateTime,
185   - createUser=it.CreateUser,
186   - createUserStoreId=it.CreateUserStoreId,
187   - approveStatus=it.ApproveStatus,
188   - approveUser=it.ApproveUser,
189   - approveTime=it.ApproveTime,
190   - applicationId=it.ApplicationId,
191   - }).MergeTable().OrderBy(sidx+" "+input.sort).ToListAsync();
192   - return data;
  177 + reimbursementCategoryId = it.ReimbursementCategoryId,
  178 + reimbursementCategoryName = it.ReimbursementCategoryName,
  179 + unitPrice = it.UnitPrice,
  180 + quantity = it.Quantity,
  181 + amount = it.Amount,
  182 + memo = it.Memo,
  183 + purchaseTime = it.PurchaseTime,
  184 + createTime = it.CreateTime,
  185 + createUser = it.CreateUser,
  186 + createUserStoreId = it.CreateUserStoreId,
  187 + approveStatus = it.ApproveStatus,
  188 + approveUser = it.ApproveUser,
  189 + approveTime = it.ApproveTime,
  190 + applicationId = it.ApplicationId,
  191 + }).MergeTable().OrderBy(sidx + " " + input.sort).ToListAsync();
  192 + return data;
193 193 }
194 194  
195 195 /// <summary>
... ... @@ -211,7 +211,7 @@ namespace NCC.Extend.LqPurchaseRecords
211 211 {
212 212 exportData = await this.GetNoPagingList(input);
213 213 }
214   - List<ParamsModel> paramList = "[{\"value\":\"记录编号\",\"field\":\"id\"},{\"value\":\"购买物品编号\",\"field\":\"reimbursementCategoryId\"},{\"value\":\"购买物品名称\",\"field\":\"reimbursementCategoryName\"},{\"value\":\"单价\",\"field\":\"unitPrice\"},{\"value\":\"数量\",\"field\":\"quantity\"},{\"value\":\"总金额\",\"field\":\"amount\"},{\"value\":\"备注说明\",\"field\":\"memo\"},{\"value\":\"购买时间\",\"field\":\"purchaseTime\"},{\"value\":\"创建时间\",\"field\":\"createTime\"},{\"value\":\"创建人\",\"field\":\"createUser\"},{\"value\":\"创建人门店\",\"field\":\"createUserStoreId\"},{\"value\":\"审批状态\",\"field\":\"approveStatus\"},{\"value\":\"审批人\",\"field\":\"approveUser\"},{\"value\":\"审批时间\",\"field\":\"approveTime\"},{\"value\":\"审批单编号\",\"field\":\"applicationId\"},]".ToList<ParamsModel>();
  214 + List<ParamsModel> paramList = "[{\"value\":\"记录编号\",\"field\":\"id\"},{\"value\":\"购买物品编号\",\"field\":\"reimbursementCategoryId\"},{\"value\":\"购买物品名称\",\"field\":\"reimbursementCategoryName\"},{\"value\":\"单价\",\"field\":\"unitPrice\"},{\"value\":\"数量\",\"field\":\"quantity\"},{\"value\":\"总金额\",\"field\":\"amount\"},{\"value\":\"备注说明\",\"field\":\"memo\"},{\"value\":\"购买时间\",\"field\":\"purchaseTime\"},{\"value\":\"创建时间\",\"field\":\"createTime\"},{\"value\":\"创建人\",\"field\":\"createUser\"},{\"value\":\"创建人门店\",\"field\":\"createUserStoreId\"},{\"value\":\"审批状态\",\"field\":\"approveStatus\"},{\"value\":\"审批人\",\"field\":\"approveUser\"},{\"value\":\"审批时间\",\"field\":\"approveTime\"},{\"value\":\"审批单编号\",\"field\":\"applicationId\"},]".ToList<ParamsModel>();
215 215 ExcelConfig excelconfig = new ExcelConfig();
216 216 excelconfig.FileName = "购买记录表.xls";
217 217 excelconfig.HeadFont = "微软雅黑";
... ... @@ -254,7 +254,7 @@ namespace NCC.Extend.LqPurchaseRecords
254 254 //开启事务
255 255 _db.BeginTran();
256 256 //批量删除购买记录表
257   - await _db.Deleteable<LqPurchaseRecordsEntity>().In(d => d.Id,ids).ExecuteCommandAsync();
  257 + await _db.Deleteable<LqPurchaseRecordsEntity>().In(d => d.Id, ids).ExecuteCommandAsync();
258 258 //关闭事务
259 259 _db.CommitTran();
260 260 }
... ...
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 {
... ... @@ -3504,6 +3507,7 @@ namespace NCC.Extend.LqStatistics
3504 3507 AND F_PersonType = '健康师'
3505 3508 AND F_WorkMonth = '{month}'
3506 3509 AND F_IsEffective = 1
  3510 + AND F_HasBilling = 1
3507 3511 GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity
3508 3512 ) as distinct_records";
3509 3513  
... ... @@ -3531,7 +3535,8 @@ namespace NCC.Extend.LqStatistics
3531 3535 WHERE F_PersonId = '{userId}'
3532 3536 AND F_PersonType = '健康师'
3533 3537 AND F_WorkMonth = '{month}'
3534   - AND F_IsEffective = 1
  3538 + AND F_IsEffective = 1
  3539 + AND F_HasBilling = 1
3535 3540 GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity
3536 3541 ) as distinct_records";
3537 3542  
... ... @@ -3947,6 +3952,754 @@ namespace NCC.Extend.LqStatistics
3947 3952 }
3948 3953 #endregion
3949 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 +
3950 4703  
3951 4704  
3952 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  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
... ... @@ -1757,11 +1757,20 @@ namespace NCC.Extend.LqXhHyhk
1757 1757 var kjbsyjList = await _db.Queryable<LqXhKjbsyjEntity>().Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync();
1758 1758  
1759 1759 // 查询已存在的人次记录(按BusinessId去重,避免重复添加)
1760   - var existingBusinessIds = await _db.Queryable<LqPersonTimesRecordEntity>()
1761   - .Where(x => consumeIds.Contains(x.BusinessId) && x.BusinessType == "耗卡" && x.IsEffective == StatusEnum.有效.GetHashCode())
1762   - .Select(x => x.BusinessId)
1763   - .Distinct()
1764   - .ToListAsync();
  1760 + var existingBusinessIds = await _db.Queryable<LqPersonTimesRecordEntity>().Where(x => consumeIds.Contains(x.BusinessId) && x.BusinessType == "耗卡" && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => x.BusinessId).Distinct().ToListAsync();
  1761 +
  1762 + // 批量查询所有会员是否有开单记录(优化:避免在循环中执行N次查询)
  1763 + var memberIds = consumeList.Where(x => !string.IsNullOrEmpty(x.Hy)).Select(x => x.Hy).Distinct().ToList();
  1764 + var membersWithBilling = new HashSet<string>();
  1765 + if (memberIds.Any())
  1766 + {
  1767 + var billingMemberIds = await _db.Queryable<LqKdKdjlbEntity>()
  1768 + .Where(x => memberIds.Contains(x.Kdhy) && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Sfyj > 0)
  1769 + .Select(x => x.Kdhy)
  1770 + .Distinct()
  1771 + .ToListAsync();
  1772 + membersWithBilling = new HashSet<string>(billingMemberIds);
  1773 + }
1765 1774  
1766 1775 // 3. 构建人次记录列表
1767 1776 var personTimesRecords = new List<LqPersonTimesRecordEntity>();
... ... @@ -1782,19 +1791,14 @@ namespace NCC.Extend.LqXhHyhk
1782 1791 }
1783 1792 var workDate = consume.Hksj.Value.Date; // 工作日期(用于人次统计)
1784 1793 var workMonth = consume.Hksj.Value.ToString("yyyyMM"); // 工作月份(用于人头统计)
1785   -
  1794 + //查看该会员是否在开单记录表中存在开单记录(从批量查询结果中查找)
  1795 + var billingRecord = !string.IsNullOrEmpty(consume.Hy) && membersWithBilling.Contains(consume.Hy);
1786 1796 // 处理健康师业绩:去重后计算人次数量(剔除T区健康师)
1787   - var consumeJksyjList = jksyjList.Where(x => x.Glkdbh == consume.Id
1788   - && !string.IsNullOrEmpty(x.Jks)
1789   - && !string.IsNullOrEmpty(x.Jksxm)
1790   - && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
  1797 + var consumeJksyjList = jksyjList.Where(x => x.Glkdbh == consume.Id && !string.IsNullOrEmpty(x.Jks) && !string.IsNullOrEmpty(x.Jksxm) && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
1791 1798 if (consumeJksyjList.Any())
1792 1799 {
1793 1800 // 按健康师ID去重
1794   - var distinctJksyjList = consumeJksyjList
1795   - .GroupBy(x => x.Jks)
1796   - .Select(g => g.First())
1797   - .ToList();
  1801 + var distinctJksyjList = consumeJksyjList.GroupBy(x => x.Jks).Select(g => g.First()).ToList();
1798 1802  
1799 1803 // 计算人次数量:1 / 健康师数量
1800 1804 var jksQuantity = distinctJksyjList.Count > 0 ? 1.0m / distinctJksyjList.Count : 0;
... ... @@ -1815,23 +1819,18 @@ namespace NCC.Extend.LqXhHyhk
1815 1819 WorkMonth = workMonth,
1816 1820 Quantity = jksQuantity,
1817 1821 CreateTime = DateTime.Now,
1818   - IsEffective = StatusEnum.有效.GetHashCode()
  1822 + IsEffective = StatusEnum.有效.GetHashCode(),
  1823 + HasBilling = billingRecord ? 1 : 0
1819 1824 });
1820 1825 }
1821 1826 }
1822 1827  
1823 1828 // 处理科技老师业绩:去重后计算人次数量(剔除T区科技老师)
1824   - var consumeKjbsyjList = kjbsyjList.Where(x => x.Glkdbh == consume.Id
1825   - && !string.IsNullOrEmpty(x.Kjbls)
1826   - && !string.IsNullOrEmpty(x.Kjblsxm)
1827   - && (x.Kjblsxm == null || !x.Kjblsxm.Contains("T区"))).ToList();
  1829 + var consumeKjbsyjList = kjbsyjList.Where(x => x.Glkdbh == consume.Id && !string.IsNullOrEmpty(x.Kjbls) && !string.IsNullOrEmpty(x.Kjblsxm) && (x.Kjblsxm == null || !x.Kjblsxm.Contains("T区"))).ToList();
1828 1830 if (consumeKjbsyjList.Any())
1829 1831 {
1830 1832 // 按科技老师ID去重
1831   - var distinctKjbsyjList = consumeKjbsyjList
1832   - .GroupBy(x => x.Kjbls)
1833   - .Select(g => g.First())
1834   - .ToList();
  1833 + var distinctKjbsyjList = consumeKjbsyjList.GroupBy(x => x.Kjbls).Select(g => g.First()).ToList();
1835 1834  
1836 1835 // 计算人次数量:1 / 科技老师数量
1837 1836 var kjbsQuantity = distinctKjbsyjList.Count > 0 ? 1.0m / distinctKjbsyjList.Count : 0;
... ... @@ -1852,7 +1851,8 @@ namespace NCC.Extend.LqXhHyhk
1852 1851 WorkMonth = workMonth,
1853 1852 Quantity = kjbsQuantity,
1854 1853 CreateTime = DateTime.Now,
1855   - IsEffective = StatusEnum.有效.GetHashCode()
  1854 + IsEffective = StatusEnum.有效.GetHashCode(),
  1855 + HasBilling = billingRecord ? 1 : 0
1856 1856 });
1857 1857 }
1858 1858 }
... ... @@ -1865,9 +1865,7 @@ namespace NCC.Extend.LqXhHyhk
1865 1865 // 如果没有指定耗卡ID,不删除任何记录(因为已经在构建记录列表时跳过了已存在的记录)
1866 1866 if (!string.IsNullOrEmpty(consumeId))
1867 1867 {
1868   - await _db.Deleteable<LqPersonTimesRecordEntity>()
1869   - .Where(x => x.BusinessId == consumeId && x.BusinessType == "耗卡")
1870   - .ExecuteCommandAsync();
  1868 + await _db.Deleteable<LqPersonTimesRecordEntity>().Where(x => x.BusinessId == consumeId && x.BusinessType == "耗卡").ExecuteCommandAsync();
1871 1869 }
1872 1870  
1873 1871 // 批量插入新记录
... ...
sql/分析健康师消耗项目数差异.sql 0 → 100644
  1 +-- ============================================
  2 +-- 分析健康师消耗项目数差异
  3 +-- 员工ID: 18566028067 (李芳)
  4 +-- ============================================
  5 +
  6 +-- 1. 接口统计逻辑(使用耗卡时间,时间范围:2025-11-01 到 2025-11-25 11:55:32)
  7 +SELECT
  8 + jksyj.jkszh as EmployeeId,
  9 + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount_接口逻辑
  10 +FROM lq_xh_jksyj jksyj
  11 +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  12 +WHERE jksyj.jkszh = '18566028067'
  13 + AND jksyj.F_IsEffective = 1
  14 + AND hyhk.F_IsEffective = 1
  15 + AND hyhk.hksj >= '2025-11-01 00:00:00'
  16 + AND hyhk.hksj <= '2025-11-25 11:55:32'
  17 +GROUP BY jksyj.jkszh;
  18 +
  19 +-- 2. 整个11月的统计(使用业绩时间)
  20 +SELECT
  21 + jksyj.jkszh as EmployeeId,
  22 + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount_整个11月
  23 +FROM lq_xh_jksyj jksyj
  24 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  25 + AND jksyj.F_IsEffective = 1
  26 + AND DATE_FORMAT(jksyj.yjsj, '%Y%m') = '202511'
  27 +GROUP BY jksyj.jkszh;
  28 +
  29 +-- 3. 查看11月25日之后的记录(可能导致差异的原因)
  30 +SELECT
  31 + jksyj.jkszh as EmployeeId,
  32 + jksyj.F_kdpxNumber as 项目数,
  33 + jksyj.yjsj as 业绩时间,
  34 + hyhk.hksj as 耗卡时间,
  35 + hyhk.F_IsEffective as 耗卡记录是否有效,
  36 + jksyj.F_IsEffective as 业绩记录是否有效
  37 +FROM lq_xh_jksyj jksyj
  38 +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  39 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  40 + AND jksyj.F_IsEffective = 1
  41 + AND (
  42 + (jksyj.yjsj >= '2025-11-25 11:55:32' AND jksyj.yjsj < '2025-12-01')
  43 + OR (hyhk.hksj >= '2025-11-25 11:55:32' AND hyhk.hksj < '2025-12-01')
  44 + )
  45 +ORDER BY jksyj.yjsj;
  46 +
  47 +-- 4. 查看耗卡记录无效但业绩记录有效的记录(可能导致差异的原因)
  48 +SELECT
  49 + jksyj.jkszh as EmployeeId,
  50 + jksyj.F_kdpxNumber as 项目数,
  51 + jksyj.yjsj as 业绩时间,
  52 + hyhk.hksj as 耗卡时间,
  53 + hyhk.F_IsEffective as 耗卡记录是否有效,
  54 + jksyj.F_IsEffective as 业绩记录是否有效
  55 +FROM lq_xh_jksyj jksyj
  56 +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  57 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  58 + AND jksyj.F_IsEffective = 1
  59 + AND (hyhk.F_IsEffective != 1 OR hyhk.F_IsEffective IS NULL)
  60 + AND jksyj.yjsj >= '2025-11-01'
  61 + AND jksyj.yjsj < '2025-12-01'
  62 +ORDER BY jksyj.yjsj;
  63 +
  64 +-- 5. 查看耗卡时间和业绩时间不一致的记录(可能导致差异的原因)
  65 +SELECT
  66 + jksyj.jkszh as EmployeeId,
  67 + jksyj.F_kdpxNumber as 项目数,
  68 + jksyj.yjsj as 业绩时间,
  69 + hyhk.hksj as 耗卡时间,
  70 + DATEDIFF(jksyj.yjsj, hyhk.hksj) as 时间差_天,
  71 + CASE
  72 + WHEN hyhk.hksj >= '2025-11-01 00:00:00' AND hyhk.hksj <= '2025-11-25 11:55:32' THEN '在接口时间范围内'
  73 + ELSE '不在接口时间范围内'
  74 + END as 是否在接口时间范围
  75 +FROM lq_xh_jksyj jksyj
  76 +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  77 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  78 + AND jksyj.F_IsEffective = 1
  79 + AND hyhk.F_IsEffective = 1
  80 + AND (
  81 + (jksyj.yjsj >= '2025-11-01' AND jksyj.yjsj < '2025-12-01')
  82 + OR (hyhk.hksj >= '2025-11-01' AND hyhk.hksj < '2025-12-01')
  83 + )
  84 + AND (
  85 + jksyj.yjsj < '2025-11-01'
  86 + OR jksyj.yjsj >= '2025-12-01'
  87 + OR hyhk.hksj < '2025-11-01'
  88 + OR hyhk.hksj >= '2025-12-01'
  89 + OR DATEDIFF(jksyj.yjsj, hyhk.hksj) != 0
  90 + )
  91 +ORDER BY jksyj.yjsj;
  92 +
  93 +
... ...
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/查询员工11月25日之后的记录.sql 0 → 100644
  1 +-- ============================================
  2 +-- 查询员工在2025-11-25 11:55:32之后的记录
  3 +-- 员工ID: 18566028067 (李芳)
  4 +-- ============================================
  5 +
  6 +-- 1. 查询11月25日11:55:32之后的业绩记录(使用业绩时间)
  7 +SELECT
  8 + jksyj.jkszh as 健康师账号,
  9 + jksyj.jksxm as 健康师姓名,
  10 + jksyj.F_kdpxNumber as 项目数,
  11 + jksyj.yjsj as 业绩时间,
  12 + hyhk.hksj as 耗卡时间,
  13 + hyhk.F_IsEffective as 耗卡记录是否有效,
  14 + jksyj.F_IsEffective as 业绩记录是否有效,
  15 + hyhk.F_Id as 耗卡记录ID
  16 +FROM lq_xh_jksyj jksyj
  17 +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  18 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  19 + AND jksyj.F_IsEffective = 1
  20 + AND jksyj.yjsj > '2025-11-25 11:55:32'
  21 + AND jksyj.yjsj < '2025-12-01'
  22 +ORDER BY jksyj.yjsj;
  23 +
  24 +-- 2. 查询11月25日11:55:32之后的耗卡记录(使用耗卡时间,这是接口统计使用的条件)
  25 +SELECT
  26 + jksyj.jkszh as 健康师账号,
  27 + jksyj.jksxm as 健康师姓名,
  28 + jksyj.F_kdpxNumber as 项目数,
  29 + jksyj.yjsj as 业绩时间,
  30 + hyhk.hksj as 耗卡时间,
  31 + hyhk.F_IsEffective as 耗卡记录是否有效,
  32 + jksyj.F_IsEffective as 业绩记录是否有效,
  33 + hyhk.F_Id as 耗卡记录ID
  34 +FROM lq_xh_jksyj jksyj
  35 +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  36 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  37 + AND jksyj.F_IsEffective = 1
  38 + AND hyhk.F_IsEffective = 1
  39 + AND hyhk.hksj > '2025-11-25 11:55:32'
  40 + AND hyhk.hksj < '2025-12-01'
  41 +ORDER BY hyhk.hksj;
  42 +
  43 +-- 3. 统计11月25日11:55:32之后的项目数总和(使用耗卡时间,接口统计逻辑)
  44 +SELECT
  45 + jksyj.jkszh as 健康师账号,
  46 + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as 项目数总和
  47 +FROM lq_xh_jksyj jksyj
  48 +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  49 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  50 + AND jksyj.F_IsEffective = 1
  51 + AND hyhk.F_IsEffective = 1
  52 + AND hyhk.hksj > '2025-11-25 11:55:32'
  53 + AND hyhk.hksj < '2025-12-01'
  54 +GROUP BY jksyj.jkszh;
  55 +
  56 +
... ...
sql/查询员工消耗项目数.sql 0 → 100644
  1 +-- ============================================
  2 +-- 查询员工在指定月份的消耗项目数
  3 +-- ============================================
  4 +-- 员工ID: 18566028067
  5 +-- 查询月份: 2025年11月 (202511)
  6 +-- ============================================
  7 +
  8 +-- 方式1:从健康师业绩表统计(推荐,使用F_kdpxNumber字段,包含原始+加班+陪同项目数)
  9 +SELECT
  10 + jksyj.jks as 健康师ID,
  11 + jksyj.jksxm as 健康师姓名,
  12 + jksyj.jkszh as 健康师账号,
  13 + COALESCE(SUM(jksyj.F_kdpxNumber), 0) as 消耗项目总数,
  14 + COALESCE(SUM(COALESCE(jksyj.F_OriginalKdpxNumber, jksyj.F_kdpxNumber)), 0) as 原始项目数,
  15 + COALESCE(SUM(COALESCE(jksyj.F_OvertimeKdpxNumber, 0)), 0) as 加班项目数,
  16 + COALESCE(SUM(COALESCE(jksyj.F_AccompaniedProjectNumber, 0)), 0) as 陪同项目数
  17 +FROM lq_xh_jksyj jksyj
  18 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  19 + AND jksyj.F_IsEffective = 1
  20 + AND DATE_FORMAT(jksyj.yjsj, '%Y%m') = '202511'
  21 +GROUP BY jksyj.jks, jksyj.jksxm, jksyj.jkszh;
  22 +
  23 +-- 方式2:从品项明细表统计(备用方式,统计F_ProjectNumber字段)
  24 +SELECT
  25 + jksyj.jks as 健康师ID,
  26 + jksyj.jksxm as 健康师姓名,
  27 + jksyj.jkszh as 健康师账号,
  28 + COALESCE(SUM(pxmx.F_ProjectNumber), 0) as 消耗项目数
  29 +FROM lq_xh_jksyj jksyj
  30 +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
  31 +INNER JOIN lq_xh_pxmx pxmx ON pxmx.F_ConsumeInfoId = hyhk.F_Id
  32 +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067')
  33 + AND jksyj.F_IsEffective = 1
  34 + AND hyhk.F_IsEffective = 1
  35 + AND pxmx.F_IsEffective = 1
  36 + AND DATE_FORMAT(hyhk.hksj, '%Y%m') = '202511'
  37 +GROUP BY jksyj.jks, jksyj.jksxm, jksyj.jkszh;
  38 +
  39 +
... ...
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 +
... ...
sql/添加人次记录表字段.sql
... ... @@ -16,3 +16,5 @@ ADD COLUMN `F_HasBilling` INT(11) DEFAULT 0 COMMENT &#39;是否有开单(0-否,1-是
16 16 -- WHERE TABLE_NAME = 'lq_person_times_record'
17 17 -- AND COLUMN_NAME = 'F_HasBilling';
18 18  
  19 +
  20 +
... ...