From 6b0052c5fe34050b6495c0eab6db95382e942c52 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Wed, 29 Oct 2025 20:24:40 +0800 Subject: [PATCH] feat: 新增统计功能和性能优化 --- netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs | 17 +++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs | 27 +++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs | 19 +++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs | 26 ++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs | 32 ++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs | 25 +++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs | 26 ++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs | 29 +++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs | 5 +++++ netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs | 13 +++++++++++-- netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs | 1725 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs | 2 +- netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs | 23 ++++++++++++++++------- netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs | 42 +++++++++++++++++++++++++++++++++++++++++- 优化GetStoreRemainingRights性能索引.sql | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 创建会员开单耗卡项目数视图.sql | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 查询所有会员开单耗卡项目数.sql | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 2632 insertions(+), 86 deletions(-) create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs create mode 100644 优化GetStoreRemainingRights性能索引.sql create mode 100644 创建会员开单耗卡项目数视图.sql create mode 100644 查询所有会员开单耗卡项目数.sql diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs new file mode 100644 index 0000000..11dbef0 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace NCC.Extend.Entitys.Dto.LqJinsanjiaoUser +{ + /// + /// 删除金三角用户绑定关系输入 + /// + public class LqJinsanjiaoUserDeleteInput + { + /// + /// 金三角用户关系ID + /// + [Required(ErrorMessage = "ID不能为空")] + public string Id { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs new file mode 100644 index 0000000..a90c11a --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 客户到店次数统计输入 + /// + public class CustomerVisitFrequencyInput + { + /// + /// 开始时间(可选,默认为当月1号) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(可选,默认为当前时间) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可选) + /// + public List StoreIds { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs new file mode 100644 index 0000000..7d60b68 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs @@ -0,0 +1,19 @@ +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 客户到店次数统计输出 + /// + public class CustomerVisitFrequencyOutput + { + /// + /// 消耗次数(客户到店次数) + /// + public int VisitCount { get; set; } + + /// + /// 人数(达到该次数的客户人数) + /// + public int CustomerCount { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs new file mode 100644 index 0000000..984c93b --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 健康师排行榜输出 + /// + public class HealthCoachRankingOutput + { + /// + /// 开单业绩排行榜(前20名) + /// + public List BillingRanking { get; set; } + + /// + /// 耗卡业绩排行榜(前20名) + /// + public List ConsumeRanking { get; set; } + + /// + /// 退卡业绩排行榜(前20名) + /// + public List RefundRanking { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs new file mode 100644 index 0000000..8fb2e1b --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 健康师统计数据输入 + /// + public class HealthCoachStatisticsInput + { + /// + /// 开始时间(可选,默认为当月1号) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(可选,默认为当前时间) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可选) + /// + public List StoreIds { get; set; } + + /// + /// 健康师ID列表(可选) + /// + public List HealthCoachIds { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs new file mode 100644 index 0000000..0d4c896 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs @@ -0,0 +1,49 @@ +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 健康师统计数据输出 + /// + public class HealthCoachStatisticsOutput + { + /// + /// 健康师ID + /// + public string HealthCoachId { get; set; } + + /// + /// 健康师姓名 + /// + public string HealthCoachName { get; set; } + + /// + /// 开单业绩 + /// + public decimal BillingPerformance { get; set; } + + /// + /// 消耗业绩 + /// + public decimal ConsumePerformance { get; set; } + + /// + /// 退单业绩 + /// + public decimal RefundPerformance { get; set; } + + /// + /// 开单项目数 + /// + public int BillingProjectCount { get; set; } + + /// + /// 消耗项目数 + /// + public int ConsumeProjectCount { get; set; } + + /// + /// 退单项目数 + /// + public int RefundProjectCount { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs new file mode 100644 index 0000000..602a70c --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs @@ -0,0 +1,25 @@ +using System; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 门店项目指标统计输入 + /// + public class StoreItemStatisticsInput + { + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表 + /// + public string[] StoreIds { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs new file mode 100644 index 0000000..2cb351a --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs @@ -0,0 +1,53 @@ +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 门店项目指标统计输出 + /// + public class StoreItemStatisticsOutput + { + /// + /// 门店ID + /// + public string StoreId { get; set; } + + /// + /// 门店名称 + /// + public string StoreName { get; set; } + + /// + /// 消耗项目数(项目次数总和) + /// + public int ConsumeProjectCount { get; set; } + + /// + /// 消耗率(消耗金额/开单金额 × 100%) + /// + public decimal ConsumeRate { get; set; } + + /// + /// 客单项目数(项目数/消耗人次) + /// + public decimal AvgProjectPerConsume { get; set; } + + /// + /// 消耗客单价(消耗业绩/消耗人次) + /// + public decimal AvgAmountPerConsume { get; set; } + + /// + /// 开单金额 + /// + public decimal BillingAmount { get; set; } + + /// + /// 消耗金额 + /// + public decimal ConsumeAmount { get; set; } + + /// + /// 消耗人次 + /// + public int ConsumePersonCount { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs new file mode 100644 index 0000000..a41abc3 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs @@ -0,0 +1,26 @@ +using System; + +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 门店剩余权益统计输入 + /// + public class StoreRemainingRightsInput + { + /// + /// 开始时间(可选,默认为当月1号) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(可选,默认为当前时间) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可选,不传则统计所有门店) + /// + public string[] StoreIds { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs new file mode 100644 index 0000000..f188aff --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs @@ -0,0 +1,29 @@ +namespace NCC.Extend.Entitys.Dto.LqReport +{ + /// + /// 门店剩余权益统计输出 + /// + public class StoreRemainingRightsOutput + { + /// + /// 门店ID + /// + public string StoreId { get; set; } + + /// + /// 门店名称 + /// + public string StoreName { get; set; } + + /// + /// 剩余权益累计金额(开单总金额 - 消耗总金额 - 退卡总金额) + /// + public decimal RemainingRightsAmount { get; set; } + + /// + /// 剩余权益累计人数(有剩余权益的会员数量) + /// + public int RemainingRightsPersonCount { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs index 3850358..c82c199 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs @@ -21,6 +21,11 @@ namespace NCC.Extend.Entitys.Dto.LqTkjlb /// 活动ID(可选) /// public string EventId { get; set; } + + /// + /// 门店ID(可选) + /// + public string[] StoreId { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs index 3f1b0a9..13d618e 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs @@ -22,6 +22,7 @@ using NCC.DataEncryption; using NCC.ClayObject; using NCC.Extend.Entitys.Dto.LqDailyReport; using NCC.Extend.Entitys.lq_kd_kdjlb; +using Microsoft.AspNetCore.Authorization; namespace NCC.Extend { @@ -103,6 +104,7 @@ namespace NCC.Extend /// 成功返回门店统计列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-store-daily-statistics")] public async Task> GetStoreDailyStatistics(StoreDailyStatisticsInput input) { @@ -198,6 +200,7 @@ namespace NCC.Extend /// 成功返回门店业绩完成情况列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-store-performance-completion")] public async Task> GetStorePerformanceCompletion(StorePerformanceCompletionInput input) { @@ -288,6 +291,7 @@ namespace NCC.Extend /// 成功返回事业部业绩完成情况列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-business-unit-performance-completion")] public async Task> GetBusinessUnitPerformanceCompletion(BusinessUnitPerformanceCompletionInput input) { @@ -421,6 +425,7 @@ namespace NCC.Extend /// 成功返回天王团业绩完成情况列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-tianwang-group-performance-completion")] public async Task> GetTianwangGroupPerformanceCompletion(TianwangGroupPerformanceCompletionInput input) { @@ -595,6 +600,7 @@ namespace NCC.Extend /// 成功返回经理业绩完成情况列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-manager-performance-completion")] public async Task> GetManagerPerformanceCompletion(ManagerPerformanceCompletionInput input) { @@ -699,6 +705,7 @@ namespace NCC.Extend /// 成功返回经理汇总业绩完成情况列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-manager-summary-performance-completion")] public async Task> GetManagerSummaryPerformanceCompletion(ManagerPerformanceCompletionInput input) { @@ -808,6 +815,7 @@ namespace NCC.Extend /// 成功返回科技部老师统计列表 /// 日期格式错误或参数无效 /// 服务器内部错误 + [AllowAnonymous] [HttpPost("get-tech-teacher-daily-statistics")] public async Task> GetTechTeacherDailyStatistics(TechTeacherDailyStatisticsInput input) { @@ -841,7 +849,7 @@ namespace NCC.Extend techDept.F_Id as TechDepartmentId, techDept.F_FullName as TechDepartmentName, consume.kjbls as TeacherId, - consume.kjblsxm as TeacherName, + user.F_RealName as TeacherName, COUNT(DISTINCT hyhk.hy) as CustomerCount, SUM(consume.F_hdpxNumber) as ConsumeProjectCount, SUM(consume.kjblsyj) as ConsumeAchievement @@ -849,13 +857,14 @@ namespace NCC.Extend INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id INNER JOIN lq_mdxx store ON hyhk.md = store.F_Id LEFT JOIN base_organize techDept ON store.kjb = techDept.F_Id + LEFT JOIN BASE_USER user ON consume.kjbls = user.F_Id WHERE consume.F_IsEffective = 1 AND hyhk.F_IsEffective = 1 AND DATE(hyhk.hksj) >= '{startDate:yyyy-MM-dd}' AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}' {techFilter} {teacherFilter} - GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, consume.kjblsxm"; + GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, user.F_RealName"; var consumeResult = await _db.Ado.SqlQueryAsync(consumeSql); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs index c3c4cad..eca335d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs @@ -106,8 +106,8 @@ namespace NCC.Extend if (input.StoreIds != null && input.StoreIds.Any()) { - sql += " AND s.F_StoreId IN @storeIds"; - parameters = new { startMonth = input.StartMonth, endMonth = input.EndMonth, storeIds = input.StoreIds }; + var storeIdsStr = string.Join("','", input.StoreIds); + sql += $" AND s.F_StoreId IN ('{storeIdsStr}')"; } sql += " ORDER BY s.F_StoreName, s.F_StatisticsMonth"; @@ -751,15 +751,12 @@ namespace NCC.Extend AND kd.kdrq >= @startTime AND kd.kdrq <= @endTime"; - object billingParameters; + object billingParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - billingSql += " AND kd.djmd IN @storeIds"; - billingParameters = new { startTime, endTime, storeIds = input.StoreIds }; - } - else - { - billingParameters = new { startTime, endTime }; + var storeIdsStr = string.Join("','", input.StoreIds); + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; } var billingResult = await _db.Ado.SqlQueryAsync(billingSql, billingParameters); @@ -776,15 +773,12 @@ namespace NCC.Extend AND xh.hksj >= @startTime AND xh.hksj <= @endTime"; - object consumeParameters; + object consumeParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - consumeSql += " AND xh.md IN @storeIds"; - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds }; - } - else - { - consumeParameters = new { startTime, endTime }; + var storeIdsStr = string.Join("','", input.StoreIds); + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; } var consumeResult = await _db.Ado.SqlQueryAsync(consumeSql, consumeParameters); @@ -801,15 +795,12 @@ namespace NCC.Extend AND hytk.tksj >= @startTime AND hytk.tksj <= @endTime"; - object refundParameters; + object refundParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - refundSql += " AND hytk.md IN @storeIds"; - refundParameters = new { startTime, endTime, storeIds = input.StoreIds }; - } - else - { - refundParameters = new { startTime, endTime }; + var storeIdsStr = string.Join("','", input.StoreIds); + refundSql += $" AND hytk.md IN ('{storeIdsStr}')"; } var refundResult = await _db.Ado.SqlQueryAsync(refundSql, refundParameters); @@ -818,28 +809,26 @@ namespace NCC.Extend // 第四步:获取消耗目标业绩(所有门店xhyj字段的总和) var targetConsumeSql = "SELECT COALESCE(SUM(CAST(md.xhyj AS DECIMAL(18,2))), 0) as target_consume_amount FROM lq_mdxx md WHERE 1=1"; - object targetConsumeParameters = null; if (input.StoreIds != null && input.StoreIds.Any()) { - targetConsumeSql += " AND md.F_Id IN @storeIds"; - targetConsumeParameters = new { storeIds = input.StoreIds }; + var storeIdsStr = string.Join("','", input.StoreIds); + targetConsumeSql += $" AND md.F_Id IN ('{storeIdsStr}')"; } - var targetConsumeResult = await _db.Ado.SqlQueryAsync(targetConsumeSql, targetConsumeParameters); + var targetConsumeResult = await _db.Ado.SqlQueryAsync(targetConsumeSql); var targetConsumeAmount = Convert.ToDecimal(targetConsumeResult?.FirstOrDefault()?.target_consume_amount ?? 0m); // 第五步:获取开单目标业绩(所有门店xsyj字段的总和) var targetBillingSql = "SELECT COALESCE(SUM(CAST(md.xsyj AS DECIMAL(18,2))), 0) as target_billing_amount FROM lq_mdxx md WHERE 1=1"; - object targetBillingParameters = null; if (input.StoreIds != null && input.StoreIds.Any()) { - targetBillingSql += " AND md.F_Id IN @storeIds"; - targetBillingParameters = new { storeIds = input.StoreIds }; + var storeIdsStr = string.Join("','", input.StoreIds); + targetBillingSql += $" AND md.F_Id IN ('{storeIdsStr}')"; } - var targetBillingResult = await _db.Ado.SqlQueryAsync(targetBillingSql, targetBillingParameters); + var targetBillingResult = await _db.Ado.SqlQueryAsync(targetBillingSql); var targetBillingAmount = Convert.ToDecimal(targetBillingResult?.FirstOrDefault()?.target_billing_amount ?? 0m); // 计算开单完成业绩(开单总金额 - 退卡总金额) @@ -958,15 +947,12 @@ namespace NCC.Extend WHERE tk.F_CreateTime >= @startTime AND tk.F_CreateTime <= @endTime"; - object inviteParameters; + object inviteParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - inviteSql += " AND tk.F_StoreId IN @storeIds"; - inviteParameters = new { startTime, endTime, storeIds = input.StoreIds }; - } - else - { - inviteParameters = new { startTime, endTime }; + var storeIdsStr = string.Join("','", input.StoreIds); + inviteSql += $" AND tk.F_StoreId IN ('{storeIdsStr}')"; } var inviteResult = await _db.Ado.SqlQueryAsync(inviteSql, inviteParameters); @@ -981,15 +967,12 @@ namespace NCC.Extend AND xh.hksj <= @endTime AND xh.xfje > 0"; - object consumeParameters; + object consumeParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - consumeSql += " AND xh.md IN @storeIds"; - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds }; - } - else - { - consumeParameters = new { startTime, endTime }; + var storeIdsStr = string.Join("','", input.StoreIds); + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; } var consumeResult = await _db.Ado.SqlQueryAsync(consumeSql, consumeParameters); @@ -1066,49 +1049,106 @@ namespace NCC.Extend var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); var endTime = input.EndTime ?? DateTime.Now; - // 构建SQL查询 - var sql = @" + // 先获取门店基础信息和目标业绩 + var storeSql = "SELECT F_Id, dm, xsyj FROM lq_mdxx WHERE 1=1"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; + } + + var stores = await _db.Ado.SqlQueryAsync(storeSql); + + // 构建门店字典 + var storeDict = new Dictionary(); + var targetDict = new Dictionary(); + foreach (var store in stores) + { + var storeId = store.F_Id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; + targetDict[storeId] = Convert.ToDecimal(store.xsyj ?? 0); + } + } + + // 统计开单业绩 + var billingSql = @" SELECT - md.F_Id as store_id, - md.dm as store_name, - COALESCE(CAST(md.xsyj AS DECIMAL(18,2)), 0) as target_performance, - COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as actual_performance, - COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as actual_consume_performance - FROM lq_mdxx md - LEFT JOIN lq_kd_kdjlb kd ON md.F_Id = kd.djmd - AND kd.F_IsEffective = 1 + kd.djmd as store_id, + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as actual_performance + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 AND kd.kdrq >= @startTime - AND kd.kdrq <= @endTime - LEFT JOIN lq_xh_hyhk xh ON md.F_Id = xh.md - AND xh.F_IsEffective = 1 - AND xh.hksj >= @startTime - AND xh.hksj <= @endTime"; + AND kd.kdrq <= @endTime"; + + object billingParameters = new { startTime, endTime }; - object parameters; if (input.StoreIds != null && input.StoreIds.Any()) { - sql += " WHERE md.F_Id IN @storeIds"; - parameters = new { startTime, endTime, storeIds = input.StoreIds }; + var storeIdsStr = string.Join("','", input.StoreIds); + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; } - else + + billingSql += " GROUP BY kd.djmd"; + + var billingResults = await _db.Ado.SqlQueryAsync(billingSql, billingParameters); + + // 构建开单业绩字典 + var billingDict = new Dictionary(); + foreach (var item in billingResults) { - sql += " WHERE 1=1"; - parameters = new { startTime, endTime }; + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + billingDict[storeId] = Convert.ToDecimal(item.actual_performance ?? 0); + } } - sql += " GROUP BY md.F_Id, md.dm, md.xsyj ORDER BY actual_performance DESC"; + // 统计消耗业绩 + var consumeSql = @" + SELECT + xh.md as store_id, + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as actual_consume_performance + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + AND xh.hksj >= @startTime + AND xh.hksj <= @endTime"; - var results = await _db.Ado.SqlQueryAsync(sql, parameters); + object consumeParameters = new { startTime, endTime }; - var storePerformanceList = new List(); + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; + } - foreach (var item in results) + consumeSql += " GROUP BY xh.md"; + + var consumeResults = await _db.Ado.SqlQueryAsync(consumeSql, consumeParameters); + + // 构建消耗业绩字典 + var consumeDict = new Dictionary(); + foreach (var item in consumeResults) { var storeId = item.store_id?.ToString(); - var storeName = item.store_name?.ToString(); - var targetPerformance = Convert.ToDecimal(item.target_performance ?? 0); - var actualPerformance = Convert.ToDecimal(item.actual_performance ?? 0); - var actualConsumePerformance = Convert.ToDecimal(item.actual_consume_performance ?? 0); + if (!string.IsNullOrEmpty(storeId)) + { + consumeDict[storeId] = Convert.ToDecimal(item.actual_consume_performance ?? 0); + } + } + + // 合并数据 + var storePerformanceList = new List(); + + foreach (var kvp in storeDict) + { + var storeId = kvp.Key; + var storeName = kvp.Value; + var targetPerformance = targetDict.ContainsKey(storeId) ? targetDict[storeId] : 0; + var actualPerformance = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0; + var actualConsumePerformance = consumeDict.ContainsKey(storeId) ? consumeDict[storeId] : 0; // 计算完成率 var completionRate = targetPerformance > 0 @@ -1134,6 +1174,9 @@ namespace NCC.Extend }); } + // 按实际业绩排序 + storePerformanceList = storePerformanceList.OrderByDescending(x => x.ActualPerformance).ToList(); + return storePerformanceList; } catch (Exception ex) @@ -1275,7 +1318,7 @@ namespace NCC.Extend SELECT F_Id FROM lq_xh_hyhk WHERE hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' AND hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' - {(input.StoreIds != null && input.StoreIds.Any() ? $"AND F_StoreId IN ('{string.Join("','", input.StoreIds)}')" : "")} + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND md IN ('{string.Join("','", input.StoreIds)}')" : "")} ) GROUP BY xh.px"; @@ -1334,5 +1377,1537 @@ namespace NCC.Extend } #endregion + + #region 获取门店项目指标统计数据 + + /// + /// 获取门店项目指标统计数据 + /// + /// + /// 统计指定时间范围内各门店的项目指标数据 + /// 包括:消耗项目数、消耗率、客单项目数、消耗客单价、开单金额、消耗人次 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - startTime: 开始时间(可选,默认为当月1号) + /// - endTime: 结束时间(可选,默认为当前时间) + /// - storeIds: 门店ID列表(可选) + /// + /// 返回字段说明: + /// - StoreId: 门店ID + /// - StoreName: 门店名称 + /// - ConsumeProjectCount: 消耗项目数(项目次数总和) + /// - ConsumeRate: 消耗率(消耗金额/开单金额 × 100%) + /// - AvgProjectPerConsume: 客单项目数(项目数/消耗人次) + /// - AvgAmountPerConsume: 消耗客单价(消耗业绩/消耗人次) + /// - BillingAmount: 开单金额 + /// - ConsumeAmount: 消耗金额 + /// - ConsumePersonCount: 消耗人次(去重客户数) + /// + /// 查询参数 + /// 门店项目指标统计数据列表 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-store-item-statistics")] + public async Task GetStoreItemStatistics(StoreItemStatisticsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 第一步:获取门店基础信息 + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; + } + + var stores = await _db.Ado.SqlQueryAsync(storeSql); + + // 构建门店字典 + var storeDict = new Dictionary(); + foreach (var store in stores) + { + var storeId = store.F_Id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; + } + } + + // 第二步:统计开单金额 + var billingSql = @" + SELECT + kd.djmd as store_id, + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + AND kd.kdrq >= @startTime + AND kd.kdrq <= @endTime"; + + object billingParameters = new { startTime, endTime }; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; + } + + billingSql += " GROUP BY kd.djmd"; + + var billingResults = await _db.Ado.SqlQueryAsync(billingSql, billingParameters); + + // 构建开单金额字典 + var billingDict = new Dictionary(); + foreach (var item in billingResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + billingDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0); + } + } + + // 第三步:统计消耗金额和消耗人次(直接从lq_xh_hyhk表,避免JOIN导致重复计算) + var consumeAmountSql = @" + SELECT + xh.md as store_id, + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount, + COUNT(DISTINCT xh.hy) as consume_person_count + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + AND xh.hksj >= @startTime + AND xh.hksj <= @endTime"; + + object consumeAmountParameters = new { startTime, endTime }; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeAmountSql += $" AND xh.md IN ('{storeIdsStr}')"; + } + + consumeAmountSql += " GROUP BY xh.md"; + + var consumeAmountResults = await _db.Ado.SqlQueryAsync(consumeAmountSql, consumeAmountParameters); + + // 构建消耗金额和人次字典 + var consumeAmountDict = new Dictionary(); + var consumePersonCountDict = new Dictionary(); + + foreach (var item in consumeAmountResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0); + consumePersonCountDict[storeId] = Convert.ToInt32(item.consume_person_count ?? 0); + } + } + + // 第四步:统计消耗项目数(需要JOIN品项明细表) + var consumeProjectSql = @" + SELECT + xh.md as store_id, + COALESCE(SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))), 0) as consume_project_count + FROM lq_xh_hyhk xh + INNER JOIN lq_xh_pxmx xhpx ON xhpx.F_ConsumeInfoId = xh.F_Id AND xhpx.F_IsEffective = 1 + WHERE xh.F_IsEffective = 1 + AND xh.hksj >= @startTime + AND xh.hksj <= @endTime"; + + object consumeProjectParameters = new { startTime, endTime }; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeProjectSql += $" AND xh.md IN ('{storeIdsStr}')"; + } + + consumeProjectSql += " GROUP BY xh.md"; + + var consumeProjectResults = await _db.Ado.SqlQueryAsync(consumeProjectSql, consumeProjectParameters); + + // 构建消耗项目数字典 + var consumeProjectCountDict = new Dictionary(); + + foreach (var item in consumeProjectResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + consumeProjectCountDict[storeId] = Convert.ToDecimal(item.consume_project_count ?? 0); + } + } + + // 第五步:合并数据并计算指标 + var resultList = new List(); + + foreach (var kvp in storeDict) + { + var storeId = kvp.Key; + var storeName = kvp.Value; + var billingAmount = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0; + + var consumeProjectCount = consumeProjectCountDict.ContainsKey(storeId) ? consumeProjectCountDict[storeId] : 0; + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; + var consumePersonCount = consumePersonCountDict.ContainsKey(storeId) ? consumePersonCountDict[storeId] : 0; + + // 如果有消耗数据或开单数据,都返回该门店 + if (consumeAmount > 0 || consumeProjectCount > 0 || billingAmount > 0) + { + // 计算消耗率 + var consumeRate = billingAmount > 0 + ? decimal.Round(consumeAmount / billingAmount * 100m, 2) + : 0; + + // 计算客单项目数 + var avgProjectPerConsume = consumePersonCount > 0 + ? decimal.Round(consumeProjectCount / consumePersonCount, 2) + : 0; + + // 计算消耗客单价 + var avgAmountPerConsume = consumePersonCount > 0 + ? decimal.Round(consumeAmount / consumePersonCount, 2) + : 0; + + resultList.Add(new StoreItemStatisticsOutput + { + StoreId = storeId, + StoreName = storeName, + ConsumeProjectCount = Convert.ToInt32(consumeProjectCount), + ConsumeRate = consumeRate, + AvgProjectPerConsume = avgProjectPerConsume, + AvgAmountPerConsume = avgAmountPerConsume, + BillingAmount = billingAmount, + ConsumeAmount = consumeAmount, + ConsumePersonCount = consumePersonCount + }); + } + } + + // 按消耗人次排序 + resultList = resultList.OrderByDescending(x => x.ConsumePersonCount).ToList(); + + return resultList; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取门店项目指标统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取门店项目指标统计数据失败: {ex.Message}"); + } + } + + #endregion + + #region 客户到店次数统计 + + /// + /// 获取客户到店次数统计 + /// + /// + /// 统计指定时间范围内,客户按到店次数(消耗次数)的分布情况 + /// 一次消耗开单即为一次到店 + /// 支持门店筛选,但统计结果不分门店,按次数汇总所有门店的人数 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - startTime: 开始时间(可选,默认为当月1号) + /// - endTime: 结束时间(可选,默认为当前时间) + /// - storeIds: 门店ID列表(可选,用于筛选) + /// + /// 返回说明: + /// - VisitCount: 消耗次数(客户到店次数) + /// - CustomerCount: 人数(达到该次数的客户人数) + /// + /// 查询参数 + /// 客户到店次数统计数据列表 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-customer-visit-frequency")] + public async Task> GetCustomerVisitFrequency(CustomerVisitFrequencyInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 统计每个客户的消耗次数(跨门店汇总) + var visitSql = $@" + SELECT + xh.hy as customer_id, + COUNT(DISTINCT CONCAT(xh.md, '_', DATE(xh.hksj))) as visit_count + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + AND xh.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + visitSql += $" AND xh.md IN ('{storeIdsStr}')"; + } + + visitSql += " GROUP BY xh.hy"; + + var visitData = await _db.Ado.SqlQueryAsync(visitSql); + + // 统计各次数的人数分布 + var visitCountDict = new Dictionary(); + + foreach (var item in visitData) + { + var visitCount = Convert.ToInt32(item.visit_count); + + if (visitCountDict.ContainsKey(visitCount)) + { + visitCountDict[visitCount]++; + } + else + { + visitCountDict[visitCount] = 1; + } + } + + // 构建结果列表,按次数排序 + var resultList = visitCountDict + .OrderBy(x => x.Key) + .Select(x => new CustomerVisitFrequencyOutput + { + VisitCount = x.Key, + CustomerCount = x.Value + }) + .ToList(); + + return resultList; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取客户到店次数统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取客户到店次数统计数据失败: {ex.Message}"); + } + } + + #endregion + + #region 健康师统计数据 + + /// + /// 获取健康师统计数据 + /// + /// + /// 统计指定时间范围内健康师的开单业绩、消耗业绩、退单业绩及项目数 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"], + /// "healthCoachIds": ["健康师ID1", "健康师ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - startTime: 开始时间(可选,默认为当月1号) + /// - endTime: 结束时间(可选,默认为当前时间) + /// - storeIds: 门店ID列表(可选) + /// - healthCoachIds: 健康师ID列表(可选) + /// + /// 返回说明: + /// - HealthCoachId: 健康师ID + /// - HealthCoachName: 健康师姓名 + /// - BillingPerformance: 开单业绩 + /// - ConsumePerformance: 消耗业绩 + /// - RefundPerformance: 退单业绩 + /// - BillingProjectCount: 开单项目数 + /// - ConsumeProjectCount: 消耗项目数 + /// - RefundProjectCount: 退单项目数 + /// + /// 查询参数 + /// 健康师统计数据列表 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-health-coach-statistics")] + public async Task> GetHealthCoachStatistics(HealthCoachStatisticsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 健康师数据字典 + var healthCoachDict = new Dictionary(); + + // 1. 统计开单业绩和开单项目数 + var billingSql = $@" + SELECT + jks.jks as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count + FROM lq_kd_jksyj jks + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + WHERE jks.F_IsEffective = 1 + AND kdjlb.F_IsEffective = 1 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + billingSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; + } + + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) + { + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); + billingSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; + } + + billingSql += " GROUP BY jks.jks, jks.jksxm"; + + var billingData = await _db.Ado.SqlQueryAsync(billingSql); + + foreach (var item in billingData) + { + var healthCoachId = item.health_coach_id?.ToString(); + if (!string.IsNullOrEmpty(healthCoachId)) + { + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput + { + HealthCoachId = healthCoachId, + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", + BillingPerformance = Convert.ToDecimal(item.billing_performance ?? 0), + BillingProjectCount = Convert.ToInt32(item.billing_project_count ?? 0), + ConsumePerformance = 0, + RefundPerformance = 0, + ConsumeProjectCount = 0, + RefundProjectCount = 0 + }; + } + } + + // 2. 统计消耗业绩和消耗项目数 + var consumeSql = $@" + SELECT + jks.jks as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count + FROM lq_xh_jksyj jks + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id + WHERE jks.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeSql += $" AND hyhk.md IN ('{storeIdsStr}')"; + } + + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) + { + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); + consumeSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; + } + + consumeSql += " GROUP BY jks.jks, jks.jksxm"; + + var consumeData = await _db.Ado.SqlQueryAsync(consumeSql); + + foreach (var item in consumeData) + { + var healthCoachId = item.health_coach_id?.ToString(); + if (!string.IsNullOrEmpty(healthCoachId)) + { + if (healthCoachDict.ContainsKey(healthCoachId)) + { + healthCoachDict[healthCoachId].ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0); + healthCoachDict[healthCoachId].ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0); + } + else + { + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput + { + HealthCoachId = healthCoachId, + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", + BillingPerformance = 0, + ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0), + RefundPerformance = 0, + BillingProjectCount = 0, + ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0), + RefundProjectCount = 0 + }; + } + } + } + + // 3. 统计退单业绩和退单项目数 + var refundSql = $@" + SELECT + jks.jks as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count + FROM lq_hytk_jksyj jks + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id + WHERE jks.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + refundSql += $" AND hytk.md IN ('{storeIdsStr}')"; + } + + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) + { + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); + refundSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; + } + + refundSql += " GROUP BY jks.jks, jks.jksxm"; + + var refundData = await _db.Ado.SqlQueryAsync(refundSql); + + foreach (var item in refundData) + { + var healthCoachId = item.health_coach_id?.ToString(); + if (!string.IsNullOrEmpty(healthCoachId)) + { + if (healthCoachDict.ContainsKey(healthCoachId)) + { + healthCoachDict[healthCoachId].RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0); + healthCoachDict[healthCoachId].RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0); + } + else + { + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput + { + HealthCoachId = healthCoachId, + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", + BillingPerformance = 0, + ConsumePerformance = 0, + RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0), + BillingProjectCount = 0, + ConsumeProjectCount = 0, + RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) + }; + } + } + } + + // 返回结果,按健康师姓名排序 + var resultList = healthCoachDict.Values + .OrderBy(x => x.HealthCoachName) + .ToList(); + + return resultList; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取健康师统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取健康师统计数据失败: {ex.Message}"); + } + } + + + /// + /// 获取健康师开单业绩排行榜 + /// + /// + /// 获取按开单业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 查询参数 + /// 开单业绩排行榜(前20名) + /// 成功返回排行榜数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-health-coach-billing-ranking")] + public async Task> GetHealthCoachBillingRanking(HealthCoachStatisticsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 第一步:获取开单业绩排行榜前20名健康师(使用jkszh作为ID) + var rankingIdsSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name + FROM lq_kd_jksyj jks + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id + WHERE jks.F_IsEffective = 1 + AND kdjlb.F_IsEffective = 1 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IS NOT NULL + AND jks.jkszh != ''"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + rankingIdsSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; + } + + rankingIdsSql += @" + GROUP BY jks.jkszh, jks.jksxm + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC + LIMIT 20"; + + var rankingData = (await _db.Ado.SqlQueryAsync(rankingIdsSql)) + .Select(item => new + { + Id = item.health_coach_id?.ToString(), + Name = item.health_coach_name?.ToString() + }) + .Where(x => !string.IsNullOrEmpty(x.Id)) + .ToList(); + + var rankingIds = rankingData.Select(x => x.Id).ToList(); + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); + + if (!rankingIds.Any()) + { + return new List(); + } + + // 第二步:为这些健康师查询完整的三个业绩数据 + var healthCoachIdsStr = string.Join("','", rankingIds); + + // 2.1 查询开单业绩数据(使用jkszh作为ID) + var billingDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count + FROM lq_kd_jksyj jks + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + WHERE jks.F_IsEffective = 1 + AND kdjlb.F_IsEffective = 1 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; + } + + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var billingData = (await _db.Ado.SqlQueryAsync(billingDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.billing_performance ?? 0), + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) + } + ); + + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) + var consumeDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count + FROM lq_xh_jksyj jks + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id + WHERE jks.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; + } + + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var consumeData = (await _db.Ado.SqlQueryAsync(consumeDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.consume_performance ?? 0), + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) + } + ); + + // 2.3 查询退卡业绩数据(使用jkszh作为ID) + var refundDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count + FROM lq_hytk_jksyj jks + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id + WHERE jks.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; + } + + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var refundData = (await _db.Ado.SqlQueryAsync(refundDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.refund_performance ?? 0), + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) + } + ); + + // 第三步:合并数据,按原始排序构建排行榜 + var ranking = rankingIds + .Select(id => + { + var billing = billingData.ContainsKey(id) ? billingData[id] : null; + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; + var refund = refundData.ContainsKey(id) ? refundData[id] : null; + + return new HealthCoachStatisticsOutput + { + HealthCoachId = id, + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), + BillingPerformance = billing?.Performance ?? 0, + ConsumePerformance = consume?.Performance ?? 0, + RefundPerformance = refund?.Performance ?? 0, + BillingProjectCount = billing?.ProjectCount ?? 0, + ConsumeProjectCount = consume?.ProjectCount ?? 0, + RefundProjectCount = refund?.ProjectCount ?? 0 + }; + }) + .ToList(); + + return ranking; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取健康师开单业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取健康师开单业绩排行榜失败: {ex.Message}"); + } + } + + /// + /// 获取健康师耗卡业绩排行榜 + /// + /// + /// 获取按耗卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 查询参数 + /// 耗卡业绩排行榜(前20名) + /// 成功返回排行榜数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-health-coach-consume-ranking")] + public async Task> GetHealthCoachConsumeRanking(HealthCoachStatisticsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 第一步:获取耗卡业绩排行榜前20名健康师(使用jkszh作为ID) + var rankingIdsSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name + FROM lq_xh_jksyj jks + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id + WHERE jks.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IS NOT NULL + AND jks.jkszh != ''"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + rankingIdsSql += $" AND hyhk.md IN ('{storeIdsStr}')"; + } + + rankingIdsSql += @" + GROUP BY jks.jkszh, jks.jksxm + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC + LIMIT 20"; + + var rankingData = (await _db.Ado.SqlQueryAsync(rankingIdsSql)) + .Select(item => new + { + Id = item.health_coach_id?.ToString(), + Name = item.health_coach_name?.ToString() + }) + .Where(x => !string.IsNullOrEmpty(x.Id)) + .ToList(); + + var rankingIds = rankingData.Select(x => x.Id).ToList(); + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); + + if (!rankingIds.Any()) + { + return new List(); + } + + // 第二步:为这些健康师查询完整的三个业绩数据 + var healthCoachIdsStr = string.Join("','", rankingIds); + + // 2.1 查询开单业绩数据(使用jkszh作为ID) + var billingDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count + FROM lq_kd_jksyj jks + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + WHERE jks.F_IsEffective = 1 + AND kdjlb.F_IsEffective = 1 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; + } + + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var billingData = (await _db.Ado.SqlQueryAsync(billingDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.billing_performance ?? 0), + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) + } + ); + + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) + var consumeDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count + FROM lq_xh_jksyj jks + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id + WHERE jks.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; + } + + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var consumeData = (await _db.Ado.SqlQueryAsync(consumeDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.consume_performance ?? 0), + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) + } + ); + + // 2.3 查询退卡业绩数据(使用jkszh作为ID) + var refundDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count + FROM lq_hytk_jksyj jks + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id + WHERE jks.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; + } + + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var refundData = (await _db.Ado.SqlQueryAsync(refundDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.refund_performance ?? 0), + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) + } + ); + + // 第三步:合并数据,按原始排序构建排行榜 + var ranking = rankingIds + .Select(id => + { + var billing = billingData.ContainsKey(id) ? billingData[id] : null; + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; + var refund = refundData.ContainsKey(id) ? refundData[id] : null; + + return new HealthCoachStatisticsOutput + { + HealthCoachId = id, + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), + BillingPerformance = billing?.Performance ?? 0, + ConsumePerformance = consume?.Performance ?? 0, + RefundPerformance = refund?.Performance ?? 0, + BillingProjectCount = billing?.ProjectCount ?? 0, + ConsumeProjectCount = consume?.ProjectCount ?? 0, + RefundProjectCount = refund?.ProjectCount ?? 0 + }; + }) + .ToList(); + + return ranking; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取健康师耗卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取健康师耗卡业绩排行榜失败: {ex.Message}"); + } + } + + /// + /// 获取健康师退卡业绩排行榜 + /// + /// + /// 获取按退卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 查询参数 + /// 退卡业绩排行榜(前20名) + /// 成功返回排行榜数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-health-coach-refund-ranking")] + public async Task> GetHealthCoachRefundRanking(HealthCoachStatisticsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + + // 第一步:获取退卡业绩排行榜前20名健康师(使用jkszh作为ID) + var rankingIdsSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name + FROM lq_hytk_jksyj jks + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id + WHERE jks.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IS NOT NULL + AND jks.jkszh != ''"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + rankingIdsSql += $" AND hytk.md IN ('{storeIdsStr}')"; + } + + rankingIdsSql += @" + GROUP BY jks.jkszh, jks.jksxm + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC + LIMIT 20"; + + var rankingData = (await _db.Ado.SqlQueryAsync(rankingIdsSql)) + .Select(item => new + { + Id = item.health_coach_id?.ToString(), + Name = item.health_coach_name?.ToString() + }) + .Where(x => !string.IsNullOrEmpty(x.Id)) + .ToList(); + + var rankingIds = rankingData.Select(x => x.Id).ToList(); + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); + + if (!rankingIds.Any()) + { + return new List(); + } + + // 第二步:为这些健康师查询完整的三个业绩数据 + var healthCoachIdsStr = string.Join("','", rankingIds); + + // 2.1 查询开单业绩数据(使用jkszh作为ID) + var billingDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count + FROM lq_kd_jksyj jks + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 + WHERE jks.F_IsEffective = 1 + AND kdjlb.F_IsEffective = 1 + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; + } + + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var billingData = (await _db.Ado.SqlQueryAsync(billingDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.billing_performance ?? 0), + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) + } + ); + + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) + var consumeDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count + FROM lq_xh_jksyj jks + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id + WHERE jks.F_IsEffective = 1 + AND hyhk.F_IsEffective = 1 + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; + } + + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var consumeData = (await _db.Ado.SqlQueryAsync(consumeDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.consume_performance ?? 0), + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) + } + ); + + // 2.3 查询退卡业绩数据(使用jkszh作为ID) + var refundDataSql = $@" + SELECT + jks.jkszh as health_coach_id, + jks.jksxm as health_coach_name, + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count + FROM lq_hytk_jksyj jks + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id + WHERE jks.F_IsEffective = 1 + AND hytk.F_IsEffective = 1 + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND jks.jkszh IN ('{healthCoachIdsStr}')"; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; + } + + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; + + var refundData = (await _db.Ado.SqlQueryAsync(refundDataSql)) + .ToDictionary( + item => item.health_coach_id?.ToString(), + item => new + { + Name = item.health_coach_name?.ToString() ?? "未知", + Performance = Convert.ToDecimal(item.refund_performance ?? 0), + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) + } + ); + + // 第三步:合并数据,按原始排序构建排行榜 + var ranking = rankingIds + .Select(id => + { + var billing = billingData.ContainsKey(id) ? billingData[id] : null; + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; + var refund = refundData.ContainsKey(id) ? refundData[id] : null; + + return new HealthCoachStatisticsOutput + { + HealthCoachId = id, + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), + BillingPerformance = billing?.Performance ?? 0, + ConsumePerformance = consume?.Performance ?? 0, + RefundPerformance = refund?.Performance ?? 0, + BillingProjectCount = billing?.ProjectCount ?? 0, + ConsumeProjectCount = consume?.ProjectCount ?? 0, + RefundProjectCount = refund?.ProjectCount ?? 0 + }; + }) + .ToList(); + + return ranking; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取健康师退卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取健康师退卡业绩排行榜失败: {ex.Message}"); + } + } + + #endregion + + #region 门店剩余权益统计 + + /// + /// 获取门店剩余权益统计 + /// + /// + /// 统计指定时间范围内各门店的剩余权益数据,包括剩余权益累计金额和剩余权益累计人数 + /// + /// 剩余权益累计金额 = 开单总金额 - 消耗总金额 - 退卡总金额 + /// 剩余权益累计人数 = 有剩余权益的会员数量(开单品项总次数 - 消耗品项总次数 - 退卡品项总次数 > 0) + /// + /// 示例请求: + /// ```json + /// { + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - startTime: 开始时间(可选,默认为当月1号) + /// - endTime: 结束时间(可选,默认为当前时间) + /// - storeIds: 门店ID列表(可选,不传则统计所有门店) + /// + /// 返回字段说明: + /// - StoreId: 门店ID + /// - StoreName: 门店名称 + /// - RemainingRightsAmount: 剩余权益累计金额 + /// - RemainingRightsPersonCount: 剩余权益累计人数 + /// + /// 查询参数 + /// 门店剩余权益统计数据列表 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-store-remaining-rights")] + public async Task> GetStoreRemainingRights(StoreRemainingRightsInput input) + { + try + { + // 设置默认时间范围(如果未提供,默认为当月) + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var endTime = input.EndTime ?? DateTime.Now; + // 第一步:获取门店基础信息 + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1"; + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; + } + var stores = await _db.Ado.SqlQueryAsync(storeSql); + // 构建门店字典 + var storeDict = new Dictionary(); + foreach (var store in stores) + { + var storeId = store.F_Id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; + } + } + + if (!storeDict.Any()) + { + return new List(); + } + + var allStoreIds = storeDict.Keys.ToList(); + var storeIdsStrForSql = string.Join("','", allStoreIds); + + // 第二步:统计开单总金额(按门店) + var billingAmountSql = $@" + SELECT + kd.djmd as store_id, + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + AND kd.djmd IN ('{storeIdsStrForSql}') + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + GROUP BY kd.djmd"; + + var billingAmountResults = await _db.Ado.SqlQueryAsync(billingAmountSql); + var billingAmountDict = new Dictionary(); + foreach (var item in billingAmountResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + billingAmountDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0); + } + } + + // 第三步:统计消耗总金额(按门店) + var consumeAmountSql = $@" + SELECT + xh.md as store_id, + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + AND xh.md IN ('{storeIdsStrForSql}') + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + GROUP BY xh.md"; + + var consumeAmountResults = await _db.Ado.SqlQueryAsync(consumeAmountSql); + var consumeAmountDict = new Dictionary(); + foreach (var item in consumeAmountResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0); + } + } + + // 第四步:统计退卡总金额(按门店) + var refundAmountSql = $@" + SELECT + hytk.md as store_id, + COALESCE(SUM(CAST(hytk.tkje AS DECIMAL(18,2))), 0) as refund_amount + FROM lq_hytk_hytk hytk + WHERE hytk.F_IsEffective = 1 + AND hytk.md IN ('{storeIdsStrForSql}') + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + GROUP BY hytk.md"; + + var refundAmountResults = await _db.Ado.SqlQueryAsync(refundAmountSql); + var refundAmountDict = new Dictionary(); + foreach (var item in refundAmountResults) + { + var storeId = item.store_id?.ToString(); + if (!string.IsNullOrEmpty(storeId)) + { + refundAmountDict[storeId] = Convert.ToDecimal(item.refund_amount ?? 0); + } + } + + // 第五步:优化策略 - 先获取有开单的会员,然后只查询这些会员的消耗和退卡数据 + // 5.1 首先获取有开单记录的会员ID(按门店分组) + var billingMembersSql = $@" + SELECT DISTINCT + kd.djmd as store_id, + px.F_MemberId as member_id + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND kd.djmd IN ('{storeIdsStrForSql}') + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != ''"; + + var billingMembersResults = await _db.Ado.SqlQueryAsync(billingMembersSql); + + // 按门店分组会员ID + var storeMemberDict = new Dictionary>(); + foreach (var item in billingMembersResults) + { + var storeId = item.store_id?.ToString(); + var memberId = item.member_id?.ToString(); + if (!string.IsNullOrEmpty(storeId) && !string.IsNullOrEmpty(memberId)) + { + if (!storeMemberDict.ContainsKey(storeId)) + { + storeMemberDict[storeId] = new HashSet(); + } + storeMemberDict[storeId].Add(memberId); + } + } + + // 如果没有开单会员,直接返回 + if (!storeMemberDict.Any()) + { + var emptyResultList = new List(); + foreach (var kvp in storeDict) + { + var storeId = kvp.Key; + var storeName = kvp.Value; + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0; + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0; + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount; + + emptyResultList.Add(new StoreRemainingRightsOutput + { + StoreId = storeId, + StoreName = storeName, + RemainingRightsAmount = remainingRightsAmount, + RemainingRightsPersonCount = 0 + }); + } + return emptyResultList.OrderByDescending(x => x.RemainingRightsAmount).ToList(); + } + + // 5.2 分别查询这些会员的开单、消耗、退卡项目数(使用IN子句,大幅减少查询范围) + var remainingPersonCountDict = new Dictionary(); + + foreach (var storeKvp in storeMemberDict) + { + var storeId = storeKvp.Key; + var memberIds = storeKvp.Value.ToList(); + + if (!memberIds.Any()) + { + remainingPersonCountDict[storeId] = 0; + continue; + } + + // 分批处理会员ID(避免IN子句过长) + var batchSize = 500; + var remainingMembers = new HashSet(); + + for (int i = 0; i < memberIds.Count; i += batchSize) + { + var batchMemberIds = memberIds.Skip(i).Take(batchSize).ToList(); + var memberIdsStr = string.Join("','", batchMemberIds); + + // 查询这批会员的开单品项数 + var billingBatchSql = $@" + SELECT + px.F_MemberId as member_id, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND kd.djmd = '{storeId}' + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND px.F_MemberId IN ('{memberIdsStr}') + GROUP BY px.F_MemberId"; + + // 查询这批会员的消耗品项数 + var consumeBatchSql = $@" + SELECT + xhpx.F_MemberId as member_id, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xh.md = '{storeId}' + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + AND xhpx.F_MemberId IN ('{memberIdsStr}') + GROUP BY xhpx.F_MemberId"; + + // 查询这批会员的退卡品项数 + var refundBatchSql = $@" + SELECT + px.F_MemberId as member_id, + SUM(CAST(hytkmx.F_ProjectNumber AS DECIMAL(18,2))) as refund_count + FROM lq_hytk_mx hytkmx + INNER JOIN ( + SELECT F_Id + FROM lq_hytk_hytk + WHERE F_IsEffective = 1 + AND md = '{storeId}' + AND tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + ) hytk ON hytkmx.F_RefundInfoId = hytk.F_Id + INNER JOIN lq_kd_pxmx px ON hytkmx.F_BillingItemId = px.F_Id AND px.F_IsEffective = 1 + WHERE hytkmx.F_IsEffective = 1 + AND px.F_MemberId IN ('{memberIdsStr}') + GROUP BY px.F_MemberId"; + + // 并行查询三个数据 + var billingTask = _db.Ado.SqlQueryAsync(billingBatchSql); + var consumeTask = _db.Ado.SqlQueryAsync(consumeBatchSql); + var refundTask = _db.Ado.SqlQueryAsync(refundBatchSql); + + await Task.WhenAll(billingTask, consumeTask, refundTask); + + var billingData = (await billingTask).ToDictionary( + x => x.member_id?.ToString(), + x => Convert.ToDecimal(x.billing_count ?? 0) + ); + var consumeData = (await consumeTask).ToDictionary( + x => x.member_id?.ToString(), + x => Convert.ToDecimal(x.consume_count ?? 0) + ); + var refundData = (await refundTask).ToDictionary( + x => x.member_id?.ToString(), + x => Convert.ToDecimal(x.refund_count ?? 0) + ); + + // 计算这批会员的剩余次数 + foreach (var memberId in batchMemberIds) + { + var billingCount = billingData.ContainsKey(memberId) ? billingData[memberId] : 0; + var consumeCount = consumeData.ContainsKey(memberId) ? consumeData[memberId] : 0; + var refundCount = refundData.ContainsKey(memberId) ? refundData[memberId] : 0; + var remainingCount = billingCount - consumeCount - refundCount; + + if (remainingCount > 0) + { + remainingMembers.Add(memberId); + } + } + } + + remainingPersonCountDict[storeId] = remainingMembers.Count; + } + + // 第六步:合并数据并计算剩余权益 + var resultList = new List(); + + foreach (var kvp in storeDict) + { + var storeId = kvp.Key; + var storeName = kvp.Value; + + // 计算剩余权益金额 + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0; + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0; + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount; + + // 获取剩余权益人数(已在SQL中计算) + var remainingPersonCount = remainingPersonCountDict.ContainsKey(storeId) ? remainingPersonCountDict[storeId] : 0; + + resultList.Add(new StoreRemainingRightsOutput + { + StoreId = storeId, + StoreName = storeName, + RemainingRightsAmount = remainingRightsAmount, + RemainingRightsPersonCount = remainingPersonCount + }); + } + + // 按剩余权益金额排序 + resultList = resultList.OrderByDescending(x => x.RemainingRightsAmount).ToList(); + + return resultList; + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取门店剩余权益统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); + throw NCCException.Oh($"获取门店剩余权益统计数据失败: {ex.Message}"); + } + } + + #endregion } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs index 4ead485..90f05d1 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs @@ -971,7 +971,7 @@ namespace NCC.Extend.LqStatistics + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n" + $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n" + $"完成率:{monthlyStats.CompletionRate:F2}%\n\n" - + $"http://lvqian.antissoft.com/html/dailyReportnew.html"; + + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html"; var result = await _weChatBotService.SendTextMessage(messageContent); return new diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs index 2d709db..fd9ab12 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs @@ -972,7 +972,8 @@ namespace NCC.Extend.LqTkjlb /// { /// "startTime": "2025-10-01", /// "endTime": "2025-10-31", - /// "eventId": "活动ID" + /// "eventId": "活动ID", + /// "storeId": ["门店ID1", "门店ID2"] /// } /// ``` /// @@ -980,6 +981,7 @@ namespace NCC.Extend.LqTkjlb /// - startTime: 开始时间(可选) /// - endTime: 结束时间(可选) /// - eventId: 活动ID(可选) + /// - storeId: 门店ID数组(可选,可传入多个门店ID进行筛选) /// /// 返回字段说明: /// - TkCount: 拓客人数 @@ -1024,11 +1026,18 @@ namespace NCC.Extend.LqTkjlb eventFilter = $"AND tk.F_EventId = '{input.EventId}'"; } + string storeFilter = ""; + if (input.StoreId != null && input.StoreId.Any() && input.StoreId.Any(s => !string.IsNullOrWhiteSpace(s))) + { + var storeIdsStr = string.Join("','", input.StoreId.Where(s => !string.IsNullOrWhiteSpace(s))); + storeFilter = $"AND tk.F_StoreId IN ('{storeIdsStr}')"; + } + // 第一步:获取拓客人数(去重会员ID) var tkSql = $@" SELECT COUNT(DISTINCT tk.F_MemberId) as tk_count FROM lq_tkjlb tk - WHERE 1=1 {timeFilter} {eventFilter}"; + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; var tkResult = await _db.Ado.SqlQueryAsync(tkSql); var tkCount = Convert.ToInt32(tkResult?.FirstOrDefault()?.tk_count ?? 0); @@ -1039,7 +1048,7 @@ namespace NCC.Extend.LqTkjlb FROM lq_tkjlb tk INNER JOIN lq_yaoyjl yy ON tk.F_MemberId = yy.yykh AND yy.F_StoreId = tk.F_StoreId - WHERE 1=1 {timeFilter} {eventFilter}"; + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; var yaoyResult = await _db.Ado.SqlQueryAsync(yaoySql); var yaoyCount = Convert.ToInt32(yaoyResult?.FirstOrDefault()?.yaoy_count ?? 0); @@ -1050,7 +1059,7 @@ namespace NCC.Extend.LqTkjlb FROM lq_tkjlb tk INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk AND yyjl.F_Status = '已确认' - WHERE 1=1 {timeFilter} {eventFilter}"; + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; var yyResult = await _db.Ado.SqlQueryAsync(yySql); var yyCount = Convert.ToInt32(yyResult?.FirstOrDefault()?.yy_count ?? 0); @@ -1062,7 +1071,7 @@ namespace NCC.Extend.LqTkjlb INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk AND yyjl.F_Status = '已确认' WHERE yyjl.yysj <= NOW() - AND 1=1 {timeFilter} {eventFilter}"; + AND 1=1 {timeFilter} {eventFilter} {storeFilter}"; var ddResult = await _db.Ado.SqlQueryAsync(ddSql); var ddCount = Convert.ToInt32(ddResult?.FirstOrDefault()?.dd_count ?? 0); @@ -1075,7 +1084,7 @@ namespace NCC.Extend.LqTkjlb FROM lq_tkjlb tk INNER JOIN lq_kd_kdjlb kd ON tk.F_MemberId = kd.kdhy AND kd.F_IsEffective = 1 - WHERE 1=1 {timeFilter} {eventFilter}"; + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; var kdResult = await _db.Ado.SqlQueryAsync(kdSql); var kdCount = Convert.ToInt32(kdResult?.FirstOrDefault()?.kd_count ?? 0); @@ -1089,7 +1098,7 @@ namespace NCC.Extend.LqTkjlb FROM lq_tkjlb tk INNER JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy AND xh.F_IsEffective = 1 - WHERE 1=1 {timeFilter} {eventFilter}"; + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; var xfResult = await _db.Ado.SqlQueryAsync(xfSql); var xfCount = Convert.ToInt32(xfResult?.FirstOrDefault()?.xf_count ?? 0); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs index 8194187..f80a985 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs @@ -18,6 +18,7 @@ using NCC.DataEncryption; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqYcsdJsj; +using NCC.Extend.Entitys.Dto.LqJinsanjiaoUser; using NCC.Extend.Entitys.lq_jinsanjiao_user; using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_mdxx; @@ -83,7 +84,7 @@ namespace NCC.Extend.LqYcsdJsj // 获取成员信息 var members = await _db.Queryable() - .Where(x => x.JsjId == id && x.Status == "ACTIVE") + .Where(x => x.JsjId == id && x.Status == "ACTIVE" && x.DeleteMark == 0) .OrderBy(x => x.SortOrder) .Select(x => new { @@ -271,6 +272,45 @@ namespace NCC.Extend.LqYcsdJsj } #endregion + #region 标记删除用户金三角关联信息 + /// + /// 标记删除用户金三角关联信息 + /// + /// + /// 根据金三角用户关系ID,将绑定关系的删除标记设置为已删除 + /// + /// 示例请求: + /// ```json + /// { + /// "id": "关系ID" + /// } + /// ``` + /// + /// 参数 + /// + /// 成功删除 + /// 参数错误 + /// 服务器错误 + [HttpPost("Actions/DeleteJsjUserRelation")] + public async Task DeleteJsjUserRelation([FromBody] LqJinsanjiaoUserDeleteInput input) + { + if (string.IsNullOrEmpty(input.Id)) + throw NCCException.Oh(ErrorCode.COM1000, "ID不能为空"); + + var isOk = await _db.Updateable() + .SetColumns(x => new LqJinsanjiaoUserEntity + { + DeleteMark = 1, + Status = "INACTIVE" + }) + .Where(x => x.Id == input.Id && x.DeleteMark == 0) + .ExecuteCommandAsync(); + + if (!(isOk > 0)) + throw NCCException.Oh(ErrorCode.COM1000, "未找到要删除的记录"); + } + #endregion + #region 新建金三角 /// /// 新建金三角 diff --git a/优化GetStoreRemainingRights性能索引.sql b/优化GetStoreRemainingRights性能索引.sql new file mode 100644 index 0000000..d006c29 --- /dev/null +++ b/优化GetStoreRemainingRights性能索引.sql @@ -0,0 +1,161 @@ +-- ============================================ +-- 优化 GetStoreRemainingRights 方法性能的索引 +-- ============================================ +-- 说明:这些索引专门为统计门店剩余权益功能优化 +-- 执行前请检查索引是否已存在,避免重复创建 + +-- ============================================ +-- 1. lq_kd_pxmx (开单品项明细表) 索引 +-- ============================================ +-- 用于:查询开单品项总数、按会员分组统计、JOIN关联 +-- 查询条件:F_IsEffective, F_MemberId, glkdbh + +-- 索引1:用于F_MemberId IN查询和JOIN +-- MySQL 5.7以下版本请先检查索引是否存在,或直接执行(如果已存在会报错,可忽略) +CREATE INDEX idx_kd_pxmx_member_effective_glkdbh +ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh); + +-- 索引2:用于JOIN和按会员分组(补充索引,覆盖JOIN场景) +CREATE INDEX idx_kd_pxmx_glkdbh_member_effective +ON lq_kd_pxmx(glkdbh, F_MemberId, F_IsEffective); + +-- ============================================ +-- 2. lq_kd_kdjlb (开单记录表) 索引 +-- ============================================ +-- 用于:按门店和时间范围过滤、JOIN关联 +-- 查询条件:F_IsEffective, djmd, kdrq + +-- 索引1:用于门店+时间范围查询(最常用) +CREATE INDEX idx_kd_kdjlb_store_date_effective +ON lq_kd_kdjlb(djmd, kdrq, F_IsEffective); + +-- 索引2:用于JOIN(补充,F_Id是主键可能不需要) +-- 如果F_Id是主键,则不需要此索引 +-- CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_id_effective +-- ON lq_kd_kdjlb(F_Id, F_IsEffective); + +-- ============================================ +-- 3. lq_xh_pxmx (消耗品项明细表) 索引 +-- ============================================ +-- 用于:查询消耗品项总数、按会员分组统计、JOIN关联 +-- 查询条件:F_IsEffective, F_MemberId, F_ConsumeInfoId + +-- 索引1:用于F_MemberId IN查询和JOIN +CREATE INDEX idx_xh_pxmx_member_effective_consume +ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId); + +-- 索引2:用于JOIN场景(补充索引) +CREATE INDEX idx_xh_pxmx_consume_member_effective +ON lq_xh_pxmx(F_ConsumeInfoId, F_MemberId, F_IsEffective); + +-- ============================================ +-- 4. lq_xh_hyhk (消耗会员耗卡表) 索引 +-- ============================================ +-- 用于:按门店和时间范围过滤、JOIN关联 +-- 查询条件:F_IsEffective, md, hksj + +-- 索引1:用于门店+时间范围查询(最常用) +CREATE INDEX idx_xh_hyhk_store_date_effective +ON lq_xh_hyhk(md, hksj, F_IsEffective); + +-- 索引2:用于JOIN(F_Id是主键则不需要) +-- CREATE INDEX IF NOT EXISTS idx_xh_hyhk_id_effective +-- ON lq_xh_hyhk(F_Id, F_IsEffective); + +-- ============================================ +-- 5. lq_hytk_hytk (退卡记录表) 索引 +-- ============================================ +-- 用于:按门店和时间范围过滤、JOIN关联 +-- 查询条件:F_IsEffective, md, tksj + +-- 索引1:用于门店+时间范围查询(最常用) +CREATE INDEX idx_hytk_hytk_store_date_effective +ON lq_hytk_hytk(md, tksj, F_IsEffective); + +-- 索引2:用于JOIN(F_Id是主键则不需要) +-- CREATE INDEX IF NOT EXISTS idx_hytk_hytk_id_effective +-- ON lq_hytk_hytk(F_Id, F_IsEffective); + +-- ============================================ +-- 6. lq_hytk_mx (退卡明细表) 索引 +-- ============================================ +-- 用于:JOIN关联、按会员分组统计 +-- 查询条件:F_IsEffective, F_RefundInfoId, F_BillingItemId + +-- 索引1:用于JOIN lq_hytk_hytk +CREATE INDEX idx_hytk_mx_refund_effective +ON lq_hytk_mx(F_RefundInfoId, F_IsEffective); + +-- 索引2:用于JOIN lq_kd_pxmx +CREATE INDEX idx_hytk_mx_billing_effective +ON lq_hytk_mx(F_BillingItemId, F_IsEffective); + +-- ============================================ +-- 7. 复合索引(补充优化) +-- ============================================ +-- 如果需要进一步优化,可以考虑以下复合索引 + +-- lq_kd_pxmx: 覆盖查询(包含F_ProjectNumber,避免回表) +-- 注意:此索引较大,请根据数据量决定是否创建 +-- CREATE INDEX IF NOT EXISTS idx_kd_pxmx_cover +-- ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh, F_ProjectNumber); + +-- lq_xh_pxmx: 覆盖查询(包含F_ProjectNumber) +-- CREATE INDEX IF NOT EXISTS idx_xh_pxmx_cover +-- ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId, F_ProjectNumber); + +-- ============================================ +-- 索引创建前的检查(避免重复创建) +-- ============================================ +-- 执行以下SQL检查索引是否已存在: + +-- SELECT +-- TABLE_NAME, +-- INDEX_NAME, +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS +-- FROM INFORMATION_SCHEMA.STATISTICS +-- WHERE TABLE_SCHEMA = DATABASE() +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx') +-- AND INDEX_NAME LIKE 'idx_%' +-- GROUP BY TABLE_NAME, INDEX_NAME +-- ORDER BY TABLE_NAME, INDEX_NAME; + +-- ============================================ +-- 索引创建后的验证 +-- ============================================ +-- 执行以下SQL验证索引是否创建成功: + +-- SELECT +-- TABLE_NAME, +-- INDEX_NAME, +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS, +-- NON_UNIQUE, +-- CARDINALITY +-- FROM INFORMATION_SCHEMA.STATISTICS +-- WHERE TABLE_SCHEMA = DATABASE() +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx') +-- AND INDEX_NAME LIKE 'idx_%' +-- GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE, CARDINALITY +-- ORDER BY TABLE_NAME, INDEX_NAME; + +-- ============================================ +-- 性能测试建议 +-- ============================================ +-- 1. 创建索引前,记录查询时间 +-- 2. 创建索引后,使用相同参数重新测试 +-- 3. 使用 EXPLAIN 分析查询计划,确认索引被使用 +-- 4. 如果索引未被使用,检查WHERE条件顺序是否匹配索引列顺序 + +-- ============================================ +-- 注意事项 +-- ============================================ +-- 1. 索引会占用存储空间,请定期监控 +-- 2. 索引会影响INSERT/UPDATE性能,但查询性能提升显著 +-- 3. 如果表数据量很大,创建索引可能需要较长时间 +-- 4. 建议在业务低峰期执行索引创建 +-- 5. MySQL不同版本的兼容性: +-- - MySQL 8.0+ 支持 CREATE INDEX IF NOT EXISTS +-- - MySQL 5.7及以下不支持 IF NOT EXISTS,需要先检查索引是否存在 +-- 本文件已移除 IF NOT EXISTS,如索引已存在会报错,可安全忽略 +-- 6. 如果报错 "Duplicate key name",说明索引已存在,可以继续执行后面的语句 + diff --git a/创建会员开单耗卡项目数视图.sql b/创建会员开单耗卡项目数视图.sql new file mode 100644 index 0000000..585bfad --- /dev/null +++ b/创建会员开单耗卡项目数视图.sql @@ -0,0 +1,235 @@ +-- ============================================ +-- 创建会员开单耗卡项目数视图 +-- ============================================ +-- 说明:创建视图可以提升查询效率,避免每次都执行复杂的JOIN和聚合操作 +-- 视图会预先聚合好数据,查询时直接使用 + +-- ============================================ +-- 方案1:基础聚合视图(推荐) +-- ============================================ +-- 优点:查询简单、性能好、可以灵活按时间范围查询 +-- 缺点:仍然需要每次查询时过滤时间范围 + +-- 删除已存在的视图(如果存在) +DROP VIEW IF EXISTS v_member_billing_consume_project; + +-- 创建视图:会员开单项目数汇总(不限制时间,包含所有数据) +CREATE VIEW v_member_billing_project AS +SELECT + px.F_MemberId as member_id, + kd.djmd as store_id, + kd.kdrq as billing_date, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count +FROM lq_kd_pxmx px +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id +WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' +GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq); + +-- 创建视图:会员耗卡项目数汇总(不限制时间,包含所有数据) +CREATE VIEW v_member_consume_project AS +SELECT + xhpx.F_MemberId as member_id, + xh.md as store_id, + xh.hksj as consume_date, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count +FROM lq_xh_pxmx xhpx +INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id +WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' +GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj); + +-- 创建组合视图:会员开单耗卡项目数(按日汇总) +CREATE VIEW v_member_billing_consume_daily AS +SELECT + COALESCE(billing.member_id, consume.member_id) as member_id, + COALESCE(billing.store_id, consume.store_id) as store_id, + COALESCE(billing.billing_date, consume.consume_date) as statistics_date, + COALESCE(billing.billing_project_count, 0) as billing_project_count, + COALESCE(consume.consume_project_count, 0) as consume_project_count, + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count +FROM v_member_billing_project billing +FULL OUTER JOIN v_member_consume_project consume + ON billing.member_id = consume.member_id + AND billing.store_id = consume.store_id + AND DATE(billing.billing_date) = DATE(consume.consume_date); + +-- ============================================ +-- 方案2:使用UNION ALL的视图(兼容MySQL,推荐) +-- ============================================ +-- 优点:兼容MySQL所有版本、查询简单、性能好 + +DROP VIEW IF EXISTS v_member_billing_consume_project; + +CREATE VIEW v_member_billing_consume_project AS +SELECT + member_id, + store_id, + statistics_date, + SUM(billing_project_count) as billing_project_count, + SUM(consume_project_count) as consume_project_count, + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count +FROM ( + -- 开单项目数(按会员、门店、日期分组) + SELECT + px.F_MemberId as member_id, + kd.djmd as store_id, + DATE(kd.kdrq) as statistics_date, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, + 0 as consume_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq) + + UNION ALL + + -- 耗卡项目数(按会员、门店、日期分组) + SELECT + xhpx.F_MemberId as member_id, + xh.md as store_id, + DATE(xh.hksj) as statistics_date, + 0 as billing_project_count, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj) +) all_data +GROUP BY member_id, store_id, statistics_date; + +-- ============================================ +-- 方案3:按会员汇总的总视图(不按日期,累计汇总) +-- ============================================ +-- 优点:查询最简单、性能最好(适合查询总量) +-- 缺点:无法按时间范围查询 + +DROP VIEW IF EXISTS v_member_billing_consume_total; + +CREATE VIEW v_member_billing_consume_total AS +SELECT + member_id, + SUM(billing_project_count) as total_billing_project_count, + SUM(consume_project_count) as total_consume_project_count, + SUM(billing_project_count) - SUM(consume_project_count) as total_remaining_project_count, + COUNT(DISTINCT store_id) as store_count +FROM ( + -- 开单项目数(按会员、门店汇总) + SELECT + px.F_MemberId as member_id, + kd.djmd as store_id, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, + 0 as consume_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + GROUP BY px.F_MemberId, kd.djmd + + UNION ALL + + -- 耗卡项目数(按会员、门店汇总) + SELECT + xhpx.F_MemberId as member_id, + xh.md as store_id, + 0 as billing_project_count, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + GROUP BY xhpx.F_MemberId, xh.md +) all_data +GROUP BY member_id; + + +-- ============================================ +-- 使用视图的查询示例 +-- ============================================ + +-- 示例1:查询所有会员的开单耗卡项目数(按会员汇总,不限制时间) +SELECT + member_id, + SUM(billing_project_count) as billing_project_count, + SUM(consume_project_count) as consume_project_count, + SUM(remaining_project_count) as remaining_project_count +FROM v_member_billing_consume_project +GROUP BY member_id +ORDER BY billing_project_count DESC; + +-- 示例2:查询指定时间范围内的数据 +SELECT + member_id, + SUM(billing_project_count) as billing_project_count, + SUM(consume_project_count) as consume_project_count, + SUM(remaining_project_count) as remaining_project_count +FROM v_member_billing_consume_project +WHERE statistics_date >= '2025-10-01' + AND statistics_date <= '2025-10-30' +GROUP BY member_id +ORDER BY billing_project_count DESC; + +-- 示例3:查询指定门店的数据 +SELECT + member_id, + SUM(billing_project_count) as billing_project_count, + SUM(consume_project_count) as consume_project_count, + SUM(remaining_project_count) as remaining_project_count +FROM v_member_billing_consume_project +WHERE store_id = '1649328471923847169' + AND statistics_date >= '2025-10-01' + AND statistics_date <= '2025-10-30' +GROUP BY member_id +ORDER BY billing_project_count DESC; + +-- 示例4:使用总视图(最简单的查询,累计所有数据) +SELECT + member_id, + total_billing_project_count, + total_consume_project_count, + total_remaining_project_count, + store_count +FROM v_member_billing_consume_total +ORDER BY total_billing_project_count DESC +LIMIT 100; + + +-- ============================================ +-- 视图性能优化建议 +-- ============================================ +-- 1. 确保基础表已创建索引(参考优化GetStoreRemainingRights性能索引.sql) +-- 2. 如果数据量很大,可以考虑: +-- a. 创建物化视图(但MySQL不支持,需要定期刷新汇总表) +-- b. 创建汇总表,定期更新(推荐) +-- 3. 定期分析视图性能: +-- EXPLAIN SELECT * FROM v_member_billing_consume_project WHERE statistics_date >= '2025-10-01'; + +-- ============================================ +-- 视图维护 +-- ============================================ +-- 查看视图定义 +-- SHOW CREATE VIEW v_member_billing_consume_project; + +-- 查看所有视图 +-- SELECT TABLE_NAME +-- FROM INFORMATION_SCHEMA.VIEWS +-- WHERE TABLE_SCHEMA = DATABASE(); + +-- 删除视图 +-- DROP VIEW IF EXISTS v_member_billing_consume_project; +-- DROP VIEW IF EXISTS v_member_billing_consume_total; + diff --git a/查询所有会员开单耗卡项目数.sql b/查询所有会员开单耗卡项目数.sql new file mode 100644 index 0000000..d075955 --- /dev/null +++ b/查询所有会员开单耗卡项目数.sql @@ -0,0 +1,209 @@ +-- ============================================ +-- 查询所有会员的开单项目数和耗卡项目数 +-- ============================================ +-- 说明:统计每个会员在指定时间范围内的开单项目总数和耗卡项目总数 + +-- ============================================ +-- 版本1:基础查询(不限制时间范围) +-- ============================================ +SELECT + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, + COALESCE(billing.billing_project_count, 0) as billing_project_count, + COALESCE(consume.consume_project_count, 0) as consume_project_count, + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count +FROM ( + -- 开单项目数(按会员分组) + SELECT + px.F_MemberId, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + GROUP BY px.F_MemberId +) billing +FULL OUTER JOIN ( + -- 耗卡项目数(按会员分组) + SELECT + xhpx.F_MemberId, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + GROUP BY xhpx.F_MemberId +) consume ON billing.F_MemberId = consume.F_MemberId +ORDER BY billing_project_count DESC; + + +-- ============================================ +-- 版本2:带时间范围限制(推荐使用) +-- ============================================ +-- 使用方法:修改下面的时间范围 +SELECT + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, + COALESCE(billing.billing_project_count, 0) as billing_project_count, + COALESCE(consume.consume_project_count, 0) as consume_project_count, + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count +FROM ( + -- 开单项目数(按会员分组) + SELECT + px.F_MemberId, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY px.F_MemberId +) billing +FULL OUTER JOIN ( + -- 耗卡项目数(按会员分组) + SELECT + xhpx.F_MemberId, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY xhpx.F_MemberId +) consume ON billing.F_MemberId = consume.F_MemberId +ORDER BY billing_project_count DESC; + + +-- ============================================ +-- 版本3:兼容MySQL的写法(MySQL不支持FULL OUTER JOIN) +-- ============================================ +SELECT + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, + COALESCE(billing.billing_project_count, 0) as billing_project_count, + COALESCE(consume.consume_project_count, 0) as consume_project_count, + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count +FROM ( + -- 开单项目数(按会员分组) + SELECT + px.F_MemberId, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY px.F_MemberId +) billing +LEFT JOIN ( + -- 耗卡项目数(按会员分组) + SELECT + xhpx.F_MemberId, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY xhpx.F_MemberId +) consume ON billing.F_MemberId = consume.F_MemberId + +UNION + +SELECT + consume.F_MemberId as member_id, + 0 as billing_project_count, + consume.consume_project_count, + (0 - consume.consume_project_count) as remaining_project_count +FROM ( + -- 耗卡项目数(按会员分组) + SELECT + xhpx.F_MemberId, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY xhpx.F_MemberId +) consume +LEFT JOIN ( + -- 开单项目数(按会员分组) + SELECT + px.F_MemberId, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY px.F_MemberId +) billing ON consume.F_MemberId = billing.F_MemberId +WHERE billing.F_MemberId IS NULL + +ORDER BY billing_project_count DESC; + + +-- ============================================ +-- 版本4:最简单版本(推荐,使用UNION ALL + GROUP BY实现FULL OUTER JOIN效果) +-- ============================================ +SELECT + member_id, + SUM(billing_project_count) as billing_project_count, + SUM(consume_project_count) as consume_project_count, + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count +FROM ( + -- 开单项目数 + SELECT + px.F_MemberId as member_id, + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, + 0 as consume_project_count + FROM lq_kd_pxmx px + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND px.F_MemberId IS NOT NULL + AND px.F_MemberId != '' + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY px.F_MemberId + + UNION ALL + + -- 耗卡项目数 + SELECT + xhpx.F_MemberId as member_id, + 0 as billing_project_count, + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count + FROM lq_xh_pxmx xhpx + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id + WHERE xhpx.F_IsEffective = 1 + AND xh.F_IsEffective = 1 + AND xhpx.F_MemberId IS NOT NULL + AND xhpx.F_MemberId != '' + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) + GROUP BY xhpx.F_MemberId +) all_data +GROUP BY member_id +ORDER BY billing_project_count DESC; + -- libgit2 0.21.4