Commit d19ef8a50f9f253d7d01ea9d33835dd990b990c5
Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP
Showing
29 changed files
with
2693 additions
and
93 deletions
antis-ncc-admin/.env.development
| ... | ... | @@ -2,6 +2,6 @@ |
| 2 | 2 | |
| 3 | 3 | VUE_CLI_BABEL_TRANSPILE_MODULES = true |
| 4 | 4 | # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' |
| 5 | -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' | |
| 6 | -# VUE_APP_BASE_API = 'http://localhost:2011' | |
| 5 | +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' | |
| 6 | +VUE_APP_BASE_API = 'http://localhost:2011' | |
| 7 | 7 | VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqJinsanjiaoUser/LqJinsanjiaoUserDeleteInput.cs
0 → 100644
| 1 | +using System.ComponentModel.DataAnnotations; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqJinsanjiaoUser | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 删除金三角用户绑定关系输入 | |
| 7 | + /// </summary> | |
| 8 | + public class LqJinsanjiaoUserDeleteInput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 金三角用户关系ID | |
| 12 | + /// </summary> | |
| 13 | + [Required(ErrorMessage = "ID不能为空")] | |
| 14 | + public string Id { get; set; } | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 客户到店次数统计输入 | |
| 8 | + /// </summary> | |
| 9 | + public class CustomerVisitFrequencyInput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 开始时间(可选,默认为当月1号) | |
| 13 | + /// </summary> | |
| 14 | + public DateTime? StartTime { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 结束时间(可选,默认为当前时间) | |
| 18 | + /// </summary> | |
| 19 | + public DateTime? EndTime { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 门店ID列表(可选) | |
| 23 | + /// </summary> | |
| 24 | + public List<string> StoreIds { get; set; } | |
| 25 | + } | |
| 26 | +} | |
| 27 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/CustomerVisitFrequencyOutput.cs
0 → 100644
| 1 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 2 | +{ | |
| 3 | + /// <summary> | |
| 4 | + /// 客户到店次数统计输出 | |
| 5 | + /// </summary> | |
| 6 | + public class CustomerVisitFrequencyOutput | |
| 7 | + { | |
| 8 | + /// <summary> | |
| 9 | + /// 消耗次数(客户到店次数) | |
| 10 | + /// </summary> | |
| 11 | + public int VisitCount { get; set; } | |
| 12 | + | |
| 13 | + /// <summary> | |
| 14 | + /// 人数(达到该次数的客户人数) | |
| 15 | + /// </summary> | |
| 16 | + public int CustomerCount { get; set; } | |
| 17 | + } | |
| 18 | +} | |
| 19 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachRankingOutput.cs
0 → 100644
| 1 | +using System.Collections.Generic; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 健康师排行榜输出 | |
| 7 | + /// </summary> | |
| 8 | + public class HealthCoachRankingOutput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 开单业绩排行榜(前20名) | |
| 12 | + /// </summary> | |
| 13 | + public List<HealthCoachStatisticsOutput> BillingRanking { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 耗卡业绩排行榜(前20名) | |
| 17 | + /// </summary> | |
| 18 | + public List<HealthCoachStatisticsOutput> ConsumeRanking { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 退卡业绩排行榜(前20名) | |
| 22 | + /// </summary> | |
| 23 | + public List<HealthCoachStatisticsOutput> RefundRanking { get; set; } | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 健康师统计数据输入 | |
| 8 | + /// </summary> | |
| 9 | + public class HealthCoachStatisticsInput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 开始时间(可选,默认为当月1号) | |
| 13 | + /// </summary> | |
| 14 | + public DateTime? StartTime { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 结束时间(可选,默认为当前时间) | |
| 18 | + /// </summary> | |
| 19 | + public DateTime? EndTime { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 门店ID列表(可选) | |
| 23 | + /// </summary> | |
| 24 | + public List<string> StoreIds { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 健康师ID列表(可选) | |
| 28 | + /// </summary> | |
| 29 | + public List<string> HealthCoachIds { get; set; } | |
| 30 | + } | |
| 31 | +} | |
| 32 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/HealthCoachStatisticsOutput.cs
0 → 100644
| 1 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 2 | +{ | |
| 3 | + /// <summary> | |
| 4 | + /// 健康师统计数据输出 | |
| 5 | + /// </summary> | |
| 6 | + public class HealthCoachStatisticsOutput | |
| 7 | + { | |
| 8 | + /// <summary> | |
| 9 | + /// 健康师ID | |
| 10 | + /// </summary> | |
| 11 | + public string HealthCoachId { get; set; } | |
| 12 | + | |
| 13 | + /// <summary> | |
| 14 | + /// 健康师姓名 | |
| 15 | + /// </summary> | |
| 16 | + public string HealthCoachName { get; set; } | |
| 17 | + | |
| 18 | + /// <summary> | |
| 19 | + /// 开单业绩 | |
| 20 | + /// </summary> | |
| 21 | + public decimal BillingPerformance { get; set; } | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 消耗业绩 | |
| 25 | + /// </summary> | |
| 26 | + public decimal ConsumePerformance { get; set; } | |
| 27 | + | |
| 28 | + /// <summary> | |
| 29 | + /// 退单业绩 | |
| 30 | + /// </summary> | |
| 31 | + public decimal RefundPerformance { get; set; } | |
| 32 | + | |
| 33 | + /// <summary> | |
| 34 | + /// 开单项目数 | |
| 35 | + /// </summary> | |
| 36 | + public int BillingProjectCount { get; set; } | |
| 37 | + | |
| 38 | + /// <summary> | |
| 39 | + /// 消耗项目数 | |
| 40 | + /// </summary> | |
| 41 | + public int ConsumeProjectCount { get; set; } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// 退单项目数 | |
| 45 | + /// </summary> | |
| 46 | + public int RefundProjectCount { get; set; } | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 门店项目指标统计输入 | |
| 7 | + /// </summary> | |
| 8 | + public class StoreItemStatisticsInput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 开始时间 | |
| 12 | + /// </summary> | |
| 13 | + public DateTime? StartTime { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 结束时间 | |
| 17 | + /// </summary> | |
| 18 | + public DateTime? EndTime { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 门店ID列表 | |
| 22 | + /// </summary> | |
| 23 | + public string[] StoreIds { get; set; } | |
| 24 | + } | |
| 25 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreItemStatisticsOutput.cs
0 → 100644
| 1 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 2 | +{ | |
| 3 | + /// <summary> | |
| 4 | + /// 门店项目指标统计输出 | |
| 5 | + /// </summary> | |
| 6 | + public class StoreItemStatisticsOutput | |
| 7 | + { | |
| 8 | + /// <summary> | |
| 9 | + /// 门店ID | |
| 10 | + /// </summary> | |
| 11 | + public string StoreId { get; set; } | |
| 12 | + | |
| 13 | + /// <summary> | |
| 14 | + /// 门店名称 | |
| 15 | + /// </summary> | |
| 16 | + public string StoreName { get; set; } | |
| 17 | + | |
| 18 | + /// <summary> | |
| 19 | + /// 消耗项目数(项目次数总和) | |
| 20 | + /// </summary> | |
| 21 | + public int ConsumeProjectCount { get; set; } | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 消耗率(消耗金额/开单金额 × 100%) | |
| 25 | + /// </summary> | |
| 26 | + public decimal ConsumeRate { get; set; } | |
| 27 | + | |
| 28 | + /// <summary> | |
| 29 | + /// 客单项目数(项目数/消耗人次) | |
| 30 | + /// </summary> | |
| 31 | + public decimal AvgProjectPerConsume { get; set; } | |
| 32 | + | |
| 33 | + /// <summary> | |
| 34 | + /// 消耗客单价(消耗业绩/消耗人次) | |
| 35 | + /// </summary> | |
| 36 | + public decimal AvgAmountPerConsume { get; set; } | |
| 37 | + | |
| 38 | + /// <summary> | |
| 39 | + /// 开单金额 | |
| 40 | + /// </summary> | |
| 41 | + public decimal BillingAmount { get; set; } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// 消耗金额 | |
| 45 | + /// </summary> | |
| 46 | + public decimal ConsumeAmount { get; set; } | |
| 47 | + | |
| 48 | + /// <summary> | |
| 49 | + /// 消耗人次 | |
| 50 | + /// </summary> | |
| 51 | + public int ConsumePersonCount { get; set; } | |
| 52 | + } | |
| 53 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StorePerformanceComparisonOutput.cs
| ... | ... | @@ -39,5 +39,10 @@ namespace NCC.Extend.Entitys.Dto.LqReport |
| 39 | 39 | /// 是否达标(实际业绩 >= 目标业绩) |
| 40 | 40 | /// </summary> |
| 41 | 41 | public bool IsTargetAchieved { get; set; } |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// 实际消耗业绩 | |
| 45 | + /// </summary> | |
| 46 | + public decimal ActualConsumePerformance { get; set; } | |
| 42 | 47 | } |
| 43 | 48 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 门店剩余权益统计输入 | |
| 7 | + /// </summary> | |
| 8 | + public class StoreRemainingRightsInput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 开始时间(可选,默认为当月1号) | |
| 12 | + /// </summary> | |
| 13 | + public DateTime? StartTime { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 结束时间(可选,默认为当前时间) | |
| 17 | + /// </summary> | |
| 18 | + public DateTime? EndTime { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 门店ID列表(可选,不传则统计所有门店) | |
| 22 | + /// </summary> | |
| 23 | + public string[] StoreIds { get; set; } | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreRemainingRightsOutput.cs
0 → 100644
| 1 | +namespace NCC.Extend.Entitys.Dto.LqReport | |
| 2 | +{ | |
| 3 | + /// <summary> | |
| 4 | + /// 门店剩余权益统计输出 | |
| 5 | + /// </summary> | |
| 6 | + public class StoreRemainingRightsOutput | |
| 7 | + { | |
| 8 | + /// <summary> | |
| 9 | + /// 门店ID | |
| 10 | + /// </summary> | |
| 11 | + public string StoreId { get; set; } | |
| 12 | + | |
| 13 | + /// <summary> | |
| 14 | + /// 门店名称 | |
| 15 | + /// </summary> | |
| 16 | + public string StoreName { get; set; } | |
| 17 | + | |
| 18 | + /// <summary> | |
| 19 | + /// 剩余权益累计金额(开单总金额 - 消耗总金额 - 退卡总金额) | |
| 20 | + /// </summary> | |
| 21 | + public decimal RemainingRightsAmount { get; set; } | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 剩余权益累计人数(有剩余权益的会员数量) | |
| 25 | + /// </summary> | |
| 26 | + public int RemainingRightsPersonCount { get; set; } | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/TkStatisticsInput.cs
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_hytk_mx/LqHytkMxEntity.cs
| ... | ... | @@ -31,6 +31,12 @@ namespace NCC.Extend.Entitys.lq_hytk_mx |
| 31 | 31 | public string BillingItemId { get; set; } |
| 32 | 32 | |
| 33 | 33 | /// <summary> |
| 34 | + /// 会员id | |
| 35 | + /// </summary> | |
| 36 | + [SugarColumn(ColumnName = "F_MemberId")] | |
| 37 | + public string MemberId { get; set; } | |
| 38 | + | |
| 39 | + /// <summary> | |
| 34 | 40 | /// 品项 |
| 35 | 41 | /// </summary> |
| 36 | 42 | [SugarColumn(ColumnName = "px")] | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/StatusEnum.cs
netcore/src/Modularity/Extend/NCC.Extend/LqCardTransferLogService.cs
| ... | ... | @@ -18,7 +18,7 @@ namespace NCC.Extend.LqCardTransferLog |
| 18 | 18 | /// <summary> |
| 19 | 19 | /// 转卡日志服务 |
| 20 | 20 | /// </summary> |
| 21 | - [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 100)] | |
| 21 | + [ApiDescriptionSettings(Tag = "绿纤转卡日志服务", Name = "LqCardTransferLog", Order = 200)] | |
| 22 | 22 | [Route("api/Extend/[controller]")] |
| 23 | 23 | public class LqCardTransferLogService : IDynamicApiController, ILqCardTransferLogService, ITransient |
| 24 | 24 | { | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
| ... | ... | @@ -22,6 +22,7 @@ using NCC.DataEncryption; |
| 22 | 22 | using NCC.ClayObject; |
| 23 | 23 | using NCC.Extend.Entitys.Dto.LqDailyReport; |
| 24 | 24 | using NCC.Extend.Entitys.lq_kd_kdjlb; |
| 25 | +using Microsoft.AspNetCore.Authorization; | |
| 25 | 26 | |
| 26 | 27 | namespace NCC.Extend |
| 27 | 28 | { |
| ... | ... | @@ -103,6 +104,7 @@ namespace NCC.Extend |
| 103 | 104 | /// <response code="200">成功返回门店统计列表</response> |
| 104 | 105 | /// <response code="400">日期格式错误或参数无效</response> |
| 105 | 106 | /// <response code="500">服务器内部错误</response> |
| 107 | + [AllowAnonymous] | |
| 106 | 108 | [HttpPost("get-store-daily-statistics")] |
| 107 | 109 | public async Task<List<StoreDailyStatisticsOutput>> GetStoreDailyStatistics(StoreDailyStatisticsInput input) |
| 108 | 110 | { |
| ... | ... | @@ -198,6 +200,7 @@ namespace NCC.Extend |
| 198 | 200 | /// <response code="200">成功返回门店业绩完成情况列表</response> |
| 199 | 201 | /// <response code="400">日期格式错误或参数无效</response> |
| 200 | 202 | /// <response code="500">服务器内部错误</response> |
| 203 | + [AllowAnonymous] | |
| 201 | 204 | [HttpPost("get-store-performance-completion")] |
| 202 | 205 | public async Task<List<StorePerformanceCompletionOutput>> GetStorePerformanceCompletion(StorePerformanceCompletionInput input) |
| 203 | 206 | { |
| ... | ... | @@ -288,6 +291,7 @@ namespace NCC.Extend |
| 288 | 291 | /// <response code="200">成功返回事业部业绩完成情况列表</response> |
| 289 | 292 | /// <response code="400">日期格式错误或参数无效</response> |
| 290 | 293 | /// <response code="500">服务器内部错误</response> |
| 294 | + [AllowAnonymous] | |
| 291 | 295 | [HttpPost("get-business-unit-performance-completion")] |
| 292 | 296 | public async Task<List<BusinessUnitPerformanceCompletionOutput>> GetBusinessUnitPerformanceCompletion(BusinessUnitPerformanceCompletionInput input) |
| 293 | 297 | { |
| ... | ... | @@ -421,6 +425,7 @@ namespace NCC.Extend |
| 421 | 425 | /// <response code="200">成功返回天王团业绩完成情况列表</response> |
| 422 | 426 | /// <response code="400">日期格式错误或参数无效</response> |
| 423 | 427 | /// <response code="500">服务器内部错误</response> |
| 428 | + [AllowAnonymous] | |
| 424 | 429 | [HttpPost("get-tianwang-group-performance-completion")] |
| 425 | 430 | public async Task<List<TianwangGroupPerformanceCompletionOutput>> GetTianwangGroupPerformanceCompletion(TianwangGroupPerformanceCompletionInput input) |
| 426 | 431 | { |
| ... | ... | @@ -595,6 +600,7 @@ namespace NCC.Extend |
| 595 | 600 | /// <response code="200">成功返回经理业绩完成情况列表</response> |
| 596 | 601 | /// <response code="400">日期格式错误或参数无效</response> |
| 597 | 602 | /// <response code="500">服务器内部错误</response> |
| 603 | + [AllowAnonymous] | |
| 598 | 604 | [HttpPost("get-manager-performance-completion")] |
| 599 | 605 | public async Task<List<ManagerPerformanceCompletionOutput>> GetManagerPerformanceCompletion(ManagerPerformanceCompletionInput input) |
| 600 | 606 | { |
| ... | ... | @@ -699,6 +705,7 @@ namespace NCC.Extend |
| 699 | 705 | /// <response code="200">成功返回经理汇总业绩完成情况列表</response> |
| 700 | 706 | /// <response code="400">日期格式错误或参数无效</response> |
| 701 | 707 | /// <response code="500">服务器内部错误</response> |
| 708 | + [AllowAnonymous] | |
| 702 | 709 | [HttpPost("get-manager-summary-performance-completion")] |
| 703 | 710 | public async Task<List<ManagerSummaryPerformanceCompletionOutput>> GetManagerSummaryPerformanceCompletion(ManagerPerformanceCompletionInput input) |
| 704 | 711 | { |
| ... | ... | @@ -808,6 +815,7 @@ namespace NCC.Extend |
| 808 | 815 | /// <response code="200">成功返回科技部老师统计列表</response> |
| 809 | 816 | /// <response code="400">日期格式错误或参数无效</response> |
| 810 | 817 | /// <response code="500">服务器内部错误</response> |
| 818 | + [AllowAnonymous] | |
| 811 | 819 | [HttpPost("get-tech-teacher-daily-statistics")] |
| 812 | 820 | public async Task<List<TechTeacherDailyStatisticsOutput>> GetTechTeacherDailyStatistics(TechTeacherDailyStatisticsInput input) |
| 813 | 821 | { |
| ... | ... | @@ -841,7 +849,7 @@ namespace NCC.Extend |
| 841 | 849 | techDept.F_Id as TechDepartmentId, |
| 842 | 850 | techDept.F_FullName as TechDepartmentName, |
| 843 | 851 | consume.kjbls as TeacherId, |
| 844 | - consume.kjblsxm as TeacherName, | |
| 852 | + user.F_RealName as TeacherName, | |
| 845 | 853 | COUNT(DISTINCT hyhk.hy) as CustomerCount, |
| 846 | 854 | SUM(consume.F_hdpxNumber) as ConsumeProjectCount, |
| 847 | 855 | SUM(consume.kjblsyj) as ConsumeAchievement |
| ... | ... | @@ -849,13 +857,14 @@ namespace NCC.Extend |
| 849 | 857 | INNER JOIN lq_xh_hyhk hyhk ON consume.glkdbh = hyhk.F_Id |
| 850 | 858 | INNER JOIN lq_mdxx store ON hyhk.md = store.F_Id |
| 851 | 859 | LEFT JOIN base_organize techDept ON store.kjb = techDept.F_Id |
| 860 | + LEFT JOIN BASE_USER user ON consume.kjbls = user.F_Id | |
| 852 | 861 | WHERE consume.F_IsEffective = 1 |
| 853 | 862 | AND hyhk.F_IsEffective = 1 |
| 854 | 863 | AND DATE(hyhk.hksj) >= '{startDate:yyyy-MM-dd}' |
| 855 | 864 | AND DATE(hyhk.hksj) <= '{endDate:yyyy-MM-dd}' |
| 856 | 865 | {techFilter} |
| 857 | 866 | {teacherFilter} |
| 858 | - GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, consume.kjblsxm"; | |
| 867 | + GROUP BY techDept.F_Id, techDept.F_FullName, consume.kjbls, user.F_RealName"; | |
| 859 | 868 | |
| 860 | 869 | var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql); |
| 861 | 870 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqGzService.cs
netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs
| ... | ... | @@ -24,6 +24,7 @@ using NCC.Extend.Entitys.lq_hytk_hytk; |
| 24 | 24 | using NCC.Extend.Entitys.lq_hytk_jksyj; |
| 25 | 25 | using NCC.Extend.Entitys.lq_hytk_kjbsyj; |
| 26 | 26 | using NCC.Extend.Entitys.lq_hytk_mx; |
| 27 | +using NCC.Extend.Entitys.lq_kd_pxmx; | |
| 27 | 28 | using NCC.Extend.Interfaces.LqHytkHytk; |
| 28 | 29 | using NCC.FriendlyException; |
| 29 | 30 | using NCC.JsonSerialization; |
| ... | ... | @@ -230,6 +231,7 @@ namespace NCC.Extend.LqHytkHytk |
| 230 | 231 | Id = YitIdHelper.NextId().ToString(), |
| 231 | 232 | RefundInfoId = newEntity.Id, |
| 232 | 233 | BillingItemId = item.billingItemId, |
| 234 | + MemberId = newEntity.Hy, | |
| 233 | 235 | CreateTime = DateTime.Now, |
| 234 | 236 | CreateUser = userInfo.userId, |
| 235 | 237 | Px = item.px, |
| ... | ... | @@ -368,18 +370,19 @@ namespace NCC.Extend.LqHytkHytk |
| 368 | 370 | var allMxEntities = new List<LqHytkMxEntity>(); |
| 369 | 371 | var allJksyjEntities = new List<LqHytkJksyjEntity>(); |
| 370 | 372 | var allKjbsyjEntities = new List<LqHytkKjbsyjEntity>(); |
| 371 | - | |
| 372 | 373 | // 处理品项明细列表 |
| 373 | 374 | if (input.lqHytkMxList != null && input.lqHytkMxList.Any()) |
| 374 | 375 | { |
| 375 | 376 | foreach (var item in input.lqHytkMxList) |
| 376 | 377 | { |
| 378 | + | |
| 377 | 379 | // 创建品项明细实体 |
| 378 | 380 | var lqHytkMxEntity = new LqHytkMxEntity |
| 379 | 381 | { |
| 380 | 382 | Id = YitIdHelper.NextId().ToString(), |
| 381 | 383 | RefundInfoId = id, |
| 382 | 384 | BillingItemId = item.billingItemId, |
| 385 | + MemberId = entity.Hy, | |
| 383 | 386 | CreateTime = DateTime.Now, |
| 384 | 387 | CreateUser = userInfo.userId, |
| 385 | 388 | Tksj = input.tksj, | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
| ... | ... | @@ -2478,6 +2478,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 2478 | 2478 | Id = YitIdHelper.NextId().ToString(), |
| 2479 | 2479 | RefundInfoId = refundId, |
| 2480 | 2480 | BillingItemId = item.BillingItemId, |
| 2481 | + MemberId = input.FromMemberId, // 转卡时使用转出方会员ID | |
| 2481 | 2482 | CreateTime = transferTime, |
| 2482 | 2483 | CreateUser = userInfo.userId, |
| 2483 | 2484 | Px = refundPxmxEntity.Px, | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
| ... | ... | @@ -106,8 +106,8 @@ namespace NCC.Extend |
| 106 | 106 | |
| 107 | 107 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 108 | 108 | { |
| 109 | - sql += " AND s.F_StoreId IN @storeIds"; | |
| 110 | - parameters = new { startMonth = input.StartMonth, endMonth = input.EndMonth, storeIds = input.StoreIds }; | |
| 109 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 110 | + sql += $" AND s.F_StoreId IN ('{storeIdsStr}')"; | |
| 111 | 111 | } |
| 112 | 112 | |
| 113 | 113 | sql += " ORDER BY s.F_StoreName, s.F_StatisticsMonth"; |
| ... | ... | @@ -751,15 +751,12 @@ namespace NCC.Extend |
| 751 | 751 | AND kd.kdrq >= @startTime |
| 752 | 752 | AND kd.kdrq <= @endTime"; |
| 753 | 753 | |
| 754 | - object billingParameters; | |
| 754 | + object billingParameters = new { startTime, endTime }; | |
| 755 | + | |
| 755 | 756 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 756 | 757 | { |
| 757 | - billingSql += " AND kd.djmd IN @storeIds"; | |
| 758 | - billingParameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 759 | - } | |
| 760 | - else | |
| 761 | - { | |
| 762 | - billingParameters = new { startTime, endTime }; | |
| 758 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 759 | + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; | |
| 763 | 760 | } |
| 764 | 761 | |
| 765 | 762 | var billingResult = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters); |
| ... | ... | @@ -776,15 +773,12 @@ namespace NCC.Extend |
| 776 | 773 | AND xh.hksj >= @startTime |
| 777 | 774 | AND xh.hksj <= @endTime"; |
| 778 | 775 | |
| 779 | - object consumeParameters; | |
| 776 | + object consumeParameters = new { startTime, endTime }; | |
| 777 | + | |
| 780 | 778 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 781 | 779 | { |
| 782 | - consumeSql += " AND xh.md IN @storeIds"; | |
| 783 | - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 784 | - } | |
| 785 | - else | |
| 786 | - { | |
| 787 | - consumeParameters = new { startTime, endTime }; | |
| 780 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 781 | + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 788 | 782 | } |
| 789 | 783 | |
| 790 | 784 | var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters); |
| ... | ... | @@ -801,15 +795,12 @@ namespace NCC.Extend |
| 801 | 795 | AND hytk.tksj >= @startTime |
| 802 | 796 | AND hytk.tksj <= @endTime"; |
| 803 | 797 | |
| 804 | - object refundParameters; | |
| 798 | + object refundParameters = new { startTime, endTime }; | |
| 799 | + | |
| 805 | 800 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 806 | 801 | { |
| 807 | - refundSql += " AND hytk.md IN @storeIds"; | |
| 808 | - refundParameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 809 | - } | |
| 810 | - else | |
| 811 | - { | |
| 812 | - refundParameters = new { startTime, endTime }; | |
| 802 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 803 | + refundSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 813 | 804 | } |
| 814 | 805 | |
| 815 | 806 | var refundResult = await _db.Ado.SqlQueryAsync<dynamic>(refundSql, refundParameters); |
| ... | ... | @@ -818,28 +809,26 @@ namespace NCC.Extend |
| 818 | 809 | |
| 819 | 810 | // 第四步:获取消耗目标业绩(所有门店xhyj字段的总和) |
| 820 | 811 | var targetConsumeSql = "SELECT COALESCE(SUM(CAST(md.xhyj AS DECIMAL(18,2))), 0) as target_consume_amount FROM lq_mdxx md WHERE 1=1"; |
| 821 | - object targetConsumeParameters = null; | |
| 822 | 812 | |
| 823 | 813 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 824 | 814 | { |
| 825 | - targetConsumeSql += " AND md.F_Id IN @storeIds"; | |
| 826 | - targetConsumeParameters = new { storeIds = input.StoreIds }; | |
| 815 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 816 | + targetConsumeSql += $" AND md.F_Id IN ('{storeIdsStr}')"; | |
| 827 | 817 | } |
| 828 | 818 | |
| 829 | - var targetConsumeResult = await _db.Ado.SqlQueryAsync<dynamic>(targetConsumeSql, targetConsumeParameters); | |
| 819 | + var targetConsumeResult = await _db.Ado.SqlQueryAsync<dynamic>(targetConsumeSql); | |
| 830 | 820 | var targetConsumeAmount = Convert.ToDecimal(targetConsumeResult?.FirstOrDefault()?.target_consume_amount ?? 0m); |
| 831 | 821 | |
| 832 | 822 | // 第五步:获取开单目标业绩(所有门店xsyj字段的总和) |
| 833 | 823 | var targetBillingSql = "SELECT COALESCE(SUM(CAST(md.xsyj AS DECIMAL(18,2))), 0) as target_billing_amount FROM lq_mdxx md WHERE 1=1"; |
| 834 | - object targetBillingParameters = null; | |
| 835 | 824 | |
| 836 | 825 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 837 | 826 | { |
| 838 | - targetBillingSql += " AND md.F_Id IN @storeIds"; | |
| 839 | - targetBillingParameters = new { storeIds = input.StoreIds }; | |
| 827 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 828 | + targetBillingSql += $" AND md.F_Id IN ('{storeIdsStr}')"; | |
| 840 | 829 | } |
| 841 | 830 | |
| 842 | - var targetBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(targetBillingSql, targetBillingParameters); | |
| 831 | + var targetBillingResult = await _db.Ado.SqlQueryAsync<dynamic>(targetBillingSql); | |
| 843 | 832 | var targetBillingAmount = Convert.ToDecimal(targetBillingResult?.FirstOrDefault()?.target_billing_amount ?? 0m); |
| 844 | 833 | |
| 845 | 834 | // 计算开单完成业绩(开单总金额 - 退卡总金额) |
| ... | ... | @@ -958,15 +947,12 @@ namespace NCC.Extend |
| 958 | 947 | WHERE tk.F_CreateTime >= @startTime |
| 959 | 948 | AND tk.F_CreateTime <= @endTime"; |
| 960 | 949 | |
| 961 | - object inviteParameters; | |
| 950 | + object inviteParameters = new { startTime, endTime }; | |
| 951 | + | |
| 962 | 952 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 963 | 953 | { |
| 964 | - inviteSql += " AND tk.F_StoreId IN @storeIds"; | |
| 965 | - inviteParameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 966 | - } | |
| 967 | - else | |
| 968 | - { | |
| 969 | - inviteParameters = new { startTime, endTime }; | |
| 954 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 955 | + inviteSql += $" AND tk.F_StoreId IN ('{storeIdsStr}')"; | |
| 970 | 956 | } |
| 971 | 957 | |
| 972 | 958 | var inviteResult = await _db.Ado.SqlQueryAsync<dynamic>(inviteSql, inviteParameters); |
| ... | ... | @@ -981,15 +967,12 @@ namespace NCC.Extend |
| 981 | 967 | AND xh.hksj <= @endTime |
| 982 | 968 | AND xh.xfje > 0"; |
| 983 | 969 | |
| 984 | - object consumeParameters; | |
| 970 | + object consumeParameters = new { startTime, endTime }; | |
| 971 | + | |
| 985 | 972 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 986 | 973 | { |
| 987 | - consumeSql += " AND xh.md IN @storeIds"; | |
| 988 | - consumeParameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 989 | - } | |
| 990 | - else | |
| 991 | - { | |
| 992 | - consumeParameters = new { startTime, endTime }; | |
| 974 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 975 | + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 993 | 976 | } |
| 994 | 977 | |
| 995 | 978 | var consumeResult = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters); |
| ... | ... | @@ -1047,6 +1030,7 @@ namespace NCC.Extend |
| 1047 | 1030 | /// - StoreName: 门店名称 |
| 1048 | 1031 | /// - TargetPerformance: 目标业绩(来自门店资料表xsyj字段) |
| 1049 | 1032 | /// - ActualPerformance: 实际开单业绩(统计期间内的开单业绩总和) |
| 1033 | + /// - ActualConsumePerformance: 实际消耗业绩(统计期间内的消耗业绩总和) | |
| 1050 | 1034 | /// - CompletionRate: 完成率(实际业绩/目标业绩 × 100%) |
| 1051 | 1035 | /// - Difference: 差额(实际业绩-目标业绩) |
| 1052 | 1036 | /// - IsTargetAchieved: 是否达标(实际业绩 >= 目标业绩) |
| ... | ... | @@ -1065,42 +1049,106 @@ namespace NCC.Extend |
| 1065 | 1049 | var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); |
| 1066 | 1050 | var endTime = input.EndTime ?? DateTime.Now; |
| 1067 | 1051 | |
| 1068 | - // 构建SQL查询 | |
| 1069 | - var sql = @" | |
| 1052 | + // 先获取门店基础信息和目标业绩 | |
| 1053 | + var storeSql = "SELECT F_Id, dm, xsyj FROM lq_mdxx WHERE 1=1"; | |
| 1054 | + | |
| 1055 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1056 | + { | |
| 1057 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1058 | + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; | |
| 1059 | + } | |
| 1060 | + | |
| 1061 | + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql); | |
| 1062 | + | |
| 1063 | + // 构建门店字典 | |
| 1064 | + var storeDict = new Dictionary<string, string>(); | |
| 1065 | + var targetDict = new Dictionary<string, decimal>(); | |
| 1066 | + foreach (var store in stores) | |
| 1067 | + { | |
| 1068 | + var storeId = store.F_Id?.ToString(); | |
| 1069 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1070 | + { | |
| 1071 | + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; | |
| 1072 | + targetDict[storeId] = Convert.ToDecimal(store.xsyj ?? 0); | |
| 1073 | + } | |
| 1074 | + } | |
| 1075 | + | |
| 1076 | + // 统计开单业绩 | |
| 1077 | + var billingSql = @" | |
| 1070 | 1078 | SELECT |
| 1071 | - md.F_Id as store_id, | |
| 1072 | - md.dm as store_name, | |
| 1073 | - COALESCE(CAST(md.xsyj AS DECIMAL(18,2)), 0) as target_performance, | |
| 1079 | + kd.djmd as store_id, | |
| 1074 | 1080 | COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as actual_performance |
| 1075 | - FROM lq_mdxx md | |
| 1076 | - LEFT JOIN lq_kd_kdjlb kd ON md.F_Id = kd.djmd | |
| 1077 | - AND kd.F_IsEffective = 1 | |
| 1081 | + FROM lq_kd_kdjlb kd | |
| 1082 | + WHERE kd.F_IsEffective = 1 | |
| 1078 | 1083 | AND kd.kdrq >= @startTime |
| 1079 | 1084 | AND kd.kdrq <= @endTime"; |
| 1080 | 1085 | |
| 1081 | - object parameters; | |
| 1086 | + object billingParameters = new { startTime, endTime }; | |
| 1087 | + | |
| 1082 | 1088 | if (input.StoreIds != null && input.StoreIds.Any()) |
| 1083 | 1089 | { |
| 1084 | - sql += " AND md.F_Id IN @storeIds"; | |
| 1085 | - parameters = new { startTime, endTime, storeIds = input.StoreIds }; | |
| 1090 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1091 | + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; | |
| 1086 | 1092 | } |
| 1087 | - else | |
| 1093 | + | |
| 1094 | + billingSql += " GROUP BY kd.djmd"; | |
| 1095 | + | |
| 1096 | + var billingResults = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters); | |
| 1097 | + | |
| 1098 | + // 构建开单业绩字典 | |
| 1099 | + var billingDict = new Dictionary<string, decimal>(); | |
| 1100 | + foreach (var item in billingResults) | |
| 1088 | 1101 | { |
| 1089 | - parameters = new { startTime, endTime }; | |
| 1102 | + var storeId = item.store_id?.ToString(); | |
| 1103 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1104 | + { | |
| 1105 | + billingDict[storeId] = Convert.ToDecimal(item.actual_performance ?? 0); | |
| 1106 | + } | |
| 1090 | 1107 | } |
| 1091 | 1108 | |
| 1092 | - sql += " GROUP BY md.F_Id, md.dm, md.xsyj ORDER BY actual_performance DESC"; | |
| 1109 | + // 统计消耗业绩 | |
| 1110 | + var consumeSql = @" | |
| 1111 | + SELECT | |
| 1112 | + xh.md as store_id, | |
| 1113 | + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as actual_consume_performance | |
| 1114 | + FROM lq_xh_hyhk xh | |
| 1115 | + WHERE xh.F_IsEffective = 1 | |
| 1116 | + AND xh.hksj >= @startTime | |
| 1117 | + AND xh.hksj <= @endTime"; | |
| 1093 | 1118 | |
| 1094 | - var results = await _db.Ado.SqlQueryAsync<dynamic>(sql, parameters); | |
| 1119 | + object consumeParameters = new { startTime, endTime }; | |
| 1095 | 1120 | |
| 1096 | - var storePerformanceList = new List<StorePerformanceComparisonOutput>(); | |
| 1121 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1122 | + { | |
| 1123 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1124 | + consumeSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 1125 | + } | |
| 1126 | + | |
| 1127 | + consumeSql += " GROUP BY xh.md"; | |
| 1097 | 1128 | |
| 1098 | - foreach (var item in results) | |
| 1129 | + var consumeResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, consumeParameters); | |
| 1130 | + | |
| 1131 | + // 构建消耗业绩字典 | |
| 1132 | + var consumeDict = new Dictionary<string, decimal>(); | |
| 1133 | + foreach (var item in consumeResults) | |
| 1099 | 1134 | { |
| 1100 | 1135 | var storeId = item.store_id?.ToString(); |
| 1101 | - var storeName = item.store_name?.ToString(); | |
| 1102 | - var targetPerformance = Convert.ToDecimal(item.target_performance ?? 0); | |
| 1103 | - var actualPerformance = Convert.ToDecimal(item.actual_performance ?? 0); | |
| 1136 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1137 | + { | |
| 1138 | + consumeDict[storeId] = Convert.ToDecimal(item.actual_consume_performance ?? 0); | |
| 1139 | + } | |
| 1140 | + } | |
| 1141 | + | |
| 1142 | + // 合并数据 | |
| 1143 | + var storePerformanceList = new List<StorePerformanceComparisonOutput>(); | |
| 1144 | + | |
| 1145 | + foreach (var kvp in storeDict) | |
| 1146 | + { | |
| 1147 | + var storeId = kvp.Key; | |
| 1148 | + var storeName = kvp.Value; | |
| 1149 | + var targetPerformance = targetDict.ContainsKey(storeId) ? targetDict[storeId] : 0; | |
| 1150 | + var actualPerformance = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0; | |
| 1151 | + var actualConsumePerformance = consumeDict.ContainsKey(storeId) ? consumeDict[storeId] : 0; | |
| 1104 | 1152 | |
| 1105 | 1153 | // 计算完成率 |
| 1106 | 1154 | var completionRate = targetPerformance > 0 |
| ... | ... | @@ -1119,12 +1167,16 @@ namespace NCC.Extend |
| 1119 | 1167 | StoreName = storeName, |
| 1120 | 1168 | TargetPerformance = targetPerformance, |
| 1121 | 1169 | ActualPerformance = actualPerformance, |
| 1170 | + ActualConsumePerformance = actualConsumePerformance, | |
| 1122 | 1171 | CompletionRate = completionRate, |
| 1123 | 1172 | Difference = difference, |
| 1124 | 1173 | IsTargetAchieved = isTargetAchieved |
| 1125 | 1174 | }); |
| 1126 | 1175 | } |
| 1127 | 1176 | |
| 1177 | + // 按实际业绩排序 | |
| 1178 | + storePerformanceList = storePerformanceList.OrderByDescending(x => x.ActualPerformance).ToList(); | |
| 1179 | + | |
| 1128 | 1180 | return storePerformanceList; |
| 1129 | 1181 | } |
| 1130 | 1182 | catch (Exception ex) |
| ... | ... | @@ -1266,7 +1318,7 @@ namespace NCC.Extend |
| 1266 | 1318 | SELECT F_Id FROM lq_xh_hyhk |
| 1267 | 1319 | WHERE hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' |
| 1268 | 1320 | AND hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' |
| 1269 | - {(input.StoreIds != null && input.StoreIds.Any() ? $"AND F_StoreId IN ('{string.Join("','", input.StoreIds)}')" : "")} | |
| 1321 | + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND md IN ('{string.Join("','", input.StoreIds)}')" : "")} | |
| 1270 | 1322 | ) |
| 1271 | 1323 | GROUP BY xh.px"; |
| 1272 | 1324 | |
| ... | ... | @@ -1325,5 +1377,1537 @@ namespace NCC.Extend |
| 1325 | 1377 | } |
| 1326 | 1378 | |
| 1327 | 1379 | #endregion |
| 1380 | + | |
| 1381 | + #region 获取门店项目指标统计数据 | |
| 1382 | + | |
| 1383 | + /// <summary> | |
| 1384 | + /// 获取门店项目指标统计数据 | |
| 1385 | + /// </summary> | |
| 1386 | + /// <remarks> | |
| 1387 | + /// 统计指定时间范围内各门店的项目指标数据 | |
| 1388 | + /// 包括:消耗项目数、消耗率、客单项目数、消耗客单价、开单金额、消耗人次 | |
| 1389 | + /// | |
| 1390 | + /// 示例请求: | |
| 1391 | + /// ```json | |
| 1392 | + /// { | |
| 1393 | + /// "startTime": "2025-10-01", | |
| 1394 | + /// "endTime": "2025-10-31", | |
| 1395 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 1396 | + /// } | |
| 1397 | + /// ``` | |
| 1398 | + /// | |
| 1399 | + /// 参数说明: | |
| 1400 | + /// - startTime: 开始时间(可选,默认为当月1号) | |
| 1401 | + /// - endTime: 结束时间(可选,默认为当前时间) | |
| 1402 | + /// - storeIds: 门店ID列表(可选) | |
| 1403 | + /// | |
| 1404 | + /// 返回字段说明: | |
| 1405 | + /// - StoreId: 门店ID | |
| 1406 | + /// - StoreName: 门店名称 | |
| 1407 | + /// - ConsumeProjectCount: 消耗项目数(项目次数总和) | |
| 1408 | + /// - ConsumeRate: 消耗率(消耗金额/开单金额 × 100%) | |
| 1409 | + /// - AvgProjectPerConsume: 客单项目数(项目数/消耗人次) | |
| 1410 | + /// - AvgAmountPerConsume: 消耗客单价(消耗业绩/消耗人次) | |
| 1411 | + /// - BillingAmount: 开单金额 | |
| 1412 | + /// - ConsumeAmount: 消耗金额 | |
| 1413 | + /// - ConsumePersonCount: 消耗人次(去重客户数) | |
| 1414 | + /// </remarks> | |
| 1415 | + /// <param name="input">查询参数</param> | |
| 1416 | + /// <returns>门店项目指标统计数据列表</returns> | |
| 1417 | + /// <response code="200">成功返回统计数据</response> | |
| 1418 | + /// <response code="400">参数错误</response> | |
| 1419 | + /// <response code="500">服务器错误</response> | |
| 1420 | + [HttpPost("get-store-item-statistics")] | |
| 1421 | + public async Task<object> GetStoreItemStatistics(StoreItemStatisticsInput input) | |
| 1422 | + { | |
| 1423 | + try | |
| 1424 | + { | |
| 1425 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 1426 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 1427 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 1428 | + | |
| 1429 | + // 第一步:获取门店基础信息 | |
| 1430 | + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1"; | |
| 1431 | + | |
| 1432 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1433 | + { | |
| 1434 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1435 | + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; | |
| 1436 | + } | |
| 1437 | + | |
| 1438 | + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql); | |
| 1439 | + | |
| 1440 | + // 构建门店字典 | |
| 1441 | + var storeDict = new Dictionary<string, string>(); | |
| 1442 | + foreach (var store in stores) | |
| 1443 | + { | |
| 1444 | + var storeId = store.F_Id?.ToString(); | |
| 1445 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1446 | + { | |
| 1447 | + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; | |
| 1448 | + } | |
| 1449 | + } | |
| 1450 | + | |
| 1451 | + // 第二步:统计开单金额 | |
| 1452 | + var billingSql = @" | |
| 1453 | + SELECT | |
| 1454 | + kd.djmd as store_id, | |
| 1455 | + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount | |
| 1456 | + FROM lq_kd_kdjlb kd | |
| 1457 | + WHERE kd.F_IsEffective = 1 | |
| 1458 | + AND kd.kdrq >= @startTime | |
| 1459 | + AND kd.kdrq <= @endTime"; | |
| 1460 | + | |
| 1461 | + object billingParameters = new { startTime, endTime }; | |
| 1462 | + | |
| 1463 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1464 | + { | |
| 1465 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1466 | + billingSql += $" AND kd.djmd IN ('{storeIdsStr}')"; | |
| 1467 | + } | |
| 1468 | + | |
| 1469 | + billingSql += " GROUP BY kd.djmd"; | |
| 1470 | + | |
| 1471 | + var billingResults = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, billingParameters); | |
| 1472 | + | |
| 1473 | + // 构建开单金额字典 | |
| 1474 | + var billingDict = new Dictionary<string, decimal>(); | |
| 1475 | + foreach (var item in billingResults) | |
| 1476 | + { | |
| 1477 | + var storeId = item.store_id?.ToString(); | |
| 1478 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1479 | + { | |
| 1480 | + billingDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0); | |
| 1481 | + } | |
| 1482 | + } | |
| 1483 | + | |
| 1484 | + // 第三步:统计消耗金额和消耗人次(直接从lq_xh_hyhk表,避免JOIN导致重复计算) | |
| 1485 | + var consumeAmountSql = @" | |
| 1486 | + SELECT | |
| 1487 | + xh.md as store_id, | |
| 1488 | + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount, | |
| 1489 | + COUNT(DISTINCT xh.hy) as consume_person_count | |
| 1490 | + FROM lq_xh_hyhk xh | |
| 1491 | + WHERE xh.F_IsEffective = 1 | |
| 1492 | + AND xh.hksj >= @startTime | |
| 1493 | + AND xh.hksj <= @endTime"; | |
| 1494 | + | |
| 1495 | + object consumeAmountParameters = new { startTime, endTime }; | |
| 1496 | + | |
| 1497 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1498 | + { | |
| 1499 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1500 | + consumeAmountSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 1501 | + } | |
| 1502 | + | |
| 1503 | + consumeAmountSql += " GROUP BY xh.md"; | |
| 1504 | + | |
| 1505 | + var consumeAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeAmountSql, consumeAmountParameters); | |
| 1506 | + | |
| 1507 | + // 构建消耗金额和人次字典 | |
| 1508 | + var consumeAmountDict = new Dictionary<string, decimal>(); | |
| 1509 | + var consumePersonCountDict = new Dictionary<string, int>(); | |
| 1510 | + | |
| 1511 | + foreach (var item in consumeAmountResults) | |
| 1512 | + { | |
| 1513 | + var storeId = item.store_id?.ToString(); | |
| 1514 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1515 | + { | |
| 1516 | + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0); | |
| 1517 | + consumePersonCountDict[storeId] = Convert.ToInt32(item.consume_person_count ?? 0); | |
| 1518 | + } | |
| 1519 | + } | |
| 1520 | + | |
| 1521 | + // 第四步:统计消耗项目数(需要JOIN品项明细表) | |
| 1522 | + var consumeProjectSql = @" | |
| 1523 | + SELECT | |
| 1524 | + xh.md as store_id, | |
| 1525 | + COALESCE(SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))), 0) as consume_project_count | |
| 1526 | + FROM lq_xh_hyhk xh | |
| 1527 | + INNER JOIN lq_xh_pxmx xhpx ON xhpx.F_ConsumeInfoId = xh.F_Id AND xhpx.F_IsEffective = 1 | |
| 1528 | + WHERE xh.F_IsEffective = 1 | |
| 1529 | + AND xh.hksj >= @startTime | |
| 1530 | + AND xh.hksj <= @endTime"; | |
| 1531 | + | |
| 1532 | + object consumeProjectParameters = new { startTime, endTime }; | |
| 1533 | + | |
| 1534 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1535 | + { | |
| 1536 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1537 | + consumeProjectSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 1538 | + } | |
| 1539 | + | |
| 1540 | + consumeProjectSql += " GROUP BY xh.md"; | |
| 1541 | + | |
| 1542 | + var consumeProjectResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeProjectSql, consumeProjectParameters); | |
| 1543 | + | |
| 1544 | + // 构建消耗项目数字典 | |
| 1545 | + var consumeProjectCountDict = new Dictionary<string, decimal>(); | |
| 1546 | + | |
| 1547 | + foreach (var item in consumeProjectResults) | |
| 1548 | + { | |
| 1549 | + var storeId = item.store_id?.ToString(); | |
| 1550 | + if (!string.IsNullOrEmpty(storeId)) | |
| 1551 | + { | |
| 1552 | + consumeProjectCountDict[storeId] = Convert.ToDecimal(item.consume_project_count ?? 0); | |
| 1553 | + } | |
| 1554 | + } | |
| 1555 | + | |
| 1556 | + // 第五步:合并数据并计算指标 | |
| 1557 | + var resultList = new List<StoreItemStatisticsOutput>(); | |
| 1558 | + | |
| 1559 | + foreach (var kvp in storeDict) | |
| 1560 | + { | |
| 1561 | + var storeId = kvp.Key; | |
| 1562 | + var storeName = kvp.Value; | |
| 1563 | + var billingAmount = billingDict.ContainsKey(storeId) ? billingDict[storeId] : 0; | |
| 1564 | + | |
| 1565 | + var consumeProjectCount = consumeProjectCountDict.ContainsKey(storeId) ? consumeProjectCountDict[storeId] : 0; | |
| 1566 | + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; | |
| 1567 | + var consumePersonCount = consumePersonCountDict.ContainsKey(storeId) ? consumePersonCountDict[storeId] : 0; | |
| 1568 | + | |
| 1569 | + // 如果有消耗数据或开单数据,都返回该门店 | |
| 1570 | + if (consumeAmount > 0 || consumeProjectCount > 0 || billingAmount > 0) | |
| 1571 | + { | |
| 1572 | + // 计算消耗率 | |
| 1573 | + var consumeRate = billingAmount > 0 | |
| 1574 | + ? decimal.Round(consumeAmount / billingAmount * 100m, 2) | |
| 1575 | + : 0; | |
| 1576 | + | |
| 1577 | + // 计算客单项目数 | |
| 1578 | + var avgProjectPerConsume = consumePersonCount > 0 | |
| 1579 | + ? decimal.Round(consumeProjectCount / consumePersonCount, 2) | |
| 1580 | + : 0; | |
| 1581 | + | |
| 1582 | + // 计算消耗客单价 | |
| 1583 | + var avgAmountPerConsume = consumePersonCount > 0 | |
| 1584 | + ? decimal.Round(consumeAmount / consumePersonCount, 2) | |
| 1585 | + : 0; | |
| 1586 | + | |
| 1587 | + resultList.Add(new StoreItemStatisticsOutput | |
| 1588 | + { | |
| 1589 | + StoreId = storeId, | |
| 1590 | + StoreName = storeName, | |
| 1591 | + ConsumeProjectCount = Convert.ToInt32(consumeProjectCount), | |
| 1592 | + ConsumeRate = consumeRate, | |
| 1593 | + AvgProjectPerConsume = avgProjectPerConsume, | |
| 1594 | + AvgAmountPerConsume = avgAmountPerConsume, | |
| 1595 | + BillingAmount = billingAmount, | |
| 1596 | + ConsumeAmount = consumeAmount, | |
| 1597 | + ConsumePersonCount = consumePersonCount | |
| 1598 | + }); | |
| 1599 | + } | |
| 1600 | + } | |
| 1601 | + | |
| 1602 | + // 按消耗人次排序 | |
| 1603 | + resultList = resultList.OrderByDescending(x => x.ConsumePersonCount).ToList(); | |
| 1604 | + | |
| 1605 | + return resultList; | |
| 1606 | + } | |
| 1607 | + catch (Exception ex) | |
| 1608 | + { | |
| 1609 | + _logger.LogError(ex, $"获取门店项目指标统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 1610 | + throw NCCException.Oh($"获取门店项目指标统计数据失败: {ex.Message}"); | |
| 1611 | + } | |
| 1612 | + } | |
| 1613 | + | |
| 1614 | + #endregion | |
| 1615 | + | |
| 1616 | + #region 客户到店次数统计 | |
| 1617 | + | |
| 1618 | + /// <summary> | |
| 1619 | + /// 获取客户到店次数统计 | |
| 1620 | + /// </summary> | |
| 1621 | + /// <remarks> | |
| 1622 | + /// 统计指定时间范围内,客户按到店次数(消耗次数)的分布情况 | |
| 1623 | + /// 一次消耗开单即为一次到店 | |
| 1624 | + /// 支持门店筛选,但统计结果不分门店,按次数汇总所有门店的人数 | |
| 1625 | + /// | |
| 1626 | + /// 示例请求: | |
| 1627 | + /// ```json | |
| 1628 | + /// { | |
| 1629 | + /// "startTime": "2025-10-01", | |
| 1630 | + /// "endTime": "2025-10-31", | |
| 1631 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 1632 | + /// } | |
| 1633 | + /// ``` | |
| 1634 | + /// | |
| 1635 | + /// 参数说明: | |
| 1636 | + /// - startTime: 开始时间(可选,默认为当月1号) | |
| 1637 | + /// - endTime: 结束时间(可选,默认为当前时间) | |
| 1638 | + /// - storeIds: 门店ID列表(可选,用于筛选) | |
| 1639 | + /// | |
| 1640 | + /// 返回说明: | |
| 1641 | + /// - VisitCount: 消耗次数(客户到店次数) | |
| 1642 | + /// - CustomerCount: 人数(达到该次数的客户人数) | |
| 1643 | + /// </remarks> | |
| 1644 | + /// <param name="input">查询参数</param> | |
| 1645 | + /// <returns>客户到店次数统计数据列表</returns> | |
| 1646 | + /// <response code="200">成功返回统计数据</response> | |
| 1647 | + /// <response code="400">参数错误</response> | |
| 1648 | + /// <response code="500">服务器错误</response> | |
| 1649 | + [HttpPost("get-customer-visit-frequency")] | |
| 1650 | + public async Task<List<CustomerVisitFrequencyOutput>> GetCustomerVisitFrequency(CustomerVisitFrequencyInput input) | |
| 1651 | + { | |
| 1652 | + try | |
| 1653 | + { | |
| 1654 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 1655 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 1656 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 1657 | + | |
| 1658 | + // 统计每个客户的消耗次数(跨门店汇总) | |
| 1659 | + var visitSql = $@" | |
| 1660 | + SELECT | |
| 1661 | + xh.hy as customer_id, | |
| 1662 | + COUNT(DISTINCT CONCAT(xh.md, '_', DATE(xh.hksj))) as visit_count | |
| 1663 | + FROM lq_xh_hyhk xh | |
| 1664 | + WHERE xh.F_IsEffective = 1 | |
| 1665 | + AND xh.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 1666 | + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; | |
| 1667 | + | |
| 1668 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1669 | + { | |
| 1670 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1671 | + visitSql += $" AND xh.md IN ('{storeIdsStr}')"; | |
| 1672 | + } | |
| 1673 | + | |
| 1674 | + visitSql += " GROUP BY xh.hy"; | |
| 1675 | + | |
| 1676 | + var visitData = await _db.Ado.SqlQueryAsync<dynamic>(visitSql); | |
| 1677 | + | |
| 1678 | + // 统计各次数的人数分布 | |
| 1679 | + var visitCountDict = new Dictionary<int, int>(); | |
| 1680 | + | |
| 1681 | + foreach (var item in visitData) | |
| 1682 | + { | |
| 1683 | + var visitCount = Convert.ToInt32(item.visit_count); | |
| 1684 | + | |
| 1685 | + if (visitCountDict.ContainsKey(visitCount)) | |
| 1686 | + { | |
| 1687 | + visitCountDict[visitCount]++; | |
| 1688 | + } | |
| 1689 | + else | |
| 1690 | + { | |
| 1691 | + visitCountDict[visitCount] = 1; | |
| 1692 | + } | |
| 1693 | + } | |
| 1694 | + | |
| 1695 | + // 构建结果列表,按次数排序 | |
| 1696 | + var resultList = visitCountDict | |
| 1697 | + .OrderBy(x => x.Key) | |
| 1698 | + .Select(x => new CustomerVisitFrequencyOutput | |
| 1699 | + { | |
| 1700 | + VisitCount = x.Key, | |
| 1701 | + CustomerCount = x.Value | |
| 1702 | + }) | |
| 1703 | + .ToList(); | |
| 1704 | + | |
| 1705 | + return resultList; | |
| 1706 | + } | |
| 1707 | + catch (Exception ex) | |
| 1708 | + { | |
| 1709 | + _logger.LogError(ex, $"获取客户到店次数统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 1710 | + throw NCCException.Oh($"获取客户到店次数统计数据失败: {ex.Message}"); | |
| 1711 | + } | |
| 1712 | + } | |
| 1713 | + | |
| 1714 | + #endregion | |
| 1715 | + | |
| 1716 | + #region 健康师统计数据 | |
| 1717 | + | |
| 1718 | + /// <summary> | |
| 1719 | + /// 获取健康师统计数据 | |
| 1720 | + /// </summary> | |
| 1721 | + /// <remarks> | |
| 1722 | + /// 统计指定时间范围内健康师的开单业绩、消耗业绩、退单业绩及项目数 | |
| 1723 | + /// | |
| 1724 | + /// 示例请求: | |
| 1725 | + /// ```json | |
| 1726 | + /// { | |
| 1727 | + /// "startTime": "2025-10-01", | |
| 1728 | + /// "endTime": "2025-10-31", | |
| 1729 | + /// "storeIds": ["门店ID1", "门店ID2"], | |
| 1730 | + /// "healthCoachIds": ["健康师ID1", "健康师ID2"] | |
| 1731 | + /// } | |
| 1732 | + /// ``` | |
| 1733 | + /// | |
| 1734 | + /// 参数说明: | |
| 1735 | + /// - startTime: 开始时间(可选,默认为当月1号) | |
| 1736 | + /// - endTime: 结束时间(可选,默认为当前时间) | |
| 1737 | + /// - storeIds: 门店ID列表(可选) | |
| 1738 | + /// - healthCoachIds: 健康师ID列表(可选) | |
| 1739 | + /// | |
| 1740 | + /// 返回说明: | |
| 1741 | + /// - HealthCoachId: 健康师ID | |
| 1742 | + /// - HealthCoachName: 健康师姓名 | |
| 1743 | + /// - BillingPerformance: 开单业绩 | |
| 1744 | + /// - ConsumePerformance: 消耗业绩 | |
| 1745 | + /// - RefundPerformance: 退单业绩 | |
| 1746 | + /// - BillingProjectCount: 开单项目数 | |
| 1747 | + /// - ConsumeProjectCount: 消耗项目数 | |
| 1748 | + /// - RefundProjectCount: 退单项目数 | |
| 1749 | + /// </remarks> | |
| 1750 | + /// <param name="input">查询参数</param> | |
| 1751 | + /// <returns>健康师统计数据列表</returns> | |
| 1752 | + /// <response code="200">成功返回统计数据</response> | |
| 1753 | + /// <response code="400">参数错误</response> | |
| 1754 | + /// <response code="500">服务器错误</response> | |
| 1755 | + [HttpPost("get-health-coach-statistics")] | |
| 1756 | + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachStatistics(HealthCoachStatisticsInput input) | |
| 1757 | + { | |
| 1758 | + try | |
| 1759 | + { | |
| 1760 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 1761 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 1762 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 1763 | + | |
| 1764 | + // 健康师数据字典 | |
| 1765 | + var healthCoachDict = new Dictionary<string, HealthCoachStatisticsOutput>(); | |
| 1766 | + | |
| 1767 | + // 1. 统计开单业绩和开单项目数 | |
| 1768 | + var billingSql = $@" | |
| 1769 | + SELECT | |
| 1770 | + jks.jks as health_coach_id, | |
| 1771 | + jks.jksxm as health_coach_name, | |
| 1772 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, | |
| 1773 | + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count | |
| 1774 | + FROM lq_kd_jksyj jks | |
| 1775 | + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id | |
| 1776 | + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 | |
| 1777 | + WHERE jks.F_IsEffective = 1 | |
| 1778 | + AND kdjlb.F_IsEffective = 1 | |
| 1779 | + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 1780 | + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; | |
| 1781 | + | |
| 1782 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1783 | + { | |
| 1784 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1785 | + billingSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; | |
| 1786 | + } | |
| 1787 | + | |
| 1788 | + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) | |
| 1789 | + { | |
| 1790 | + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); | |
| 1791 | + billingSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; | |
| 1792 | + } | |
| 1793 | + | |
| 1794 | + billingSql += " GROUP BY jks.jks, jks.jksxm"; | |
| 1795 | + | |
| 1796 | + var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql); | |
| 1797 | + | |
| 1798 | + foreach (var item in billingData) | |
| 1799 | + { | |
| 1800 | + var healthCoachId = item.health_coach_id?.ToString(); | |
| 1801 | + if (!string.IsNullOrEmpty(healthCoachId)) | |
| 1802 | + { | |
| 1803 | + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput | |
| 1804 | + { | |
| 1805 | + HealthCoachId = healthCoachId, | |
| 1806 | + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", | |
| 1807 | + BillingPerformance = Convert.ToDecimal(item.billing_performance ?? 0), | |
| 1808 | + BillingProjectCount = Convert.ToInt32(item.billing_project_count ?? 0), | |
| 1809 | + ConsumePerformance = 0, | |
| 1810 | + RefundPerformance = 0, | |
| 1811 | + ConsumeProjectCount = 0, | |
| 1812 | + RefundProjectCount = 0 | |
| 1813 | + }; | |
| 1814 | + } | |
| 1815 | + } | |
| 1816 | + | |
| 1817 | + // 2. 统计消耗业绩和消耗项目数 | |
| 1818 | + var consumeSql = $@" | |
| 1819 | + SELECT | |
| 1820 | + jks.jks as health_coach_id, | |
| 1821 | + jks.jksxm as health_coach_name, | |
| 1822 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, | |
| 1823 | + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count | |
| 1824 | + FROM lq_xh_jksyj jks | |
| 1825 | + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id | |
| 1826 | + WHERE jks.F_IsEffective = 1 | |
| 1827 | + AND hyhk.F_IsEffective = 1 | |
| 1828 | + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 1829 | + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; | |
| 1830 | + | |
| 1831 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1832 | + { | |
| 1833 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1834 | + consumeSql += $" AND hyhk.md IN ('{storeIdsStr}')"; | |
| 1835 | + } | |
| 1836 | + | |
| 1837 | + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) | |
| 1838 | + { | |
| 1839 | + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); | |
| 1840 | + consumeSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; | |
| 1841 | + } | |
| 1842 | + | |
| 1843 | + consumeSql += " GROUP BY jks.jks, jks.jksxm"; | |
| 1844 | + | |
| 1845 | + var consumeData = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql); | |
| 1846 | + | |
| 1847 | + foreach (var item in consumeData) | |
| 1848 | + { | |
| 1849 | + var healthCoachId = item.health_coach_id?.ToString(); | |
| 1850 | + if (!string.IsNullOrEmpty(healthCoachId)) | |
| 1851 | + { | |
| 1852 | + if (healthCoachDict.ContainsKey(healthCoachId)) | |
| 1853 | + { | |
| 1854 | + healthCoachDict[healthCoachId].ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0); | |
| 1855 | + healthCoachDict[healthCoachId].ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0); | |
| 1856 | + } | |
| 1857 | + else | |
| 1858 | + { | |
| 1859 | + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput | |
| 1860 | + { | |
| 1861 | + HealthCoachId = healthCoachId, | |
| 1862 | + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", | |
| 1863 | + BillingPerformance = 0, | |
| 1864 | + ConsumePerformance = Convert.ToDecimal(item.consume_performance ?? 0), | |
| 1865 | + RefundPerformance = 0, | |
| 1866 | + BillingProjectCount = 0, | |
| 1867 | + ConsumeProjectCount = Convert.ToInt32(item.consume_project_count ?? 0), | |
| 1868 | + RefundProjectCount = 0 | |
| 1869 | + }; | |
| 1870 | + } | |
| 1871 | + } | |
| 1872 | + } | |
| 1873 | + | |
| 1874 | + // 3. 统计退单业绩和退单项目数 | |
| 1875 | + var refundSql = $@" | |
| 1876 | + SELECT | |
| 1877 | + jks.jks as health_coach_id, | |
| 1878 | + jks.jksxm as health_coach_name, | |
| 1879 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, | |
| 1880 | + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count | |
| 1881 | + FROM lq_hytk_jksyj jks | |
| 1882 | + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id | |
| 1883 | + WHERE jks.F_IsEffective = 1 | |
| 1884 | + AND hytk.F_IsEffective = 1 | |
| 1885 | + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 1886 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}'"; | |
| 1887 | + | |
| 1888 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1889 | + { | |
| 1890 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1891 | + refundSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 1892 | + } | |
| 1893 | + | |
| 1894 | + if (input.HealthCoachIds != null && input.HealthCoachIds.Any()) | |
| 1895 | + { | |
| 1896 | + var healthCoachIdsStr = string.Join("','", input.HealthCoachIds); | |
| 1897 | + refundSql += $" AND jks.jks IN ('{healthCoachIdsStr}')"; | |
| 1898 | + } | |
| 1899 | + | |
| 1900 | + refundSql += " GROUP BY jks.jks, jks.jksxm"; | |
| 1901 | + | |
| 1902 | + var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql); | |
| 1903 | + | |
| 1904 | + foreach (var item in refundData) | |
| 1905 | + { | |
| 1906 | + var healthCoachId = item.health_coach_id?.ToString(); | |
| 1907 | + if (!string.IsNullOrEmpty(healthCoachId)) | |
| 1908 | + { | |
| 1909 | + if (healthCoachDict.ContainsKey(healthCoachId)) | |
| 1910 | + { | |
| 1911 | + healthCoachDict[healthCoachId].RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0); | |
| 1912 | + healthCoachDict[healthCoachId].RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0); | |
| 1913 | + } | |
| 1914 | + else | |
| 1915 | + { | |
| 1916 | + healthCoachDict[healthCoachId] = new HealthCoachStatisticsOutput | |
| 1917 | + { | |
| 1918 | + HealthCoachId = healthCoachId, | |
| 1919 | + HealthCoachName = item.health_coach_name?.ToString() ?? "未知", | |
| 1920 | + BillingPerformance = 0, | |
| 1921 | + ConsumePerformance = 0, | |
| 1922 | + RefundPerformance = Convert.ToDecimal(item.refund_performance ?? 0), | |
| 1923 | + BillingProjectCount = 0, | |
| 1924 | + ConsumeProjectCount = 0, | |
| 1925 | + RefundProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) | |
| 1926 | + }; | |
| 1927 | + } | |
| 1928 | + } | |
| 1929 | + } | |
| 1930 | + | |
| 1931 | + // 返回结果,按健康师姓名排序 | |
| 1932 | + var resultList = healthCoachDict.Values | |
| 1933 | + .OrderBy(x => x.HealthCoachName) | |
| 1934 | + .ToList(); | |
| 1935 | + | |
| 1936 | + return resultList; | |
| 1937 | + } | |
| 1938 | + catch (Exception ex) | |
| 1939 | + { | |
| 1940 | + _logger.LogError(ex, $"获取健康师统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 1941 | + throw NCCException.Oh($"获取健康师统计数据失败: {ex.Message}"); | |
| 1942 | + } | |
| 1943 | + } | |
| 1944 | + | |
| 1945 | + | |
| 1946 | + /// <summary> | |
| 1947 | + /// 获取健康师开单业绩排行榜 | |
| 1948 | + /// </summary> | |
| 1949 | + /// <remarks> | |
| 1950 | + /// 获取按开单业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 | |
| 1951 | + /// | |
| 1952 | + /// 示例请求: | |
| 1953 | + /// ```json | |
| 1954 | + /// { | |
| 1955 | + /// "startTime": "2025-10-01", | |
| 1956 | + /// "endTime": "2025-10-31", | |
| 1957 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 1958 | + /// } | |
| 1959 | + /// ``` | |
| 1960 | + /// </remarks> | |
| 1961 | + /// <param name="input">查询参数</param> | |
| 1962 | + /// <returns>开单业绩排行榜(前20名)</returns> | |
| 1963 | + /// <response code="200">成功返回排行榜数据</response> | |
| 1964 | + /// <response code="400">参数错误</response> | |
| 1965 | + /// <response code="500">服务器错误</response> | |
| 1966 | + [HttpPost("get-health-coach-billing-ranking")] | |
| 1967 | + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachBillingRanking(HealthCoachStatisticsInput input) | |
| 1968 | + { | |
| 1969 | + try | |
| 1970 | + { | |
| 1971 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 1972 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 1973 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 1974 | + | |
| 1975 | + // 第一步:获取开单业绩排行榜前20名健康师(使用jkszh作为ID) | |
| 1976 | + var rankingIdsSql = $@" | |
| 1977 | + SELECT | |
| 1978 | + jks.jkszh as health_coach_id, | |
| 1979 | + jks.jksxm as health_coach_name | |
| 1980 | + FROM lq_kd_jksyj jks | |
| 1981 | + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id | |
| 1982 | + WHERE jks.F_IsEffective = 1 | |
| 1983 | + AND kdjlb.F_IsEffective = 1 | |
| 1984 | + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 1985 | + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 1986 | + AND jks.jkszh IS NOT NULL | |
| 1987 | + AND jks.jkszh != ''"; | |
| 1988 | + | |
| 1989 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 1990 | + { | |
| 1991 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 1992 | + rankingIdsSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; | |
| 1993 | + } | |
| 1994 | + | |
| 1995 | + rankingIdsSql += @" | |
| 1996 | + GROUP BY jks.jkszh, jks.jksxm | |
| 1997 | + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC | |
| 1998 | + LIMIT 20"; | |
| 1999 | + | |
| 2000 | + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql)) | |
| 2001 | + .Select(item => new | |
| 2002 | + { | |
| 2003 | + Id = item.health_coach_id?.ToString(), | |
| 2004 | + Name = item.health_coach_name?.ToString() | |
| 2005 | + }) | |
| 2006 | + .Where(x => !string.IsNullOrEmpty(x.Id)) | |
| 2007 | + .ToList(); | |
| 2008 | + | |
| 2009 | + var rankingIds = rankingData.Select(x => x.Id).ToList(); | |
| 2010 | + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); | |
| 2011 | + | |
| 2012 | + if (!rankingIds.Any()) | |
| 2013 | + { | |
| 2014 | + return new List<HealthCoachStatisticsOutput>(); | |
| 2015 | + } | |
| 2016 | + | |
| 2017 | + // 第二步:为这些健康师查询完整的三个业绩数据 | |
| 2018 | + var healthCoachIdsStr = string.Join("','", rankingIds); | |
| 2019 | + | |
| 2020 | + // 2.1 查询开单业绩数据(使用jkszh作为ID) | |
| 2021 | + var billingDataSql = $@" | |
| 2022 | + SELECT | |
| 2023 | + jks.jkszh as health_coach_id, | |
| 2024 | + jks.jksxm as health_coach_name, | |
| 2025 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, | |
| 2026 | + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count | |
| 2027 | + FROM lq_kd_jksyj jks | |
| 2028 | + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id | |
| 2029 | + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 | |
| 2030 | + WHERE jks.F_IsEffective = 1 | |
| 2031 | + AND kdjlb.F_IsEffective = 1 | |
| 2032 | + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2033 | + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2034 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2035 | + | |
| 2036 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2037 | + { | |
| 2038 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2039 | + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; | |
| 2040 | + } | |
| 2041 | + | |
| 2042 | + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2043 | + | |
| 2044 | + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql)) | |
| 2045 | + .ToDictionary( | |
| 2046 | + item => item.health_coach_id?.ToString(), | |
| 2047 | + item => new | |
| 2048 | + { | |
| 2049 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2050 | + Performance = Convert.ToDecimal(item.billing_performance ?? 0), | |
| 2051 | + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) | |
| 2052 | + } | |
| 2053 | + ); | |
| 2054 | + | |
| 2055 | + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) | |
| 2056 | + var consumeDataSql = $@" | |
| 2057 | + SELECT | |
| 2058 | + jks.jkszh as health_coach_id, | |
| 2059 | + jks.jksxm as health_coach_name, | |
| 2060 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, | |
| 2061 | + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count | |
| 2062 | + FROM lq_xh_jksyj jks | |
| 2063 | + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id | |
| 2064 | + WHERE jks.F_IsEffective = 1 | |
| 2065 | + AND hyhk.F_IsEffective = 1 | |
| 2066 | + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2067 | + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2068 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2069 | + | |
| 2070 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2071 | + { | |
| 2072 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2073 | + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; | |
| 2074 | + } | |
| 2075 | + | |
| 2076 | + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2077 | + | |
| 2078 | + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql)) | |
| 2079 | + .ToDictionary( | |
| 2080 | + item => item.health_coach_id?.ToString(), | |
| 2081 | + item => new | |
| 2082 | + { | |
| 2083 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2084 | + Performance = Convert.ToDecimal(item.consume_performance ?? 0), | |
| 2085 | + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) | |
| 2086 | + } | |
| 2087 | + ); | |
| 2088 | + | |
| 2089 | + // 2.3 查询退卡业绩数据(使用jkszh作为ID) | |
| 2090 | + var refundDataSql = $@" | |
| 2091 | + SELECT | |
| 2092 | + jks.jkszh as health_coach_id, | |
| 2093 | + jks.jksxm as health_coach_name, | |
| 2094 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, | |
| 2095 | + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count | |
| 2096 | + FROM lq_hytk_jksyj jks | |
| 2097 | + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id | |
| 2098 | + WHERE jks.F_IsEffective = 1 | |
| 2099 | + AND hytk.F_IsEffective = 1 | |
| 2100 | + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2101 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2102 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2103 | + | |
| 2104 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2105 | + { | |
| 2106 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2107 | + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 2108 | + } | |
| 2109 | + | |
| 2110 | + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2111 | + | |
| 2112 | + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql)) | |
| 2113 | + .ToDictionary( | |
| 2114 | + item => item.health_coach_id?.ToString(), | |
| 2115 | + item => new | |
| 2116 | + { | |
| 2117 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2118 | + Performance = Convert.ToDecimal(item.refund_performance ?? 0), | |
| 2119 | + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) | |
| 2120 | + } | |
| 2121 | + ); | |
| 2122 | + | |
| 2123 | + // 第三步:合并数据,按原始排序构建排行榜 | |
| 2124 | + var ranking = rankingIds | |
| 2125 | + .Select(id => | |
| 2126 | + { | |
| 2127 | + var billing = billingData.ContainsKey(id) ? billingData[id] : null; | |
| 2128 | + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; | |
| 2129 | + var refund = refundData.ContainsKey(id) ? refundData[id] : null; | |
| 2130 | + | |
| 2131 | + return new HealthCoachStatisticsOutput | |
| 2132 | + { | |
| 2133 | + HealthCoachId = id, | |
| 2134 | + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), | |
| 2135 | + BillingPerformance = billing?.Performance ?? 0, | |
| 2136 | + ConsumePerformance = consume?.Performance ?? 0, | |
| 2137 | + RefundPerformance = refund?.Performance ?? 0, | |
| 2138 | + BillingProjectCount = billing?.ProjectCount ?? 0, | |
| 2139 | + ConsumeProjectCount = consume?.ProjectCount ?? 0, | |
| 2140 | + RefundProjectCount = refund?.ProjectCount ?? 0 | |
| 2141 | + }; | |
| 2142 | + }) | |
| 2143 | + .ToList(); | |
| 2144 | + | |
| 2145 | + return ranking; | |
| 2146 | + } | |
| 2147 | + catch (Exception ex) | |
| 2148 | + { | |
| 2149 | + _logger.LogError(ex, $"获取健康师开单业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 2150 | + throw NCCException.Oh($"获取健康师开单业绩排行榜失败: {ex.Message}"); | |
| 2151 | + } | |
| 2152 | + } | |
| 2153 | + | |
| 2154 | + /// <summary> | |
| 2155 | + /// 获取健康师耗卡业绩排行榜 | |
| 2156 | + /// </summary> | |
| 2157 | + /// <remarks> | |
| 2158 | + /// 获取按耗卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 | |
| 2159 | + /// | |
| 2160 | + /// 示例请求: | |
| 2161 | + /// ```json | |
| 2162 | + /// { | |
| 2163 | + /// "startTime": "2025-10-01", | |
| 2164 | + /// "endTime": "2025-10-31", | |
| 2165 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 2166 | + /// } | |
| 2167 | + /// ``` | |
| 2168 | + /// </remarks> | |
| 2169 | + /// <param name="input">查询参数</param> | |
| 2170 | + /// <returns>耗卡业绩排行榜(前20名)</returns> | |
| 2171 | + /// <response code="200">成功返回排行榜数据</response> | |
| 2172 | + /// <response code="400">参数错误</response> | |
| 2173 | + /// <response code="500">服务器错误</response> | |
| 2174 | + [HttpPost("get-health-coach-consume-ranking")] | |
| 2175 | + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachConsumeRanking(HealthCoachStatisticsInput input) | |
| 2176 | + { | |
| 2177 | + try | |
| 2178 | + { | |
| 2179 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 2180 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 2181 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 2182 | + | |
| 2183 | + // 第一步:获取耗卡业绩排行榜前20名健康师(使用jkszh作为ID) | |
| 2184 | + var rankingIdsSql = $@" | |
| 2185 | + SELECT | |
| 2186 | + jks.jkszh as health_coach_id, | |
| 2187 | + jks.jksxm as health_coach_name | |
| 2188 | + FROM lq_xh_jksyj jks | |
| 2189 | + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id | |
| 2190 | + WHERE jks.F_IsEffective = 1 | |
| 2191 | + AND hyhk.F_IsEffective = 1 | |
| 2192 | + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2193 | + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2194 | + AND jks.jkszh IS NOT NULL | |
| 2195 | + AND jks.jkszh != ''"; | |
| 2196 | + | |
| 2197 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2198 | + { | |
| 2199 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2200 | + rankingIdsSql += $" AND hyhk.md IN ('{storeIdsStr}')"; | |
| 2201 | + } | |
| 2202 | + | |
| 2203 | + rankingIdsSql += @" | |
| 2204 | + GROUP BY jks.jkszh, jks.jksxm | |
| 2205 | + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC | |
| 2206 | + LIMIT 20"; | |
| 2207 | + | |
| 2208 | + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql)) | |
| 2209 | + .Select(item => new | |
| 2210 | + { | |
| 2211 | + Id = item.health_coach_id?.ToString(), | |
| 2212 | + Name = item.health_coach_name?.ToString() | |
| 2213 | + }) | |
| 2214 | + .Where(x => !string.IsNullOrEmpty(x.Id)) | |
| 2215 | + .ToList(); | |
| 2216 | + | |
| 2217 | + var rankingIds = rankingData.Select(x => x.Id).ToList(); | |
| 2218 | + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); | |
| 2219 | + | |
| 2220 | + if (!rankingIds.Any()) | |
| 2221 | + { | |
| 2222 | + return new List<HealthCoachStatisticsOutput>(); | |
| 2223 | + } | |
| 2224 | + | |
| 2225 | + // 第二步:为这些健康师查询完整的三个业绩数据 | |
| 2226 | + var healthCoachIdsStr = string.Join("','", rankingIds); | |
| 2227 | + | |
| 2228 | + // 2.1 查询开单业绩数据(使用jkszh作为ID) | |
| 2229 | + var billingDataSql = $@" | |
| 2230 | + SELECT | |
| 2231 | + jks.jkszh as health_coach_id, | |
| 2232 | + jks.jksxm as health_coach_name, | |
| 2233 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, | |
| 2234 | + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count | |
| 2235 | + FROM lq_kd_jksyj jks | |
| 2236 | + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id | |
| 2237 | + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 | |
| 2238 | + WHERE jks.F_IsEffective = 1 | |
| 2239 | + AND kdjlb.F_IsEffective = 1 | |
| 2240 | + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2241 | + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2242 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2243 | + | |
| 2244 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2245 | + { | |
| 2246 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2247 | + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; | |
| 2248 | + } | |
| 2249 | + | |
| 2250 | + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2251 | + | |
| 2252 | + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql)) | |
| 2253 | + .ToDictionary( | |
| 2254 | + item => item.health_coach_id?.ToString(), | |
| 2255 | + item => new | |
| 2256 | + { | |
| 2257 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2258 | + Performance = Convert.ToDecimal(item.billing_performance ?? 0), | |
| 2259 | + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) | |
| 2260 | + } | |
| 2261 | + ); | |
| 2262 | + | |
| 2263 | + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) | |
| 2264 | + var consumeDataSql = $@" | |
| 2265 | + SELECT | |
| 2266 | + jks.jkszh as health_coach_id, | |
| 2267 | + jks.jksxm as health_coach_name, | |
| 2268 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, | |
| 2269 | + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count | |
| 2270 | + FROM lq_xh_jksyj jks | |
| 2271 | + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id | |
| 2272 | + WHERE jks.F_IsEffective = 1 | |
| 2273 | + AND hyhk.F_IsEffective = 1 | |
| 2274 | + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2275 | + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2276 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2277 | + | |
| 2278 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2279 | + { | |
| 2280 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2281 | + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; | |
| 2282 | + } | |
| 2283 | + | |
| 2284 | + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2285 | + | |
| 2286 | + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql)) | |
| 2287 | + .ToDictionary( | |
| 2288 | + item => item.health_coach_id?.ToString(), | |
| 2289 | + item => new | |
| 2290 | + { | |
| 2291 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2292 | + Performance = Convert.ToDecimal(item.consume_performance ?? 0), | |
| 2293 | + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) | |
| 2294 | + } | |
| 2295 | + ); | |
| 2296 | + | |
| 2297 | + // 2.3 查询退卡业绩数据(使用jkszh作为ID) | |
| 2298 | + var refundDataSql = $@" | |
| 2299 | + SELECT | |
| 2300 | + jks.jkszh as health_coach_id, | |
| 2301 | + jks.jksxm as health_coach_name, | |
| 2302 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, | |
| 2303 | + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count | |
| 2304 | + FROM lq_hytk_jksyj jks | |
| 2305 | + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id | |
| 2306 | + WHERE jks.F_IsEffective = 1 | |
| 2307 | + AND hytk.F_IsEffective = 1 | |
| 2308 | + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2309 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2310 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2311 | + | |
| 2312 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2313 | + { | |
| 2314 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2315 | + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 2316 | + } | |
| 2317 | + | |
| 2318 | + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2319 | + | |
| 2320 | + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql)) | |
| 2321 | + .ToDictionary( | |
| 2322 | + item => item.health_coach_id?.ToString(), | |
| 2323 | + item => new | |
| 2324 | + { | |
| 2325 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2326 | + Performance = Convert.ToDecimal(item.refund_performance ?? 0), | |
| 2327 | + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) | |
| 2328 | + } | |
| 2329 | + ); | |
| 2330 | + | |
| 2331 | + // 第三步:合并数据,按原始排序构建排行榜 | |
| 2332 | + var ranking = rankingIds | |
| 2333 | + .Select(id => | |
| 2334 | + { | |
| 2335 | + var billing = billingData.ContainsKey(id) ? billingData[id] : null; | |
| 2336 | + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; | |
| 2337 | + var refund = refundData.ContainsKey(id) ? refundData[id] : null; | |
| 2338 | + | |
| 2339 | + return new HealthCoachStatisticsOutput | |
| 2340 | + { | |
| 2341 | + HealthCoachId = id, | |
| 2342 | + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), | |
| 2343 | + BillingPerformance = billing?.Performance ?? 0, | |
| 2344 | + ConsumePerformance = consume?.Performance ?? 0, | |
| 2345 | + RefundPerformance = refund?.Performance ?? 0, | |
| 2346 | + BillingProjectCount = billing?.ProjectCount ?? 0, | |
| 2347 | + ConsumeProjectCount = consume?.ProjectCount ?? 0, | |
| 2348 | + RefundProjectCount = refund?.ProjectCount ?? 0 | |
| 2349 | + }; | |
| 2350 | + }) | |
| 2351 | + .ToList(); | |
| 2352 | + | |
| 2353 | + return ranking; | |
| 2354 | + } | |
| 2355 | + catch (Exception ex) | |
| 2356 | + { | |
| 2357 | + _logger.LogError(ex, $"获取健康师耗卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 2358 | + throw NCCException.Oh($"获取健康师耗卡业绩排行榜失败: {ex.Message}"); | |
| 2359 | + } | |
| 2360 | + } | |
| 2361 | + | |
| 2362 | + /// <summary> | |
| 2363 | + /// 获取健康师退卡业绩排行榜 | |
| 2364 | + /// </summary> | |
| 2365 | + /// <remarks> | |
| 2366 | + /// 获取按退卡业绩排名前20的健康师,每个健康师包含完整的开单、耗卡、退卡业绩数据 | |
| 2367 | + /// | |
| 2368 | + /// 示例请求: | |
| 2369 | + /// ```json | |
| 2370 | + /// { | |
| 2371 | + /// "startTime": "2025-10-01", | |
| 2372 | + /// "endTime": "2025-10-31", | |
| 2373 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 2374 | + /// } | |
| 2375 | + /// ``` | |
| 2376 | + /// </remarks> | |
| 2377 | + /// <param name="input">查询参数</param> | |
| 2378 | + /// <returns>退卡业绩排行榜(前20名)</returns> | |
| 2379 | + /// <response code="200">成功返回排行榜数据</response> | |
| 2380 | + /// <response code="400">参数错误</response> | |
| 2381 | + /// <response code="500">服务器错误</response> | |
| 2382 | + [HttpPost("get-health-coach-refund-ranking")] | |
| 2383 | + public async Task<List<HealthCoachStatisticsOutput>> GetHealthCoachRefundRanking(HealthCoachStatisticsInput input) | |
| 2384 | + { | |
| 2385 | + try | |
| 2386 | + { | |
| 2387 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 2388 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 2389 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 2390 | + | |
| 2391 | + // 第一步:获取退卡业绩排行榜前20名健康师(使用jkszh作为ID) | |
| 2392 | + var rankingIdsSql = $@" | |
| 2393 | + SELECT | |
| 2394 | + jks.jkszh as health_coach_id, | |
| 2395 | + jks.jksxm as health_coach_name | |
| 2396 | + FROM lq_hytk_jksyj jks | |
| 2397 | + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id | |
| 2398 | + WHERE jks.F_IsEffective = 1 | |
| 2399 | + AND hytk.F_IsEffective = 1 | |
| 2400 | + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2401 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2402 | + AND jks.jkszh IS NOT NULL | |
| 2403 | + AND jks.jkszh != ''"; | |
| 2404 | + | |
| 2405 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2406 | + { | |
| 2407 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2408 | + rankingIdsSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 2409 | + } | |
| 2410 | + | |
| 2411 | + rankingIdsSql += @" | |
| 2412 | + GROUP BY jks.jkszh, jks.jksxm | |
| 2413 | + ORDER BY SUM(CAST(jks.jksyj AS DECIMAL(18,2))) DESC | |
| 2414 | + LIMIT 20"; | |
| 2415 | + | |
| 2416 | + var rankingData = (await _db.Ado.SqlQueryAsync<dynamic>(rankingIdsSql)) | |
| 2417 | + .Select(item => new | |
| 2418 | + { | |
| 2419 | + Id = item.health_coach_id?.ToString(), | |
| 2420 | + Name = item.health_coach_name?.ToString() | |
| 2421 | + }) | |
| 2422 | + .Where(x => !string.IsNullOrEmpty(x.Id)) | |
| 2423 | + .ToList(); | |
| 2424 | + | |
| 2425 | + var rankingIds = rankingData.Select(x => x.Id).ToList(); | |
| 2426 | + var rankingIdNameDict = rankingData.ToDictionary(x => x.Id, x => x.Name ?? "未知"); | |
| 2427 | + | |
| 2428 | + if (!rankingIds.Any()) | |
| 2429 | + { | |
| 2430 | + return new List<HealthCoachStatisticsOutput>(); | |
| 2431 | + } | |
| 2432 | + | |
| 2433 | + // 第二步:为这些健康师查询完整的三个业绩数据 | |
| 2434 | + var healthCoachIdsStr = string.Join("','", rankingIds); | |
| 2435 | + | |
| 2436 | + // 2.1 查询开单业绩数据(使用jkszh作为ID) | |
| 2437 | + var billingDataSql = $@" | |
| 2438 | + SELECT | |
| 2439 | + jks.jkszh as health_coach_id, | |
| 2440 | + jks.jksxm as health_coach_name, | |
| 2441 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as billing_performance, | |
| 2442 | + COALESCE(SUM(CAST(pxmx.F_ProjectNumber AS DECIMAL(18,2))), 0) as billing_project_count | |
| 2443 | + FROM lq_kd_jksyj jks | |
| 2444 | + INNER JOIN lq_kd_kdjlb kdjlb ON jks.glkdbh = kdjlb.F_Id | |
| 2445 | + LEFT JOIN lq_kd_pxmx pxmx ON jks.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1 | |
| 2446 | + WHERE jks.F_IsEffective = 1 | |
| 2447 | + AND kdjlb.F_IsEffective = 1 | |
| 2448 | + AND kdjlb.kdrq >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2449 | + AND kdjlb.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2450 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2451 | + | |
| 2452 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2453 | + { | |
| 2454 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2455 | + billingDataSql += $" AND kdjlb.djmd IN ('{storeIdsStr}')"; | |
| 2456 | + } | |
| 2457 | + | |
| 2458 | + billingDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2459 | + | |
| 2460 | + var billingData = (await _db.Ado.SqlQueryAsync<dynamic>(billingDataSql)) | |
| 2461 | + .ToDictionary( | |
| 2462 | + item => item.health_coach_id?.ToString(), | |
| 2463 | + item => new | |
| 2464 | + { | |
| 2465 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2466 | + Performance = Convert.ToDecimal(item.billing_performance ?? 0), | |
| 2467 | + ProjectCount = Convert.ToInt32(item.billing_project_count ?? 0) | |
| 2468 | + } | |
| 2469 | + ); | |
| 2470 | + | |
| 2471 | + // 2.2 查询耗卡业绩数据(使用jkszh作为ID) | |
| 2472 | + var consumeDataSql = $@" | |
| 2473 | + SELECT | |
| 2474 | + jks.jkszh as health_coach_id, | |
| 2475 | + jks.jksxm as health_coach_name, | |
| 2476 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as consume_performance, | |
| 2477 | + COALESCE(SUM(CAST(jks.F_kdpxNumber AS DECIMAL(18,2))), 0) as consume_project_count | |
| 2478 | + FROM lq_xh_jksyj jks | |
| 2479 | + INNER JOIN lq_xh_hyhk hyhk ON jks.glkdbh = hyhk.F_Id | |
| 2480 | + WHERE jks.F_IsEffective = 1 | |
| 2481 | + AND hyhk.F_IsEffective = 1 | |
| 2482 | + AND hyhk.hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2483 | + AND hyhk.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2484 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2485 | + | |
| 2486 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2487 | + { | |
| 2488 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2489 | + consumeDataSql += $" AND hyhk.md IN ('{storeIdsStr}')"; | |
| 2490 | + } | |
| 2491 | + | |
| 2492 | + consumeDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2493 | + | |
| 2494 | + var consumeData = (await _db.Ado.SqlQueryAsync<dynamic>(consumeDataSql)) | |
| 2495 | + .ToDictionary( | |
| 2496 | + item => item.health_coach_id?.ToString(), | |
| 2497 | + item => new | |
| 2498 | + { | |
| 2499 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2500 | + Performance = Convert.ToDecimal(item.consume_performance ?? 0), | |
| 2501 | + ProjectCount = Convert.ToInt32(item.consume_project_count ?? 0) | |
| 2502 | + } | |
| 2503 | + ); | |
| 2504 | + | |
| 2505 | + // 2.3 查询退卡业绩数据(使用jkszh作为ID) | |
| 2506 | + var refundDataSql = $@" | |
| 2507 | + SELECT | |
| 2508 | + jks.jkszh as health_coach_id, | |
| 2509 | + jks.jksxm as health_coach_name, | |
| 2510 | + COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as refund_performance, | |
| 2511 | + COALESCE(SUM(CAST(jks.F_tkpxNumber AS DECIMAL(18,2))), 0) as refund_project_count | |
| 2512 | + FROM lq_hytk_jksyj jks | |
| 2513 | + INNER JOIN lq_hytk_hytk hytk ON jks.gltkbh = hytk.F_Id | |
| 2514 | + WHERE jks.F_IsEffective = 1 | |
| 2515 | + AND hytk.F_IsEffective = 1 | |
| 2516 | + AND hytk.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' | |
| 2517 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2518 | + AND jks.jkszh IN ('{healthCoachIdsStr}')"; | |
| 2519 | + | |
| 2520 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2521 | + { | |
| 2522 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2523 | + refundDataSql += $" AND hytk.md IN ('{storeIdsStr}')"; | |
| 2524 | + } | |
| 2525 | + | |
| 2526 | + refundDataSql += " GROUP BY jks.jkszh, jks.jksxm"; | |
| 2527 | + | |
| 2528 | + var refundData = (await _db.Ado.SqlQueryAsync<dynamic>(refundDataSql)) | |
| 2529 | + .ToDictionary( | |
| 2530 | + item => item.health_coach_id?.ToString(), | |
| 2531 | + item => new | |
| 2532 | + { | |
| 2533 | + Name = item.health_coach_name?.ToString() ?? "未知", | |
| 2534 | + Performance = Convert.ToDecimal(item.refund_performance ?? 0), | |
| 2535 | + ProjectCount = Convert.ToInt32(item.refund_project_count ?? 0) | |
| 2536 | + } | |
| 2537 | + ); | |
| 2538 | + | |
| 2539 | + // 第三步:合并数据,按原始排序构建排行榜 | |
| 2540 | + var ranking = rankingIds | |
| 2541 | + .Select(id => | |
| 2542 | + { | |
| 2543 | + var billing = billingData.ContainsKey(id) ? billingData[id] : null; | |
| 2544 | + var consume = consumeData.ContainsKey(id) ? consumeData[id] : null; | |
| 2545 | + var refund = refundData.ContainsKey(id) ? refundData[id] : null; | |
| 2546 | + | |
| 2547 | + return new HealthCoachStatisticsOutput | |
| 2548 | + { | |
| 2549 | + HealthCoachId = id, | |
| 2550 | + HealthCoachName = rankingIdNameDict.ContainsKey(id) ? rankingIdNameDict[id] : (billing?.Name ?? consume?.Name ?? refund?.Name ?? "未知"), | |
| 2551 | + BillingPerformance = billing?.Performance ?? 0, | |
| 2552 | + ConsumePerformance = consume?.Performance ?? 0, | |
| 2553 | + RefundPerformance = refund?.Performance ?? 0, | |
| 2554 | + BillingProjectCount = billing?.ProjectCount ?? 0, | |
| 2555 | + ConsumeProjectCount = consume?.ProjectCount ?? 0, | |
| 2556 | + RefundProjectCount = refund?.ProjectCount ?? 0 | |
| 2557 | + }; | |
| 2558 | + }) | |
| 2559 | + .ToList(); | |
| 2560 | + | |
| 2561 | + return ranking; | |
| 2562 | + } | |
| 2563 | + catch (Exception ex) | |
| 2564 | + { | |
| 2565 | + _logger.LogError(ex, $"获取健康师退卡业绩排行榜失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 2566 | + throw NCCException.Oh($"获取健康师退卡业绩排行榜失败: {ex.Message}"); | |
| 2567 | + } | |
| 2568 | + } | |
| 2569 | + | |
| 2570 | + #endregion | |
| 2571 | + | |
| 2572 | + #region 门店剩余权益统计 | |
| 2573 | + | |
| 2574 | + /// <summary> | |
| 2575 | + /// 获取门店剩余权益统计 | |
| 2576 | + /// </summary> | |
| 2577 | + /// <remarks> | |
| 2578 | + /// 统计指定时间范围内各门店的剩余权益数据,包括剩余权益累计金额和剩余权益累计人数 | |
| 2579 | + /// | |
| 2580 | + /// 剩余权益累计金额 = 开单总金额 - 消耗总金额 - 退卡总金额 | |
| 2581 | + /// 剩余权益累计人数 = 有剩余权益的会员数量(开单品项总次数 - 消耗品项总次数 - 退卡品项总次数 > 0) | |
| 2582 | + /// | |
| 2583 | + /// 示例请求: | |
| 2584 | + /// ```json | |
| 2585 | + /// { | |
| 2586 | + /// "startTime": "2025-10-01", | |
| 2587 | + /// "endTime": "2025-10-31", | |
| 2588 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 2589 | + /// } | |
| 2590 | + /// ``` | |
| 2591 | + /// | |
| 2592 | + /// 参数说明: | |
| 2593 | + /// - startTime: 开始时间(可选,默认为当月1号) | |
| 2594 | + /// - endTime: 结束时间(可选,默认为当前时间) | |
| 2595 | + /// - storeIds: 门店ID列表(可选,不传则统计所有门店) | |
| 2596 | + /// | |
| 2597 | + /// 返回字段说明: | |
| 2598 | + /// - StoreId: 门店ID | |
| 2599 | + /// - StoreName: 门店名称 | |
| 2600 | + /// - RemainingRightsAmount: 剩余权益累计金额 | |
| 2601 | + /// - RemainingRightsPersonCount: 剩余权益累计人数 | |
| 2602 | + /// </remarks> | |
| 2603 | + /// <param name="input">查询参数</param> | |
| 2604 | + /// <returns>门店剩余权益统计数据列表</returns> | |
| 2605 | + /// <response code="200">成功返回统计数据</response> | |
| 2606 | + /// <response code="400">参数错误</response> | |
| 2607 | + /// <response code="500">服务器错误</response> | |
| 2608 | + [HttpPost("get-store-remaining-rights")] | |
| 2609 | + public async Task<List<StoreRemainingRightsOutput>> GetStoreRemainingRights(StoreRemainingRightsInput input) | |
| 2610 | + { | |
| 2611 | + try | |
| 2612 | + { | |
| 2613 | + // 设置默认时间范围(如果未提供,默认为当月) | |
| 2614 | + var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); | |
| 2615 | + var endTime = input.EndTime ?? DateTime.Now; | |
| 2616 | + // 第一步:获取门店基础信息 | |
| 2617 | + var storeSql = "SELECT F_Id, dm FROM lq_mdxx WHERE 1=1"; | |
| 2618 | + if (input.StoreIds != null && input.StoreIds.Any()) | |
| 2619 | + { | |
| 2620 | + var storeIdsStr = string.Join("','", input.StoreIds); | |
| 2621 | + storeSql += $" AND F_Id IN ('{storeIdsStr}')"; | |
| 2622 | + } | |
| 2623 | + var stores = await _db.Ado.SqlQueryAsync<dynamic>(storeSql); | |
| 2624 | + // 构建门店字典 | |
| 2625 | + var storeDict = new Dictionary<string, string>(); | |
| 2626 | + foreach (var store in stores) | |
| 2627 | + { | |
| 2628 | + var storeId = store.F_Id?.ToString(); | |
| 2629 | + if (!string.IsNullOrEmpty(storeId)) | |
| 2630 | + { | |
| 2631 | + storeDict[storeId] = store.dm?.ToString() ?? "未知门店"; | |
| 2632 | + } | |
| 2633 | + } | |
| 2634 | + | |
| 2635 | + if (!storeDict.Any()) | |
| 2636 | + { | |
| 2637 | + return new List<StoreRemainingRightsOutput>(); | |
| 2638 | + } | |
| 2639 | + | |
| 2640 | + var allStoreIds = storeDict.Keys.ToList(); | |
| 2641 | + var storeIdsStrForSql = string.Join("','", allStoreIds); | |
| 2642 | + | |
| 2643 | + // 第二步:统计开单总金额(按门店) | |
| 2644 | + var billingAmountSql = $@" | |
| 2645 | + SELECT | |
| 2646 | + kd.djmd as store_id, | |
| 2647 | + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as billing_amount | |
| 2648 | + FROM lq_kd_kdjlb kd | |
| 2649 | + WHERE kd.F_IsEffective = 1 | |
| 2650 | + AND kd.djmd IN ('{storeIdsStrForSql}') | |
| 2651 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2652 | + GROUP BY kd.djmd"; | |
| 2653 | + | |
| 2654 | + var billingAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(billingAmountSql); | |
| 2655 | + var billingAmountDict = new Dictionary<string, decimal>(); | |
| 2656 | + foreach (var item in billingAmountResults) | |
| 2657 | + { | |
| 2658 | + var storeId = item.store_id?.ToString(); | |
| 2659 | + if (!string.IsNullOrEmpty(storeId)) | |
| 2660 | + { | |
| 2661 | + billingAmountDict[storeId] = Convert.ToDecimal(item.billing_amount ?? 0); | |
| 2662 | + } | |
| 2663 | + } | |
| 2664 | + | |
| 2665 | + // 第三步:统计消耗总金额(按门店) | |
| 2666 | + var consumeAmountSql = $@" | |
| 2667 | + SELECT | |
| 2668 | + xh.md as store_id, | |
| 2669 | + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as consume_amount | |
| 2670 | + FROM lq_xh_hyhk xh | |
| 2671 | + WHERE xh.F_IsEffective = 1 | |
| 2672 | + AND xh.md IN ('{storeIdsStrForSql}') | |
| 2673 | + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2674 | + GROUP BY xh.md"; | |
| 2675 | + | |
| 2676 | + var consumeAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(consumeAmountSql); | |
| 2677 | + var consumeAmountDict = new Dictionary<string, decimal>(); | |
| 2678 | + foreach (var item in consumeAmountResults) | |
| 2679 | + { | |
| 2680 | + var storeId = item.store_id?.ToString(); | |
| 2681 | + if (!string.IsNullOrEmpty(storeId)) | |
| 2682 | + { | |
| 2683 | + consumeAmountDict[storeId] = Convert.ToDecimal(item.consume_amount ?? 0); | |
| 2684 | + } | |
| 2685 | + } | |
| 2686 | + | |
| 2687 | + // 第四步:统计退卡总金额(按门店) | |
| 2688 | + var refundAmountSql = $@" | |
| 2689 | + SELECT | |
| 2690 | + hytk.md as store_id, | |
| 2691 | + COALESCE(SUM(CAST(hytk.tkje AS DECIMAL(18,2))), 0) as refund_amount | |
| 2692 | + FROM lq_hytk_hytk hytk | |
| 2693 | + WHERE hytk.F_IsEffective = 1 | |
| 2694 | + AND hytk.md IN ('{storeIdsStrForSql}') | |
| 2695 | + AND hytk.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2696 | + GROUP BY hytk.md"; | |
| 2697 | + | |
| 2698 | + var refundAmountResults = await _db.Ado.SqlQueryAsync<dynamic>(refundAmountSql); | |
| 2699 | + var refundAmountDict = new Dictionary<string, decimal>(); | |
| 2700 | + foreach (var item in refundAmountResults) | |
| 2701 | + { | |
| 2702 | + var storeId = item.store_id?.ToString(); | |
| 2703 | + if (!string.IsNullOrEmpty(storeId)) | |
| 2704 | + { | |
| 2705 | + refundAmountDict[storeId] = Convert.ToDecimal(item.refund_amount ?? 0); | |
| 2706 | + } | |
| 2707 | + } | |
| 2708 | + | |
| 2709 | + // 第五步:优化策略 - 先获取有开单的会员,然后只查询这些会员的消耗和退卡数据 | |
| 2710 | + // 5.1 首先获取有开单记录的会员ID(按门店分组) | |
| 2711 | + var billingMembersSql = $@" | |
| 2712 | + SELECT DISTINCT | |
| 2713 | + kd.djmd as store_id, | |
| 2714 | + px.F_MemberId as member_id | |
| 2715 | + FROM lq_kd_pxmx px | |
| 2716 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 2717 | + WHERE px.F_IsEffective = 1 | |
| 2718 | + AND kd.F_IsEffective = 1 | |
| 2719 | + AND kd.djmd IN ('{storeIdsStrForSql}') | |
| 2720 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2721 | + AND px.F_MemberId IS NOT NULL | |
| 2722 | + AND px.F_MemberId != ''"; | |
| 2723 | + | |
| 2724 | + var billingMembersResults = await _db.Ado.SqlQueryAsync<dynamic>(billingMembersSql); | |
| 2725 | + | |
| 2726 | + // 按门店分组会员ID | |
| 2727 | + var storeMemberDict = new Dictionary<string, HashSet<string>>(); | |
| 2728 | + foreach (var item in billingMembersResults) | |
| 2729 | + { | |
| 2730 | + var storeId = item.store_id?.ToString(); | |
| 2731 | + var memberId = item.member_id?.ToString(); | |
| 2732 | + if (!string.IsNullOrEmpty(storeId) && !string.IsNullOrEmpty(memberId)) | |
| 2733 | + { | |
| 2734 | + if (!storeMemberDict.ContainsKey(storeId)) | |
| 2735 | + { | |
| 2736 | + storeMemberDict[storeId] = new HashSet<string>(); | |
| 2737 | + } | |
| 2738 | + storeMemberDict[storeId].Add(memberId); | |
| 2739 | + } | |
| 2740 | + } | |
| 2741 | + | |
| 2742 | + // 如果没有开单会员,直接返回 | |
| 2743 | + if (!storeMemberDict.Any()) | |
| 2744 | + { | |
| 2745 | + var emptyResultList = new List<StoreRemainingRightsOutput>(); | |
| 2746 | + foreach (var kvp in storeDict) | |
| 2747 | + { | |
| 2748 | + var storeId = kvp.Key; | |
| 2749 | + var storeName = kvp.Value; | |
| 2750 | + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0; | |
| 2751 | + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; | |
| 2752 | + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0; | |
| 2753 | + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount; | |
| 2754 | + | |
| 2755 | + emptyResultList.Add(new StoreRemainingRightsOutput | |
| 2756 | + { | |
| 2757 | + StoreId = storeId, | |
| 2758 | + StoreName = storeName, | |
| 2759 | + RemainingRightsAmount = remainingRightsAmount, | |
| 2760 | + RemainingRightsPersonCount = 0 | |
| 2761 | + }); | |
| 2762 | + } | |
| 2763 | + return emptyResultList.OrderByDescending(x => x.RemainingRightsAmount).ToList(); | |
| 2764 | + } | |
| 2765 | + | |
| 2766 | + // 5.2 分别查询这些会员的开单、消耗、退卡项目数(使用IN子句,大幅减少查询范围) | |
| 2767 | + var remainingPersonCountDict = new Dictionary<string, int>(); | |
| 2768 | + | |
| 2769 | + foreach (var storeKvp in storeMemberDict) | |
| 2770 | + { | |
| 2771 | + var storeId = storeKvp.Key; | |
| 2772 | + var memberIds = storeKvp.Value.ToList(); | |
| 2773 | + | |
| 2774 | + if (!memberIds.Any()) | |
| 2775 | + { | |
| 2776 | + remainingPersonCountDict[storeId] = 0; | |
| 2777 | + continue; | |
| 2778 | + } | |
| 2779 | + | |
| 2780 | + // 分批处理会员ID(避免IN子句过长) | |
| 2781 | + var batchSize = 500; | |
| 2782 | + var remainingMembers = new HashSet<string>(); | |
| 2783 | + | |
| 2784 | + for (int i = 0; i < memberIds.Count; i += batchSize) | |
| 2785 | + { | |
| 2786 | + var batchMemberIds = memberIds.Skip(i).Take(batchSize).ToList(); | |
| 2787 | + var memberIdsStr = string.Join("','", batchMemberIds); | |
| 2788 | + | |
| 2789 | + // 查询这批会员的开单品项数 | |
| 2790 | + var billingBatchSql = $@" | |
| 2791 | + SELECT | |
| 2792 | + px.F_MemberId as member_id, | |
| 2793 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_count | |
| 2794 | + FROM lq_kd_pxmx px | |
| 2795 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 2796 | + WHERE px.F_IsEffective = 1 | |
| 2797 | + AND kd.F_IsEffective = 1 | |
| 2798 | + AND kd.djmd = '{storeId}' | |
| 2799 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2800 | + AND px.F_MemberId IN ('{memberIdsStr}') | |
| 2801 | + GROUP BY px.F_MemberId"; | |
| 2802 | + | |
| 2803 | + // 查询这批会员的消耗品项数 | |
| 2804 | + var consumeBatchSql = $@" | |
| 2805 | + SELECT | |
| 2806 | + xhpx.F_MemberId as member_id, | |
| 2807 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_count | |
| 2808 | + FROM lq_xh_pxmx xhpx | |
| 2809 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 2810 | + WHERE xhpx.F_IsEffective = 1 | |
| 2811 | + AND xh.F_IsEffective = 1 | |
| 2812 | + AND xh.md = '{storeId}' | |
| 2813 | + AND xh.hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2814 | + AND xhpx.F_MemberId IN ('{memberIdsStr}') | |
| 2815 | + GROUP BY xhpx.F_MemberId"; | |
| 2816 | + | |
| 2817 | + // 查询这批会员的退卡品项数 | |
| 2818 | + var refundBatchSql = $@" | |
| 2819 | + SELECT | |
| 2820 | + px.F_MemberId as member_id, | |
| 2821 | + SUM(CAST(hytkmx.F_ProjectNumber AS DECIMAL(18,2))) as refund_count | |
| 2822 | + FROM lq_hytk_mx hytkmx | |
| 2823 | + INNER JOIN ( | |
| 2824 | + SELECT F_Id | |
| 2825 | + FROM lq_hytk_hytk | |
| 2826 | + WHERE F_IsEffective = 1 | |
| 2827 | + AND md = '{storeId}' | |
| 2828 | + AND tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' | |
| 2829 | + ) hytk ON hytkmx.F_RefundInfoId = hytk.F_Id | |
| 2830 | + INNER JOIN lq_kd_pxmx px ON hytkmx.F_BillingItemId = px.F_Id AND px.F_IsEffective = 1 | |
| 2831 | + WHERE hytkmx.F_IsEffective = 1 | |
| 2832 | + AND px.F_MemberId IN ('{memberIdsStr}') | |
| 2833 | + GROUP BY px.F_MemberId"; | |
| 2834 | + | |
| 2835 | + // 并行查询三个数据 | |
| 2836 | + var billingTask = _db.Ado.SqlQueryAsync<dynamic>(billingBatchSql); | |
| 2837 | + var consumeTask = _db.Ado.SqlQueryAsync<dynamic>(consumeBatchSql); | |
| 2838 | + var refundTask = _db.Ado.SqlQueryAsync<dynamic>(refundBatchSql); | |
| 2839 | + | |
| 2840 | + await Task.WhenAll(billingTask, consumeTask, refundTask); | |
| 2841 | + | |
| 2842 | + var billingData = (await billingTask).ToDictionary( | |
| 2843 | + x => x.member_id?.ToString(), | |
| 2844 | + x => Convert.ToDecimal(x.billing_count ?? 0) | |
| 2845 | + ); | |
| 2846 | + var consumeData = (await consumeTask).ToDictionary( | |
| 2847 | + x => x.member_id?.ToString(), | |
| 2848 | + x => Convert.ToDecimal(x.consume_count ?? 0) | |
| 2849 | + ); | |
| 2850 | + var refundData = (await refundTask).ToDictionary( | |
| 2851 | + x => x.member_id?.ToString(), | |
| 2852 | + x => Convert.ToDecimal(x.refund_count ?? 0) | |
| 2853 | + ); | |
| 2854 | + | |
| 2855 | + // 计算这批会员的剩余次数 | |
| 2856 | + foreach (var memberId in batchMemberIds) | |
| 2857 | + { | |
| 2858 | + var billingCount = billingData.ContainsKey(memberId) ? billingData[memberId] : 0; | |
| 2859 | + var consumeCount = consumeData.ContainsKey(memberId) ? consumeData[memberId] : 0; | |
| 2860 | + var refundCount = refundData.ContainsKey(memberId) ? refundData[memberId] : 0; | |
| 2861 | + var remainingCount = billingCount - consumeCount - refundCount; | |
| 2862 | + | |
| 2863 | + if (remainingCount > 0) | |
| 2864 | + { | |
| 2865 | + remainingMembers.Add(memberId); | |
| 2866 | + } | |
| 2867 | + } | |
| 2868 | + } | |
| 2869 | + | |
| 2870 | + remainingPersonCountDict[storeId] = remainingMembers.Count; | |
| 2871 | + } | |
| 2872 | + | |
| 2873 | + // 第六步:合并数据并计算剩余权益 | |
| 2874 | + var resultList = new List<StoreRemainingRightsOutput>(); | |
| 2875 | + | |
| 2876 | + foreach (var kvp in storeDict) | |
| 2877 | + { | |
| 2878 | + var storeId = kvp.Key; | |
| 2879 | + var storeName = kvp.Value; | |
| 2880 | + | |
| 2881 | + // 计算剩余权益金额 | |
| 2882 | + var billingAmount = billingAmountDict.ContainsKey(storeId) ? billingAmountDict[storeId] : 0; | |
| 2883 | + var consumeAmount = consumeAmountDict.ContainsKey(storeId) ? consumeAmountDict[storeId] : 0; | |
| 2884 | + var refundAmount = refundAmountDict.ContainsKey(storeId) ? refundAmountDict[storeId] : 0; | |
| 2885 | + var remainingRightsAmount = billingAmount - consumeAmount - refundAmount; | |
| 2886 | + | |
| 2887 | + // 获取剩余权益人数(已在SQL中计算) | |
| 2888 | + var remainingPersonCount = remainingPersonCountDict.ContainsKey(storeId) ? remainingPersonCountDict[storeId] : 0; | |
| 2889 | + | |
| 2890 | + resultList.Add(new StoreRemainingRightsOutput | |
| 2891 | + { | |
| 2892 | + StoreId = storeId, | |
| 2893 | + StoreName = storeName, | |
| 2894 | + RemainingRightsAmount = remainingRightsAmount, | |
| 2895 | + RemainingRightsPersonCount = remainingPersonCount | |
| 2896 | + }); | |
| 2897 | + } | |
| 2898 | + | |
| 2899 | + // 按剩余权益金额排序 | |
| 2900 | + resultList = resultList.OrderByDescending(x => x.RemainingRightsAmount).ToList(); | |
| 2901 | + | |
| 2902 | + return resultList; | |
| 2903 | + } | |
| 2904 | + catch (Exception ex) | |
| 2905 | + { | |
| 2906 | + _logger.LogError(ex, $"获取门店剩余权益统计数据失败 - 开始时间: {input?.StartTime}, 结束时间: {input?.EndTime}"); | |
| 2907 | + throw NCCException.Oh($"获取门店剩余权益统计数据失败: {ex.Message}"); | |
| 2908 | + } | |
| 2909 | + } | |
| 2910 | + | |
| 2911 | + #endregion | |
| 1328 | 2912 | } |
| 1329 | 2913 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
| ... | ... | @@ -971,7 +971,7 @@ namespace NCC.Extend.LqStatistics |
| 971 | 971 | + $"本月全门店目标业绩:{monthlyStats.TargetPerformance:N0}元\n" |
| 972 | 972 | + $"本月已完成业绩:{monthlyStats.ActualPerformance:N0}元\n" |
| 973 | 973 | + $"完成率:{monthlyStats.CompletionRate:F2}%\n\n" |
| 974 | - + $"http://lvqian.antissoft.com/html/dailyReportnew.html"; | |
| 974 | + + $"https://erp.lvqianmeiye.com/html/dailyReportnew.html"; | |
| 975 | 975 | |
| 976 | 976 | var result = await _weChatBotService.SendTextMessage(messageContent); |
| 977 | 977 | return new |
| ... | ... | @@ -2586,7 +2586,7 @@ namespace NCC.Extend.LqStatistics |
| 2586 | 2586 | LEFT JOIN ( |
| 2587 | 2587 | SELECT |
| 2588 | 2588 | hytk.md as F_StoreId, |
| 2589 | - COALESCE(SUM(hytk.tkje), 0) as F_RefundAmount, | |
| 2589 | + COALESCE(SUM(hytk.F_ActualRefundAmount), 0) as F_RefundAmount, | |
| 2590 | 2590 | COUNT(DISTINCT hytk.F_Id) as F_RefundCount |
| 2591 | 2591 | FROM lq_hytk_hytk hytk |
| 2592 | 2592 | WHERE hytk.F_IsEffective = 1 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
| ... | ... | @@ -972,7 +972,8 @@ namespace NCC.Extend.LqTkjlb |
| 972 | 972 | /// { |
| 973 | 973 | /// "startTime": "2025-10-01", |
| 974 | 974 | /// "endTime": "2025-10-31", |
| 975 | - /// "eventId": "活动ID" | |
| 975 | + /// "eventId": "活动ID", | |
| 976 | + /// "storeId": ["门店ID1", "门店ID2"] | |
| 976 | 977 | /// } |
| 977 | 978 | /// ``` |
| 978 | 979 | /// |
| ... | ... | @@ -980,6 +981,7 @@ namespace NCC.Extend.LqTkjlb |
| 980 | 981 | /// - startTime: 开始时间(可选) |
| 981 | 982 | /// - endTime: 结束时间(可选) |
| 982 | 983 | /// - eventId: 活动ID(可选) |
| 984 | + /// - storeId: 门店ID数组(可选,可传入多个门店ID进行筛选) | |
| 983 | 985 | /// |
| 984 | 986 | /// 返回字段说明: |
| 985 | 987 | /// - TkCount: 拓客人数 |
| ... | ... | @@ -1024,11 +1026,18 @@ namespace NCC.Extend.LqTkjlb |
| 1024 | 1026 | eventFilter = $"AND tk.F_EventId = '{input.EventId}'"; |
| 1025 | 1027 | } |
| 1026 | 1028 | |
| 1029 | + string storeFilter = ""; | |
| 1030 | + if (input.StoreId != null && input.StoreId.Any() && input.StoreId.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 1031 | + { | |
| 1032 | + var storeIdsStr = string.Join("','", input.StoreId.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 1033 | + storeFilter = $"AND tk.F_StoreId IN ('{storeIdsStr}')"; | |
| 1034 | + } | |
| 1035 | + | |
| 1027 | 1036 | // 第一步:获取拓客人数(去重会员ID) |
| 1028 | 1037 | var tkSql = $@" |
| 1029 | 1038 | SELECT COUNT(DISTINCT tk.F_MemberId) as tk_count |
| 1030 | 1039 | FROM lq_tkjlb tk |
| 1031 | - WHERE 1=1 {timeFilter} {eventFilter}"; | |
| 1040 | + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1032 | 1041 | |
| 1033 | 1042 | var tkResult = await _db.Ado.SqlQueryAsync<dynamic>(tkSql); |
| 1034 | 1043 | var tkCount = Convert.ToInt32(tkResult?.FirstOrDefault()?.tk_count ?? 0); |
| ... | ... | @@ -1039,7 +1048,7 @@ namespace NCC.Extend.LqTkjlb |
| 1039 | 1048 | FROM lq_tkjlb tk |
| 1040 | 1049 | INNER JOIN lq_yaoyjl yy ON tk.F_MemberId = yy.yykh |
| 1041 | 1050 | AND yy.F_StoreId = tk.F_StoreId |
| 1042 | - WHERE 1=1 {timeFilter} {eventFilter}"; | |
| 1051 | + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1043 | 1052 | |
| 1044 | 1053 | var yaoyResult = await _db.Ado.SqlQueryAsync<dynamic>(yaoySql); |
| 1045 | 1054 | var yaoyCount = Convert.ToInt32(yaoyResult?.FirstOrDefault()?.yaoy_count ?? 0); |
| ... | ... | @@ -1050,7 +1059,7 @@ namespace NCC.Extend.LqTkjlb |
| 1050 | 1059 | FROM lq_tkjlb tk |
| 1051 | 1060 | INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk |
| 1052 | 1061 | AND yyjl.F_Status = '已确认' |
| 1053 | - WHERE 1=1 {timeFilter} {eventFilter}"; | |
| 1062 | + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1054 | 1063 | |
| 1055 | 1064 | var yyResult = await _db.Ado.SqlQueryAsync<dynamic>(yySql); |
| 1056 | 1065 | var yyCount = Convert.ToInt32(yyResult?.FirstOrDefault()?.yy_count ?? 0); |
| ... | ... | @@ -1062,7 +1071,7 @@ namespace NCC.Extend.LqTkjlb |
| 1062 | 1071 | INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk |
| 1063 | 1072 | AND yyjl.F_Status = '已确认' |
| 1064 | 1073 | WHERE yyjl.yysj <= NOW() |
| 1065 | - AND 1=1 {timeFilter} {eventFilter}"; | |
| 1074 | + AND 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1066 | 1075 | |
| 1067 | 1076 | var ddResult = await _db.Ado.SqlQueryAsync<dynamic>(ddSql); |
| 1068 | 1077 | var ddCount = Convert.ToInt32(ddResult?.FirstOrDefault()?.dd_count ?? 0); |
| ... | ... | @@ -1075,7 +1084,7 @@ namespace NCC.Extend.LqTkjlb |
| 1075 | 1084 | FROM lq_tkjlb tk |
| 1076 | 1085 | INNER JOIN lq_kd_kdjlb kd ON tk.F_MemberId = kd.kdhy |
| 1077 | 1086 | AND kd.F_IsEffective = 1 |
| 1078 | - WHERE 1=1 {timeFilter} {eventFilter}"; | |
| 1087 | + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1079 | 1088 | |
| 1080 | 1089 | var kdResult = await _db.Ado.SqlQueryAsync<dynamic>(kdSql); |
| 1081 | 1090 | var kdCount = Convert.ToInt32(kdResult?.FirstOrDefault()?.kd_count ?? 0); |
| ... | ... | @@ -1089,7 +1098,7 @@ namespace NCC.Extend.LqTkjlb |
| 1089 | 1098 | FROM lq_tkjlb tk |
| 1090 | 1099 | INNER JOIN lq_xh_hyhk xh ON tk.F_MemberId = xh.hy |
| 1091 | 1100 | AND xh.F_IsEffective = 1 |
| 1092 | - WHERE 1=1 {timeFilter} {eventFilter}"; | |
| 1101 | + WHERE 1=1 {timeFilter} {eventFilter} {storeFilter}"; | |
| 1093 | 1102 | |
| 1094 | 1103 | var xfResult = await _db.Ado.SqlQueryAsync<dynamic>(xfSql); |
| 1095 | 1104 | var xfCount = Convert.ToInt32(xfResult?.FirstOrDefault()?.xf_count ?? 0); | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs
| ... | ... | @@ -18,6 +18,7 @@ using NCC.DataEncryption; |
| 18 | 18 | using NCC.Dependency; |
| 19 | 19 | using NCC.DynamicApiController; |
| 20 | 20 | using NCC.Extend.Entitys.Dto.LqYcsdJsj; |
| 21 | +using NCC.Extend.Entitys.Dto.LqJinsanjiaoUser; | |
| 21 | 22 | using NCC.Extend.Entitys.lq_jinsanjiao_user; |
| 22 | 23 | using NCC.Extend.Entitys.lq_kd_jksyj; |
| 23 | 24 | using NCC.Extend.Entitys.lq_mdxx; |
| ... | ... | @@ -83,7 +84,7 @@ namespace NCC.Extend.LqYcsdJsj |
| 83 | 84 | |
| 84 | 85 | // 获取成员信息 |
| 85 | 86 | var members = await _db.Queryable<NCC.Extend.Entitys.lq_jinsanjiao_user.LqJinsanjiaoUserEntity>() |
| 86 | - .Where(x => x.JsjId == id && x.Status == "ACTIVE") | |
| 87 | + .Where(x => x.JsjId == id && x.Status == "ACTIVE" && x.DeleteMark == 0) | |
| 87 | 88 | .OrderBy(x => x.SortOrder) |
| 88 | 89 | .Select(x => new |
| 89 | 90 | { |
| ... | ... | @@ -271,6 +272,45 @@ namespace NCC.Extend.LqYcsdJsj |
| 271 | 272 | } |
| 272 | 273 | #endregion |
| 273 | 274 | |
| 275 | + #region 标记删除用户金三角关联信息 | |
| 276 | + /// <summary> | |
| 277 | + /// 标记删除用户金三角关联信息 | |
| 278 | + /// </summary> | |
| 279 | + /// <remarks> | |
| 280 | + /// 根据金三角用户关系ID,将绑定关系的删除标记设置为已删除 | |
| 281 | + /// | |
| 282 | + /// 示例请求: | |
| 283 | + /// ```json | |
| 284 | + /// { | |
| 285 | + /// "id": "关系ID" | |
| 286 | + /// } | |
| 287 | + /// ``` | |
| 288 | + /// </remarks> | |
| 289 | + /// <param name="input">参数</param> | |
| 290 | + /// <returns></returns> | |
| 291 | + /// <response code="200">成功删除</response> | |
| 292 | + /// <response code="400">参数错误</response> | |
| 293 | + /// <response code="500">服务器错误</response> | |
| 294 | + [HttpPost("Actions/DeleteJsjUserRelation")] | |
| 295 | + public async Task DeleteJsjUserRelation([FromBody] LqJinsanjiaoUserDeleteInput input) | |
| 296 | + { | |
| 297 | + if (string.IsNullOrEmpty(input.Id)) | |
| 298 | + throw NCCException.Oh(ErrorCode.COM1000, "ID不能为空"); | |
| 299 | + | |
| 300 | + var isOk = await _db.Updateable<LqJinsanjiaoUserEntity>() | |
| 301 | + .SetColumns(x => new LqJinsanjiaoUserEntity | |
| 302 | + { | |
| 303 | + DeleteMark = 1, | |
| 304 | + Status = "INACTIVE" | |
| 305 | + }) | |
| 306 | + .Where(x => x.Id == input.Id && x.DeleteMark == 0) | |
| 307 | + .ExecuteCommandAsync(); | |
| 308 | + | |
| 309 | + if (!(isOk > 0)) | |
| 310 | + throw NCCException.Oh(ErrorCode.COM1000, "未找到要删除的记录"); | |
| 311 | + } | |
| 312 | + #endregion | |
| 313 | + | |
| 274 | 314 | #region 新建金三角 |
| 275 | 315 | /// <summary> |
| 276 | 316 | /// 新建金三角 | ... | ... |
优化GetStoreRemainingRights性能索引.sql
0 → 100644
| 1 | +-- ============================================ | |
| 2 | +-- 优化 GetStoreRemainingRights 方法性能的索引 | |
| 3 | +-- ============================================ | |
| 4 | +-- 说明:这些索引专门为统计门店剩余权益功能优化 | |
| 5 | +-- 执行前请检查索引是否已存在,避免重复创建 | |
| 6 | + | |
| 7 | +-- ============================================ | |
| 8 | +-- 1. lq_kd_pxmx (开单品项明细表) 索引 | |
| 9 | +-- ============================================ | |
| 10 | +-- 用于:查询开单品项总数、按会员分组统计、JOIN关联 | |
| 11 | +-- 查询条件:F_IsEffective, F_MemberId, glkdbh | |
| 12 | + | |
| 13 | +-- 索引1:用于F_MemberId IN查询和JOIN | |
| 14 | +-- MySQL 5.7以下版本请先检查索引是否存在,或直接执行(如果已存在会报错,可忽略) | |
| 15 | +CREATE INDEX idx_kd_pxmx_member_effective_glkdbh | |
| 16 | +ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh); | |
| 17 | + | |
| 18 | +-- 索引2:用于JOIN和按会员分组(补充索引,覆盖JOIN场景) | |
| 19 | +CREATE INDEX idx_kd_pxmx_glkdbh_member_effective | |
| 20 | +ON lq_kd_pxmx(glkdbh, F_MemberId, F_IsEffective); | |
| 21 | + | |
| 22 | +-- ============================================ | |
| 23 | +-- 2. lq_kd_kdjlb (开单记录表) 索引 | |
| 24 | +-- ============================================ | |
| 25 | +-- 用于:按门店和时间范围过滤、JOIN关联 | |
| 26 | +-- 查询条件:F_IsEffective, djmd, kdrq | |
| 27 | + | |
| 28 | +-- 索引1:用于门店+时间范围查询(最常用) | |
| 29 | +CREATE INDEX idx_kd_kdjlb_store_date_effective | |
| 30 | +ON lq_kd_kdjlb(djmd, kdrq, F_IsEffective); | |
| 31 | + | |
| 32 | +-- 索引2:用于JOIN(补充,F_Id是主键可能不需要) | |
| 33 | +-- 如果F_Id是主键,则不需要此索引 | |
| 34 | +-- CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_id_effective | |
| 35 | +-- ON lq_kd_kdjlb(F_Id, F_IsEffective); | |
| 36 | + | |
| 37 | +-- ============================================ | |
| 38 | +-- 3. lq_xh_pxmx (消耗品项明细表) 索引 | |
| 39 | +-- ============================================ | |
| 40 | +-- 用于:查询消耗品项总数、按会员分组统计、JOIN关联 | |
| 41 | +-- 查询条件:F_IsEffective, F_MemberId, F_ConsumeInfoId | |
| 42 | + | |
| 43 | +-- 索引1:用于F_MemberId IN查询和JOIN | |
| 44 | +CREATE INDEX idx_xh_pxmx_member_effective_consume | |
| 45 | +ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId); | |
| 46 | + | |
| 47 | +-- 索引2:用于JOIN场景(补充索引) | |
| 48 | +CREATE INDEX idx_xh_pxmx_consume_member_effective | |
| 49 | +ON lq_xh_pxmx(F_ConsumeInfoId, F_MemberId, F_IsEffective); | |
| 50 | + | |
| 51 | +-- ============================================ | |
| 52 | +-- 4. lq_xh_hyhk (消耗会员耗卡表) 索引 | |
| 53 | +-- ============================================ | |
| 54 | +-- 用于:按门店和时间范围过滤、JOIN关联 | |
| 55 | +-- 查询条件:F_IsEffective, md, hksj | |
| 56 | + | |
| 57 | +-- 索引1:用于门店+时间范围查询(最常用) | |
| 58 | +CREATE INDEX idx_xh_hyhk_store_date_effective | |
| 59 | +ON lq_xh_hyhk(md, hksj, F_IsEffective); | |
| 60 | + | |
| 61 | +-- 索引2:用于JOIN(F_Id是主键则不需要) | |
| 62 | +-- CREATE INDEX IF NOT EXISTS idx_xh_hyhk_id_effective | |
| 63 | +-- ON lq_xh_hyhk(F_Id, F_IsEffective); | |
| 64 | + | |
| 65 | +-- ============================================ | |
| 66 | +-- 5. lq_hytk_hytk (退卡记录表) 索引 | |
| 67 | +-- ============================================ | |
| 68 | +-- 用于:按门店和时间范围过滤、JOIN关联 | |
| 69 | +-- 查询条件:F_IsEffective, md, tksj | |
| 70 | + | |
| 71 | +-- 索引1:用于门店+时间范围查询(最常用) | |
| 72 | +CREATE INDEX idx_hytk_hytk_store_date_effective | |
| 73 | +ON lq_hytk_hytk(md, tksj, F_IsEffective); | |
| 74 | + | |
| 75 | +-- 索引2:用于JOIN(F_Id是主键则不需要) | |
| 76 | +-- CREATE INDEX IF NOT EXISTS idx_hytk_hytk_id_effective | |
| 77 | +-- ON lq_hytk_hytk(F_Id, F_IsEffective); | |
| 78 | + | |
| 79 | +-- ============================================ | |
| 80 | +-- 6. lq_hytk_mx (退卡明细表) 索引 | |
| 81 | +-- ============================================ | |
| 82 | +-- 用于:JOIN关联、按会员分组统计 | |
| 83 | +-- 查询条件:F_IsEffective, F_RefundInfoId, F_BillingItemId | |
| 84 | + | |
| 85 | +-- 索引1:用于JOIN lq_hytk_hytk | |
| 86 | +CREATE INDEX idx_hytk_mx_refund_effective | |
| 87 | +ON lq_hytk_mx(F_RefundInfoId, F_IsEffective); | |
| 88 | + | |
| 89 | +-- 索引2:用于JOIN lq_kd_pxmx | |
| 90 | +CREATE INDEX idx_hytk_mx_billing_effective | |
| 91 | +ON lq_hytk_mx(F_BillingItemId, F_IsEffective); | |
| 92 | + | |
| 93 | +-- ============================================ | |
| 94 | +-- 7. 复合索引(补充优化) | |
| 95 | +-- ============================================ | |
| 96 | +-- 如果需要进一步优化,可以考虑以下复合索引 | |
| 97 | + | |
| 98 | +-- lq_kd_pxmx: 覆盖查询(包含F_ProjectNumber,避免回表) | |
| 99 | +-- 注意:此索引较大,请根据数据量决定是否创建 | |
| 100 | +-- CREATE INDEX IF NOT EXISTS idx_kd_pxmx_cover | |
| 101 | +-- ON lq_kd_pxmx(F_IsEffective, F_MemberId, glkdbh, F_ProjectNumber); | |
| 102 | + | |
| 103 | +-- lq_xh_pxmx: 覆盖查询(包含F_ProjectNumber) | |
| 104 | +-- CREATE INDEX IF NOT EXISTS idx_xh_pxmx_cover | |
| 105 | +-- ON lq_xh_pxmx(F_IsEffective, F_MemberId, F_ConsumeInfoId, F_ProjectNumber); | |
| 106 | + | |
| 107 | +-- ============================================ | |
| 108 | +-- 索引创建前的检查(避免重复创建) | |
| 109 | +-- ============================================ | |
| 110 | +-- 执行以下SQL检查索引是否已存在: | |
| 111 | + | |
| 112 | +-- SELECT | |
| 113 | +-- TABLE_NAME, | |
| 114 | +-- INDEX_NAME, | |
| 115 | +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS | |
| 116 | +-- FROM INFORMATION_SCHEMA.STATISTICS | |
| 117 | +-- WHERE TABLE_SCHEMA = DATABASE() | |
| 118 | +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx') | |
| 119 | +-- AND INDEX_NAME LIKE 'idx_%' | |
| 120 | +-- GROUP BY TABLE_NAME, INDEX_NAME | |
| 121 | +-- ORDER BY TABLE_NAME, INDEX_NAME; | |
| 122 | + | |
| 123 | +-- ============================================ | |
| 124 | +-- 索引创建后的验证 | |
| 125 | +-- ============================================ | |
| 126 | +-- 执行以下SQL验证索引是否创建成功: | |
| 127 | + | |
| 128 | +-- SELECT | |
| 129 | +-- TABLE_NAME, | |
| 130 | +-- INDEX_NAME, | |
| 131 | +-- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as COLUMNS, | |
| 132 | +-- NON_UNIQUE, | |
| 133 | +-- CARDINALITY | |
| 134 | +-- FROM INFORMATION_SCHEMA.STATISTICS | |
| 135 | +-- WHERE TABLE_SCHEMA = DATABASE() | |
| 136 | +-- AND TABLE_NAME IN ('lq_kd_pxmx', 'lq_kd_kdjlb', 'lq_xh_pxmx', 'lq_xh_hyhk', 'lq_hytk_hytk', 'lq_hytk_mx') | |
| 137 | +-- AND INDEX_NAME LIKE 'idx_%' | |
| 138 | +-- GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE, CARDINALITY | |
| 139 | +-- ORDER BY TABLE_NAME, INDEX_NAME; | |
| 140 | + | |
| 141 | +-- ============================================ | |
| 142 | +-- 性能测试建议 | |
| 143 | +-- ============================================ | |
| 144 | +-- 1. 创建索引前,记录查询时间 | |
| 145 | +-- 2. 创建索引后,使用相同参数重新测试 | |
| 146 | +-- 3. 使用 EXPLAIN 分析查询计划,确认索引被使用 | |
| 147 | +-- 4. 如果索引未被使用,检查WHERE条件顺序是否匹配索引列顺序 | |
| 148 | + | |
| 149 | +-- ============================================ | |
| 150 | +-- 注意事项 | |
| 151 | +-- ============================================ | |
| 152 | +-- 1. 索引会占用存储空间,请定期监控 | |
| 153 | +-- 2. 索引会影响INSERT/UPDATE性能,但查询性能提升显著 | |
| 154 | +-- 3. 如果表数据量很大,创建索引可能需要较长时间 | |
| 155 | +-- 4. 建议在业务低峰期执行索引创建 | |
| 156 | +-- 5. MySQL不同版本的兼容性: | |
| 157 | +-- - MySQL 8.0+ 支持 CREATE INDEX IF NOT EXISTS | |
| 158 | +-- - MySQL 5.7及以下不支持 IF NOT EXISTS,需要先检查索引是否存在 | |
| 159 | +-- 本文件已移除 IF NOT EXISTS,如索引已存在会报错,可安全忽略 | |
| 160 | +-- 6. 如果报错 "Duplicate key name",说明索引已存在,可以继续执行后面的语句 | |
| 161 | + | ... | ... |
创建会员开单耗卡项目数视图.sql
0 → 100644
| 1 | +-- ============================================ | |
| 2 | +-- 创建会员开单耗卡项目数视图 | |
| 3 | +-- ============================================ | |
| 4 | +-- 说明:创建视图可以提升查询效率,避免每次都执行复杂的JOIN和聚合操作 | |
| 5 | +-- 视图会预先聚合好数据,查询时直接使用 | |
| 6 | + | |
| 7 | +-- ============================================ | |
| 8 | +-- 方案1:基础聚合视图(推荐) | |
| 9 | +-- ============================================ | |
| 10 | +-- 优点:查询简单、性能好、可以灵活按时间范围查询 | |
| 11 | +-- 缺点:仍然需要每次查询时过滤时间范围 | |
| 12 | + | |
| 13 | +-- 删除已存在的视图(如果存在) | |
| 14 | +DROP VIEW IF EXISTS v_member_billing_consume_project; | |
| 15 | + | |
| 16 | +-- 创建视图:会员开单项目数汇总(不限制时间,包含所有数据) | |
| 17 | +CREATE VIEW v_member_billing_project AS | |
| 18 | +SELECT | |
| 19 | + px.F_MemberId as member_id, | |
| 20 | + kd.djmd as store_id, | |
| 21 | + kd.kdrq as billing_date, | |
| 22 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count | |
| 23 | +FROM lq_kd_pxmx px | |
| 24 | +INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 25 | +WHERE px.F_IsEffective = 1 | |
| 26 | + AND kd.F_IsEffective = 1 | |
| 27 | + AND px.F_MemberId IS NOT NULL | |
| 28 | + AND px.F_MemberId != '' | |
| 29 | +GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq); | |
| 30 | + | |
| 31 | +-- 创建视图:会员耗卡项目数汇总(不限制时间,包含所有数据) | |
| 32 | +CREATE VIEW v_member_consume_project AS | |
| 33 | +SELECT | |
| 34 | + xhpx.F_MemberId as member_id, | |
| 35 | + xh.md as store_id, | |
| 36 | + xh.hksj as consume_date, | |
| 37 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 38 | +FROM lq_xh_pxmx xhpx | |
| 39 | +INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 40 | +WHERE xhpx.F_IsEffective = 1 | |
| 41 | + AND xh.F_IsEffective = 1 | |
| 42 | + AND xhpx.F_MemberId IS NOT NULL | |
| 43 | + AND xhpx.F_MemberId != '' | |
| 44 | +GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj); | |
| 45 | + | |
| 46 | +-- 创建组合视图:会员开单耗卡项目数(按日汇总) | |
| 47 | +CREATE VIEW v_member_billing_consume_daily AS | |
| 48 | +SELECT | |
| 49 | + COALESCE(billing.member_id, consume.member_id) as member_id, | |
| 50 | + COALESCE(billing.store_id, consume.store_id) as store_id, | |
| 51 | + COALESCE(billing.billing_date, consume.consume_date) as statistics_date, | |
| 52 | + COALESCE(billing.billing_project_count, 0) as billing_project_count, | |
| 53 | + COALESCE(consume.consume_project_count, 0) as consume_project_count, | |
| 54 | + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count | |
| 55 | +FROM v_member_billing_project billing | |
| 56 | +FULL OUTER JOIN v_member_consume_project consume | |
| 57 | + ON billing.member_id = consume.member_id | |
| 58 | + AND billing.store_id = consume.store_id | |
| 59 | + AND DATE(billing.billing_date) = DATE(consume.consume_date); | |
| 60 | + | |
| 61 | +-- ============================================ | |
| 62 | +-- 方案2:使用UNION ALL的视图(兼容MySQL,推荐) | |
| 63 | +-- ============================================ | |
| 64 | +-- 优点:兼容MySQL所有版本、查询简单、性能好 | |
| 65 | + | |
| 66 | +DROP VIEW IF EXISTS v_member_billing_consume_project; | |
| 67 | + | |
| 68 | +CREATE VIEW v_member_billing_consume_project AS | |
| 69 | +SELECT | |
| 70 | + member_id, | |
| 71 | + store_id, | |
| 72 | + statistics_date, | |
| 73 | + SUM(billing_project_count) as billing_project_count, | |
| 74 | + SUM(consume_project_count) as consume_project_count, | |
| 75 | + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count | |
| 76 | +FROM ( | |
| 77 | + -- 开单项目数(按会员、门店、日期分组) | |
| 78 | + SELECT | |
| 79 | + px.F_MemberId as member_id, | |
| 80 | + kd.djmd as store_id, | |
| 81 | + DATE(kd.kdrq) as statistics_date, | |
| 82 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, | |
| 83 | + 0 as consume_project_count | |
| 84 | + FROM lq_kd_pxmx px | |
| 85 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 86 | + WHERE px.F_IsEffective = 1 | |
| 87 | + AND kd.F_IsEffective = 1 | |
| 88 | + AND px.F_MemberId IS NOT NULL | |
| 89 | + AND px.F_MemberId != '' | |
| 90 | + GROUP BY px.F_MemberId, kd.djmd, DATE(kd.kdrq) | |
| 91 | + | |
| 92 | + UNION ALL | |
| 93 | + | |
| 94 | + -- 耗卡项目数(按会员、门店、日期分组) | |
| 95 | + SELECT | |
| 96 | + xhpx.F_MemberId as member_id, | |
| 97 | + xh.md as store_id, | |
| 98 | + DATE(xh.hksj) as statistics_date, | |
| 99 | + 0 as billing_project_count, | |
| 100 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 101 | + FROM lq_xh_pxmx xhpx | |
| 102 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 103 | + WHERE xhpx.F_IsEffective = 1 | |
| 104 | + AND xh.F_IsEffective = 1 | |
| 105 | + AND xhpx.F_MemberId IS NOT NULL | |
| 106 | + AND xhpx.F_MemberId != '' | |
| 107 | + GROUP BY xhpx.F_MemberId, xh.md, DATE(xh.hksj) | |
| 108 | +) all_data | |
| 109 | +GROUP BY member_id, store_id, statistics_date; | |
| 110 | + | |
| 111 | +-- ============================================ | |
| 112 | +-- 方案3:按会员汇总的总视图(不按日期,累计汇总) | |
| 113 | +-- ============================================ | |
| 114 | +-- 优点:查询最简单、性能最好(适合查询总量) | |
| 115 | +-- 缺点:无法按时间范围查询 | |
| 116 | + | |
| 117 | +DROP VIEW IF EXISTS v_member_billing_consume_total; | |
| 118 | + | |
| 119 | +CREATE VIEW v_member_billing_consume_total AS | |
| 120 | +SELECT | |
| 121 | + member_id, | |
| 122 | + SUM(billing_project_count) as total_billing_project_count, | |
| 123 | + SUM(consume_project_count) as total_consume_project_count, | |
| 124 | + SUM(billing_project_count) - SUM(consume_project_count) as total_remaining_project_count, | |
| 125 | + COUNT(DISTINCT store_id) as store_count | |
| 126 | +FROM ( | |
| 127 | + -- 开单项目数(按会员、门店汇总) | |
| 128 | + SELECT | |
| 129 | + px.F_MemberId as member_id, | |
| 130 | + kd.djmd as store_id, | |
| 131 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, | |
| 132 | + 0 as consume_project_count | |
| 133 | + FROM lq_kd_pxmx px | |
| 134 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 135 | + WHERE px.F_IsEffective = 1 | |
| 136 | + AND kd.F_IsEffective = 1 | |
| 137 | + AND px.F_MemberId IS NOT NULL | |
| 138 | + AND px.F_MemberId != '' | |
| 139 | + GROUP BY px.F_MemberId, kd.djmd | |
| 140 | + | |
| 141 | + UNION ALL | |
| 142 | + | |
| 143 | + -- 耗卡项目数(按会员、门店汇总) | |
| 144 | + SELECT | |
| 145 | + xhpx.F_MemberId as member_id, | |
| 146 | + xh.md as store_id, | |
| 147 | + 0 as billing_project_count, | |
| 148 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 149 | + FROM lq_xh_pxmx xhpx | |
| 150 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 151 | + WHERE xhpx.F_IsEffective = 1 | |
| 152 | + AND xh.F_IsEffective = 1 | |
| 153 | + AND xhpx.F_MemberId IS NOT NULL | |
| 154 | + AND xhpx.F_MemberId != '' | |
| 155 | + GROUP BY xhpx.F_MemberId, xh.md | |
| 156 | +) all_data | |
| 157 | +GROUP BY member_id; | |
| 158 | + | |
| 159 | + | |
| 160 | +-- ============================================ | |
| 161 | +-- 使用视图的查询示例 | |
| 162 | +-- ============================================ | |
| 163 | + | |
| 164 | +-- 示例1:查询所有会员的开单耗卡项目数(按会员汇总,不限制时间) | |
| 165 | +SELECT | |
| 166 | + member_id, | |
| 167 | + SUM(billing_project_count) as billing_project_count, | |
| 168 | + SUM(consume_project_count) as consume_project_count, | |
| 169 | + SUM(remaining_project_count) as remaining_project_count | |
| 170 | +FROM v_member_billing_consume_project | |
| 171 | +GROUP BY member_id | |
| 172 | +ORDER BY billing_project_count DESC; | |
| 173 | + | |
| 174 | +-- 示例2:查询指定时间范围内的数据 | |
| 175 | +SELECT | |
| 176 | + member_id, | |
| 177 | + SUM(billing_project_count) as billing_project_count, | |
| 178 | + SUM(consume_project_count) as consume_project_count, | |
| 179 | + SUM(remaining_project_count) as remaining_project_count | |
| 180 | +FROM v_member_billing_consume_project | |
| 181 | +WHERE statistics_date >= '2025-10-01' | |
| 182 | + AND statistics_date <= '2025-10-30' | |
| 183 | +GROUP BY member_id | |
| 184 | +ORDER BY billing_project_count DESC; | |
| 185 | + | |
| 186 | +-- 示例3:查询指定门店的数据 | |
| 187 | +SELECT | |
| 188 | + member_id, | |
| 189 | + SUM(billing_project_count) as billing_project_count, | |
| 190 | + SUM(consume_project_count) as consume_project_count, | |
| 191 | + SUM(remaining_project_count) as remaining_project_count | |
| 192 | +FROM v_member_billing_consume_project | |
| 193 | +WHERE store_id = '1649328471923847169' | |
| 194 | + AND statistics_date >= '2025-10-01' | |
| 195 | + AND statistics_date <= '2025-10-30' | |
| 196 | +GROUP BY member_id | |
| 197 | +ORDER BY billing_project_count DESC; | |
| 198 | + | |
| 199 | +-- 示例4:使用总视图(最简单的查询,累计所有数据) | |
| 200 | +SELECT | |
| 201 | + member_id, | |
| 202 | + total_billing_project_count, | |
| 203 | + total_consume_project_count, | |
| 204 | + total_remaining_project_count, | |
| 205 | + store_count | |
| 206 | +FROM v_member_billing_consume_total | |
| 207 | +ORDER BY total_billing_project_count DESC | |
| 208 | +LIMIT 100; | |
| 209 | + | |
| 210 | + | |
| 211 | +-- ============================================ | |
| 212 | +-- 视图性能优化建议 | |
| 213 | +-- ============================================ | |
| 214 | +-- 1. 确保基础表已创建索引(参考优化GetStoreRemainingRights性能索引.sql) | |
| 215 | +-- 2. 如果数据量很大,可以考虑: | |
| 216 | +-- a. 创建物化视图(但MySQL不支持,需要定期刷新汇总表) | |
| 217 | +-- b. 创建汇总表,定期更新(推荐) | |
| 218 | +-- 3. 定期分析视图性能: | |
| 219 | +-- EXPLAIN SELECT * FROM v_member_billing_consume_project WHERE statistics_date >= '2025-10-01'; | |
| 220 | + | |
| 221 | +-- ============================================ | |
| 222 | +-- 视图维护 | |
| 223 | +-- ============================================ | |
| 224 | +-- 查看视图定义 | |
| 225 | +-- SHOW CREATE VIEW v_member_billing_consume_project; | |
| 226 | + | |
| 227 | +-- 查看所有视图 | |
| 228 | +-- SELECT TABLE_NAME | |
| 229 | +-- FROM INFORMATION_SCHEMA.VIEWS | |
| 230 | +-- WHERE TABLE_SCHEMA = DATABASE(); | |
| 231 | + | |
| 232 | +-- 删除视图 | |
| 233 | +-- DROP VIEW IF EXISTS v_member_billing_consume_project; | |
| 234 | +-- DROP VIEW IF EXISTS v_member_billing_consume_total; | |
| 235 | + | ... | ... |
数据库说明.md
| ... | ... | @@ -90,7 +90,20 @@ |
| 90 | 90 | - `lq_xh_jksyj.F_kdpxid` ↔ `lq_xh_pxmx.F_Id` (健康师业绩关联品项明细) |
| 91 | 91 | - `lq_xh_kjbsyj.F_hkpxid` ↔ `lq_xh_pxmx.F_Id` (科技部老师业绩关联品项明细) |
| 92 | 92 | |
| 93 | -### 5. 业绩统计关系 | |
| 93 | +### 5. 退卡业务关系 | |
| 94 | +- **退卡记录**: `lq_hytk_hytk` (退卡记录表) | |
| 95 | +- **退卡品项明细**: `lq_hytk_mx` (退卡品项明细) | |
| 96 | +- **退卡健康师业绩**: `lq_hytk_jksyj` (退卡健康师业绩) | |
| 97 | +- **退卡科技部老师业绩**: `lq_hytk_kjbsyj` (退卡科技部老师业绩) | |
| 98 | +- **关联字段**: | |
| 99 | + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_mx.F_RefundInfoId` (退卡记录关联品项明细) | |
| 100 | + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_jksyj.gltkbh` (退卡记录关联健康师业绩) | |
| 101 | + - `lq_hytk_hytk.F_Id` ↔ `lq_hytk_kjbsyj.gltkbh` (退卡记录关联科技部老师业绩) | |
| 102 | + - `lq_hytk_mx.F_BillingItemId` ↔ `lq_kd_pxmx.F_Id` (退卡明细关联开单品项明细) | |
| 103 | + - `lq_hytk_mx.F_MemberId` ↔ `lq_khxx.F_Id` (退卡明细关联会员,通过会员ID) | |
| 104 | + - `lq_hytk_hytk.hy` ↔ `lq_khxx.F_Id` (退卡记录关联会员) | |
| 105 | + | |
| 106 | +### 6. 业绩统计关系 | |
| 94 | 107 | - **业绩明细**: `lq_yjmxb` (业绩统计表) |
| 95 | 108 | - **关联字段**: |
| 96 | 109 | - `lq_yjmxb.jks` ↔ `BASE_USER.F_REALNAME` (健康师姓名) | ... | ... |
查询所有会员开单耗卡项目数.sql
0 → 100644
| 1 | +-- ============================================ | |
| 2 | +-- 查询所有会员的开单项目数和耗卡项目数 | |
| 3 | +-- ============================================ | |
| 4 | +-- 说明:统计每个会员在指定时间范围内的开单项目总数和耗卡项目总数 | |
| 5 | + | |
| 6 | +-- ============================================ | |
| 7 | +-- 版本1:基础查询(不限制时间范围) | |
| 8 | +-- ============================================ | |
| 9 | +SELECT | |
| 10 | + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, | |
| 11 | + COALESCE(billing.billing_project_count, 0) as billing_project_count, | |
| 12 | + COALESCE(consume.consume_project_count, 0) as consume_project_count, | |
| 13 | + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count | |
| 14 | +FROM ( | |
| 15 | + -- 开单项目数(按会员分组) | |
| 16 | + SELECT | |
| 17 | + px.F_MemberId, | |
| 18 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count | |
| 19 | + FROM lq_kd_pxmx px | |
| 20 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 21 | + WHERE px.F_IsEffective = 1 | |
| 22 | + AND kd.F_IsEffective = 1 | |
| 23 | + AND px.F_MemberId IS NOT NULL | |
| 24 | + AND px.F_MemberId != '' | |
| 25 | + GROUP BY px.F_MemberId | |
| 26 | +) billing | |
| 27 | +FULL OUTER JOIN ( | |
| 28 | + -- 耗卡项目数(按会员分组) | |
| 29 | + SELECT | |
| 30 | + xhpx.F_MemberId, | |
| 31 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 32 | + FROM lq_xh_pxmx xhpx | |
| 33 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 34 | + WHERE xhpx.F_IsEffective = 1 | |
| 35 | + AND xh.F_IsEffective = 1 | |
| 36 | + AND xhpx.F_MemberId IS NOT NULL | |
| 37 | + AND xhpx.F_MemberId != '' | |
| 38 | + GROUP BY xhpx.F_MemberId | |
| 39 | +) consume ON billing.F_MemberId = consume.F_MemberId | |
| 40 | +ORDER BY billing_project_count DESC; | |
| 41 | + | |
| 42 | + | |
| 43 | +-- ============================================ | |
| 44 | +-- 版本2:带时间范围限制(推荐使用) | |
| 45 | +-- ============================================ | |
| 46 | +-- 使用方法:修改下面的时间范围 | |
| 47 | +SELECT | |
| 48 | + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, | |
| 49 | + COALESCE(billing.billing_project_count, 0) as billing_project_count, | |
| 50 | + COALESCE(consume.consume_project_count, 0) as consume_project_count, | |
| 51 | + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count | |
| 52 | +FROM ( | |
| 53 | + -- 开单项目数(按会员分组) | |
| 54 | + SELECT | |
| 55 | + px.F_MemberId, | |
| 56 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count | |
| 57 | + FROM lq_kd_pxmx px | |
| 58 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 59 | + WHERE px.F_IsEffective = 1 | |
| 60 | + AND kd.F_IsEffective = 1 | |
| 61 | + AND px.F_MemberId IS NOT NULL | |
| 62 | + AND px.F_MemberId != '' | |
| 63 | + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 64 | + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 65 | + GROUP BY px.F_MemberId | |
| 66 | +) billing | |
| 67 | +FULL OUTER JOIN ( | |
| 68 | + -- 耗卡项目数(按会员分组) | |
| 69 | + SELECT | |
| 70 | + xhpx.F_MemberId, | |
| 71 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 72 | + FROM lq_xh_pxmx xhpx | |
| 73 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 74 | + WHERE xhpx.F_IsEffective = 1 | |
| 75 | + AND xh.F_IsEffective = 1 | |
| 76 | + AND xhpx.F_MemberId IS NOT NULL | |
| 77 | + AND xhpx.F_MemberId != '' | |
| 78 | + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 79 | + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 80 | + GROUP BY xhpx.F_MemberId | |
| 81 | +) consume ON billing.F_MemberId = consume.F_MemberId | |
| 82 | +ORDER BY billing_project_count DESC; | |
| 83 | + | |
| 84 | + | |
| 85 | +-- ============================================ | |
| 86 | +-- 版本3:兼容MySQL的写法(MySQL不支持FULL OUTER JOIN) | |
| 87 | +-- ============================================ | |
| 88 | +SELECT | |
| 89 | + COALESCE(billing.F_MemberId, consume.F_MemberId) as member_id, | |
| 90 | + COALESCE(billing.billing_project_count, 0) as billing_project_count, | |
| 91 | + COALESCE(consume.consume_project_count, 0) as consume_project_count, | |
| 92 | + (COALESCE(billing.billing_project_count, 0) - COALESCE(consume.consume_project_count, 0)) as remaining_project_count | |
| 93 | +FROM ( | |
| 94 | + -- 开单项目数(按会员分组) | |
| 95 | + SELECT | |
| 96 | + px.F_MemberId, | |
| 97 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count | |
| 98 | + FROM lq_kd_pxmx px | |
| 99 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 100 | + WHERE px.F_IsEffective = 1 | |
| 101 | + AND kd.F_IsEffective = 1 | |
| 102 | + AND px.F_MemberId IS NOT NULL | |
| 103 | + AND px.F_MemberId != '' | |
| 104 | + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 105 | + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 106 | + GROUP BY px.F_MemberId | |
| 107 | +) billing | |
| 108 | +LEFT JOIN ( | |
| 109 | + -- 耗卡项目数(按会员分组) | |
| 110 | + SELECT | |
| 111 | + xhpx.F_MemberId, | |
| 112 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 113 | + FROM lq_xh_pxmx xhpx | |
| 114 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 115 | + WHERE xhpx.F_IsEffective = 1 | |
| 116 | + AND xh.F_IsEffective = 1 | |
| 117 | + AND xhpx.F_MemberId IS NOT NULL | |
| 118 | + AND xhpx.F_MemberId != '' | |
| 119 | + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 120 | + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 121 | + GROUP BY xhpx.F_MemberId | |
| 122 | +) consume ON billing.F_MemberId = consume.F_MemberId | |
| 123 | + | |
| 124 | +UNION | |
| 125 | + | |
| 126 | +SELECT | |
| 127 | + consume.F_MemberId as member_id, | |
| 128 | + 0 as billing_project_count, | |
| 129 | + consume.consume_project_count, | |
| 130 | + (0 - consume.consume_project_count) as remaining_project_count | |
| 131 | +FROM ( | |
| 132 | + -- 耗卡项目数(按会员分组) | |
| 133 | + SELECT | |
| 134 | + xhpx.F_MemberId, | |
| 135 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 136 | + FROM lq_xh_pxmx xhpx | |
| 137 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 138 | + WHERE xhpx.F_IsEffective = 1 | |
| 139 | + AND xh.F_IsEffective = 1 | |
| 140 | + AND xhpx.F_MemberId IS NOT NULL | |
| 141 | + AND xhpx.F_MemberId != '' | |
| 142 | + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 143 | + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 144 | + GROUP BY xhpx.F_MemberId | |
| 145 | +) consume | |
| 146 | +LEFT JOIN ( | |
| 147 | + -- 开单项目数(按会员分组) | |
| 148 | + SELECT | |
| 149 | + px.F_MemberId, | |
| 150 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count | |
| 151 | + FROM lq_kd_pxmx px | |
| 152 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 153 | + WHERE px.F_IsEffective = 1 | |
| 154 | + AND kd.F_IsEffective = 1 | |
| 155 | + AND px.F_MemberId IS NOT NULL | |
| 156 | + AND px.F_MemberId != '' | |
| 157 | + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 158 | + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 159 | + GROUP BY px.F_MemberId | |
| 160 | +) billing ON consume.F_MemberId = billing.F_MemberId | |
| 161 | +WHERE billing.F_MemberId IS NULL | |
| 162 | + | |
| 163 | +ORDER BY billing_project_count DESC; | |
| 164 | + | |
| 165 | + | |
| 166 | +-- ============================================ | |
| 167 | +-- 版本4:最简单版本(推荐,使用UNION ALL + GROUP BY实现FULL OUTER JOIN效果) | |
| 168 | +-- ============================================ | |
| 169 | +SELECT | |
| 170 | + member_id, | |
| 171 | + SUM(billing_project_count) as billing_project_count, | |
| 172 | + SUM(consume_project_count) as consume_project_count, | |
| 173 | + SUM(billing_project_count) - SUM(consume_project_count) as remaining_project_count | |
| 174 | +FROM ( | |
| 175 | + -- 开单项目数 | |
| 176 | + SELECT | |
| 177 | + px.F_MemberId as member_id, | |
| 178 | + SUM(CAST(px.F_ProjectNumber AS DECIMAL(18,2))) as billing_project_count, | |
| 179 | + 0 as consume_project_count | |
| 180 | + FROM lq_kd_pxmx px | |
| 181 | + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id | |
| 182 | + WHERE px.F_IsEffective = 1 | |
| 183 | + AND kd.F_IsEffective = 1 | |
| 184 | + AND px.F_MemberId IS NOT NULL | |
| 185 | + AND px.F_MemberId != '' | |
| 186 | + AND kd.kdrq >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 187 | + AND kd.kdrq <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 188 | + GROUP BY px.F_MemberId | |
| 189 | + | |
| 190 | + UNION ALL | |
| 191 | + | |
| 192 | + -- 耗卡项目数 | |
| 193 | + SELECT | |
| 194 | + xhpx.F_MemberId as member_id, | |
| 195 | + 0 as billing_project_count, | |
| 196 | + SUM(CAST(xhpx.F_ProjectNumber AS DECIMAL(18,2))) as consume_project_count | |
| 197 | + FROM lq_xh_pxmx xhpx | |
| 198 | + INNER JOIN lq_xh_hyhk xh ON xhpx.F_ConsumeInfoId = xh.F_Id | |
| 199 | + WHERE xhpx.F_IsEffective = 1 | |
| 200 | + AND xh.F_IsEffective = 1 | |
| 201 | + AND xhpx.F_MemberId IS NOT NULL | |
| 202 | + AND xhpx.F_MemberId != '' | |
| 203 | + AND xh.hksj >= '2025-10-01 00:00:00' -- 开始时间(修改这里) | |
| 204 | + AND xh.hksj <= '2025-10-30 23:59:59' -- 结束时间(修改这里) | |
| 205 | + GROUP BY xhpx.F_MemberId | |
| 206 | +) all_data | |
| 207 | +GROUP BY member_id | |
| 208 | +ORDER BY billing_project_count DESC; | |
| 209 | + | ... | ... |
添加lq_hytk_mx表会员ID字段.sql
0 → 100644
| 1 | +-- ============================================ | |
| 2 | +-- 为 lq_hytk_mx 表添加会员ID字段 | |
| 3 | +-- ============================================ | |
| 4 | + | |
| 5 | +-- 1. 添加会员ID字段 | |
| 6 | +ALTER TABLE lq_hytk_mx | |
| 7 | +ADD COLUMN F_MemberId VARCHAR(50) NULL COMMENT '会员id' AFTER F_BillingItemId; | |
| 8 | + | |
| 9 | +-- 2. 创建索引以优化查询性能 | |
| 10 | +CREATE INDEX idx_hytk_mx_member_id ON lq_hytk_mx(F_MemberId); | |
| 11 | + | |
| 12 | +-- 3. 根据退卡信息表填充现有数据的会员ID(通过 F_RefundInfoId 关联) | |
| 13 | +UPDATE lq_hytk_mx mx | |
| 14 | +INNER JOIN lq_hytk_hytk hytk ON mx.F_RefundInfoId = hytk.F_Id | |
| 15 | +SET mx.F_MemberId = hytk.hy | |
| 16 | +WHERE mx.F_MemberId IS NULL | |
| 17 | + AND hytk.hy IS NOT NULL | |
| 18 | + AND hytk.hy != ''; | |
| 19 | + | |
| 20 | +-- 4. 根据开单品项明细表填充会员ID(如果退卡信息表中没有,则从开单品项明细表获取) | |
| 21 | +UPDATE lq_hytk_mx mx | |
| 22 | +INNER JOIN lq_kd_pxmx pxmx ON mx.F_BillingItemId = pxmx.F_Id | |
| 23 | +SET mx.F_MemberId = pxmx.F_MemberId | |
| 24 | +WHERE mx.F_MemberId IS NULL | |
| 25 | + AND pxmx.F_MemberId IS NOT NULL | |
| 26 | + AND pxmx.F_MemberId != ''; | |
| 27 | + | ... | ... |