From 7855bf0414f0cc3bc486b44ed0f9a2ec86a5c6f9 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Thu, 23 Oct 2025 20:38:33 +0800 Subject: [PATCH] feat: 优化品项统计查询性能并新增营销活动统计功能 --- netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsInput.cs | 32 ++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsOutput.cs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------- 4 files changed, 379 insertions(+), 63 deletions(-) create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsInput.cs create mode 100644 netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsOutput.cs diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsInput.cs new file mode 100644 index 0000000..be246de --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsInput.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace NCC.Extend.Entitys.Dto.LqPackageInfo +{ + /// + /// 营销活动统计查询输入参数 + /// + public class ActivityStatisticsInput + { + /// + /// 营销活动ID + /// + [Required(ErrorMessage = "营销活动ID不能为空")] + public string ActivityId { get; set; } + + /// + /// 开始时间(可选,默认为活动开始时间) + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间(可选,默认为活动结束时间) + /// + public DateTime? EndTime { get; set; } + + /// + /// 门店ID列表(可选) + /// + public string[] StoreIds { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsOutput.cs new file mode 100644 index 0000000..2cc73dc --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPackageInfo/ActivityStatisticsOutput.cs @@ -0,0 +1,55 @@ +using System; + +namespace NCC.Extend.Entitys.Dto.LqPackageInfo +{ + /// + /// 营销活动统计输出结果 + /// + public class ActivityStatisticsOutput + { + /// + /// 营销活动ID + /// + public string ActivityId { get; set; } + + /// + /// 营销活动名称 + /// + public string ActivityName { get; set; } + + /// + /// 开单数量 + /// + public int BillingCount { get; set; } + + /// + /// 开单金额 + /// + public decimal BillingAmount { get; set; } + + /// + /// 退卡数量 + /// + public int RefundCount { get; set; } + + /// + /// 退卡金额 + /// + public decimal RefundAmount { get; set; } + + /// + /// 净开单数量(开单数量 - 退卡数量) + /// + public int NetBillingCount { get; set; } + + /// + /// 净开单金额(开单金额 - 退卡金额) + /// + public decimal NetBillingAmount { get; set; } + + /// + /// 退卡率(退卡数量 / 开单数量) + /// + public decimal RefundRate { get; set; } + } +} diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs index 709f452..2b4aff6 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqPackageInfoService.cs @@ -551,5 +551,163 @@ namespace NCC.Extend.LqPackageInfo return output; } #endregion + + #region 营销活动统计 + /// + /// 获取营销活动统计数据 + /// + /// + /// 统计指定营销活动的开单、退卡相关数据 + /// 包括:开单数量、开单金额、退卡数量、退卡金额、净开单数量、净开单金额、退卡率 + /// + /// 示例请求: + /// ```json + /// { + /// "activityId": "营销活动ID", + /// "startTime": "2025-10-01", + /// "endTime": "2025-10-31", + /// "storeIds": ["门店ID1", "门店ID2"] + /// } + /// ``` + /// + /// 参数说明: + /// - activityId: 营销活动ID(必填) + /// - startTime: 开始时间(可选,默认为活动开始时间) + /// - endTime: 结束时间(可选,默认为活动结束时间) + /// - storeIds: 门店ID列表(可选) + /// + /// 返回字段说明: + /// - ActivityId: 营销活动ID + /// - ActivityName: 营销活动名称 + /// - BillingCount: 开单数量 + /// - BillingAmount: 开单金额 + /// - RefundCount: 退卡数量 + /// - RefundAmount: 退卡金额 + /// - NetBillingCount: 净开单数量(开单数量 - 退卡数量) + /// - NetBillingAmount: 净开单金额(开单金额 - 退卡金额) + /// - RefundRate: 退卡率(退卡数量 / 开单数量) + /// + /// 查询参数 + /// 营销活动统计数据 + /// 成功返回统计数据 + /// 参数错误 + /// 服务器错误 + [HttpPost("get-activity-statistics")] + public async Task GetActivityStatistics(ActivityStatisticsInput input) + { + try + { + // 1. 获取营销活动信息 + var activity = await _db.Queryable() + .Where(x => x.Id == input.ActivityId && x.IsEffective == StatusEnum.有效.GetHashCode()) + .FirstAsync(); + + if (activity == null) + { + throw NCCException.Oh("营销活动不存在或已失效"); + } + + // 2. 设置时间范围(如果未提供,使用活动时间范围) + var startTime = input.StartTime ?? activity.StartTime; + var endTime = input.EndTime ?? activity.EndTime; + + // 3. 获取营销活动关联的品项ID列表 + var itemIds = await _db.Queryable() + .Where(x => x.ActivityId == input.ActivityId && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => x.ItemId) + .ToListAsync(); + + if (!itemIds.Any()) + { + return new ActivityStatisticsOutput + { + ActivityId = input.ActivityId, + ActivityName = activity.ActivityName, + BillingCount = 0, + BillingAmount = 0, + RefundCount = 0, + RefundAmount = 0, + NetBillingCount = 0, + NetBillingAmount = 0, + RefundRate = 0 + }; + } + + // 4. 构建品项ID过滤条件 + var itemIdsStr = string.Join("','", itemIds); + var itemIdsFilter = $"AND px.px IN ('{itemIdsStr}')"; + var itemIdsFilterRefund = $"AND hytkmx.px IN ('{itemIdsStr}')"; + + // 5. 构建门店过滤条件 + string storeFilter = ""; + string storeFilterRefund = ""; + + if (input.StoreIds != null && input.StoreIds.Any()) + { + var storeIdsStr = string.Join("','", input.StoreIds); + storeFilter = $"AND kd.djmd IN ('{storeIdsStr}')"; + storeFilterRefund = $"AND hytk.md IN ('{storeIdsStr}')"; + } + + // 6. 开单统计 + var billingSql = $@" + SELECT + COUNT(DISTINCT kd.F_Id) as billing_count, + SUM(CAST(px.F_ActualPrice AS DECIMAL(18,2))) as billing_amount + FROM lq_kd_pxmx px + LEFT JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id + WHERE px.F_IsEffective = 1 + {itemIdsFilter} + AND px.yjsj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND px.yjsj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + {storeFilter}"; + + var billingData = await _db.Ado.SqlQueryAsync(billingSql); + var billingCount = Convert.ToInt32(billingData.FirstOrDefault()?.billing_count ?? 0); + var billingAmount = Convert.ToDecimal(billingData.FirstOrDefault()?.billing_amount ?? 0); + + // 7. 退卡统计 + var refundSql = $@" + SELECT + COUNT(DISTINCT hytk.F_Id) as refund_count, + SUM(CAST(hytkmx.tkje AS DECIMAL(18,2))) as refund_amount + FROM lq_hytk_mx hytkmx + LEFT JOIN lq_hytk_hytk hytk ON hytkmx.F_RefundInfoId = hytk.F_Id + WHERE hytkmx.F_IsEffective = 1 + {itemIdsFilterRefund} + AND hytkmx.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytkmx.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + {storeFilterRefund}"; + + var refundData = await _db.Ado.SqlQueryAsync(refundSql); + var refundCount = Convert.ToInt32(refundData.FirstOrDefault()?.refund_count ?? 0); + var refundAmount = Convert.ToDecimal(refundData.FirstOrDefault()?.refund_amount ?? 0); + + // 8. 计算净值和退卡率 + var netBillingCount = billingCount - refundCount; + var netBillingAmount = billingAmount - refundAmount; + var refundRate = billingCount > 0 ? Math.Round((decimal)refundCount / billingCount * 100, 2) : 0; + + // 9. 返回统计结果 + return new ActivityStatisticsOutput + { + ActivityId = input.ActivityId, + ActivityName = activity.ActivityName, + BillingCount = billingCount, + BillingAmount = billingAmount, + RefundCount = refundCount, + RefundAmount = refundAmount, + NetBillingCount = netBillingCount, + NetBillingAmount = netBillingAmount, + RefundRate = refundRate + }; + } + catch (Exception ex) + { + throw NCCException.Oh($"获取营销活动统计数据失败: {ex.Message}"); + } + } + #endregion + } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs index 99fca07..d440254 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs @@ -55,8 +55,6 @@ namespace NCC.Extend _logger = logger; } - - #region 门店业绩报表 /// @@ -558,7 +556,7 @@ namespace NCC.Extend #endregion - #region 综合仪表盘 + #region 获取综合仪表盘数据 /// /// 获取综合仪表盘数据 @@ -691,6 +689,10 @@ namespace NCC.Extend } } + #endregion + + #region 获取业务统计数据 + /// /// 获取业务统计数据 /// @@ -828,6 +830,9 @@ namespace NCC.Extend } } + #endregion + + #region 获取客户类型统计数据 /// /// 获取客户类型统计数据 /// @@ -967,6 +972,9 @@ namespace NCC.Extend } } + #endregion + + #region 获取门店业绩对比统计数据 /// /// 获取门店业绩对比统计数据 /// @@ -1079,6 +1087,9 @@ namespace NCC.Extend throw NCCException.Oh($"获取门店业绩对比统计数据失败: {ex.Message}"); } } + #endregion + + #region 获取品项统计数据 /// /// 获取品项统计数据 @@ -1126,78 +1137,138 @@ namespace NCC.Extend var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); var endTime = input.EndTime ?? DateTime.Now; - // 构建SQL查询 - var sql = @" - SELECT - xm.F_Id as item_id, - xm.xmmc as item_name, - xm.xmbh as item_number, - - -- 开单统计 - COALESCE(SUM(CASE WHEN px.F_IsEffective = 1 AND px.yjsj >= @startTime AND px.yjsj <= @endTime THEN px.F_ProjectNumber ELSE 0 END), 0) as billing_count, - COALESCE(SUM(CASE WHEN px.F_IsEffective = 1 AND px.yjsj >= @startTime AND px.yjsj <= @endTime THEN CAST(px.F_ActualPrice AS DECIMAL(18,2)) ELSE 0 END), 0) as billing_amount, - - -- 消耗统计 - COALESCE(SUM(CASE WHEN xh.F_IsEffective = 1 AND xhhyhk.hksj >= @startTime AND xhhyhk.hksj <= @endTime THEN xh.F_ProjectNumber ELSE 0 END), 0) as consume_count, - COALESCE(SUM(CASE WHEN xh.F_IsEffective = 1 AND xhhyhk.hksj >= @startTime AND xhhyhk.hksj <= @endTime THEN CAST(xh.F_TotalPrice AS DECIMAL(18,2)) ELSE 0 END), 0) as consume_amount, - - -- 退卡统计 - COALESCE(SUM(CASE WHEN hytkmx.F_IsEffective = 1 AND hytkmx.tksj >= @startTime AND hytkmx.tksj <= @endTime THEN hytkmx.F_ProjectNumber ELSE 0 END), 0) as refund_count, - COALESCE(SUM(CASE WHEN hytkmx.F_IsEffective = 1 AND hytkmx.tksj >= @startTime AND hytkmx.tksj <= @endTime THEN CAST(hytkmx.tkje AS DECIMAL(18,2)) ELSE 0 END), 0) as refund_amount - - FROM lq_xmzl xm - - -- 开单品项明细 - LEFT JOIN lq_kd_pxmx px ON xm.F_Id = px.px - - -- 消耗品项明细 - LEFT JOIN lq_xh_pxmx xh ON xm.F_Id = xh.px - LEFT JOIN lq_xh_hyhk xhhyhk ON xh.F_ConsumeInfoId = xhhyhk.F_Id - - -- 退卡品项明细 - LEFT JOIN lq_hytk_mx hytkmx ON xm.F_Id = hytkmx.px - - WHERE xm.F_IsEffective = 1"; - - object parameters; + // 构建门店过滤条件 + string storeCondition = ""; if (input.StoreIds != null && input.StoreIds.Any()) { - sql += @" AND ( - (px.glkdbh IN (SELECT F_Id FROM lq_kd_kdjlb WHERE djmd IN @storeIds)) OR - (xhhyhk.F_StoreId IN @storeIds) OR - (hytkmx.F_RefundInfoId IN (SELECT F_Id FROM lq_hytk_hytk WHERE F_StoreId IN @storeIds)) - )"; - parameters = new { startTime, endTime, storeIds = input.StoreIds }; + var storeIdsStr = string.Join("','", input.StoreIds); + storeCondition = $@" + AND ( + kd.djmd IN ('{storeIdsStr}') OR + xhhyhk.F_StoreId IN ('{storeIdsStr}') OR + hytk.md IN ('{storeIdsStr}') + )"; } - else + + // 使用最激进的优化:分步查询,避免复杂JOIN + var itemStatisticsDict = new Dictionary(); + + // 1. 先获取品项基础信息 + var itemSql = @" + SELECT F_Id, xmmc, xmbh + FROM lq_xmzl + WHERE F_IsEffective = 1"; + + var itemData = await _db.Ado.SqlQueryAsync(itemSql); + + // 初始化品项字典 + foreach (var item in itemData) { - parameters = new { startTime, endTime }; + var itemId = item.F_Id?.ToString(); + if (!string.IsNullOrEmpty(itemId)) + { + itemStatisticsDict[itemId] = new ItemStatisticsOutput + { + ItemId = itemId, + ItemName = item.xmmc?.ToString() ?? "未知品项", + ItemNumber = item.xmbh?.ToString() ?? "", + BillingCount = 0, + BillingAmount = 0, + ConsumeCount = 0, + ConsumeAmount = 0, + RefundCount = 0, + RefundAmount = 0 + }; + } } - sql += @" GROUP BY xm.F_Id, xm.xmmc, xm.xmbh - HAVING (billing_count > 0 OR consume_count > 0 OR refund_count > 0) - ORDER BY billing_amount DESC"; - - var results = await _db.Ado.SqlQueryAsync(sql, parameters); + // 2. 开单统计 - 直接查询,避免JOIN + var billingSql = $@" + SELECT + px.px as item_id, + SUM(px.F_ProjectNumber) as billing_count, + SUM(CAST(px.F_ActualPrice AS DECIMAL(18,2))) as billing_amount + FROM lq_kd_pxmx px + WHERE px.F_IsEffective = 1 + AND px.yjsj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND px.yjsj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND px.glkdbh IN (SELECT F_Id FROM lq_kd_kdjlb WHERE djmd IN ('{string.Join("','", input.StoreIds)}'))" : "")} + GROUP BY px.px"; + + var billingData = await _db.Ado.SqlQueryAsync(billingSql); + + // 合并开单数据 + foreach (var billing in billingData) + { + var itemId = billing.item_id?.ToString(); + if (!string.IsNullOrEmpty(itemId) && itemStatisticsDict.ContainsKey(itemId)) + { + itemStatisticsDict[itemId].BillingCount = Convert.ToInt32(billing.billing_count ?? 0); + itemStatisticsDict[itemId].BillingAmount = Convert.ToDecimal(billing.billing_amount ?? 0); + } + } - var itemStatisticsList = new List(); + // 3. 消耗统计 - 直接查询,避免JOIN + var consumeSql = $@" + SELECT + xh.px as item_id, + SUM(xh.F_ProjectNumber) as consume_count, + SUM(CAST(xh.F_TotalPrice AS DECIMAL(18,2))) as consume_amount + FROM lq_xh_pxmx xh + WHERE xh.F_IsEffective = 1 + AND xh.F_ConsumeInfoId IN ( + SELECT F_Id FROM lq_xh_hyhk + WHERE hksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND F_StoreId IN ('{string.Join("','", input.StoreIds)}')" : "")} + ) + GROUP BY xh.px"; + + var consumeData = await _db.Ado.SqlQueryAsync(consumeSql); + + // 合并消耗数据 + foreach (var consume in consumeData) + { + var itemId = consume.item_id?.ToString(); + if (!string.IsNullOrEmpty(itemId) && itemStatisticsDict.ContainsKey(itemId)) + { + itemStatisticsDict[itemId].ConsumeCount = Convert.ToInt32(consume.consume_count ?? 0); + itemStatisticsDict[itemId].ConsumeAmount = Convert.ToDecimal(consume.consume_amount ?? 0); + } + } - foreach (var item in results) + // 4. 退卡统计 - 直接查询,避免JOIN + var refundSql = $@" + SELECT + hytkmx.px as item_id, + SUM(hytkmx.F_ProjectNumber) as refund_count, + SUM(CAST(hytkmx.tkje AS DECIMAL(18,2))) as refund_amount + FROM lq_hytk_mx hytkmx + WHERE hytkmx.F_IsEffective = 1 + AND hytkmx.tksj >= '{startTime:yyyy-MM-dd HH:mm:ss}' + AND hytkmx.tksj <= '{endTime:yyyy-MM-dd HH:mm:ss}' + {(input.StoreIds != null && input.StoreIds.Any() ? $"AND hytkmx.F_RefundInfoId IN (SELECT F_Id FROM lq_hytk_hytk WHERE md IN ('{string.Join("','", input.StoreIds)}'))" : "")} + GROUP BY hytkmx.px"; + + var refundData = await _db.Ado.SqlQueryAsync(refundSql); + + // 合并退卡数据 + foreach (var refund in refundData) { - itemStatisticsList.Add(new ItemStatisticsOutput + var itemId = refund.item_id?.ToString(); + if (!string.IsNullOrEmpty(itemId) && itemStatisticsDict.ContainsKey(itemId)) { - ItemId = item.item_id?.ToString(), - ItemName = item.item_name?.ToString(), - ItemNumber = item.item_number?.ToString(), - BillingCount = Convert.ToInt32(item.billing_count ?? 0), - BillingAmount = Convert.ToDecimal(item.billing_amount ?? 0), - ConsumeCount = Convert.ToInt32(item.consume_count ?? 0), - ConsumeAmount = Convert.ToDecimal(item.consume_amount ?? 0), - RefundCount = Convert.ToInt32(item.refund_count ?? 0), - RefundAmount = Convert.ToDecimal(item.refund_amount ?? 0) - }); + itemStatisticsDict[itemId].RefundCount = Convert.ToInt32(refund.refund_count ?? 0); + itemStatisticsDict[itemId].RefundAmount = Convert.ToDecimal(refund.refund_amount ?? 0); + } } + // 5. 过滤并排序 + var itemStatisticsList = itemStatisticsDict.Values + .Where(x => x.BillingCount > 0 || x.ConsumeCount > 0 || x.RefundCount > 0) + .OrderByDescending(x => x.BillingAmount) + .ToList(); + return itemStatisticsList; } catch (Exception ex) -- libgit2 0.21.4