diff --git a/antis-ncc-admin/.env.development b/antis-ncc-admin/.env.development index 4c02e12..e9a4583 100644 --- a/antis-ncc-admin/.env.development +++ b/antis-ncc-admin/.env.development @@ -2,6 +2,6 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' -# VUE_APP_BASE_API = 'http://localhost:2011' +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' +VUE_APP_BASE_API = 'http://localhost:2011' VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' 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/StorePerformanceComparisonOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StorePerformanceComparisonOutput.cs index c6c17bb..1bd9983 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StorePerformanceComparisonOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StorePerformanceComparisonOutput.cs @@ -39,5 +39,10 @@ namespace NCC.Extend.Entitys.Dto.LqReport /// 是否达标(实际业绩 >= 目标业绩) /// public bool IsTargetAchieved { get; set; } + + /// + /// 实际消耗业绩 + /// + public decimal ActualConsumePerformance { 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.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs index 051cc2b..8786f9b 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs @@ -31,6 +31,12 @@ namespace NCC.Extend.Entitys.lq_hytk_mx public string BillingItemId { get; set; } /// + /// 会员id + /// + [SugarColumn(ColumnName = "F_MemberId")] + public string MemberId { get; set; } + + /// /// 品项 /// [SugarColumn(ColumnName = "px")] diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs index b2b34c5..59e8dc1 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs @@ -19,10 +19,5 @@ namespace NCC.Extend.Entitys.Enum [Description("有效")] 有效 = 1, - /// - /// 删除 - /// - [Description("删除")] - 删除 = 99, } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs index ac0f8d4..0b4f4f7 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs @@ -18,7 +18,7 @@ namespace NCC.Extend.LqCardTransferLog /// /// 转卡日志服务 /// - [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 100)] + [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 200)] [Route("api/Extend/[controller]")] public class LqCardTransferLogService : IDynamicApiController, ILqCardTransferLogService, ITransient { 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/LqGzService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqGzService.cs index 191c9fd..a1f2419 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqGzService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqGzService.cs @@ -913,10 +913,5 @@ namespace NCC.Extend.LqGz } #endregion - - - - - } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs index fad3d6d..ce5fd82 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs @@ -24,6 +24,7 @@ using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_hytk_jksyj; using NCC.Extend.Entitys.lq_hytk_kjbsyj; using NCC.Extend.Entitys.lq_hytk_mx; +using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Interfaces.LqHytkHytk; using NCC.FriendlyException; using NCC.JsonSerialization; @@ -230,6 +231,7 @@ namespace NCC.Extend.LqHytkHytk Id = YitIdHelper.NextId().ToString(), RefundInfoId = newEntity.Id, BillingItemId = item.billingItemId, + MemberId = newEntity.Hy, CreateTime = DateTime.Now, CreateUser = userInfo.userId, Px = item.px, @@ -368,18 +370,19 @@ namespace NCC.Extend.LqHytkHytk var allMxEntities = new List(); var allJksyjEntities = new List(); var allKjbsyjEntities = new List(); - // 处理品项明细列表 if (input.lqHytkMxList != null && input.lqHytkMxList.Any()) { foreach (var item in input.lqHytkMxList) { + // 创建品项明细实体 var lqHytkMxEntity = new LqHytkMxEntity { Id = YitIdHelper.NextId().ToString(), RefundInfoId = id, BillingItemId = item.billingItemId, + MemberId = entity.Hy, CreateTime = DateTime.Now, CreateUser = userInfo.userId, Tksj = input.tksj, diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs index 0969449..9b25335 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs @@ -2478,6 +2478,7 @@ namespace NCC.Extend.LqKdKdjlb Id = YitIdHelper.NextId().ToString(), RefundInfoId = refundId, BillingItemId = item.BillingItemId, + MemberId = input.FromMemberId, // 转卡时使用转出方会员ID CreateTime = transferTime, CreateUser = userInfo.userId, Px = refundPxmxEntity.Px, diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs index ba5ec9e..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); @@ -1047,6 +1030,7 @@ namespace NCC.Extend /// - StoreName: 门店名称 /// - TargetPerformance: 目标业绩(来自门店资料表xsyj字段) /// - ActualPerformance: 实际开单业绩(统计期间内的开单业绩总和) + /// - ActualConsumePerformance: 实际消耗业绩(统计期间内的消耗业绩总和) /// - CompletionRate: 完成率(实际业绩/目标业绩 × 100%) /// - Difference: 差额(实际业绩-目标业绩) /// - IsTargetAchieved: 是否达标(实际业绩 >= 目标业绩) @@ -1065,42 +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, + kd.djmd as store_id, COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as actual_performance - FROM lq_mdxx md - LEFT JOIN lq_kd_kdjlb kd ON md.F_Id = kd.djmd - AND kd.F_IsEffective = 1 + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 AND kd.kdrq >= @startTime AND kd.kdrq <= @endTime"; - object parameters; + object billingParameters = new { startTime, endTime }; + if (input.StoreIds != null && input.StoreIds.Any()) { - sql += " AND 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) { - 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}')"; + } + + consumeSql += " GROUP BY xh.md"; - foreach (var item in results) + 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); + 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 @@ -1119,12 +1167,16 @@ namespace NCC.Extend StoreName = storeName, TargetPerformance = targetPerformance, ActualPerformance = actualPerformance, + ActualConsumePerformance = actualConsumePerformance, CompletionRate = completionRate, Difference = difference, IsTargetAchieved = isTargetAchieved }); } + // 按实际业绩排序 + storePerformanceList = storePerformanceList.OrderByDescending(x => x.ActualPerformance).ToList(); + return storePerformanceList; } catch (Exception ex) @@ -1266,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"; @@ -1325,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..97ac2e7 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 @@ -2586,7 +2586,7 @@ namespace NCC.Extend.LqStatistics LEFT JOIN ( SELECT hytk.md as F_StoreId, - COALESCE(SUM(hytk.tkje), 0) as F_RefundAmount, + COALESCE(SUM(hytk.F_ActualRefundAmount), 0) as F_RefundAmount, COUNT(DISTINCT hytk.F_Id) as F_RefundCount FROM lq_hytk_hytk hytk WHERE hytk.F_IsEffective = 1 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/数据库说明.md b/数据库说明.md index e194e7c..d7784c2 100644 --- a/数据库说明.md +++ b/数据库说明.md @@ -90,7 +90,20 @@ - `lq_xh_jksyj.F_kdpxid` ↔ `lq_xh_pxmx.F_Id` (健康师业绩关联品项明细) - `lq_xh_kjbsyj.F_hkpxid` ↔ `lq_xh_pxmx.F_Id` (科技部老师业绩关联品项明细) -### 5. 业绩统计关系 +### 5. 退卡业务关系 +- **退卡记录**: `lq_hytk_hytk` (退卡记录表) +- **退卡品项明细**: `lq_hytk_mx` (退卡品项明细) +- **退卡健康师业绩**: `lq_hytk_jksyj` (退卡健康师业绩) +- **退卡科技部老师业绩**: `lq_hytk_kjbsyj` (退卡科技部老师业绩) +- **关联字段**: + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_mx.F_RefundInfoId` (退卡记录关联品项明细) + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_jksyj.gltkbh` (退卡记录关联健康师业绩) + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_kjbsyj.gltkbh` (退卡记录关联科技部老师业绩) + - `lq_hytk_mx.F_BillingItemId` ↔ `lq_kd_pxmx.F_Id` (退卡明细关联开单品项明细) + - `lq_hytk_mx.F_MemberId` ↔ `lq_khxx.F_Id` (退卡明细关联会员,通过会员ID) + - `lq_hytk_hytk.hy` ↔ `lq_khxx.F_Id` (退卡记录关联会员) + +### 6. 业绩统计关系 - **业绩明细**: `lq_yjmxb` (业绩统计表) - **关联字段**: - `lq_yjmxb.jks` ↔ `BASE_USER.F_REALNAME` (健康师姓名) 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; + diff --git a/添加lq_hytk_mx表会员ID字段.sql b/添加lq_hytk_mx表会员ID字段.sql new file mode 100644 index 0000000..b3b339d --- /dev/null +++ b/添加lq_hytk_mx表会员ID字段.sql @@ -0,0 +1,27 @@ +-- ============================================ +-- 为 lq_hytk_mx 表添加会员ID字段 +-- ============================================ + +-- 1. 添加会员ID字段 +ALTER TABLE lq_hytk_mx +ADD COLUMN F_MemberId VARCHAR(50) NULL COMMENT '会员id' AFTER F_BillingItemId; + +-- 2. 创建索引以优化查询性能 +CREATE INDEX idx_hytk_mx_member_id ON lq_hytk_mx(F_MemberId); + +-- 3. 根据退卡信息表填充现有数据的会员ID(通过 F_RefundInfoId 关联) +UPDATE lq_hytk_mx mx +INNER JOIN lq_hytk_hytk hytk ON mx.F_RefundInfoId = hytk.F_Id +SET mx.F_MemberId = hytk.hy +WHERE mx.F_MemberId IS NULL + AND hytk.hy IS NOT NULL + AND hytk.hy != ''; + +-- 4. 根据开单品项明细表填充会员ID(如果退卡信息表中没有,则从开单品项明细表获取) +UPDATE lq_hytk_mx mx +INNER JOIN lq_kd_pxmx pxmx ON mx.F_BillingItemId = pxmx.F_Id +SET mx.F_MemberId = pxmx.F_MemberId +WHERE mx.F_MemberId IS NULL + AND pxmx.F_MemberId IS NOT NULL + AND pxmx.F_MemberId != ''; +