diff --git a/netcore/src/Infrastructure/NCC/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs b/netcore/src/Infrastructure/NCC/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs index 3bee836..bca08c4 100644 --- a/netcore/src/Infrastructure/NCC/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs +++ b/netcore/src/Infrastructure/NCC/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs @@ -152,6 +152,9 @@ namespace NCC.SpecificationDocument //使得 Swagger 能够正确地显示 Enum 的对应关系 if (_specificationDocumentSettings.EnableEnumSchemaFilter == true) swaggerGenOptions.SchemaFilter(); + // 使得 Swagger 能够显示实体类的描述(从 XML 注释中读取) + swaggerGenOptions.SchemaFilter(); + // 支持控制器排序操作 if (_specificationDocumentSettings.EnableTagsOrderDocumentFilter == true) swaggerGenOptions.DocumentFilter(); diff --git a/netcore/src/Infrastructure/NCC/SpecificationDocument/Filters/EntityDescriptionSchemaFilter.cs b/netcore/src/Infrastructure/NCC/SpecificationDocument/Filters/EntityDescriptionSchemaFilter.cs new file mode 100644 index 0000000..ea5902c --- /dev/null +++ b/netcore/src/Infrastructure/NCC/SpecificationDocument/Filters/EntityDescriptionSchemaFilter.cs @@ -0,0 +1,117 @@ +using NCC.Dependency; +using NCC.ConfigurableOptions; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace NCC.SpecificationDocument +{ + /// + /// 修正 规范化文档实体类描述提示 + /// + [SuppressSniffer] + public class EntityDescriptionSchemaFilter : ISchemaFilter + { + /// + /// 实现过滤器方法 + /// + /// OpenAPI Schema + /// Schema 过滤器上下文 + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var type = context.Type; + + // 只处理项目程序集中的类型 + if (!App.Assemblies.Contains(type.Assembly)) + return; + + // 尝试从 XML 注释中获取类型描述 + try + { + // 直接使用程序集名称查找 XML 文件 + var assemblyName = type.Assembly.GetName().Name; + var xmlFileName = $"{assemblyName}.xml"; + var xmlFilePath = Path.Combine(AppContext.BaseDirectory, xmlFileName); + + if (File.Exists(xmlFilePath)) + { + try + { + var xmlDoc = XDocument.Load(xmlFilePath); + var memberName = $"T:{type.FullName}"; + var memberElement = xmlDoc.Descendants("member") + .FirstOrDefault(m => m.Attribute("name")?.Value == memberName); + + if (memberElement != null) + { + var summaryElement = memberElement.Element("summary"); + if (summaryElement != null) + { + var description = summaryElement.Value.Trim(); + if (!string.IsNullOrEmpty(description)) + { + schema.Description = description; + return; + } + } + } + } + catch + { + // 忽略 XML 解析错误 + } + } + + // 如果直接查找失败,尝试从配置的 XML 注释列表中查找 + var specificationDocumentSettings = App.GetOptions(); + var xmlComments = specificationDocumentSettings?.XmlComments; + if (xmlComments != null && xmlComments.Any()) + { + foreach (var xmlComment in xmlComments) + { + var assemblyXmlName = xmlComment.EndsWith(".xml") ? xmlComment : $"{xmlComment}.xml"; + var assemblyXmlPath = Path.Combine(AppContext.BaseDirectory, assemblyXmlName); + + if (File.Exists(assemblyXmlPath) && assemblyXmlPath != xmlFilePath) + { + try + { + var xmlDoc = XDocument.Load(assemblyXmlPath); + var memberName = $"T:{type.FullName}"; + var memberElement = xmlDoc.Descendants("member") + .FirstOrDefault(m => m.Attribute("name")?.Value == memberName); + + if (memberElement != null) + { + var summaryElement = memberElement.Element("summary"); + if (summaryElement != null) + { + var description = summaryElement.Value.Trim(); + if (!string.IsNullOrEmpty(description)) + { + schema.Description = description; + break; + } + } + } + } + catch + { + // 忽略 XML 解析错误 + } + } + } + } + } + catch + { + // 忽略配置获取错误 + } + } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs index 76e94d3..235f3c5 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/HealthCoachStatisticsOutput.cs @@ -66,14 +66,24 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb public decimal consumeAmount { get; set; } /// - /// 人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重) + /// 有效人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重,F_HasBilling=1,支持小数) /// - public int headCount { get; set; } + public decimal headCount { get; set; } /// - /// 人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,同一客户不同天算多次) + /// 有效人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,F_HasBilling=1,同一客户不同天算多次,支持小数) /// - public int personCount { get; set; } + public decimal personCount { get; set; } + + /// + /// 无效人头 - 统计该健康师在指定时间周期内服务的唯一客户数量(按客户去重,F_HasBilling=0,支持小数) + /// + public decimal invalidHeadCount { get; set; } + + /// + /// 无效人次 - 统计该健康师在指定时间周期内服务的客户人次(按客户+日期去重,F_HasBilling=0,同一客户不同天算多次,支持小数) + /// + public decimal invalidPersonCount { get; set; } /// /// 消耗项目数 - 统计该健康师在指定时间周期内消耗的项目总次数 diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs index 242d116..d41cf7d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using NCC.Dependency; namespace NCC.Extend.Entitys.Dto.LqProduct { diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListOutput.cs new file mode 100644 index 0000000..b3fc690 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListOutput.cs @@ -0,0 +1,81 @@ +using System; + +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 线索池客户统计报表输出 + /// + public class LeadCustomerStatisticsListOutput + { + /// + /// 线索池客户(拓客编号) + /// + public string LeadCustomerId { get; set; } + + /// + /// 客户姓名 + /// + public string CustomerName { get; set; } + + /// + /// 拓客时间 + /// + public DateTime? ExpansionTime { get; set; } + + /// + /// 是否邀约(是/否) + /// + public string HasInvite { get; set; } + + /// + /// 是否预约(是/否) + /// + public string HasAppointment { get; set; } + + /// + /// 是否有消耗(是/否) + /// + public string HasConsume { get; set; } + + /// + /// 是否开单(是/否) + /// + public string HasBilling { get; set; } + + /// + /// 未开单原因 + /// + public string NoBillingReason { get; set; } + + /// + /// 开卡金额 + /// + public decimal BillingAmount { get; set; } + + /// + /// 开卡卡项(多个卡项用顿号分隔) + /// + public string BillingItems { get; set; } + + /// + /// 实际预约记录数(不管是否通过邀约产生) + /// + public int ActualAppointmentCount { get; set; } + + /// + /// 实际消耗记录数(不管是否通过预约产生) + /// + public int ActualConsumeCount { get; set; } + + /// + /// 实际开单记录数(不管是否通过预约产生) + /// + public int ActualBillingCount { get; set; } + + /// + /// 问题分析说明 + /// + public string Analysis { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListQueryInput.cs new file mode 100644 index 0000000..5c691bd --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/LeadCustomerStatisticsListQueryInput.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 线索池客户统计报表查询输入 + /// + public class LeadCustomerStatisticsListQueryInput + { + /// + /// 页码 + /// + [Required] + public int PageIndex { get; set; } = 1; + + /// + /// 页大小 + /// + [Required] + public int PageSize { get; set; } = 20; + + /// + /// 开始时间(拓客时间范围) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(拓客时间范围) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可以多个门店) + /// + public List StoreIds { get; set; } + + /// + /// 拓客活动ID + /// + public string EventId { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListOutput.cs new file mode 100644 index 0000000..6d1d4b2 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListOutput.cs @@ -0,0 +1,39 @@ +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 会员升单统计输出 + /// + public class MemberUpgradeStatisticsListOutput + { + /// + /// 会员ID + /// + public string MemberId { get; set; } + + /// + /// 会员姓名 + /// + public string MemberName { get; set; } + + /// + /// 会员手机号 + /// + public string MemberPhone { get; set; } + + /// + /// 前4单中是否有升医美(是/否) + /// + public string HasUpgradeMedicalBeauty { get; set; } + + /// + /// 前4单中是否有升科美(是/否) + /// + public string HasUpgradeTechBeauty { get; set; } + + /// + /// 前4单中是否有升生美(是/否) + /// + public string HasUpgradeLifeBeauty { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListQueryInput.cs new file mode 100644 index 0000000..5e4f55b --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/MemberUpgradeStatisticsListQueryInput.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 会员升单统计查询输入 + /// + public class MemberUpgradeStatisticsListQueryInput + { + /// + /// 页码 + /// + public int PageIndex { get; set; } = 1; + + /// + /// 每页数量 + /// + public int PageSize { get; set; } = 20; + + /// + /// 会员ID列表(可选,不传则查询所有会员) + /// + public List MemberIds { get; set; } + + /// + /// 是否升医美(true-是,false-否,null-不筛选) + /// + public bool? HasUpgradeMedicalBeauty { get; set; } + + /// + /// 是否升科美(true-是,false-否,null-不筛选) + /// + public bool? HasUpgradeTechBeauty { get; set; } + + /// + /// 是否升生美(true-是,false-否,null-不筛选) + /// + public bool? HasUpgradeLifeBeauty { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListOutput.cs new file mode 100644 index 0000000..59465e5 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListOutput.cs @@ -0,0 +1,54 @@ +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 门店统计报表输出 + /// + public class StoreStatisticsListOutput + { + /// + /// 门店ID + /// + public string StoreId { get; set; } + + /// + /// 门店名称 + /// + public string StoreName { get; set; } + + /// + /// 总人数(拓客记录数) + /// + public int TotalCount { get; set; } + + /// + /// 拓客人数(去重的会员数) + /// + public int TkMemberCount { get; set; } + + /// + /// 邀约数(通过拓客编号关联的邀约记录数) + /// + public int InviteCount { get; set; } + + /// + /// 预约数(通过邀约ID关联的预约记录数) + /// + public int AppointmentCount { get; set; } + + /// + /// 耗卡数(通过预约ID关联的耗卡记录数) + /// + public int ConsumeCount { get; set; } + + /// + /// 开单数(通过预约ID关联的开单记录数) + /// + public int BillingCount { get; set; } + + /// + /// 开单金额(通过预约ID关联的开单记录金额汇总) + /// + public decimal BillingAmount { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListQueryInput.cs new file mode 100644 index 0000000..9b46308 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatistics/StoreStatisticsListQueryInput.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NCC.Extend.Entitys.Dto.LqStatistics +{ + /// + /// 门店统计报表查询输入 + /// + public class StoreStatisticsListQueryInput + { + /// + /// 开始时间(拓客时间范围) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(拓客时间范围) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可以多个门店) + /// + public List StoreIds { get; set; } + + /// + /// 拓客活动ID(可选,不传则查询所有活动) + /// + public string EventId { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_jksyj/LqHytkJksyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_jksyj/LqHytkJksyjEntity.cs index 8f8bfc7..6fd7d97 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_jksyj/LqHytkJksyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_jksyj/LqHytkJksyjEntity.cs @@ -113,5 +113,17 @@ namespace NCC.Extend.Entitys.lq_hytk_jksyj /// [SugarColumn(ColumnName = "F_IsEffective")] public int IsEffective { get; set; } = StatusEnum.有效.GetHashCode(); + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_kjbsyj/LqHytkKjbsyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_kjbsyj/LqHytkKjbsyjEntity.cs index d5b6a6a..5cd3173 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_kjbsyj/LqHytkKjbsyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_kjbsyj/LqHytkKjbsyjEntity.cs @@ -108,5 +108,17 @@ namespace NCC.Extend.Entitys.lq_hytk_kjbsyj /// [SugarColumn(ColumnName = "F_IsEffective")] public int IsEffective { get; set; } = StatusEnum.有效.GetHashCode(); + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_jksyj/LqKdJksyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_jksyj/LqKdJksyjEntity.cs index 28cf514..43f808d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_jksyj/LqKdJksyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_jksyj/LqKdJksyjEntity.cs @@ -76,5 +76,17 @@ namespace NCC.Extend.Entitys.lq_kd_jksyj /// [SugarColumn(ColumnName = "F_ActivityId")] public string ActivityId { get; set; } + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_kjbsyj/LqKdKjbsyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_kjbsyj/LqKdKjbsyjEntity.cs index 14fe19e..55c506d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_kjbsyj/LqKdKjbsyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_kjbsyj/LqKdKjbsyjEntity.cs @@ -76,5 +76,17 @@ namespace NCC.Extend.Entitys.lq_kd_kjbsyj /// [SugarColumn(ColumnName = "F_ActivityId")] public string ActivityId { get; set; } + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_jksyj/LqXhJksyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_jksyj/LqXhJksyjEntity.cs index de57f7d..29d273a 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_jksyj/LqXhJksyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_jksyj/LqXhJksyjEntity.cs @@ -124,5 +124,17 @@ namespace NCC.Extend.Entitys.lq_xh_jksyj /// [SugarColumn(ColumnName = "F_AccompaniedProjectNumber")] public decimal? AccompaniedProjectNumber { get; set; } + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } \ No newline at end of file diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_kjbsyj/LqXhKjbsyjEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_kjbsyj/LqXhKjbsyjEntity.cs index 1daec75..d63b339 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_kjbsyj/LqXhKjbsyjEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_xh_kjbsyj/LqXhKjbsyjEntity.cs @@ -106,5 +106,17 @@ namespace NCC.Extend.Entitys.lq_xh_kjbsyj /// [SugarColumn(ColumnName = "F_IsEffective")] public int? IsEffective { get; set; } = 1; + + /// + /// 品项分类 + /// + [SugarColumn(ColumnName = "F_ItemCategory")] + public string ItemCategory { get; set; } + + /// + /// 品项ID + /// + [SugarColumn(ColumnName = "F_ItemId")] + public string ItemId { get; set; } } } \ No newline at end of file diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.csproj b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.csproj index ca2ef87..ba66805 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.csproj +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.csproj @@ -2,9 +2,12 @@ net6.0 - + + bin\Debug\$(AssemblyName).xml + + - bin\Release\NCC.Extend.Entitys.xml + bin\Release\$(AssemblyName).xml diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Interfaces/NCC.Extend.Interfaces.csproj b/netcore/src/Modularity/Extend/NCC.Extend.Interfaces/NCC.Extend.Interfaces.csproj index dd7192e..68cbb7d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Interfaces/NCC.Extend.Interfaces.csproj +++ b/netcore/src/Modularity/Extend/NCC.Extend.Interfaces/NCC.Extend.Interfaces.csproj @@ -3,6 +3,12 @@ net6.0 + + bin\Debug\$(AssemblyName).xml + + + bin\Release\$(AssemblyName).xml + diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs index 1079d21..72eb8b1 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs @@ -399,9 +399,7 @@ namespace NCC.Extend throw NCCException.Oh("库存ID不能为空"); } - var inventory = await _db.Queryable() - .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode()) - .FirstAsync(); + var inventory = await _db.Queryable().Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync(); if (inventory == null) { diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs index fdcdfd3..f9fca08 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs @@ -166,71 +166,59 @@ namespace NCC.Extend } // 生成批次ID(如果未提供) - var batchId = string.IsNullOrWhiteSpace(input.BatchId) - ? YitIdHelper.NextId().ToString() - : input.BatchId; - + var batchId = string.IsNullOrWhiteSpace(input.BatchId) ? YitIdHelper.NextId().ToString() : input.BatchId; var successIds = new List(); - var failItems = new List(); - _db.Ado.BeginTran(); + // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚) + var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList(); - try - { - // 按产品ID分组,批量验证库存 - var productGroups = input.UsageItems - .Select((item, index) => new { Item = item, Index = index }) - .GroupBy(x => x.Item.ProductId) - .ToList(); - - // 计算每个产品的总需求并检查库存 - foreach (var productGroup in productGroups) - { - var productId = productGroup.Key; - var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity); + // 获取所有需要检查的产品ID + var productIds = productGroups.Select(x => x.Key).Distinct().ToList(); - // 计算该产品的总库存数量 - var totalInventory = await _db.Queryable() - .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode()) - .SumAsync(x => (int?)x.Quantity) ?? 0; + // 批量查询所有产品的库存信息(总库存) + var inventoryList = await _db.Queryable().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(); + var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory)); - // 计算该产品的已使用数量 - var totalUsage = await _db.Queryable() - .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode()) - .SumAsync(x => (int?)x.UsageQuantity) ?? 0; + // 批量查询所有产品的已使用数量 + var usageList = await _db.Queryable().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(); + var usageMap = usageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage)); - // 计算可用库存 - var availableInventory = totalInventory - totalUsage; + // 批量查询所有产品的名称 + var productDict = await _db.Queryable() + .Where(x => productIds.Contains(x.Id)) + .Select(x => new { x.Id, x.ProductName }) + .ToListAsync(); + var productNameMap = productDict.ToDictionary(x => x.Id, x => x.ProductName ?? "未知产品"); - // 检查库存是否足够 - if (availableInventory < totalRequired) - { - var failIndices = productGroup.Select(x => x.Index).ToList(); - - foreach (var index in failIndices) - { - failItems.Add(new BatchCreateFailItem - { - Index = index, - ProductId = productId, - Reason = $"库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}" - }); - } - } + // 检查每个产品的库存 + foreach (var productGroup in productGroups) + { + var productId = productGroup.Key; + var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity); + + // 从字典中获取库存信息 + var totalInventory = inventoryMap.GetValueOrDefault(productId, 0); + var totalUsage = usageMap.GetValueOrDefault(productId, 0); + var availableInventory = totalInventory - totalUsage; + + // 检查库存是否足够,如果不足则直接抛出异常 + if (availableInventory < totalRequired) + { + var productName = productNameMap.GetValueOrDefault(productId, "未知产品"); + throw NCCException.Oh($"产品【{productName}】(ID: {productId}) 库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}"); } + } + + _db.Ado.BeginTran(); - // 创建成功的使用记录 + try + { + // 创建使用记录 var entitiesToInsert = new List(); for (int i = 0; i < input.UsageItems.Count; i++) { var item = input.UsageItems[i]; - // 跳过失败项 - if (failItems.Any(x => x.Index == i)) - { - continue; - } - var usageEntity = new LqInventoryUsageEntity { Id = YitIdHelper.NextId().ToString(), @@ -265,9 +253,9 @@ namespace NCC.Extend { BatchId = batchId, SuccessCount = successIds.Count, - FailCount = failItems.Count, + FailCount = 0, SuccessIds = successIds, - FailItems = failItems + FailItems = new List() }; } catch @@ -346,35 +334,34 @@ namespace NCC.Extend var sidx = input.sidx == null ? "id" : input.sidx; // 查询使用记录信息,关联产品表 - var data = await _db.Queryable( - (usage, product) => usage.ProductId == product.Id) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId) - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId) - .WhereIF(input.UsageStartTime.HasValue, (usage, product) => usage.UsageTime >= input.UsageStartTime.Value) - .WhereIF(input.UsageEndTime.HasValue, (usage, product) => usage.UsageTime <= input.UsageEndTime.Value) - .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), (usage, product) => usage.RelatedConsumeId == input.RelatedConsumeId) - .WhereIF(!string.IsNullOrWhiteSpace(input.UsageBatchId), (usage, product) => usage.UsageBatchId == input.UsageBatchId) - .WhereIF(input.IsEffective.HasValue, (usage, product) => usage.IsEffective == input.IsEffective.Value) - .Select((usage, product) => new LqInventoryUsageListOutput + var data = await _db.Queryable((u, product) => u.ProductId == product.Id) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId) + .WhereIF(input.UsageStartTime.HasValue, (u, product) => u.UsageTime >= input.UsageStartTime.Value) + .WhereIF(input.UsageEndTime.HasValue, (u, product) => u.UsageTime <= input.UsageEndTime.Value) + .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), (u, product) => u.RelatedConsumeId == input.RelatedConsumeId) + .WhereIF(!string.IsNullOrWhiteSpace(input.UsageBatchId), (u, product) => u.UsageBatchId == input.UsageBatchId) + .WhereIF(input.IsEffective.HasValue, (u, product) => u.IsEffective == input.IsEffective.Value) + .Select((u, product) => new LqInventoryUsageListOutput { - id = usage.Id, - productId = usage.ProductId, + id = u.Id, + productId = u.ProductId, productName = product.ProductName, productCategory = product.ProductCategory, productPrice = product.Price, - storeId = usage.StoreId, - storeName = SqlFunc.Subqueryable().Where(u => u.Id == usage.StoreId).Select(u => u.Dm), - usageTime = usage.UsageTime, - usageQuantity = usage.UsageQuantity, - relatedConsumeId = usage.RelatedConsumeId, - usageBatchId = usage.UsageBatchId, - createUser = usage.CreateUser, + storeId = u.StoreId, + storeName = SqlFunc.Subqueryable().Where(store => store.Id == u.StoreId).Select(store => store.Dm), + usageTime = u.UsageTime, + usageQuantity = u.UsageQuantity, + relatedConsumeId = u.RelatedConsumeId, + usageBatchId = u.UsageBatchId, + createUser = u.CreateUser, createUserName = "", - createTime = usage.CreateTime, - updateUser = usage.UpdateUser, + createTime = u.CreateTime, + updateUser = u.UpdateUser, updateUserName = "", - updateTime = usage.UpdateTime, - isEffective = usage.IsEffective + updateTime = u.UpdateTime, + isEffective = u.IsEffective }) .MergeTable() .OrderBy(sidx + " " + input.sort) @@ -459,30 +446,28 @@ namespace NCC.Extend } // 查询该批次的所有使用记录 - var usageRecords = await _db.Queryable( - (usage, product) => usage.ProductId == product.Id) - .LeftJoin((usage, product, store) => usage.StoreId == store.Id) - .Where((usage, product, store) => usage.UsageBatchId == batchId) - .Select((usage, product, store) => new LqInventoryUsageListOutput + var usageRecords = await _db.Queryable((u, product) => u.ProductId == product.Id) + .Where((u, product) => u.UsageBatchId == batchId) + .Select((u, product) => new LqInventoryUsageListOutput { - id = usage.Id, - productId = usage.ProductId, + id = u.Id, + productId = u.ProductId, productName = product.ProductName, productCategory = product.ProductCategory, productPrice = product.Price, - storeId = usage.StoreId, - storeName = store.Dm, - usageTime = usage.UsageTime, - usageQuantity = usage.UsageQuantity, - relatedConsumeId = usage.RelatedConsumeId, - usageBatchId = usage.UsageBatchId, - createUser = usage.CreateUser, + storeId = u.StoreId, + storeName = SqlFunc.Subqueryable().Where(store => store.Id == u.StoreId).Select(store => store.Dm), + usageTime = u.UsageTime, + usageQuantity = u.UsageQuantity, + relatedConsumeId = u.RelatedConsumeId, + usageBatchId = u.UsageBatchId, + createUser = u.CreateUser, createUserName = "", - createTime = usage.CreateTime, - updateUser = usage.UpdateUser, + createTime = u.CreateTime, + updateUser = u.UpdateUser, updateUserName = "", - updateTime = usage.UpdateTime, - isEffective = usage.IsEffective + updateTime = u.UpdateTime, + isEffective = u.IsEffective }) .MergeTable() .OrderBy("createTime") @@ -563,23 +548,23 @@ namespace NCC.Extend try { var data = await _db.Queryable() - .LeftJoin((usage, product) => usage.ProductId == product.Id) - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime) - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode()) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId) - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory) - .GroupBy((usage, product) => new { usage.ProductId, product.ProductName, product.ProductCategory, product.Price }) - .Select((usage, product) => new ProductUsageStatisticsOutput + .LeftJoin((u, product) => u.ProductId == product.Id) + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime) + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode()) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory) + .GroupBy((u, product) => new { u.ProductId, product.ProductName, product.ProductCategory, product.Price }) + .Select((u, product) => new ProductUsageStatisticsOutput { - ProductId = usage.ProductId, + ProductId = u.ProductId, ProductName = product.ProductName, ProductCategory = product.ProductCategory, ProductPrice = product.Price, - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity), - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price), - UsageCount = SqlFunc.AggregateCount(usage.Id), - AverageUsageQuantity = SqlFunc.AggregateAvg(usage.UsageQuantity) + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity), + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price), + UsageCount = SqlFunc.AggregateCount(u.Id), + AverageUsageQuantity = SqlFunc.AggregateAvg(u.UsageQuantity) }) .ToListAsync(); @@ -611,22 +596,22 @@ namespace NCC.Extend try { var data = await _db.Queryable() - .LeftJoin((usage, product) => usage.ProductId == product.Id) - .LeftJoin((usage, product, store) => usage.StoreId == store.Id) - .Where((usage, product, store) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime) - .Where((usage, product, store) => usage.IsEffective == StatusEnum.有效.GetHashCode()) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product, store) => usage.ProductId == input.ProductId) - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product, store) => usage.StoreId == input.StoreId) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product, store) => product.ProductCategory == input.ProductCategory) - .GroupBy((usage, product, store) => new { usage.StoreId, store.Dm }) - .Select((usage, product, store) => new StoreUsageStatisticsOutput + .LeftJoin((u, product) => u.ProductId == product.Id) + .LeftJoin((u, product, store) => u.StoreId == store.Id) + .Where((u, product, store) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime) + .Where((u, product, store) => u.IsEffective == StatusEnum.有效.GetHashCode()) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product, store) => u.ProductId == input.ProductId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product, store) => u.StoreId == input.StoreId) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product, store) => product.ProductCategory == input.ProductCategory) + .GroupBy((u, product, store) => new { u.StoreId, store.Dm }) + .Select((u, product, store) => new StoreUsageStatisticsOutput { - StoreId = usage.StoreId, + StoreId = u.StoreId, StoreName = store.Dm, - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity), - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price), - UsageCount = SqlFunc.AggregateCount(usage.Id), - ProductVarietyCount = SqlFunc.AggregateCount(usage.ProductId) + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity), + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price), + UsageCount = SqlFunc.AggregateCount(u.Id), + ProductVarietyCount = SqlFunc.AggregateCount(u.ProductId) }) .ToListAsync(); @@ -659,18 +644,18 @@ namespace NCC.Extend { // 先获取基础数据 var baseData = await _db.Queryable() - .LeftJoin((usage, product) => usage.ProductId == product.Id) - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime) - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode()) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId) - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory) - .Select((usage, product) => new + .LeftJoin((u, product) => u.ProductId == product.Id) + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime) + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode()) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory) + .Select((u, product) => new { - UsageTime = usage.UsageTime, - UsageQuantity = usage.UsageQuantity, - UsageAmount = usage.UsageQuantity * product.Price, - ProductId = usage.ProductId + UsageTime = u.UsageTime, + UsageQuantity = u.UsageQuantity, + UsageAmount = u.UsageQuantity * product.Price, + ProductId = u.ProductId }) .ToListAsync(); @@ -740,22 +725,22 @@ namespace NCC.Extend try { var data = await _db.Queryable() - .LeftJoin((usage, product) => usage.ProductId == product.Id) - .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime) - .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode()) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId) - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId) - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product) => product.ProductCategory == input.ProductCategory) - .GroupBy((usage, product) => new { usage.ProductId, product.ProductName, product.ProductCategory }) - .Select((usage, product) => new ProductUsageRankingOutput + .LeftJoin((u, product) => u.ProductId == product.Id) + .Where((u, product) => u.UsageTime >= input.StartTime && u.UsageTime <= input.EndTime) + .Where((u, product) => u.IsEffective == StatusEnum.有效.GetHashCode()) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (u, product) => u.ProductId == input.ProductId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (u, product) => u.StoreId == input.StoreId) + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (u, product) => product.ProductCategory == input.ProductCategory) + .GroupBy((u, product) => new { u.ProductId, product.ProductName, product.ProductCategory }) + .Select((u, product) => new ProductUsageRankingOutput { - ProductId = usage.ProductId, + ProductId = u.ProductId, ProductName = product.ProductName, ProductCategory = product.ProductCategory, - TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity), - TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price), - UsageCount = SqlFunc.AggregateCount(usage.Id), - StoreCount = SqlFunc.AggregateCount(usage.StoreId) + TotalUsageQuantity = SqlFunc.AggregateSum(u.UsageQuantity), + TotalUsageAmount = SqlFunc.AggregateSum(u.UsageQuantity * product.Price), + UsageCount = SqlFunc.AggregateCount(u.Id), + StoreCount = SqlFunc.AggregateCount(u.StoreId) }) .OrderBy("TotalUsageQuantity desc") .Take(input.RankingCount) diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs index cc72fb8..53b99e0 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs @@ -2549,7 +2549,8 @@ namespace NCC.Extend.LqKdKdjlb totalPrice = it.TotalPrice, actualPrice = it.ActualPrice, projectNumber = it.ProjectNumber, - remark = it.Remark + remark = it.Remark, + itemCategory = it.ItemCategory, }) .ToListAsync(); @@ -2596,7 +2597,8 @@ namespace NCC.Extend.LqKdKdjlb totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, - remark = x.remark + remark = x.remark, + itemCategory = x.itemCategory, }).ToList(), giftedItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "赠送").Select(x => new @@ -2608,7 +2610,8 @@ namespace NCC.Extend.LqKdKdjlb totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, - remark = x.remark + remark = x.remark, + itemCategory = x.itemCategory, }).ToList(), experienceItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "体验").Select(x => new @@ -2620,7 +2623,8 @@ namespace NCC.Extend.LqKdKdjlb totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, - remark = x.remark + remark = x.remark, + itemCategory = x.itemCategory, }).ToList(), // 金额信息 @@ -2670,7 +2674,6 @@ namespace NCC.Extend.LqKdKdjlb }, message = "获取开单记录汇总信息成功" }; - return result; } catch (Exception ex) @@ -3210,8 +3213,10 @@ namespace NCC.Extend.LqKdKdjlb -- 消耗相关统计 COALESCE(consume_stats.ConsumeAmount, 0) as ConsumeAmount, - COALESCE(consume_stats.HeadCount, 0) as HeadCount, - COALESCE(consume_stats.PersonCount, 0) as PersonCount, + CAST(COALESCE(headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as HeadCount, + CAST(COALESCE(personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as PersonCount, + CAST(COALESCE(invalid_headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as InvalidHeadCount, + CAST(COALESCE(invalid_personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as InvalidPersonCount, CAST(COALESCE(consume_stats.ProjectCount, 0) AS DECIMAL(18,2)) as ProjectCount FROM BASE_USER u @@ -3274,8 +3279,6 @@ namespace NCC.Extend.LqKdKdjlb SELECT jksyj.jkszh as EmployeeId, SUM(jksyj.jksyj) as ConsumeAmount, - COUNT(DISTINCT hyhk.hy) as HeadCount, - COUNT(DISTINCT CONCAT(jksyj.jkszh, '_', hyhk.hy, '_', DATE(hyhk.hksj))) as PersonCount, CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount FROM lq_xh_jksyj jksyj INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id @@ -3287,6 +3290,98 @@ namespace NCC.Extend.LqKdKdjlb GROUP BY jksyj.jkszh ) consume_stats ON u.F_Id = consume_stats.EmployeeId + -- 有效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=1) + LEFT JOIN ( + SELECT + F_PersonId as EmployeeId, + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount + FROM ( + SELECT + F_PersonId, + F_WorkMonth, + F_MemberId, + F_Quantity + FROM lq_person_times_record + WHERE F_PersonId IS NOT NULL + AND F_IsEffective = 1 + AND F_PersonType = '健康师' + AND F_HasBilling = 1 + AND F_WorkDate >= DATE(@startTime) + AND F_WorkDate <= DATE(@endTime) + GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity + ) as distinct_headcount + GROUP BY F_PersonId + ) headcount_stats ON u.F_Id = headcount_stats.EmployeeId + + -- 有效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=1) + LEFT JOIN ( + SELECT + F_PersonId as EmployeeId, + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount + FROM ( + SELECT + F_PersonId, + F_WorkDate, + F_MemberId, + F_Quantity + FROM lq_person_times_record + WHERE F_PersonId IS NOT NULL + AND F_IsEffective = 1 + AND F_PersonType = '健康师' + AND F_HasBilling = 1 + AND F_WorkDate >= DATE(@startTime) + AND F_WorkDate <= DATE(@endTime) + GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity + ) as distinct_personcount + GROUP BY F_PersonId + ) personcount_stats ON u.F_Id = personcount_stats.EmployeeId + + -- 无效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=0) + LEFT JOIN ( + SELECT + F_PersonId as EmployeeId, + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount + FROM ( + SELECT + F_PersonId, + F_WorkMonth, + F_MemberId, + F_Quantity + FROM lq_person_times_record + WHERE F_PersonId IS NOT NULL + AND F_IsEffective = 1 + AND F_PersonType = '健康师' + AND F_HasBilling = 0 + AND F_WorkDate >= DATE(@startTime) + AND F_WorkDate <= DATE(@endTime) + GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity + ) as distinct_headcount + GROUP BY F_PersonId + ) invalid_headcount_stats ON u.F_Id = invalid_headcount_stats.EmployeeId + + -- 无效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=0) + LEFT JOIN ( + SELECT + F_PersonId as EmployeeId, + CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount + FROM ( + SELECT + F_PersonId, + F_WorkDate, + F_MemberId, + F_Quantity + FROM lq_person_times_record + WHERE F_PersonId IS NOT NULL + AND F_IsEffective = 1 + AND F_PersonType = '健康师' + AND F_HasBilling = 0 + AND F_WorkDate >= DATE(@startTime) + AND F_WorkDate <= DATE(@endTime) + GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity + ) as distinct_personcount + GROUP BY F_PersonId + ) invalid_personcount_stats ON u.F_Id = invalid_personcount_stats.EmployeeId + WHERE u.F_GW = '健康师' "; diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs index 44e29ff..f6a4761 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs @@ -1595,7 +1595,7 @@ namespace NCC.Extend.LqKhxx } /// - /// 批量更新所有会员信息(高性能版:使用SQL批量更新) + /// 批量更新所有会员信息(高性能版:使用SQL批量更新)【通过定时任务去执行,每天晚上执行一次】 /// /// [HttpPost("BatchUpdateMemberInfo")] diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs index 0f188de..fad69fd 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs @@ -228,7 +228,7 @@ namespace NCC.Extend price = x.Price, productCategory = x.ProductCategory, departmentId = x.DepartmentId, - departmentName = "", + departmentName = SqlFunc.Subqueryable().Where(y => y.Id == x.DepartmentId).Select(y => y.FullName), standardUnit = x.StandardUnit, onShelfStatus = x.OnShelfStatus, statisticsCategory = x.StatisticsCategory, diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqPurchaseRecordsService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqPurchaseRecordsService.cs index 2734be1..9683234 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqPurchaseRecordsService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqPurchaseRecordsService.cs @@ -28,7 +28,7 @@ namespace NCC.Extend.LqPurchaseRecords /// /// 购买记录表服务 /// - [ApiDescriptionSettings(Tag = "Extend",Name = "LqPurchaseRecords", Order = 200)] + [ApiDescriptionSettings(Tag = "Extend", Name = "LqPurchaseRecords", Order = 200)] [Route("api/Extend/[controller]")] public class LqPurchaseRecordsService : ILqPurchaseRecordsService, IDynamicApiController, ITransient { @@ -43,7 +43,7 @@ namespace NCC.Extend.LqPurchaseRecords ISqlSugarRepository lqPurchaseRecordsRepository, IUserManager userManager) { - _lqPurchaseRecordsRepository = lqPurchaseRecordsRepository; + _lqPurchaseRecordsRepository = lqPurchaseRecordsRepository; _db = _lqPurchaseRecordsRepository.Context; _userManager = userManager; } @@ -98,25 +98,25 @@ namespace NCC.Extend.LqPurchaseRecords .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0)) .WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.applicationId), p => p.ApplicationId.Contains(input.applicationId)) - .Select(it=> new LqPurchaseRecordsListOutput + .Select(it => new LqPurchaseRecordsListOutput { id = it.Id, - reimbursementCategoryId=it.ReimbursementCategoryId, - reimbursementCategoryName=it.ReimbursementCategoryName, - unitPrice=it.UnitPrice, - quantity=it.Quantity, - amount=it.Amount, - memo=it.Memo, - purchaseTime=it.PurchaseTime, - createTime=it.CreateTime, - createUser=it.CreateUser, - createUserStoreId=it.CreateUserStoreId, - approveStatus=it.ApproveStatus, - approveUser=it.ApproveUser, - approveTime=it.ApproveTime, - applicationId=it.ApplicationId, - }).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize); - return PageResult.SqlSugarPageResult(data); + reimbursementCategoryId = it.ReimbursementCategoryId, + reimbursementCategoryName = it.ReimbursementCategoryName, + unitPrice = it.UnitPrice, + quantity = it.Quantity, + amount = it.Amount, + memo = it.Memo, + purchaseTime = it.PurchaseTime, + createTime = it.CreateTime, + createUser = it.CreateUser, + createUserStoreId = it.CreateUserStoreId, + approveStatus = it.ApproveStatus, + approveUser = it.ApproveUser, + approveTime = it.ApproveTime, + applicationId = it.ApplicationId, + }).MergeTable().OrderBy(sidx + " " + input.sort).ToPagedListAsync(input.currentPage, input.pageSize); + return PageResult.SqlSugarPageResult(data); } /// @@ -171,25 +171,25 @@ namespace NCC.Extend.LqPurchaseRecords .WhereIF(queryApproveTime != null, p => p.ApproveTime >= new DateTime(startApproveTime.ToDate().Year, startApproveTime.ToDate().Month, startApproveTime.ToDate().Day, 0, 0, 0)) .WhereIF(queryApproveTime != null, p => p.ApproveTime <= new DateTime(endApproveTime.ToDate().Year, endApproveTime.ToDate().Month, endApproveTime.ToDate().Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.applicationId), p => p.ApplicationId.Contains(input.applicationId)) - .Select(it=> new LqPurchaseRecordsListOutput + .Select(it => new LqPurchaseRecordsListOutput { id = it.Id, - reimbursementCategoryId=it.ReimbursementCategoryId, - reimbursementCategoryName=it.ReimbursementCategoryName, - unitPrice=it.UnitPrice, - quantity=it.Quantity, - amount=it.Amount, - memo=it.Memo, - purchaseTime=it.PurchaseTime, - createTime=it.CreateTime, - createUser=it.CreateUser, - createUserStoreId=it.CreateUserStoreId, - approveStatus=it.ApproveStatus, - approveUser=it.ApproveUser, - approveTime=it.ApproveTime, - applicationId=it.ApplicationId, - }).MergeTable().OrderBy(sidx+" "+input.sort).ToListAsync(); - return data; + reimbursementCategoryId = it.ReimbursementCategoryId, + reimbursementCategoryName = it.ReimbursementCategoryName, + unitPrice = it.UnitPrice, + quantity = it.Quantity, + amount = it.Amount, + memo = it.Memo, + purchaseTime = it.PurchaseTime, + createTime = it.CreateTime, + createUser = it.CreateUser, + createUserStoreId = it.CreateUserStoreId, + approveStatus = it.ApproveStatus, + approveUser = it.ApproveUser, + approveTime = it.ApproveTime, + applicationId = it.ApplicationId, + }).MergeTable().OrderBy(sidx + " " + input.sort).ToListAsync(); + return data; } /// @@ -211,7 +211,7 @@ namespace NCC.Extend.LqPurchaseRecords { exportData = await this.GetNoPagingList(input); } - List 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(); + List 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(); ExcelConfig excelconfig = new ExcelConfig(); excelconfig.FileName = "购买记录表.xls"; excelconfig.HeadFont = "微软雅黑"; @@ -254,7 +254,7 @@ namespace NCC.Extend.LqPurchaseRecords //开启事务 _db.BeginTran(); //批量删除购买记录表 - await _db.Deleteable().In(d => d.Id,ids).ExecuteCommandAsync(); + await _db.Deleteable().In(d => d.Id, ids).ExecuteCommandAsync(); //关闭事务 _db.CommitTran(); } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs index 77942db..a9cd25a 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs @@ -53,6 +53,9 @@ using SqlSugar; using Yitter.IdGenerator; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_khxx; +using NCC.Extend.Entitys.lq_tkjlb; +using NCC.Extend.Entitys.lq_yaoyjl; +using NCC.Extend.Entitys.lq_yyjl; namespace NCC.Extend.LqStatistics { @@ -3504,6 +3507,7 @@ namespace NCC.Extend.LqStatistics AND F_PersonType = '健康师' AND F_WorkMonth = '{month}' AND F_IsEffective = 1 + AND F_HasBilling = 1 GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity ) as distinct_records"; @@ -3531,7 +3535,8 @@ namespace NCC.Extend.LqStatistics WHERE F_PersonId = '{userId}' AND F_PersonType = '健康师' AND F_WorkMonth = '{month}' - AND F_IsEffective = 1 + AND F_IsEffective = 1 + AND F_HasBilling = 1 GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity ) as distinct_records"; @@ -3947,6 +3952,754 @@ namespace NCC.Extend.LqStatistics } #endregion + #region 线索池客户统计报表 + /// + /// 获取线索池客户统计报表 + /// + /// + /// 根据拓客记录统计线索池客户的邀约、预约、消耗、开单等信息 + /// + /// 业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗 + /// + /// 示例请求: + /// ```json + /// { + /// "pageIndex": 1, + /// "pageSize": 20, + /// "startTime": "2025-10-01T00:00:00", + /// "endTime": "2025-10-31T23:59:59", + /// "storeIds": ["store1", "store2"], + /// "eventId": "event123" + /// } + /// ``` + /// + /// 参数说明: + /// - pageIndex: 页码,从1开始 + /// - pageSize: 每页数量 + /// - startTime: 拓客时间范围开始时间 + /// - endTime: 拓客时间范围结束时间 + /// - storeIds: 门店ID列表,可传多个 + /// - eventId: 拓客活动ID + /// + /// 返回数据说明: + /// - LeadCustomerId: 线索池客户(拓客编号) + /// - CustomerName: 客户姓名 + /// - ExpansionTime: 拓客时间 + /// - HasInvite: 是否邀约(是/否),通过拓客编号关联邀约表 + /// - HasAppointment: 是否预约(是/否),只统计通过邀约产生的预约(预约表的F_InviteId关联邀约表) + /// - HasConsume: 是否有消耗(是/否),只统计通过预约产生的耗卡(耗卡表的F_AppointmentId关联预约表) + /// - HasBilling: 是否开单(是/否),只统计通过预约产生的开单(开单表的F_AppointmentId关联预约表) + /// - NoBillingReason: 未开单原因,从预约记录的F_NoDealRemark字段获取 + /// - BillingAmount: 开卡金额,汇总通过预约产生的开单记录的整单业绩(zdyj) + /// - BillingItems: 开卡卡项,汇总通过预约产生的开单品项名称,多个用顿号分隔 + /// - ActualAppointmentCount: 实际预约记录数(不管是否通过邀约产生),用于问题分析 + /// - ActualConsumeCount: 实际消耗记录数(不管是否通过预约产生),用于问题分析 + /// - ActualBillingCount: 实际开单记录数(不管是否通过预约产生),用于问题分析 + /// - Analysis: 问题分析说明,自动分析数据异常情况,如:有预约记录但未通过邀约产生、有消耗记录但未通过预约产生等 + /// + /// 返回示例: + /// ```json + /// { + /// "list": [ + /// { + /// "LeadCustomerId": "751248448816153862", + /// "CustomerName": "王女士", + /// "ExpansionTime": "2025-10-24T03:33:10.000Z", + /// "HasInvite": "否", + /// "HasAppointment": "否", + /// "HasConsume": "否", + /// "HasBilling": "否", + /// "NoBillingReason": null, + /// "BillingAmount": 0, + /// "BillingItems": null, + /// "ActualAppointmentCount": 3, + /// "ActualConsumeCount": 4, + /// "ActualBillingCount": 5, + /// "Analysis": "有3条预约记录,但未通过邀约产生(F_InviteId为null);有4条消耗记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生);有5条开单记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)" + /// } + /// ], + /// "pagination": { + /// "pageIndex": 1, + /// "pageSize": 20, + /// "total": 1511 + /// } + /// } + /// ``` + /// + /// 查询条件 + /// 线索池客户统计报表列表,包含统计数据和问题分析 + /// 查询成功,返回统计报表列表和分页信息 + /// 参数错误 + /// 服务器内部错误 + [HttpPost("get-lead-customer-statistics-list")] + public async Task GetLeadCustomerStatisticsList([FromBody] LeadCustomerStatisticsListQueryInput input) + { + try + { + // 构建WHERE条件 + var whereConditions = new List(); + var parameters = new List(); + + if (input.StartTime.HasValue) + { + whereConditions.Add("tk.F_ExpansionTime >= @StartTime"); + parameters.Add(new SugarParameter("@StartTime", input.StartTime.Value)); + } + + if (input.EndTime.HasValue) + { + whereConditions.Add("tk.F_ExpansionTime <= @EndTime"); + parameters.Add(new SugarParameter("@EndTime", input.EndTime.Value)); + } + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}")); + whereConditions.Add($"tk.F_StoreId IN ({storeIdParams})"); + for (int i = 0; i < input.StoreIds.Count; i++) + { + parameters.Add(new SugarParameter($"@StoreId{i}", input.StoreIds[i])); + } + } + + if (!string.IsNullOrEmpty(input.EventId)) + { + whereConditions.Add("tk.F_EventId = @EventId"); + parameters.Add(new SugarParameter("@EventId", input.EventId)); + } + + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : ""; + + // 使用子查询优化性能,避免复杂的JOIN和GROUP BY + var sql = $@" + SELECT + tk.F_Id as LeadCustomerId, + tk.F_CustomerName as CustomerName, + tk.F_ExpansionTime as ExpansionTime, + -- 是否邀约:通过拓客编号关联 + CASE WHEN yaoy_stats.has_invite = 1 THEN '是' ELSE '否' END as HasInvite, + -- 是否预约:通过邀约ID关联(只统计通过邀约产生的预约) + CASE WHEN yy_stats.has_appointment = 1 THEN '是' ELSE '否' END as HasAppointment, + -- 是否有消耗:通过预约ID关联(只统计通过预约产生的耗卡) + CASE WHEN xh_stats.has_consume = 1 THEN '是' ELSE '否' END as HasConsume, + -- 是否开单:通过预约ID关联(只统计通过预约产生的开单) + CASE WHEN kd_stats.has_billing = 1 THEN '是' ELSE '否' END as HasBilling, + -- 未开单原因:从预约记录中获取(只取通过邀约产生的预约) + yy_stats.no_billing_reason as NoBillingReason, + -- 开卡金额:汇总通过预约产生的开单记录 + COALESCE(kd_stats.billing_amount, 0) as BillingAmount, + -- 开卡卡项:汇总通过预约产生的开单品项 + kd_stats.billing_items as BillingItems, + -- 实际预约记录数(不管是否通过邀约产生) + COALESCE(yy_actual.count, 0) as ActualAppointmentCount, + -- 实际消耗记录数(不管是否通过预约产生) + COALESCE(xh_actual.count, 0) as ActualConsumeCount, + -- 实际开单记录数(不管是否通过预约产生) + COALESCE(kd_actual.count, 0) as ActualBillingCount + FROM lq_tkjlb tk + -- 邀约统计子查询 + LEFT JOIN ( + SELECT + yaoy.tkbh as tk_id, + 1 as has_invite + FROM lq_yaoyjl yaoy + GROUP BY yaoy.tkbh + ) yaoy_stats ON yaoy_stats.tk_id = tk.F_Id + -- 预约统计子查询(只统计通过邀约产生的预约) + LEFT JOIN ( + SELECT + tk_inner.F_MemberId as member_id, + 1 as has_appointment, + MAX(yy.F_NoDealRemark) as no_billing_reason + FROM lq_tkjlb tk_inner + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + GROUP BY tk_inner.F_MemberId + ) yy_stats ON yy_stats.member_id = tk.F_MemberId + -- 消耗统计子查询(只统计通过预约产生的耗卡) + LEFT JOIN ( + SELECT + tk_inner.F_MemberId as member_id, + 1 as has_consume + FROM lq_tkjlb tk_inner + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + INNER JOIN lq_xh_hyhk xh ON xh.F_AppointmentId = yy.F_Id AND xh.F_IsEffective = 1 + GROUP BY tk_inner.F_MemberId + ) xh_stats ON xh_stats.member_id = tk.F_MemberId + -- 开单统计子查询(只统计通过预约产生的开单,包含金额和品项) + LEFT JOIN ( + SELECT + tk_inner.F_MemberId as member_id, + 1 as has_billing, + SUM(kd.zdyj) as billing_amount, + GROUP_CONCAT(DISTINCT kdpx.pxmc SEPARATOR '、') as billing_items + FROM lq_tkjlb tk_inner + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk_inner.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + INNER JOIN lq_kd_kdjlb kd ON kd.F_AppointmentId = yy.F_Id AND kd.F_IsEffective = 1 + LEFT JOIN lq_kd_pxmx kdpx ON kdpx.glkdbh = kd.F_Id AND kdpx.F_IsEffective = 1 + GROUP BY tk_inner.F_MemberId + ) kd_stats ON kd_stats.member_id = tk.F_MemberId + -- 实际预约记录数统计(不管是否通过邀约产生) + LEFT JOIN ( + SELECT + yy.gk as member_id, + COUNT(*) as count + FROM lq_yyjl yy + GROUP BY yy.gk + ) yy_actual ON yy_actual.member_id = tk.F_MemberId + -- 实际消耗记录数统计(不管是否通过预约产生) + LEFT JOIN ( + SELECT + xh.hy as member_id, + COUNT(*) as count + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + GROUP BY xh.hy + ) xh_actual ON xh_actual.member_id = tk.F_MemberId + -- 实际开单记录数统计(不管是否通过预约产生) + LEFT JOIN ( + SELECT + kd.kdhy as member_id, + COUNT(*) as count + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + GROUP BY kd.kdhy + ) kd_actual ON kd_actual.member_id = tk.F_MemberId + {whereClause} + ORDER BY tk.F_ExpansionTime DESC + LIMIT @PageSize OFFSET @Offset"; + + parameters.Add(new SugarParameter("@PageSize", input.PageSize)); + parameters.Add(new SugarParameter("@Offset", (input.PageIndex - 1) * input.PageSize)); + + // 查询总数 + var countSql = $@" + SELECT COUNT(*) + FROM lq_tkjlb tk + {whereClause}"; + + var countParameters = parameters.Where(p => p.ParameterName != "@PageSize" && p.ParameterName != "@Offset").ToList(); + var totalCount = await _db.Ado.GetIntAsync(countSql, countParameters); + + // 执行查询 + var result = await _db.Ado.SqlQueryAsync(sql, parameters); + + // 生成问题分析说明 + foreach (var item in result) + { + var analysisList = new List(); + + if (item.HasInvite == "否" && item.HasAppointment == "否" && item.ActualAppointmentCount > 0) + { + analysisList.Add($"有{item.ActualAppointmentCount}条预约记录,但未通过邀约产生(F_InviteId为null)"); + } + + if (item.HasAppointment == "否" && item.HasConsume == "否" && item.ActualConsumeCount > 0) + { + analysisList.Add($"有{item.ActualConsumeCount}条消耗记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)"); + } + + if (item.HasAppointment == "否" && item.HasBilling == "否" && item.ActualBillingCount > 0) + { + analysisList.Add($"有{item.ActualBillingCount}条开单记录,但未通过预约产生(F_AppointmentId为null或预约未通过邀约产生)"); + } + + if (item.HasInvite == "是" && item.HasAppointment == "否" && item.ActualAppointmentCount > 0) + { + analysisList.Add($"有邀约记录,有{item.ActualAppointmentCount}条预约记录,但预约记录的F_InviteId未关联到邀约记录"); + } + + if (item.HasAppointment == "是" && item.HasConsume == "否" && item.ActualConsumeCount > 0) + { + analysisList.Add($"有预约记录,有{item.ActualConsumeCount}条消耗记录,但消耗记录的F_AppointmentId未关联到预约记录"); + } + + if (item.HasAppointment == "是" && item.HasBilling == "否" && item.ActualBillingCount > 0) + { + analysisList.Add($"有预约记录,有{item.ActualBillingCount}条开单记录,但开单记录的F_AppointmentId未关联到预约记录"); + } + + if (analysisList.Count == 0) + { + item.Analysis = "数据正常,符合业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗"; + } + else + { + item.Analysis = string.Join(";", analysisList); + } + } + + return new + { + list = result, + pagination = new + { + pageIndex = input.PageIndex, + pageSize = input.PageSize, + total = totalCount + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取线索池客户统计报表失败"); + throw NCCException.Oh($"获取线索池客户统计报表失败:{ex.Message}"); + } + } + #endregion + + #region 门店统计报表 + /// + /// 获取门店统计报表 + /// + /// + /// 按门店统计拓客、邀约、预约、消耗、开单等数据 + /// + /// 业务链路:拓客 -> 邀约 -> 预约 -> 开单/消耗 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01T00:00:00", + /// "endTime": "2025-10-31T23:59:59", + /// "storeIds": ["store1", "store2"], + /// "eventId": "event123" + /// } + /// ``` + /// + /// 参数说明: + /// - startTime: 拓客时间范围开始时间 + /// - endTime: 拓客时间范围结束时间 + /// - storeIds: 门店ID列表,可传多个 + /// - eventId: 拓客活动ID + /// + /// 返回数据说明: + /// - StoreId: 门店ID + /// - StoreName: 门店名称 + /// - TotalCount: 总人数(从客户信息表按归属门店统计) + /// - TkMemberCount: 拓客人数(拓客记录数,不去重) + /// - InviteCount: 邀约数(通过拓客编号关联的邀约记录数) + /// - AppointmentCount: 预约数(通过邀约ID关联的预约记录数,只统计通过邀约产生的预约) + /// - ConsumeCount: 耗卡数(通过预约ID关联的耗卡记录数,只统计通过预约产生的耗卡) + /// - BillingCount: 开单数(通过预约ID关联的开单记录数,只统计通过预约产生的开单) + /// - BillingAmount: 开单金额(通过预约ID关联的开单记录金额汇总) + /// + /// 返回示例: + /// ```json + /// { + /// "list": [ + /// { + /// "StoreId": "1649328471923847169", + /// "StoreName": "绿纤紫荆店", + /// "TotalCount": 119, + /// "TkMemberCount": 117, + /// "InviteCount": 4, + /// "AppointmentCount": 2, + /// "ConsumeCount": 1, + /// "BillingCount": 1, + /// "BillingAmount": 199.00 + /// } + /// ] + /// } + /// ``` + /// + /// 查询条件 + /// 门店统计报表列表 + /// 查询成功,返回门店统计报表列表 + /// 参数错误 + /// 服务器内部错误 + [HttpPost("get-store-statistics-list")] + public async Task GetStoreStatisticsList([FromBody] StoreStatisticsListQueryInput input) + { + try + { + // 构建WHERE条件(带表别名,用于子查询) + var whereConditions = new List(); + // 构建WHERE条件(不带表别名,用于UNION的SELECT) + var whereConditionsNoAlias = new List(); + var parameters = new List(); + + if (input.StartTime.HasValue) + { + whereConditions.Add("tk.F_ExpansionTime >= @StartTime"); + whereConditionsNoAlias.Add("F_ExpansionTime >= @StartTime"); + parameters.Add(new SugarParameter("@StartTime", input.StartTime.Value)); + } + + if (input.EndTime.HasValue) + { + whereConditions.Add("tk.F_ExpansionTime <= @EndTime"); + whereConditionsNoAlias.Add("F_ExpansionTime <= @EndTime"); + parameters.Add(new SugarParameter("@EndTime", input.EndTime.Value)); + } + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}")); + whereConditions.Add($"tk.F_StoreId IN ({storeIdParams})"); + whereConditionsNoAlias.Add($"F_StoreId IN ({storeIdParams})"); + for (int i = 0; i < input.StoreIds.Count; i++) + { + parameters.Add(new SugarParameter($"@StoreId{i}", input.StoreIds[i])); + } + } + + if (!string.IsNullOrEmpty(input.EventId)) + { + whereConditions.Add("tk.F_EventId = @EventId"); + whereConditionsNoAlias.Add("F_EventId = @EventId"); + parameters.Add(new SugarParameter("@EventId", input.EventId)); + } + + var whereClause = whereConditions.Any() ? "WHERE " + string.Join(" AND ", whereConditions) : ""; + var whereClauseNoAlias = whereConditionsNoAlias.Any() ? "WHERE " + string.Join(" AND ", whereConditionsNoAlias) : ""; + + // 构建门店筛选条件(用于客户信息表查询) + var khWhereConditions = new List(); + var khWhereConditionsNoAlias = new List(); + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdParams = string.Join(",", input.StoreIds.Select((_, i) => $"@StoreId{i}")); + khWhereConditions.Add($"kh.gsmd IN ({storeIdParams})"); + khWhereConditionsNoAlias.Add($"gsmd IN ({storeIdParams})"); + } + + var khWhereClause = khWhereConditions.Any() ? "WHERE " + string.Join(" AND ", khWhereConditions) : "WHERE kh.gsmd IS NOT NULL"; + var khWhereClauseNoAlias = khWhereConditionsNoAlias.Any() ? "WHERE " + string.Join(" AND ", khWhereConditionsNoAlias) : "WHERE gsmd IS NOT NULL"; + + // 使用子查询优化性能,避免复杂的JOIN + var sql = $@" + SELECT + COALESCE(total_stats.StoreId, tk_stats.StoreId, yaoy_stats.StoreId, yy_stats.StoreId, xh_stats.StoreId, kd_stats.StoreId) as StoreId, + COALESCE(md.dm, '') as StoreName, + COALESCE(total_stats.TotalCount, 0) as TotalCount, + COALESCE(tk_stats.TkMemberCount, 0) as TkMemberCount, + COALESCE(yaoy_stats.InviteCount, 0) as InviteCount, + COALESCE(yy_stats.AppointmentCount, 0) as AppointmentCount, + COALESCE(xh_stats.ConsumeCount, 0) as ConsumeCount, + COALESCE(kd_stats.BillingCount, 0) as BillingCount, + COALESCE(kd_stats.BillingAmount, 0) as BillingAmount + FROM ( + SELECT DISTINCT StoreId FROM ( + SELECT gsmd as StoreId FROM lq_khxx {khWhereClauseNoAlias} + UNION + SELECT F_StoreId as StoreId FROM lq_tkjlb {whereClauseNoAlias} + ) as all_stores + ) as stores + LEFT JOIN lq_mdxx md ON md.F_Id = stores.StoreId + -- 总人数统计(从客户信息表按归属门店统计) + LEFT JOIN ( + SELECT + kh.gsmd as StoreId, + COUNT(*) as TotalCount + FROM lq_khxx kh + {khWhereClause} + GROUP BY kh.gsmd + ) total_stats ON total_stats.StoreId = stores.StoreId + -- 拓客人数统计(不用去重) + LEFT JOIN ( + SELECT + tk.F_StoreId as StoreId, + COUNT(tk.F_MemberId) as TkMemberCount + FROM lq_tkjlb tk + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)} + GROUP BY tk.F_StoreId + ) tk_stats ON tk_stats.StoreId = stores.StoreId + -- 邀约数统计 + LEFT JOIN ( + SELECT + tk.F_StoreId as StoreId, + COUNT(DISTINCT yaoy.F_Id) as InviteCount + FROM lq_tkjlb tk + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)} + GROUP BY tk.F_StoreId + ) yaoy_stats ON yaoy_stats.StoreId = stores.StoreId + -- 预约数统计(通过邀约ID关联) + LEFT JOIN ( + SELECT + tk.F_StoreId as StoreId, + COUNT(DISTINCT yy.F_Id) as AppointmentCount + FROM lq_tkjlb tk + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)} + GROUP BY tk.F_StoreId + ) yy_stats ON yy_stats.StoreId = stores.StoreId + -- 耗卡数统计(通过预约ID关联) + LEFT JOIN ( + SELECT + tk.F_StoreId as StoreId, + COUNT(DISTINCT xh.F_Id) as ConsumeCount + FROM lq_tkjlb tk + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + INNER JOIN lq_xh_hyhk xh ON xh.F_AppointmentId = yy.F_Id AND xh.F_IsEffective = 1 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)} + GROUP BY tk.F_StoreId + ) xh_stats ON xh_stats.StoreId = stores.StoreId + -- 开单数和开单金额统计(通过预约ID关联) + LEFT JOIN ( + SELECT + tk.F_StoreId as StoreId, + COUNT(DISTINCT kd.F_Id) as BillingCount, + SUM(kd.zdyj) as BillingAmount + FROM lq_tkjlb tk + INNER JOIN lq_yaoyjl yaoy ON yaoy.tkbh = tk.F_Id + INNER JOIN lq_yyjl yy ON yy.F_InviteId = yaoy.F_Id + INNER JOIN lq_kd_kdjlb kd ON kd.F_AppointmentId = yy.F_Id AND kd.F_IsEffective = 1 + {(string.IsNullOrEmpty(whereClause) ? "" : whereClause)} + GROUP BY tk.F_StoreId + ) kd_stats ON kd_stats.StoreId = stores.StoreId + WHERE stores.StoreId IS NOT NULL + ORDER BY stores.StoreId"; + + // 执行查询 + var result = await _db.Ado.SqlQueryAsync(sql, parameters); + + return new + { + list = result + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取门店统计报表失败"); + throw NCCException.Oh($"获取门店统计报表失败:{ex.Message}"); + } + } + #endregion + + #region 会员升单统计 + /// + /// 获取会员升单统计(前4单中是否有升医美、升科美、升生美) + /// + /// + /// 统计每个会员的前4单开单记录中是否有升医美、升科美、升生美 + /// + /// 示例请求: + /// ```json + /// { + /// "pageIndex": 1, + /// "pageSize": 20, + /// "memberIds": ["member1", "member2"], + /// "hasUpgradeMedicalBeauty": true, + /// "hasUpgradeTechBeauty": false, + /// "hasUpgradeLifeBeauty": null + /// } + /// ``` + /// + /// 参数说明: + /// - pageIndex: 页码,从1开始 + /// - pageSize: 每页数量 + /// - memberIds: 会员ID列表(可选,不传则查询所有会员) + /// - hasUpgradeMedicalBeauty: 是否升医美(true-是,false-否,null-不筛选) + /// - hasUpgradeTechBeauty: 是否升科美(true-是,false-否,null-不筛选) + /// - hasUpgradeLifeBeauty: 是否升生美(true-是,false-否,null-不筛选) + /// + /// 返回数据说明: + /// - MemberId: 会员ID + /// - MemberName: 会员姓名 + /// - MemberPhone: 会员手机号 + /// - HasUpgradeMedicalBeauty: 前4单中是否有升医美(是/否) + /// - HasUpgradeTechBeauty: 前4单中是否有升科美(是/否) + /// - HasUpgradeLifeBeauty: 前4单中是否有升生美(是/否) + /// + /// 返回示例: + /// ```json + /// { + /// "list": [ + /// { + /// "MemberId": "744326092097062149", + /// "MemberName": "张女士", + /// "MemberPhone": "13800138000", + /// "HasUpgradeMedicalBeauty": "否", + /// "HasUpgradeTechBeauty": "否", + /// "HasUpgradeLifeBeauty": "否" + /// } + /// ], + /// "pagination": { + /// "pageIndex": 1, + /// "pageSize": 20, + /// "total": 100 + /// } + /// } + /// ``` + /// + /// 查询条件 + /// 会员升单统计列表 + /// 查询成功,返回会员升单统计列表 + /// 参数错误 + /// 服务器内部错误 + [HttpPost("get-member-upgrade-statistics-list")] + public async Task GetMemberUpgradeStatisticsList([FromBody] MemberUpgradeStatisticsListQueryInput input) + { + try + { + // 构建WHERE条件 + var whereConditions = new List(); + var parameters = new List(); + + if (input.MemberIds != null && input.MemberIds.Any()) + { + var memberIdParams = string.Join(",", input.MemberIds.Select((_, i) => $"@MemberId{i}")); + whereConditions.Add($"kd.kdhy IN ({memberIdParams})"); + for (int i = 0; i < input.MemberIds.Count; i++) + { + parameters.Add(new SugarParameter($"@MemberId{i}", input.MemberIds[i])); + } + } + + var whereClause = whereConditions.Any() ? "AND " + string.Join(" AND ", whereConditions) : ""; + + // 构建HAVING条件(用于筛选升单条件) + var havingConditions = new List(); + if (input.HasUpgradeMedicalBeauty.HasValue) + { + if (input.HasUpgradeMedicalBeauty.Value) + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1"); + } + else + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 0"); + } + } + if (input.HasUpgradeTechBeauty.HasValue) + { + if (input.HasUpgradeTechBeauty.Value) + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1"); + } + else + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 0"); + } + } + if (input.HasUpgradeLifeBeauty.HasValue) + { + if (input.HasUpgradeLifeBeauty.Value) + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1"); + } + else + { + havingConditions.Add("MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 0"); + } + } + + var havingClause = havingConditions.Any() ? "HAVING " + string.Join(" AND ", havingConditions) : ""; + + // 分页参数 + var offset = (input.PageIndex - 1) * input.PageSize; + parameters.Add(new SugarParameter("@PageSize", input.PageSize)); + parameters.Add(new SugarParameter("@Offset", offset)); + + // 查询每个会员的前4单中是否有升医美、升科美、升生美 + var sql = $@" + SELECT + kd.kdhy as MemberId, + kh.khmc as MemberName, + kh.sjh as MemberPhone, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeMedicalBeauty, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeTechBeauty, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeLifeBeauty + FROM ( + SELECT + kd.kdhy, + kd.F_Id, + kd.kdrq, + kd.F_CreateTime, + kd.F_UpgradeMedicalBeauty, + kd.F_UpgradeTechBeauty, + kd.F_UpgradeLifeBeauty + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + AND kd.kdhy IS NOT NULL + {whereClause} + AND ( + SELECT COUNT(*) + FROM lq_kd_kdjlb kd2 + WHERE kd2.kdhy = kd.kdhy + AND kd2.F_IsEffective = 1 + AND ( + kd2.kdrq > kd.kdrq + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime > kd.F_CreateTime) + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime = kd.F_CreateTime AND kd2.F_Id > kd.F_Id) + ) + ) < 4 + ) kd + LEFT JOIN lq_khxx kh ON kh.F_Id = kd.kdhy + GROUP BY kd.kdhy, kh.khmc, kh.sjh + {havingClause} + ORDER BY kd.kdhy + LIMIT @PageSize OFFSET @Offset"; + + // 查询总数(需要应用相同的HAVING条件) + var countSql = $@" + SELECT COUNT(*) + FROM ( + SELECT + kd.kdhy, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeMedicalBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeMedicalBeauty, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeTechBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeTechBeauty, + CASE WHEN MAX(CASE WHEN kd.F_UpgradeLifeBeauty = '是' THEN 1 ELSE 0 END) = 1 THEN '是' ELSE '否' END as HasUpgradeLifeBeauty + FROM ( + SELECT + kd.kdhy, + kd.F_Id, + kd.kdrq, + kd.F_CreateTime, + kd.F_UpgradeMedicalBeauty, + kd.F_UpgradeTechBeauty, + kd.F_UpgradeLifeBeauty + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + AND kd.kdhy IS NOT NULL + {whereClause} + AND ( + SELECT COUNT(*) + FROM lq_kd_kdjlb kd2 + WHERE kd2.kdhy = kd.kdhy + AND kd2.F_IsEffective = 1 + AND ( + kd2.kdrq > kd.kdrq + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime > kd.F_CreateTime) + OR (kd2.kdrq = kd.kdrq AND kd2.F_CreateTime = kd.F_CreateTime AND kd2.F_Id > kd.F_Id) + ) + ) < 4 + ) kd + GROUP BY kd.kdhy + {havingClause} + ) as filtered_results"; + + var countParameters = parameters.Where(p => p.ParameterName != "@PageSize" && p.ParameterName != "@Offset").ToList(); + var totalCount = await _db.Ado.GetIntAsync(countSql, countParameters); + + // 执行查询 + var result = await _db.Ado.SqlQueryAsync(sql, parameters); + + return new + { + list = result, + pagination = new + { + pageIndex = input.PageIndex, + pageSize = input.PageSize, + total = totalCount + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取会员升单统计失败"); + throw NCCException.Oh($"获取会员升单统计失败:{ex.Message}"); + } + } + #endregion + } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreConsumableInventoryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreConsumableInventoryService.cs index 367f998..1b799fd 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreConsumableInventoryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreConsumableInventoryService.cs @@ -1,13 +1,16 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Common.Core.Manager; using NCC.Common.Enum; +using NCC.Common.Extension; using NCC.Common.Filter; using NCC.Dependency; using NCC.DynamicApiController; +using NCC.Extend.Entitys.Dto.Common; using NCC.Extend.Entitys.Dto.LqStoreConsumableInventory; using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_mdxx; @@ -367,6 +370,26 @@ namespace NCC.Extend } } #endregion + + + #region 获取消耗品产品类型枚举内容 + /// + /// 获取消耗品产品类型枚举内容 + /// + /// 消耗品产品类型枚举列表 + [HttpGet("consumable-product-type")] + public List GetConsumableProductTypeSelector() + { + return Enum.GetValues() + .Select(e => new EnumOutput + { + Value = (int)e, + Name = e.ToString(), + Description = e.GetDescription(), + }) + .ToList(); + } + #endregion } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs index 781dce3..e13d609 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs @@ -1757,11 +1757,20 @@ namespace NCC.Extend.LqXhHyhk var kjbsyjList = await _db.Queryable().Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); // 查询已存在的人次记录(按BusinessId去重,避免重复添加) - var existingBusinessIds = await _db.Queryable() - .Where(x => consumeIds.Contains(x.BusinessId) && x.BusinessType == "耗卡" && x.IsEffective == StatusEnum.有效.GetHashCode()) - .Select(x => x.BusinessId) - .Distinct() - .ToListAsync(); + var existingBusinessIds = await _db.Queryable().Where(x => consumeIds.Contains(x.BusinessId) && x.BusinessType == "耗卡" && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => x.BusinessId).Distinct().ToListAsync(); + + // 批量查询所有会员是否有开单记录(优化:避免在循环中执行N次查询) + var memberIds = consumeList.Where(x => !string.IsNullOrEmpty(x.Hy)).Select(x => x.Hy).Distinct().ToList(); + var membersWithBilling = new HashSet(); + if (memberIds.Any()) + { + var billingMemberIds = await _db.Queryable() + .Where(x => memberIds.Contains(x.Kdhy) && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Sfyj > 0) + .Select(x => x.Kdhy) + .Distinct() + .ToListAsync(); + membersWithBilling = new HashSet(billingMemberIds); + } // 3. 构建人次记录列表 var personTimesRecords = new List(); @@ -1782,19 +1791,14 @@ namespace NCC.Extend.LqXhHyhk } var workDate = consume.Hksj.Value.Date; // 工作日期(用于人次统计) var workMonth = consume.Hksj.Value.ToString("yyyyMM"); // 工作月份(用于人头统计) - + //查看该会员是否在开单记录表中存在开单记录(从批量查询结果中查找) + var billingRecord = !string.IsNullOrEmpty(consume.Hy) && membersWithBilling.Contains(consume.Hy); // 处理健康师业绩:去重后计算人次数量(剔除T区健康师) - 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(); + 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(); if (consumeJksyjList.Any()) { // 按健康师ID去重 - var distinctJksyjList = consumeJksyjList - .GroupBy(x => x.Jks) - .Select(g => g.First()) - .ToList(); + var distinctJksyjList = consumeJksyjList.GroupBy(x => x.Jks).Select(g => g.First()).ToList(); // 计算人次数量:1 / 健康师数量 var jksQuantity = distinctJksyjList.Count > 0 ? 1.0m / distinctJksyjList.Count : 0; @@ -1815,23 +1819,18 @@ namespace NCC.Extend.LqXhHyhk WorkMonth = workMonth, Quantity = jksQuantity, CreateTime = DateTime.Now, - IsEffective = StatusEnum.有效.GetHashCode() + IsEffective = StatusEnum.有效.GetHashCode(), + HasBilling = billingRecord ? 1 : 0 }); } } // 处理科技老师业绩:去重后计算人次数量(剔除T区科技老师) - 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(); + 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(); if (consumeKjbsyjList.Any()) { // 按科技老师ID去重 - var distinctKjbsyjList = consumeKjbsyjList - .GroupBy(x => x.Kjbls) - .Select(g => g.First()) - .ToList(); + var distinctKjbsyjList = consumeKjbsyjList.GroupBy(x => x.Kjbls).Select(g => g.First()).ToList(); // 计算人次数量:1 / 科技老师数量 var kjbsQuantity = distinctKjbsyjList.Count > 0 ? 1.0m / distinctKjbsyjList.Count : 0; @@ -1852,7 +1851,8 @@ namespace NCC.Extend.LqXhHyhk WorkMonth = workMonth, Quantity = kjbsQuantity, CreateTime = DateTime.Now, - IsEffective = StatusEnum.有效.GetHashCode() + IsEffective = StatusEnum.有效.GetHashCode(), + HasBilling = billingRecord ? 1 : 0 }); } } @@ -1865,9 +1865,7 @@ namespace NCC.Extend.LqXhHyhk // 如果没有指定耗卡ID,不删除任何记录(因为已经在构建记录列表时跳过了已存在的记录) if (!string.IsNullOrEmpty(consumeId)) { - await _db.Deleteable() - .Where(x => x.BusinessId == consumeId && x.BusinessType == "耗卡") - .ExecuteCommandAsync(); + await _db.Deleteable().Where(x => x.BusinessId == consumeId && x.BusinessType == "耗卡").ExecuteCommandAsync(); } // 批量插入新记录 diff --git a/sql/分析健康师消耗项目数差异.sql b/sql/分析健康师消耗项目数差异.sql new file mode 100644 index 0000000..d3ca8f0 --- /dev/null +++ b/sql/分析健康师消耗项目数差异.sql @@ -0,0 +1,93 @@ +-- ============================================ +-- 分析健康师消耗项目数差异 +-- 员工ID: 18566028067 (李芳) +-- ============================================ + +-- 1. 接口统计逻辑(使用耗卡时间,时间范围:2025-11-01 到 2025-11-25 11:55:32) +SELECT + jksyj.jkszh as EmployeeId, + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount_接口逻辑 +FROM lq_xh_jksyj jksyj +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE jksyj.jkszh = '18566028067' + AND jksyj.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '2025-11-01 00:00:00' + AND hyhk.hksj <= '2025-11-25 11:55:32' +GROUP BY jksyj.jkszh; + +-- 2. 整个11月的统计(使用业绩时间) +SELECT + jksyj.jkszh as EmployeeId, + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount_整个11月 +FROM lq_xh_jksyj jksyj +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND DATE_FORMAT(jksyj.yjsj, '%Y%m') = '202511' +GROUP BY jksyj.jkszh; + +-- 3. 查看11月25日之后的记录(可能导致差异的原因) +SELECT + jksyj.jkszh as EmployeeId, + jksyj.F_kdpxNumber as 项目数, + jksyj.yjsj as 业绩时间, + hyhk.hksj as 耗卡时间, + hyhk.F_IsEffective as 耗卡记录是否有效, + jksyj.F_IsEffective as 业绩记录是否有效 +FROM lq_xh_jksyj jksyj +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND ( + (jksyj.yjsj >= '2025-11-25 11:55:32' AND jksyj.yjsj < '2025-12-01') + OR (hyhk.hksj >= '2025-11-25 11:55:32' AND hyhk.hksj < '2025-12-01') + ) +ORDER BY jksyj.yjsj; + +-- 4. 查看耗卡记录无效但业绩记录有效的记录(可能导致差异的原因) +SELECT + jksyj.jkszh as EmployeeId, + jksyj.F_kdpxNumber as 项目数, + jksyj.yjsj as 业绩时间, + hyhk.hksj as 耗卡时间, + hyhk.F_IsEffective as 耗卡记录是否有效, + jksyj.F_IsEffective as 业绩记录是否有效 +FROM lq_xh_jksyj jksyj +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND (hyhk.F_IsEffective != 1 OR hyhk.F_IsEffective IS NULL) + AND jksyj.yjsj >= '2025-11-01' + AND jksyj.yjsj < '2025-12-01' +ORDER BY jksyj.yjsj; + +-- 5. 查看耗卡时间和业绩时间不一致的记录(可能导致差异的原因) +SELECT + jksyj.jkszh as EmployeeId, + jksyj.F_kdpxNumber as 项目数, + jksyj.yjsj as 业绩时间, + hyhk.hksj as 耗卡时间, + DATEDIFF(jksyj.yjsj, hyhk.hksj) as 时间差_天, + CASE + WHEN hyhk.hksj >= '2025-11-01 00:00:00' AND hyhk.hksj <= '2025-11-25 11:55:32' THEN '在接口时间范围内' + ELSE '不在接口时间范围内' + END as 是否在接口时间范围 +FROM lq_xh_jksyj jksyj +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND ( + (jksyj.yjsj >= '2025-11-01' AND jksyj.yjsj < '2025-12-01') + OR (hyhk.hksj >= '2025-11-01' AND hyhk.hksj < '2025-12-01') + ) + AND ( + jksyj.yjsj < '2025-11-01' + OR jksyj.yjsj >= '2025-12-01' + OR hyhk.hksj < '2025-11-01' + OR hyhk.hksj >= '2025-12-01' + OR DATEDIFF(jksyj.yjsj, hyhk.hksj) != 0 + ) +ORDER BY jksyj.yjsj; + + diff --git a/sql/同步健康师业绩表品项分类和品项ID.sql b/sql/同步健康师业绩表品项分类和品项ID.sql new file mode 100644 index 0000000..1e55a12 --- /dev/null +++ b/sql/同步健康师业绩表品项分类和品项ID.sql @@ -0,0 +1,61 @@ +-- 同步健康师业绩表和科技老师业绩表中的品项分类和品项ID字段 +-- 数据来源:通过关联的品项明细表获取 + +-- ============================================ +-- 健康师业绩表同步 +-- ============================================ + +-- 1. 开单健康师业绩表:从开单品项明细表(lq_kd_pxmx)同步 +UPDATE lq_kd_jksyj kd +INNER JOIN lq_kd_pxmx px ON px.F_Id = kd.F_kdpxid +SET + kd.F_ItemCategory = px.F_ItemCategory, + kd.F_ItemId = px.px +WHERE kd.F_kdpxid IS NOT NULL; + +-- 2. 耗卡健康师业绩表:从耗卡品项明细表(lq_xh_pxmx)同步 +UPDATE lq_xh_jksyj xh +INNER JOIN lq_xh_pxmx px ON px.F_Id = xh.F_kdpxid +SET + xh.F_ItemCategory = px.F_ItemCategory, + xh.F_ItemId = px.px +WHERE xh.F_kdpxid IS NOT NULL; + +-- 3. 退卡健康师业绩表:从退卡品项明细表(lq_hytk_mx)同步 +-- 注意:F_CardReturn 关联到 lq_hytk_mx.F_Id,F_tkpxid 是项目资料表ID(品项ID) +UPDATE lq_hytk_jksyj tk +INNER JOIN lq_hytk_mx mx ON mx.F_Id = tk.F_CardReturn +SET + tk.F_ItemCategory = mx.F_ItemCategory, + tk.F_ItemId = mx.px +WHERE tk.F_CardReturn IS NOT NULL; + +-- ============================================ +-- 科技老师业绩表同步 +-- ============================================ + +-- 4. 开单科技老师业绩表:从开单品项明细表(lq_kd_pxmx)同步 +UPDATE lq_kd_kjbsyj kd +INNER JOIN lq_kd_pxmx px ON px.F_Id = kd.F_kdpxid +SET + kd.F_ItemCategory = px.F_ItemCategory, + kd.F_ItemId = px.px +WHERE kd.F_kdpxid IS NOT NULL; + +-- 5. 耗卡科技老师业绩表:从耗卡品项明细表(lq_xh_pxmx)同步 +UPDATE lq_xh_kjbsyj xh +INNER JOIN lq_xh_pxmx px ON px.F_Id = xh.F_hkpxid +SET + xh.F_ItemCategory = px.F_ItemCategory, + xh.F_ItemId = px.px +WHERE xh.F_hkpxid IS NOT NULL; + +-- 6. 退卡科技老师业绩表:从退卡品项明细表(lq_hytk_mx)同步 +-- 注意:F_CardReturn 关联到 lq_hytk_mx.F_Id,F_tkpxid 是项目资料表ID(品项ID) +UPDATE lq_hytk_kjbsyj tk +INNER JOIN lq_hytk_mx mx ON mx.F_Id = tk.F_CardReturn +SET + tk.F_ItemCategory = mx.F_ItemCategory, + tk.F_ItemId = mx.px +WHERE tk.F_CardReturn IS NOT NULL; + diff --git a/sql/查询员工11月25日之后的记录.sql b/sql/查询员工11月25日之后的记录.sql new file mode 100644 index 0000000..35558af --- /dev/null +++ b/sql/查询员工11月25日之后的记录.sql @@ -0,0 +1,56 @@ +-- ============================================ +-- 查询员工在2025-11-25 11:55:32之后的记录 +-- 员工ID: 18566028067 (李芳) +-- ============================================ + +-- 1. 查询11月25日11:55:32之后的业绩记录(使用业绩时间) +SELECT + jksyj.jkszh as 健康师账号, + jksyj.jksxm as 健康师姓名, + jksyj.F_kdpxNumber as 项目数, + jksyj.yjsj as 业绩时间, + hyhk.hksj as 耗卡时间, + hyhk.F_IsEffective as 耗卡记录是否有效, + jksyj.F_IsEffective as 业绩记录是否有效, + hyhk.F_Id as 耗卡记录ID +FROM lq_xh_jksyj jksyj +LEFT JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND jksyj.yjsj > '2025-11-25 11:55:32' + AND jksyj.yjsj < '2025-12-01' +ORDER BY jksyj.yjsj; + +-- 2. 查询11月25日11:55:32之后的耗卡记录(使用耗卡时间,这是接口统计使用的条件) +SELECT + jksyj.jkszh as 健康师账号, + jksyj.jksxm as 健康师姓名, + jksyj.F_kdpxNumber as 项目数, + jksyj.yjsj as 业绩时间, + hyhk.hksj as 耗卡时间, + hyhk.F_IsEffective as 耗卡记录是否有效, + jksyj.F_IsEffective as 业绩记录是否有效, + hyhk.F_Id as 耗卡记录ID +FROM lq_xh_jksyj jksyj +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj > '2025-11-25 11:55:32' + AND hyhk.hksj < '2025-12-01' +ORDER BY hyhk.hksj; + +-- 3. 统计11月25日11:55:32之后的项目数总和(使用耗卡时间,接口统计逻辑) +SELECT + jksyj.jkszh as 健康师账号, + CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as 项目数总和 +FROM lq_xh_jksyj jksyj +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj > '2025-11-25 11:55:32' + AND hyhk.hksj < '2025-12-01' +GROUP BY jksyj.jkszh; + + diff --git a/sql/查询员工消耗项目数.sql b/sql/查询员工消耗项目数.sql new file mode 100644 index 0000000..2423ada --- /dev/null +++ b/sql/查询员工消耗项目数.sql @@ -0,0 +1,39 @@ +-- ============================================ +-- 查询员工在指定月份的消耗项目数 +-- ============================================ +-- 员工ID: 18566028067 +-- 查询月份: 2025年11月 (202511) +-- ============================================ + +-- 方式1:从健康师业绩表统计(推荐,使用F_kdpxNumber字段,包含原始+加班+陪同项目数) +SELECT + jksyj.jks as 健康师ID, + jksyj.jksxm as 健康师姓名, + jksyj.jkszh as 健康师账号, + COALESCE(SUM(jksyj.F_kdpxNumber), 0) as 消耗项目总数, + COALESCE(SUM(COALESCE(jksyj.F_OriginalKdpxNumber, jksyj.F_kdpxNumber)), 0) as 原始项目数, + COALESCE(SUM(COALESCE(jksyj.F_OvertimeKdpxNumber, 0)), 0) as 加班项目数, + COALESCE(SUM(COALESCE(jksyj.F_AccompaniedProjectNumber, 0)), 0) as 陪同项目数 +FROM lq_xh_jksyj jksyj +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND DATE_FORMAT(jksyj.yjsj, '%Y%m') = '202511' +GROUP BY jksyj.jks, jksyj.jksxm, jksyj.jkszh; + +-- 方式2:从品项明细表统计(备用方式,统计F_ProjectNumber字段) +SELECT + jksyj.jks as 健康师ID, + jksyj.jksxm as 健康师姓名, + jksyj.jkszh as 健康师账号, + COALESCE(SUM(pxmx.F_ProjectNumber), 0) as 消耗项目数 +FROM lq_xh_jksyj jksyj +INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id +INNER JOIN lq_xh_pxmx pxmx ON pxmx.F_ConsumeInfoId = hyhk.F_Id +WHERE (jksyj.jks = '18566028067' OR jksyj.jkszh = '18566028067') + AND jksyj.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND pxmx.F_IsEffective = 1 + AND DATE_FORMAT(hyhk.hksj, '%Y%m') = '202511' +GROUP BY jksyj.jks, jksyj.jksxm, jksyj.jkszh; + + diff --git a/sql/添加业绩表品项分类字段.sql b/sql/添加业绩表品项分类字段.sql new file mode 100644 index 0000000..4c32578 --- /dev/null +++ b/sql/添加业绩表品项分类字段.sql @@ -0,0 +1,32 @@ +-- 为6个业绩表添加品项分类字段和品项ID字段 + +-- 1. 开单健康师业绩表 +ALTER TABLE `lq_kd_jksyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_ActivityId`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + +-- 2. 开单科技老师业绩表 +ALTER TABLE `lq_kd_kjbsyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_ActivityId`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + +-- 3. 耗卡健康师业绩表 +ALTER TABLE `lq_xh_jksyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_AccompaniedProjectNumber`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + +-- 4. 耗卡科技老师业绩表 +ALTER TABLE `lq_xh_kjbsyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + +-- 5. 退卡健康师业绩表 +ALTER TABLE `lq_hytk_jksyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + +-- 6. 退卡科技老师业绩表 +ALTER TABLE `lq_hytk_kjbsyj` +ADD COLUMN `F_ItemCategory` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项分类' AFTER `F_IsEffective`, +ADD COLUMN `F_ItemId` VARCHAR(50) NULL DEFAULT NULL COMMENT '品项ID' AFTER `F_ItemCategory`; + diff --git a/sql/添加人次记录表字段.sql b/sql/添加人次记录表字段.sql index fdbe586..0f49ae0 100644 --- a/sql/添加人次记录表字段.sql +++ b/sql/添加人次记录表字段.sql @@ -16,3 +16,5 @@ ADD COLUMN `F_HasBilling` INT(11) DEFAULT 0 COMMENT '是否有开单(0-否,1-是 -- WHERE TABLE_NAME = 'lq_person_times_record' -- AND COLUMN_NAME = 'F_HasBilling'; + +