Commit 430949ed5ffc1c4a2184350f5122200e31c566d7

Authored by “wangming”
1 parent 9bad463b

feat: 优化门店数据结构分析功能

- 创建单独的门店基础信息接口(get-store-base-info),支持单独获取门店基础信息和归属信息
- 修复门店数据结构分析接口的类型转换问题
- 优化前端调用逻辑,支持分步骤获取数据
- 添加门店归属信息显示(事业部、科技部、教育部、大项目部)
- 添加性能优化相关的SQL索引脚本
antis-ncc-admin/src/components/store-data-analysis-dialog.vue
... ... @@ -33,6 +33,22 @@
33 33 <i class="el-icon-calendar"></i>
34 34 <span>开业:{{ formatDate(baseInfo.OpenTime) }}</span>
35 35 </div>
  36 + <div class="meta-item" v-if="baseInfo.BusinessUnit">
  37 + <i class="el-icon-office-building"></i>
  38 + <span>事业部:{{ baseInfo.BusinessUnit }}</span>
  39 + </div>
  40 + <div class="meta-item" v-if="baseInfo.TechDepartment">
  41 + <i class="el-icon-cpu"></i>
  42 + <span>科技部:{{ baseInfo.TechDepartment }}</span>
  43 + </div>
  44 + <div class="meta-item" v-if="baseInfo.EducationDepartment">
  45 + <i class="el-icon-reading"></i>
  46 + <span>教育部:{{ baseInfo.EducationDepartment }}</span>
  47 + </div>
  48 + <div class="meta-item" v-if="baseInfo.MajorProjectDepartment">
  49 + <i class="el-icon-folder-opened"></i>
  50 + <span>大项目部:{{ baseInfo.MajorProjectDepartment }}</span>
  51 + </div>
36 52 </div>
37 53 </div>
38 54 <div class="header-right">
... ... @@ -501,34 +517,42 @@ export default {
501 517  
502 518 this.loading = true
503 519 try {
504   - // 第一步:先获取门店基础信息
  520 + // 准备请求参数(所有接口共用)
  521 + const requestParams = {
  522 + storeId: this.storeId,
  523 + statisticsMonth: this.statisticsMonth || this.getCurrentMonth()
  524 + }
  525 +
  526 + // 第一步:获取门店基础信息(单独接口)
505 527 console.log('开始获取门店基础信息,门店ID:', this.storeId)
  528 +
506 529 const baseInfoRes = await request({
507   - url: `/api/Extend/LqMdxx/${this.storeId}`,
508   - method: 'GET'
  530 + url: '/api/Extend/LqReport/get-store-base-info',
  531 + method: 'POST',
  532 + data: requestParams
509 533 })
510 534  
511 535 console.log('门店基础信息接口响应:', baseInfoRes)
512 536  
513 537 // 处理门店基础信息
514 538 if (baseInfoRes && baseInfoRes.code === 200 && baseInfoRes.data) {
515   - const data = baseInfoRes.data
516   - console.log('门店数据:', data)
  539 + const baseInfoData = baseInfoRes.data
  540 + console.log('门店基础信息数据:', baseInfoData)
517 541 this.baseInfo = {
518   - StoreId: data.id || this.storeId,
519   - StoreName: data.dm || '—',
520   - StoreCode: data.mdbm || '',
521   - City: data.cs || '',
522   - Address: data.dz || '',
523   - Status: data.zxzt || '',
524   - OpenTime: data.kysj || null,
525   - EmployeeCount: data.zzrs || 0,
526   - StoreType: (data.storeType != null ? data.storeType.toString() : '') || '',
527   - StoreCategory: (data.storeCategory != null ? data.storeCategory.toString() : '') || '',
528   - BusinessUnit: data.syb || '',
529   - EducationDepartment: data.jyb || '',
530   - TechDepartment: data.kjb || '',
531   - MajorProjectDepartment: data.dxmb || ''
  542 + StoreId: baseInfoData.StoreId || this.storeId,
  543 + StoreName: baseInfoData.StoreName || '—',
  544 + StoreCode: baseInfoData.StoreCode || '',
  545 + City: baseInfoData.City || '',
  546 + Address: baseInfoData.Address || '',
  547 + Status: baseInfoData.Status || '',
  548 + OpenTime: baseInfoData.OpenTime || null,
  549 + EmployeeCount: baseInfoData.EmployeeCount || 0,
  550 + StoreType: baseInfoData.StoreType || '',
  551 + StoreCategory: baseInfoData.StoreCategory || '',
  552 + BusinessUnit: baseInfoData.BusinessUnit || '',
  553 + EducationDepartment: baseInfoData.EducationDepartment || '',
  554 + TechDepartment: baseInfoData.TechDepartment || '',
  555 + MajorProjectDepartment: baseInfoData.MajorProjectDepartment || ''
532 556 }
533 557 console.log('处理后的门店基础信息:', this.baseInfo)
534 558 } else {
... ... @@ -538,14 +562,37 @@ export default {
538 562 } else {
539 563 this.$message.error('获取门店基础信息失败: 接口返回异常')
540 564 }
  565 + // 如果获取失败,尝试从门店接口获取基本信息(但归属信息会是ID)
  566 + try {
  567 + const fallbackRes = await request({
  568 + url: `/api/Extend/LqMdxx/${this.storeId}`,
  569 + method: 'GET'
  570 + })
  571 + if (fallbackRes && fallbackRes.code === 200 && fallbackRes.data) {
  572 + const data = fallbackRes.data
  573 + this.baseInfo = {
  574 + StoreId: data.id || this.storeId,
  575 + StoreName: data.dm || '—',
  576 + StoreCode: data.mdbm || '',
  577 + City: data.cs || '',
  578 + Address: data.dz || '',
  579 + Status: data.zxzt || '',
  580 + OpenTime: data.kysj || null,
  581 + EmployeeCount: data.zzrs || 0,
  582 + StoreType: (data.storeType != null ? data.storeType.toString() : '') || '',
  583 + StoreCategory: (data.storeCategory != null ? data.storeCategory.toString() : '') || '',
  584 + BusinessUnit: '', // 备用方案:无法获取名称时留空
  585 + EducationDepartment: '',
  586 + TechDepartment: '',
  587 + MajorProjectDepartment: ''
  588 + }
  589 + }
  590 + } catch (e) {
  591 + console.error('备用方案也失败:', e)
  592 + }
541 593 }
542 594  
543 595 // 第二步:获取业绩概览
544   - const requestParams = {
545   - storeId: this.storeId,
546   - statisticsMonth: this.statisticsMonth || this.getCurrentMonth()
547   - }
548   -
549 596 console.log('开始获取业绩概览,参数:', requestParams)
550 597 const performanceRes = await request({
551 598 url: '/api/Extend/LqReport/get-store-performance-overview',
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqReport/StoreDataAnalysisOutput.cs
... ... @@ -36,7 +36,7 @@ namespace NCC.Extend.Entitys.Dto.LqReport
36 36 /// <summary>
37 37 /// 品项分析
38 38 /// </summary>
39   - public ItemAnalysis Item { get; set; }
  39 + public ItemAnalysis? Item { get; set; }
40 40  
41 41 /// <summary>
42 42 /// 每日运营数据(当月)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -1130,28 +1130,47 @@ namespace NCC.Extend
1130 1130 ? DateTime.Now
1131 1131 : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
1132 1132  
1133   - // 实时查询金三角业绩
1134   - var sql = $@"
  1133 + // 使用参数化查询,优化性能
  1134 + var parameters = new List<SugarParameter>
  1135 + {
  1136 + new SugarParameter("@StatisticsMonth", input.StatisticsMonth),
  1137 + new SugarParameter("@StartDate", startDate),
  1138 + new SugarParameter("@EndDate", endDateForQuery)
  1139 + };
  1140 +
  1141 + // 优化SQL:使用子查询先聚合数据,减少JOIN的数据量
  1142 + // 1. 先过滤有效业绩数据并按金三角ID聚合(减少数据量)
  1143 + // 2. 再关联金三角设定和门店信息
  1144 + // 这样可以避免在大数据量下进行复杂的多表JOIN
  1145 + var sql = @"
1135 1146 SELECT
1136 1147 jsj.F_Id as F_GoldTriangleId,
1137 1148 jsj.jsj as F_GoldTriangleName,
1138 1149 jsj.md as F_StoreId,
1139   - mdxx.dm as F_StoreName,
1140   - COALESCE(SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))), 0) as F_TotalPerformance,
1141   - COUNT(DISTINCT jksyj.glkdbh) as F_OrderCount
  1150 + COALESCE(mdxx.dm, '') as F_StoreName,
  1151 + COALESCE(performance.F_TotalPerformance, 0) as F_TotalPerformance,
  1152 + COALESCE(performance.F_OrderCount, 0) as F_OrderCount
1142 1153 FROM lq_ycsd_jsj jsj
1143   - INNER JOIN lq_jinsanjiao_user jsjUser ON jsj.F_Id = jsjUser.jsj_id
1144   - AND jsjUser.status = 'ACTIVE'
1145   - AND jsjUser.F_DeleteMark = 0
1146   - INNER JOIN lq_kd_jksyj jksyj ON jsjUser.user_id = jksyj.jkszh
1147   - INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id
  1154 + LEFT JOIN (
  1155 + -- 子查询:先过滤并聚合有效业绩数据,减少JOIN数据量
  1156 + SELECT
  1157 + jsjUser.jsj_id,
  1158 + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as F_TotalPerformance,
  1159 + COUNT(DISTINCT jksyj.glkdbh) as F_OrderCount
  1160 + FROM lq_kd_jksyj jksyj
  1161 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id
  1162 + INNER JOIN lq_jinsanjiao_user jsjUser ON jksyj.jkszh = jsjUser.user_id
  1163 + WHERE jksyj.F_IsEffective = 1
  1164 + AND kd.F_IsEffective = 1
  1165 + AND kd.kdrq >= @StartDate
  1166 + AND kd.kdrq <= @EndDate
  1167 + AND jsjUser.status = 'ACTIVE'
  1168 + AND jsjUser.F_DeleteMark = 0
  1169 + GROUP BY jsjUser.jsj_id
  1170 + ) performance ON jsj.F_Id = performance.jsj_id
1148 1171 LEFT JOIN lq_mdxx mdxx ON jsj.md = mdxx.F_Id
1149   - WHERE jsj.yf = '{input.StatisticsMonth}'
1150   - AND jksyj.F_IsEffective = 1
1151   - AND kd.F_IsEffective = 1
1152   - AND kd.kdrq >= '{startDate:yyyy-MM-dd 00:00:00}'
1153   - AND kd.kdrq <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'
1154   - GROUP BY jsj.F_Id, jsj.jsj, jsj.md, mdxx.dm
  1172 + WHERE jsj.yf = @StatisticsMonth
  1173 + AND performance.jsj_id IS NOT NULL
1155 1174 ORDER BY F_TotalPerformance DESC";
1156 1175  
1157 1176 if (input.TopCount > 0)
... ... @@ -1159,7 +1178,7 @@ namespace NCC.Extend
1159 1178 sql += $" LIMIT {input.TopCount}";
1160 1179 }
1161 1180  
1162   - var data = await _db.Ado.SqlQueryAsync<dynamic>(sql);
  1181 + var data = await _db.Ado.SqlQueryAsync<dynamic>(sql, parameters);
1163 1182  
1164 1183 if (data == null || !data.Any())
1165 1184 {
... ... @@ -1320,31 +1339,39 @@ namespace NCC.Extend
1320 1339 ? DateTime.Now
1321 1340 : endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59);
1322 1341  
  1342 + // 使用参数化查询,优化性能
  1343 + var parameters = new List<SugarParameter>
  1344 + {
  1345 + new SugarParameter("@StartDate", startDate),
  1346 + new SugarParameter("@EndDate", endDateForQuery),
  1347 + new SugarParameter("@StatisticsMonth", statisticsMonth)
  1348 + };
  1349 +
1323 1350 // 1. 门店业绩汇总(实时查询)
1324 1351 // 先查询开单业绩
1325   - var billingSql = $@"
  1352 + var billingSql = @"
1326 1353 SELECT
1327 1354 COUNT(DISTINCT kd.djmd) as StoreCount,
1328 1355 COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as TotalBillingAmount
1329 1356 FROM lq_kd_kdjlb kd
1330 1357 WHERE kd.F_IsEffective = 1
1331   - AND kd.kdrq >= '{startDate:yyyy-MM-dd 00:00:00}'
1332   - AND kd.kdrq <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'";
  1358 + AND kd.kdrq >= @StartDate
  1359 + AND kd.kdrq <= @EndDate";
1333 1360  
1334   - var billingResult = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
  1361 + var billingResult = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, parameters);
1335 1362 var storeCount = Convert.ToInt32(billingResult?.FirstOrDefault()?.StoreCount ?? 0);
1336 1363 var totalBillingAmount = Convert.ToDecimal(billingResult?.FirstOrDefault()?.TotalBillingAmount ?? 0m);
1337 1364  
1338 1365 // 查询退卡金额
1339   - var refundSql = $@"
  1366 + var refundSql = @"
1340 1367 SELECT
1341 1368 COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, tkje, 0) AS DECIMAL(18,2))), 0) as TotalRefundAmount
1342 1369 FROM lq_hytk_hytk
1343 1370 WHERE F_IsEffective = 1
1344   - AND tksj >= '{startDate:yyyy-MM-dd 00:00:00}'
1345   - AND tksj <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'";
  1371 + AND tksj >= @StartDate
  1372 + AND tksj <= @EndDate";
1346 1373  
1347   - var refundResult = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
  1374 + var refundResult = await _db.Ado.SqlQueryAsync<dynamic>(refundSql, parameters);
1348 1375 var totalRefundAmount = Convert.ToDecimal(refundResult?.FirstOrDefault()?.TotalRefundAmount ?? 0m);
1349 1376  
1350 1377 var totalPerformance = totalBillingAmount - totalRefundAmount;
... ... @@ -1363,7 +1390,7 @@ namespace NCC.Extend
1363 1390 }.Select(x => (dynamic)x).ToList();
1364 1391  
1365 1392 // 2. 健康师业绩汇总(实时查询)
1366   - var healthCoachPerformanceSql = $@"
  1393 + var healthCoachPerformanceSql = @"
1367 1394 SELECT
1368 1395 COUNT(DISTINCT jks.jkszh) as HealthCoachCount,
1369 1396 COALESCE(SUM(CAST(jks.jksyj AS DECIMAL(18,2))), 0) as TotalPerformance,
... ... @@ -1378,38 +1405,47 @@ namespace NCC.Extend
1378 1405 INNER JOIN lq_kd_kdjlb kd ON jks.glkdbh = kd.F_Id
1379 1406 WHERE jks.F_IsEffective = 1
1380 1407 AND kd.F_IsEffective = 1
1381   - AND kd.kdrq >= '{startDate:yyyy-MM-dd 00:00:00}'
1382   - AND kd.kdrq <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'";
  1408 + AND kd.kdrq >= @StartDate
  1409 + AND kd.kdrq <= @EndDate";
1383 1410  
1384   - var healthCoachPerformance = await _db.Ado.SqlQueryAsync<dynamic>(healthCoachPerformanceSql);
  1411 + var healthCoachPerformance = await _db.Ado.SqlQueryAsync<dynamic>(healthCoachPerformanceSql, parameters);
1385 1412  
1386   - // 3. 金三角业绩汇总(实时查询)
1387   - var goldTrianglePerformanceSql = $@"
  1413 + // 3. 金三角业绩汇总(实时查询)- 使用子查询优化性能
  1414 + var goldTrianglePerformanceSql = @"
1388 1415 SELECT
1389 1416 COUNT(DISTINCT jsj.F_Id) as GoldTriangleCount,
1390   - COALESCE(SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))), 0) as TotalPerformance,
1391   - COUNT(DISTINCT jksyj.glkdbh) as TotalOrderCount,
  1417 + COALESCE(SUM(performance.F_TotalPerformance), 0) as TotalPerformance,
  1418 + COALESCE(SUM(performance.F_OrderCount), 0) as TotalOrderCount,
1392 1419 CASE
1393 1420 WHEN COUNT(DISTINCT jsj.F_Id) > 0 THEN
1394   - COALESCE(SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))), 0) / COUNT(DISTINCT jsj.F_Id)
  1421 + COALESCE(SUM(performance.F_TotalPerformance), 0) / COUNT(DISTINCT jsj.F_Id)
1395 1422 ELSE 0
1396 1423 END as AvgPerformance
1397 1424 FROM lq_ycsd_jsj jsj
1398   - INNER JOIN lq_jinsanjiao_user jsjUser ON jsj.F_Id = jsjUser.jsj_id
1399   - AND jsjUser.status = 'ACTIVE'
1400   - AND jsjUser.F_DeleteMark = 0
1401   - INNER JOIN lq_kd_jksyj jksyj ON jsjUser.user_id = jksyj.jkszh
1402   - INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id
1403   - WHERE jsj.yf = '{statisticsMonth}'
1404   - AND jksyj.F_IsEffective = 1
1405   - AND kd.F_IsEffective = 1
1406   - AND kd.kdrq >= '{startDate:yyyy-MM-dd 00:00:00}'
1407   - AND kd.kdrq <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'";
1408   -
1409   - var goldTrianglePerformance = await _db.Ado.SqlQueryAsync<dynamic>(goldTrianglePerformanceSql);
  1425 + LEFT JOIN (
  1426 + -- 子查询:先过滤并聚合有效业绩数据,减少JOIN数据量
  1427 + SELECT
  1428 + jsjUser.jsj_id,
  1429 + SUM(CAST(jksyj.jksyj AS DECIMAL(18,2))) as F_TotalPerformance,
  1430 + COUNT(DISTINCT jksyj.glkdbh) as F_OrderCount
  1431 + FROM lq_kd_jksyj jksyj
  1432 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = kd.F_Id
  1433 + INNER JOIN lq_jinsanjiao_user jsjUser ON jksyj.jkszh = jsjUser.user_id
  1434 + WHERE jksyj.F_IsEffective = 1
  1435 + AND kd.F_IsEffective = 1
  1436 + AND kd.kdrq >= @StartDate
  1437 + AND kd.kdrq <= @EndDate
  1438 + AND jsjUser.status = 'ACTIVE'
  1439 + AND jsjUser.F_DeleteMark = 0
  1440 + GROUP BY jsjUser.jsj_id
  1441 + ) performance ON jsj.F_Id = performance.jsj_id
  1442 + WHERE jsj.yf = @StatisticsMonth
  1443 + AND performance.jsj_id IS NOT NULL";
  1444 +
  1445 + var goldTrianglePerformance = await _db.Ado.SqlQueryAsync<dynamic>(goldTrianglePerformanceSql, parameters);
1410 1446  
1411 1447 // 4. 消耗业绩汇总(实时查询)
1412   - var consumePerformanceSql = $@"
  1448 + var consumePerformanceSql = @"
1413 1449 SELECT
1414 1450 COUNT(*) as ConsumeRecordCount,
1415 1451 COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as TotalConsumePerformance,
... ... @@ -1419,18 +1455,23 @@ namespace NCC.Extend
1419 1455 FROM lq_xh_hyhk xh
1420 1456 LEFT JOIN lq_xh_pxmx pxmx ON xh.F_Id = pxmx.F_ConsumeInfoId AND pxmx.F_IsEffective = 1
1421 1457 WHERE xh.F_IsEffective = 1
1422   - AND xh.hksj >= '{startDate:yyyy-MM-dd 00:00:00}'
1423   - AND xh.hksj <= '{endDateForQuery:yyyy-MM-dd HH:mm:ss}'";
  1458 + AND xh.hksj >= @StartDate
  1459 + AND xh.hksj <= @EndDate";
1424 1460  
1425   - var consumePerformance = await _db.Ado.SqlQueryAsync<dynamic>(consumePerformanceSql);
  1461 + var consumePerformance = await _db.Ado.SqlQueryAsync<dynamic>(consumePerformanceSql, parameters);
1426 1462  
1427 1463 // 5. 会员统计汇总
1428 1464 var lastMonth = DateTime.ParseExact(statisticsMonth, "yyyyMM", null).AddMonths(-1).ToString("yyyyMM");
1429   - var memberStatisticsSql = $@"
  1465 + var memberStatisticsParameters = new List<SugarParameter>
  1466 + {
  1467 + new SugarParameter("@statisticsMonth", statisticsMonth),
  1468 + new SugarParameter("@lastMonth", lastMonth)
  1469 + };
  1470 + var memberStatisticsSql = @"
1430 1471 SELECT
1431 1472 COUNT(*) as TotalMembers,
1432 1473 SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = @statisticsMonth THEN 1 ELSE 0 END) as NewMembersThisMonth,
1433   - SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = '{lastMonth}' THEN 1 ELSE 0 END) as NewMembersLastMonth,
  1474 + SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = @lastMonth THEN 1 ELSE 0 END) as NewMembersLastMonth,
1434 1475 SUM(CASE WHEN F_SleepDays IS NULL OR F_SleepDays <= 3 THEN 1 ELSE 0 END) as ActiveMembers0_3,
1435 1476 SUM(CASE WHEN F_SleepDays > 3 AND F_SleepDays < 60 THEN 1 ELSE 0 END) as ActiveMembers4_59,
1436 1477 SUM(CASE WHEN F_LastVisitTime IS NOT NULL AND DATEDIFF(NOW(), F_LastVisitTime) <= 30 THEN 1 ELSE 0 END) as ActiveMembers30d,
... ... @@ -1447,7 +1488,7 @@ namespace NCC.Extend
1447 1488 WHERE F_IsEffective = 1
1448 1489 AND khlx = '3'";
1449 1490  
1450   - var memberStatistics = await _db.Ado.SqlQueryAsync<dynamic>(memberStatisticsSql, new { statisticsMonth });
  1491 + var memberStatistics = await _db.Ado.SqlQueryAsync<dynamic>(memberStatisticsSql, memberStatisticsParameters);
1451 1492 var memberStats = memberStatistics.FirstOrDefault();
1452 1493  
1453 1494 // 6. 会员类型分布统计
... ... @@ -1496,12 +1537,13 @@ namespace NCC.Extend
1496 1537 FROM lq_kd_kdjlb kd
1497 1538 INNER JOIN lq_khxx kh ON kh.F_Id = kd.kdhy
1498 1539 WHERE kd.F_IsEffective = 1
1499   - AND DATE_FORMAT(kd.kdrq, '%Y%m') = @statisticsMonth
  1540 + AND kd.kdrq >= @StartDate
  1541 + AND kd.kdrq <= @EndDate
1500 1542 GROUP BY kh.F_Id, kh.Khmc
1501 1543 ORDER BY Amount DESC
1502 1544 LIMIT 1";
1503 1545  
1504   - var topBilling = (await _db.Ado.SqlQueryAsync<dynamic>(topBillingSql, new { statisticsMonth })).FirstOrDefault();
  1546 + var topBilling = (await _db.Ado.SqlQueryAsync<dynamic>(topBillingSql, parameters)).FirstOrDefault();
1505 1547  
1506 1548 var topConsumeSql = @"
1507 1549 SELECT
... ... @@ -1511,12 +1553,13 @@ namespace NCC.Extend
1511 1553 FROM lq_xh_hyhk xh
1512 1554 INNER JOIN lq_khxx kh ON kh.F_Id = xh.hy
1513 1555 WHERE xh.F_IsEffective = 1
1514   - AND DATE_FORMAT(xh.hksj, '%Y%m') = @statisticsMonth
  1556 + AND xh.hksj >= @StartDate
  1557 + AND xh.hksj <= @EndDate
1515 1558 GROUP BY kh.F_Id, kh.Khmc
1516 1559 ORDER BY Amount DESC
1517 1560 LIMIT 1";
1518 1561  
1519   - var topConsume = (await _db.Ado.SqlQueryAsync<dynamic>(topConsumeSql, new { statisticsMonth })).FirstOrDefault();
  1562 + var topConsume = (await _db.Ado.SqlQueryAsync<dynamic>(topConsumeSql, parameters)).FirstOrDefault();
1520 1563  
1521 1564 var totalMembers = Convert.ToInt32(memberStats?.TotalMembers ?? 0);
1522 1565 var activeMembers0_3 = Convert.ToInt32(memberStats?.ActiveMembers0_3 ?? 0);
... ... @@ -4514,6 +4557,125 @@ namespace NCC.Extend
4514 4557  
4515 4558 #region 门店数据结构分析
4516 4559 /// <summary>
  4560 + /// 获取门店基础信息
  4561 + /// </summary>
  4562 + /// <remarks>
  4563 + /// 获取单个门店的基础信息,包括门店基本信息、归属信息(事业部、科技部、教育部、大项目部)等
  4564 + ///
  4565 + /// 示例请求:
  4566 + /// ```json
  4567 + /// {
  4568 + /// "storeId": "门店ID",
  4569 + /// "statisticsMonth": "202512"
  4570 + /// }
  4571 + /// ```
  4572 + ///
  4573 + /// 参数说明:
  4574 + /// - storeId: 门店ID(必填)
  4575 + /// - statisticsMonth: 统计月份(YYYYMM格式,可选,不传则默认为当前月份)
  4576 + /// </remarks>
  4577 + /// <param name="input">查询参数</param>
  4578 + /// <returns>门店基础信息</returns>
  4579 + /// <response code="200">成功返回基础信息</response>
  4580 + /// <response code="400">参数错误</response>
  4581 + /// <response code="500">服务器错误</response>
  4582 + [HttpPost("get-store-base-info")]
  4583 + public async Task<StoreBaseInfo> GetStoreBaseInfo(StoreDataAnalysisInput input)
  4584 + {
  4585 + try
  4586 + {
  4587 + if (string.IsNullOrWhiteSpace(input.StoreId))
  4588 + {
  4589 + throw NCCException.Oh("门店ID不能为空");
  4590 + }
  4591 +
  4592 + // 确定统计月份
  4593 + var statisticsMonth = input.StatisticsMonth;
  4594 + if (string.IsNullOrWhiteSpace(statisticsMonth))
  4595 + {
  4596 + statisticsMonth = DateTime.Now.ToString("yyyyMM");
  4597 + }
  4598 +
  4599 + // 1. 获取门店基础信息
  4600 + var store = await _db.Queryable<LqMdxxEntity>()
  4601 + .Where(x => x.Id == input.StoreId)
  4602 + .FirstAsync();
  4603 +
  4604 + if (store == null)
  4605 + {
  4606 + throw NCCException.Oh("门店不存在");
  4607 + }
  4608 +
  4609 + // 2. 获取本月归属信息(优先从门店目标表获取,如果不存在则从门店表获取)
  4610 + var target = await _db.Queryable<LqMdTargetEntity>()
  4611 + .Where(x => x.StoreId == input.StoreId && x.Month == statisticsMonth)
  4612 + .FirstAsync();
  4613 +
  4614 + string businessUnitName = "";
  4615 + string techDepartmentName = "";
  4616 + string educationDepartmentName = "";
  4617 + string majorProjectDepartmentName = "";
  4618 +
  4619 + // 从门店目标表获取归属ID
  4620 + var businessUnitId = target?.BusinessUnit ?? store.Syb;
  4621 + var techDepartmentId = target?.TechDepartment ?? store.Kjb;
  4622 + var educationDepartmentId = target?.EducationDepartment ?? store.Jyb;
  4623 + var majorProjectDepartmentId = target?.MajorProjectDepartment ?? store.Dxmb;
  4624 +
  4625 + // 关联组织表获取归属名称(批量查询优化性能)
  4626 + var organizeIds = new List<string>();
  4627 + if (!string.IsNullOrEmpty(businessUnitId)) organizeIds.Add(businessUnitId);
  4628 + if (!string.IsNullOrEmpty(techDepartmentId)) organizeIds.Add(techDepartmentId);
  4629 + if (!string.IsNullOrEmpty(educationDepartmentId)) organizeIds.Add(educationDepartmentId);
  4630 + if (!string.IsNullOrEmpty(majorProjectDepartmentId)) organizeIds.Add(majorProjectDepartmentId);
  4631 +
  4632 + var organizeDict = new Dictionary<string, string>();
  4633 + if (organizeIds.Any())
  4634 + {
  4635 + var organizes = await _db.Queryable<OrganizeEntity>()
  4636 + .Where(x => organizeIds.Contains(x.Id))
  4637 + .Select(x => new { x.Id, x.FullName })
  4638 + .ToListAsync();
  4639 + organizeDict = organizes.ToDictionary(x => x.Id, x => x.FullName ?? "");
  4640 + }
  4641 +
  4642 + businessUnitName = !string.IsNullOrEmpty(businessUnitId) && organizeDict.ContainsKey(businessUnitId)
  4643 + ? organizeDict[businessUnitId] : "";
  4644 + techDepartmentName = !string.IsNullOrEmpty(techDepartmentId) && organizeDict.ContainsKey(techDepartmentId)
  4645 + ? organizeDict[techDepartmentId] : "";
  4646 + educationDepartmentName = !string.IsNullOrEmpty(educationDepartmentId) && organizeDict.ContainsKey(educationDepartmentId)
  4647 + ? organizeDict[educationDepartmentId] : "";
  4648 + majorProjectDepartmentName = !string.IsNullOrEmpty(majorProjectDepartmentId) && organizeDict.ContainsKey(majorProjectDepartmentId)
  4649 + ? organizeDict[majorProjectDepartmentId] : "";
  4650 +
  4651 + var baseInfo = new StoreBaseInfo
  4652 + {
  4653 + StoreId = store.Id,
  4654 + StoreName = store.Dm ?? "未知门店",
  4655 + StoreCode = store.Mdbm ?? "",
  4656 + City = store.Cs ?? "",
  4657 + Address = store.Dz ?? "",
  4658 + Status = store.Zxzt ?? "",
  4659 + OpenTime = store.Kysj,
  4660 + EmployeeCount = store.Zzrs,
  4661 + StoreType = store.StoreType?.ToString() ?? "",
  4662 + StoreCategory = store.StoreCategory?.ToString() ?? "",
  4663 + BusinessUnit = businessUnitName,
  4664 + EducationDepartment = educationDepartmentName,
  4665 + TechDepartment = techDepartmentName,
  4666 + MajorProjectDepartment = majorProjectDepartmentName
  4667 + };
  4668 +
  4669 + return baseInfo;
  4670 + }
  4671 + catch (Exception ex)
  4672 + {
  4673 + _logger.LogError(ex, $"获取门店基础信息失败 - 门店ID: {input?.StoreId}, 月份: {input?.StatisticsMonth}");
  4674 + throw NCCException.Oh($"获取门店基础信息失败: {ex.Message}");
  4675 + }
  4676 + }
  4677 +
  4678 + /// <summary>
4517 4679 /// 获取门店数据结构分析
4518 4680 /// </summary>
4519 4681 /// <remarks>
... ... @@ -4573,6 +4735,49 @@ namespace NCC.Extend
4573 4735 throw NCCException.Oh("门店不存在");
4574 4736 }
4575 4737  
  4738 + // 2.4 目标业绩(从门店目标表获取)
  4739 + var target = await _db.Queryable<LqMdTargetEntity>()
  4740 + .Where(x => x.StoreId == input.StoreId && x.Month == statisticsMonth)
  4741 + .FirstAsync();
  4742 +
  4743 + // 获取本月归属信息(优先从门店目标表获取,如果不存在则从门店表获取)
  4744 + string businessUnitName = "";
  4745 + string techDepartmentName = "";
  4746 + string educationDepartmentName = "";
  4747 + string majorProjectDepartmentName = "";
  4748 +
  4749 + // 从门店目标表获取归属ID
  4750 + var businessUnitId = target?.BusinessUnit ?? store.Syb;
  4751 + var techDepartmentId = target?.TechDepartment ?? store.Kjb;
  4752 + var educationDepartmentId = target?.EducationDepartment ?? store.Jyb;
  4753 + var majorProjectDepartmentId = target?.MajorProjectDepartment ?? store.Dxmb;
  4754 +
  4755 + // 关联组织表获取归属名称(批量查询优化性能)
  4756 + var organizeIds = new List<string>();
  4757 + if (!string.IsNullOrEmpty(businessUnitId)) organizeIds.Add(businessUnitId);
  4758 + if (!string.IsNullOrEmpty(techDepartmentId)) organizeIds.Add(techDepartmentId);
  4759 + if (!string.IsNullOrEmpty(educationDepartmentId)) organizeIds.Add(educationDepartmentId);
  4760 + if (!string.IsNullOrEmpty(majorProjectDepartmentId)) organizeIds.Add(majorProjectDepartmentId);
  4761 +
  4762 + var organizeDict = new Dictionary<string, string>();
  4763 + if (organizeIds.Any())
  4764 + {
  4765 + var organizes = await _db.Queryable<OrganizeEntity>()
  4766 + .Where(x => organizeIds.Contains(x.Id))
  4767 + .Select(x => new { x.Id, x.FullName })
  4768 + .ToListAsync();
  4769 + organizeDict = organizes.ToDictionary(x => x.Id, x => x.FullName ?? "");
  4770 + }
  4771 +
  4772 + businessUnitName = !string.IsNullOrEmpty(businessUnitId) && organizeDict.ContainsKey(businessUnitId)
  4773 + ? organizeDict[businessUnitId] : "";
  4774 + techDepartmentName = !string.IsNullOrEmpty(techDepartmentId) && organizeDict.ContainsKey(techDepartmentId)
  4775 + ? organizeDict[techDepartmentId] : "";
  4776 + educationDepartmentName = !string.IsNullOrEmpty(educationDepartmentId) && organizeDict.ContainsKey(educationDepartmentId)
  4777 + ? organizeDict[educationDepartmentId] : "";
  4778 + majorProjectDepartmentName = !string.IsNullOrEmpty(majorProjectDepartmentId) && organizeDict.ContainsKey(majorProjectDepartmentId)
  4779 + ? organizeDict[majorProjectDepartmentId] : "";
  4780 +
4576 4781 var baseInfo = new StoreBaseInfo
4577 4782 {
4578 4783 StoreId = store.Id,
... ... @@ -4585,10 +4790,10 @@ namespace NCC.Extend
4585 4790 EmployeeCount = store.Zzrs,
4586 4791 StoreType = store.StoreType?.ToString() ?? "",
4587 4792 StoreCategory = store.StoreCategory?.ToString() ?? "",
4588   - BusinessUnit = store.Syb ?? "",
4589   - EducationDepartment = store.Jyb ?? "",
4590   - TechDepartment = store.Kjb ?? "",
4591   - MajorProjectDepartment = store.Dxmb ?? ""
  4793 + BusinessUnit = businessUnitName,
  4794 + EducationDepartment = educationDepartmentName,
  4795 + TechDepartment = techDepartmentName,
  4796 + MajorProjectDepartment = majorProjectDepartmentName
4592 4797 };
4593 4798  
4594 4799 // 2. 获取业绩概览
... ... @@ -4598,7 +4803,7 @@ namespace NCC.Extend
4598 4803 .Where(x => x.Kdrq.HasValue && x.Kdrq.Value >= startDate && x.Kdrq.Value <= endDateTime)
4599 4804 .SumAsync(x => (decimal?)x.Sfyj) ?? 0m;
4600 4805  
4601   - var billingCount = await _db.Queryable<LqKdKdjlbEntity>()
  4806 + int billingCount = await _db.Queryable<LqKdKdjlbEntity>()
4602 4807 .Where(x => x.Djmd == input.StoreId && x.IsEffective == 1)
4603 4808 .Where(x => x.Kdrq.HasValue && x.Kdrq.Value >= startDate && x.Kdrq.Value <= endDateTime)
4604 4809 .CountAsync();
... ... @@ -4609,7 +4814,7 @@ namespace NCC.Extend
4609 4814 .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime)
4610 4815 .SumAsync(x => (decimal?)x.Xfje) ?? 0m;
4611 4816  
4612   - var consumeCount = await _db.Queryable<LqXhHyhkEntity>()
  4817 + int consumeCount = await _db.Queryable<LqXhHyhkEntity>()
4613 4818 .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
4614 4819 .Where(x => x.Hksj.HasValue && x.Hksj.Value >= startDate && x.Hksj.Value <= endDateTime)
4615 4820 .CountAsync();
... ... @@ -4620,16 +4825,11 @@ namespace NCC.Extend
4620 4825 .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date)
4621 4826 .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m;
4622 4827  
4623   - var refundCount = await _db.Queryable<LqHytkHytkEntity>()
  4828 + int refundCount = await _db.Queryable<LqHytkHytkEntity>()
4624 4829 .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
4625 4830 .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= startDate.Date && x.Tksj.Value.Date <= endDate.Date)
4626 4831 .CountAsync();
4627 4832  
4628   - // 2.4 目标业绩(从门店目标表获取)
4629   - var target = await _db.Queryable<LqMdTargetEntity>()
4630   - .Where(x => x.StoreId == input.StoreId && x.Month == statisticsMonth)
4631   - .FirstAsync();
4632   -
4633 4833 var targetPerformance = target?.StoreTarget ?? store.Xsyj ?? 0m;
4634 4834  
4635 4835 // 2.5 剩余权益总额
... ... @@ -4697,20 +4897,19 @@ namespace NCC.Extend
4697 4897 AND consume.Md = '{input.StoreId}'
4698 4898 AND DATE(consume.hksj) >= '{startDate:yyyy-MM-dd}'
4699 4899 AND DATE(consume.hksj) <= '{endDate:yyyy-MM-dd}'
4700   - GROUP BY consume.Md",
4701   - new { startDate, endDate });
  4900 + GROUP BY consume.Md");
4702 4901  
4703 4902 var operation = new OperationMetrics();
4704 4903 if (dailyStats != null && dailyStats.Any())
4705 4904 {
4706 4905 var stats = dailyStats.FirstOrDefault();
4707   - operation.HeadCount = Convert.ToInt32(Convert.ToDecimal(stats?.HeadCount ?? 0));
4708   - operation.PersonCount = Convert.ToInt32(Convert.ToDecimal(stats?.PersonCount ?? 0));
4709   - operation.ProjectCount = Convert.ToInt32(Convert.ToDecimal(stats?.ProjectCount ?? 0));
4710   - operation.TotalProjectCount = Convert.ToDecimal(stats?.TotalProjectCount ?? 0);
4711   - operation.OriginalProjectCount = Convert.ToDecimal(stats?.OriginalProjectCount ?? 0);
4712   - operation.OvertimeProjectCount = Convert.ToDecimal(stats?.OvertimeProjectCount ?? 0);
4713   - operation.AccompaniedProjectCount = Convert.ToDecimal(stats?.AccompaniedProjectCount ?? 0);
  4906 + operation.HeadCount = Convert.ToInt32(Convert.ToDecimal(stats?.HeadCount ?? 0m));
  4907 + operation.PersonCount = Convert.ToInt32(Convert.ToDecimal(stats?.PersonCount ?? 0m));
  4908 + operation.ProjectCount = Convert.ToInt32(Convert.ToDecimal(stats?.ProjectCount ?? 0m));
  4909 + operation.TotalProjectCount = Convert.ToDecimal(stats?.TotalProjectCount ?? 0m);
  4910 + operation.OriginalProjectCount = Convert.ToDecimal(stats?.OriginalProjectCount ?? 0m);
  4911 + operation.OvertimeProjectCount = Convert.ToDecimal(stats?.OvertimeProjectCount ?? 0m);
  4912 + operation.AccompaniedProjectCount = Convert.ToDecimal(stats?.AccompaniedProjectCount ?? 0m);
4714 4913 operation.AvgAmountPerPerson = operation.PersonCount > 0 ? consumeAmount / (decimal)operation.PersonCount : 0m;
4715 4914 operation.AvgAmountPerProject = operation.ProjectCount > 0 ? consumeAmount / (decimal)operation.ProjectCount : 0m;
4716 4915 operation.AvgProjectPerHead = operation.HeadCount > 0 ? (decimal)operation.ProjectCount / (decimal)operation.HeadCount : 0m;
... ... @@ -4730,8 +4929,7 @@ namespace NCC.Extend
4730 4929 FROM lq_khxx
4731 4930 WHERE F_IsEffective = 1
4732 4931 AND gsmd = '{input.StoreId}'
4733   - AND khlx = '3'",
4734   - new { statisticsMonth });
  4932 + AND khlx = '3'");
4735 4933  
4736 4934 var member = new MemberAnalysis();
4737 4935 if (memberStats != null && memberStats.Any())
... ... @@ -5052,7 +5250,7 @@ namespace NCC.Extend
5052 5250 GROUP BY kd.djmd, refund.RefundAmount
5053 5251 ORDER BY (COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) - COALESCE(refund.RefundAmount, 0)) DESC");
5054 5252  
5055   - var performanceRanking = 1;
  5253 + int performanceRanking = 1;
5056 5254 int totalStoreCount = 0;
5057 5255 if (allStoresPerformance != null)
5058 5256 {
... ... @@ -5074,7 +5272,7 @@ namespace NCC.Extend
5074 5272 .Select(x => x.Id)
5075 5273 .ToListAsync();
5076 5274  
5077   - var sameTypeStoreCount = sameTypeStores.Count;
  5275 + int sameTypeStoreCount = sameTypeStores.Count;
5078 5276 var avgPerformanceSameType = 0m;
5079 5277 if (sameTypeStoreCount > 0)
5080 5278 {
... ... @@ -5128,7 +5326,7 @@ namespace NCC.Extend
5128 5326 .ToListAsync();
5129 5327 }
5130 5328  
5131   - var sameOrgStoreCount = sameOrgStoreIds.Count;
  5329 + int sameOrgStoreCount = sameOrgStoreIds.Count;
5132 5330 var avgPerformanceSameOrg = 0m;
5133 5331 if (sameOrgStoreCount > 0)
5134 5332 {
... ... @@ -5818,41 +6016,84 @@ namespace NCC.Extend
5818 6016 throw NCCException.Oh("门店ID不能为空");
5819 6017 }
5820 6018  
  6019 + // 计算时间范围:近12个月
  6020 + var endDate = DateTime.Now;
  6021 + var startDate = endDate.AddMonths(-11);
  6022 + var startMonth = new DateTime(startDate.Year, startDate.Month, 1);
  6023 +
  6024 + // 使用参数化查询,一次性获取12个月的数据
  6025 + var parameters = new List<SugarParameter>
  6026 + {
  6027 + new SugarParameter("@StoreId", input.StoreId),
  6028 + new SugarParameter("@StartDate", startMonth),
  6029 + new SugarParameter("@EndDate", endDate)
  6030 + };
  6031 +
  6032 + // 优化:使用单个SQL查询获取所有月份的开单业绩
  6033 + var billingSql = @"
  6034 + SELECT
  6035 + DATE_FORMAT(kd.kdrq, '%Y%m') as Month,
  6036 + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as BillingPerformance
  6037 + FROM lq_kd_kdjlb kd
  6038 + WHERE kd.djmd = @StoreId
  6039 + AND kd.F_IsEffective = 1
  6040 + AND kd.kdrq >= @StartDate
  6041 + AND kd.kdrq <= @EndDate
  6042 + GROUP BY DATE_FORMAT(kd.kdrq, '%Y%m')
  6043 + ORDER BY Month";
  6044 +
  6045 + var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql, parameters);
  6046 + var billingDict = billingData.ToDictionary(x => x.Month?.ToString() ?? "", x => Convert.ToDecimal(x.BillingPerformance ?? 0m));
  6047 +
  6048 + // 优化:使用单个SQL查询获取所有月份的消耗业绩
  6049 + var consumeSql = @"
  6050 + SELECT
  6051 + DATE_FORMAT(xh.hksj, '%Y%m') as Month,
  6052 + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as ConsumePerformance
  6053 + FROM lq_xh_hyhk xh
  6054 + WHERE xh.md = @StoreId
  6055 + AND xh.F_IsEffective = 1
  6056 + AND xh.hksj >= @StartDate
  6057 + AND xh.hksj <= @EndDate
  6058 + GROUP BY DATE_FORMAT(xh.hksj, '%Y%m')
  6059 + ORDER BY Month";
  6060 +
  6061 + var consumeData = await _db.Ado.SqlQueryAsync<dynamic>(consumeSql, parameters);
  6062 + var consumeDict = consumeData.ToDictionary(x => x.Month?.ToString() ?? "", x => Convert.ToDecimal(x.ConsumePerformance ?? 0m));
  6063 +
  6064 + // 优化:使用单个SQL查询获取所有月份的退卡金额
  6065 + var refundSql = @"
  6066 + SELECT
  6067 + DATE_FORMAT(tk.tksj, '%Y%m') as Month,
  6068 + COALESCE(SUM(CAST(COALESCE(tk.F_ActualRefundAmount, tk.tkje, 0) AS DECIMAL(18,2))), 0) as RefundAmount
  6069 + FROM lq_hytk_hytk tk
  6070 + WHERE tk.md = @StoreId
  6071 + AND tk.F_IsEffective = 1
  6072 + AND tk.tksj >= @StartDate
  6073 + AND tk.tksj <= @EndDate
  6074 + GROUP BY DATE_FORMAT(tk.tksj, '%Y%m')
  6075 + ORDER BY Month";
  6076 +
  6077 + var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql, parameters);
  6078 + var refundDict = refundData.ToDictionary(x => x.Month?.ToString() ?? "", x => Convert.ToDecimal(x.RefundAmount ?? 0m));
  6079 +
  6080 + // 构建12个月的趋势数据
5821 6081 var monthlyTrend = new List<MonthlyTrendPoint>();
5822 6082 for (int i = 11; i >= 0; i--)
5823 6083 {
5824 6084 var trendMonth = DateTime.Now.AddMonths(-i);
5825 6085 var trendMonthStr = trendMonth.ToString("yyyyMM");
5826   - var trendStartDate = new DateTime(trendMonth.Year, trendMonth.Month, 1);
5827   - var trendEndDate = trendStartDate.AddMonths(1).AddDays(-1);
5828   - var trendEndDateTime = trendEndDate.Date.AddDays(1).AddSeconds(-1);
5829 6086  
5830   - if (trendMonthStr == DateTime.Now.ToString("yyyyMM"))
5831   - {
5832   - trendEndDateTime = DateTime.Now;
5833   - }
5834   -
5835   - var trendBilling = await _db.Queryable<LqKdKdjlbEntity>()
5836   - .Where(x => x.Djmd == input.StoreId && x.IsEffective == 1)
5837   - .Where(x => x.Kdrq.HasValue && x.Kdrq.Value >= trendStartDate && x.Kdrq.Value <= trendEndDateTime)
5838   - .SumAsync(x => (decimal?)x.Sfyj) ?? 0m;
5839   -
5840   - var trendConsume = await _db.Queryable<LqXhHyhkEntity>()
5841   - .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
5842   - .Where(x => x.Hksj.HasValue && x.Hksj.Value >= trendStartDate && x.Hksj.Value <= trendEndDateTime)
5843   - .SumAsync(x => (decimal?)x.Xfje) ?? 0m;
5844   -
5845   - var trendRefund = await _db.Queryable<LqHytkHytkEntity>()
5846   - .Where(x => x.Md == input.StoreId && x.IsEffective == 1)
5847   - .Where(x => x.Tksj.HasValue && x.Tksj.Value.Date >= trendStartDate.Date && x.Tksj.Value.Date <= trendEndDate.Date)
5848   - .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? (x.Tkje ?? 0))) ?? 0m;
  6087 + var billing = billingDict.GetValueOrDefault(trendMonthStr, 0m);
  6088 + var consume = consumeDict.GetValueOrDefault(trendMonthStr, 0m);
  6089 + var refund = refundDict.GetValueOrDefault(trendMonthStr, 0m);
5849 6090  
5850 6091 monthlyTrend.Add(new MonthlyTrendPoint
5851 6092 {
5852 6093 Month = trendMonthStr,
5853   - BillingPerformance = trendBilling,
5854   - ConsumePerformance = trendConsume,
5855   - NetPerformance = trendBilling - trendRefund
  6094 + BillingPerformance = billing,
  6095 + ConsumePerformance = consume,
  6096 + NetPerformance = billing - refund
5856 6097 });
5857 6098 }
5858 6099  
... ... @@ -5951,7 +6192,7 @@ namespace NCC.Extend
5951 6192 .Select(x => x.Id)
5952 6193 .ToListAsync();
5953 6194  
5954   - int sameTypeStoreCount = sameTypeStores != null ? sameTypeStores.Count : 0;
  6195 + var sameTypeStoreCount = sameTypeStores != null ? sameTypeStores.Count : 0;
5955 6196 var avgPerformanceSameType = 0m;
5956 6197 if (sameTypeStoreCount > 0)
5957 6198 {
... ... @@ -6005,7 +6246,7 @@ namespace NCC.Extend
6005 6246 .ToListAsync();
6006 6247 }
6007 6248  
6008   - int sameOrgStoreCount = sameOrgStoreIds != null ? sameOrgStoreIds.Count : 0;
  6249 + var sameOrgStoreCount = sameOrgStoreIds != null ? sameOrgStoreIds.Count : 0;
6009 6250 var avgPerformanceSameOrg = 0m;
6010 6251 if (sameOrgStoreCount > 0)
6011 6252 {
... ...
sql/优化仪表盘接口性能索引.sql 0 → 100644
  1 +-- ============================================
  2 +-- 优化 get-dashboard-data 接口性能的索引
  3 +-- ============================================
  4 +-- 说明:这些索引专门为仪表盘接口优化
  5 +-- 执行前请检查索引是否已存在,避免重复创建
  6 +
  7 +-- ============================================
  8 +-- 1. lq_kd_kdjlb (开单记录表) 索引
  9 +-- ============================================
  10 +-- 用于:门店业绩汇总、健康师业绩汇总、最高开单金额会员查询
  11 +-- 查询条件:F_IsEffective, kdrq (时间范围), djmd (门店ID)
  12 +
  13 +-- 索引:用于时间范围查询和门店统计(如果已存在 idx_kd_kdjlb_store_date_effective 可跳过)
  14 +-- CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_store_date_effective
  15 +-- ON lq_kd_kdjlb(djmd, kdrq, F_IsEffective);
  16 +
  17 +-- ============================================
  18 +-- 2. lq_hytk_hytk (退卡表) 索引
  19 +-- ============================================
  20 +-- 用于:退卡金额统计
  21 +-- 查询条件:F_IsEffective, tksj (退卡时间)
  22 +
  23 +-- 索引:用于时间范围查询
  24 +CREATE INDEX IF NOT EXISTS idx_hytk_effective_date
  25 +ON lq_hytk_hytk(F_IsEffective, tksj);
  26 +
  27 +-- ============================================
  28 +-- 3. lq_xh_hyhk (耗卡记录表) 索引
  29 +-- ============================================
  30 +-- 用于:消耗业绩汇总、最高消耗金额会员查询
  31 +-- 查询条件:F_IsEffective, hksj (耗卡时间), hy (会员ID)
  32 +
  33 +-- 索引1:用于时间范围查询
  34 +CREATE INDEX IF NOT EXISTS idx_xh_hyhk_effective_date
  35 +ON lq_xh_hyhk(F_IsEffective, hksj);
  36 +
  37 +-- 索引2:用于会员统计(补充)
  38 +CREATE INDEX IF NOT EXISTS idx_xh_hyhk_member_effective
  39 +ON lq_xh_hyhk(hy, F_IsEffective, hksj);
  40 +
  41 +-- ============================================
  42 +-- 4. lq_xh_pxmx (耗卡品项明细表) 索引
  43 +-- ============================================
  44 +-- 用于:消耗业绩汇总(JOIN)
  45 +-- 查询条件:F_ConsumeInfoId, F_IsEffective
  46 +
  47 +-- 索引:用于JOIN(如果已存在可跳过)
  48 +-- CREATE INDEX IF NOT EXISTS idx_xh_pxmx_consume_effective
  49 +-- ON lq_xh_pxmx(F_ConsumeInfoId, F_IsEffective);
  50 +
  51 +-- ============================================
  52 +-- 5. lq_khxx (客户信息表) 索引
  53 +-- ============================================
  54 +-- 用于:会员统计汇总、会员类型分布、最高剩余权益会员
  55 +-- 查询条件:F_IsEffective, khlx (客户类型), F_CreateTime, F_SleepDays, F_RemainingRightsAmount
  56 +
  57 +-- 索引1:用于会员类型和有效性过滤(最常用)
  58 +CREATE INDEX IF NOT EXISTS idx_khxx_type_effective
  59 +ON lq_khxx(khlx, F_IsEffective);
  60 +
  61 +-- 索引2:用于创建时间统计(新会员统计)
  62 +CREATE INDEX IF NOT EXISTS idx_khxx_create_time
  63 +ON lq_khxx(F_CreateTime, F_IsEffective, khlx);
  64 +
  65 +-- 索引3:用于剩余权益排序(最高剩余权益会员)
  66 +CREATE INDEX IF NOT EXISTS idx_khxx_remaining_effective
  67 +ON lq_khxx(F_RemainingRightsAmount, F_IsEffective);
  68 +
  69 +-- ============================================
  70 +-- 6. 验证索引创建
  71 +-- ============================================
  72 +-- 验证索引是否创建成功
  73 +-- SELECT
  74 +-- TABLE_NAME,
  75 +-- INDEX_NAME,
  76 +-- COLUMN_NAME,
  77 +-- SEQ_IN_INDEX
  78 +-- FROM INFORMATION_SCHEMA.STATISTICS
  79 +-- WHERE TABLE_SCHEMA = DATABASE()
  80 +-- AND TABLE_NAME IN ('lq_kd_kdjlb', 'lq_hytk_hytk', 'lq_xh_hyhk', 'lq_xh_pxmx', 'lq_khxx')
  81 +-- AND INDEX_NAME LIKE 'idx_%'
  82 +-- ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
  83 +
... ...
sql/优化金三角业绩排行榜接口性能索引.sql 0 → 100644
  1 +-- ============================================
  2 +-- 优化 get-gold-triangle-performance-ranking 接口性能的索引
  3 +-- ============================================
  4 +-- 说明:这些索引专门为金三角业绩排行榜接口优化
  5 +-- 执行前请检查索引是否已存在,避免重复创建
  6 +
  7 +-- ============================================
  8 +-- 1. lq_ycsd_jsj (金三角设定表) 索引
  9 +-- ============================================
  10 +-- 用于:WHERE jsj.yf = @StatisticsMonth 查询条件
  11 +-- 查询条件:yf (月份)
  12 +
  13 +-- 索引:用于月份查询(最常用)
  14 +CREATE INDEX IF NOT EXISTS idx_ycsd_jsj_yf
  15 +ON lq_ycsd_jsj(yf);
  16 +
  17 +-- ============================================
  18 +-- 2. lq_jinsanjiao_user (金三角用户绑定表) 索引
  19 +-- ============================================
  20 +-- 用于:JOIN和过滤条件
  21 +-- 查询条件:status = 'ACTIVE', F_DeleteMark = 0, user_id (用于JOIN)
  22 +
  23 +-- 索引1:用于JOIN和状态过滤(最常用)
  24 +CREATE INDEX IF NOT EXISTS idx_jinsanjiao_user_user_status_delete
  25 +ON lq_jinsanjiao_user(user_id, status, F_DeleteMark);
  26 +
  27 +-- 索引2:用于反向JOIN(jsj_id用于关联金三角设定)
  28 +CREATE INDEX IF NOT EXISTS idx_jinsanjiao_user_jsj_status_delete
  29 +ON lq_jinsanjiao_user(jsj_id, status, F_DeleteMark);
  30 +
  31 +-- ============================================
  32 +-- 3. lq_kd_jksyj (开单健康师业绩表) 索引
  33 +-- ============================================
  34 +-- 用于:JOIN和过滤条件
  35 +-- 查询条件:jkszh (用于JOIN), F_IsEffective = 1
  36 +
  37 +-- 索引:用于JOIN和有效性过滤
  38 +CREATE INDEX IF NOT EXISTS idx_kd_jksyj_jkszh_effective
  39 +ON lq_kd_jksyj(jkszh, F_IsEffective);
  40 +
  41 +-- ============================================
  42 +-- 4. lq_kd_kdjlb (开单记录表) 索引
  43 +-- ============================================
  44 +-- 用于:JOIN和过滤条件
  45 +-- 查询条件:F_Id (用于JOIN), F_IsEffective = 1, kdrq (时间范围)
  46 +
  47 +-- 索引:用于JOIN、有效性过滤和时间范围查询
  48 +-- 注意:如果 idx_kd_kdjlb_store_date_effective 已存在,此索引可能冗余
  49 +-- 但为了JOIN性能,保留此索引
  50 +CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_id_effective_date
  51 +ON lq_kd_kdjlb(F_Id, F_IsEffective, kdrq);
  52 +
  53 +-- ============================================
  54 +-- 5. 验证索引创建
  55 +-- ============================================
  56 +-- 验证索引是否创建成功
  57 +-- SELECT
  58 +-- TABLE_NAME,
  59 +-- INDEX_NAME,
  60 +-- COLUMN_NAME,
  61 +-- SEQ_IN_INDEX
  62 +-- FROM INFORMATION_SCHEMA.STATISTICS
  63 +-- WHERE TABLE_SCHEMA = DATABASE()
  64 +-- AND TABLE_NAME IN ('lq_ycsd_jsj', 'lq_jinsanjiao_user', 'lq_kd_jksyj', 'lq_kd_kdjlb')
  65 +-- AND INDEX_NAME LIKE 'idx_%'
  66 +-- ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
  67 +
... ...
sql/优化门店月度趋势接口性能索引.sql 0 → 100644
  1 +-- ============================================
  2 +-- 优化 get-store-monthly-trend 接口性能的索引
  3 +-- ============================================
  4 +-- 说明:这些索引专门为门店月度趋势接口优化
  5 +-- 执行前请检查索引是否已存在,避免重复创建
  6 +
  7 +-- ============================================
  8 +-- 1. lq_kd_kdjlb (开单记录表) 索引
  9 +-- ============================================
  10 +-- 用于:开单业绩按月统计
  11 +-- 查询条件:djmd (门店ID), F_IsEffective, kdrq (开单日期)
  12 +-- 分组:DATE_FORMAT(kdrq, '%Y%m')
  13 +
  14 +-- 索引:用于门店+时间范围查询(如果已存在 idx_kd_kdjlb_store_date_effective 可跳过)
  15 +-- CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_store_date_effective
  16 +-- ON lq_kd_kdjlb(djmd, kdrq, F_IsEffective);
  17 +
  18 +-- 补充索引:用于月份分组查询(优化GROUP BY性能)
  19 +CREATE INDEX IF NOT EXISTS idx_kd_kdjlb_store_date_month
  20 +ON lq_kd_kdjlb(djmd, F_IsEffective, kdrq);
  21 +
  22 +-- ============================================
  23 +-- 2. lq_xh_hyhk (耗卡记录表) 索引
  24 +-- ============================================
  25 +-- 用于:消耗业绩按月统计
  26 +-- 查询条件:md (门店ID), F_IsEffective, hksj (耗卡时间)
  27 +-- 分组:DATE_FORMAT(hksj, '%Y%m')
  28 +
  29 +-- 索引:用于门店+时间范围查询
  30 +CREATE INDEX IF NOT EXISTS idx_xh_hyhk_store_date_month
  31 +ON lq_xh_hyhk(md, F_IsEffective, hksj);
  32 +
  33 +-- ============================================
  34 +-- 3. lq_hytk_hytk (退卡表) 索引
  35 +-- ============================================
  36 +-- 用于:退卡金额按月统计
  37 +-- 查询条件:md (门店ID), F_IsEffective, tksj (退卡时间)
  38 +-- 分组:DATE_FORMAT(tksj, '%Y%m')
  39 +
  40 +-- 索引:用于门店+时间范围查询(如果已存在 idx_hytk_effective_date 可跳过)
  41 +-- CREATE INDEX IF NOT EXISTS idx_hytk_effective_date
  42 +-- ON lq_hytk_hytk(F_IsEffective, tksj);
  43 +
  44 +-- 补充索引:用于门店+时间范围查询(优化门店维度查询)
  45 +CREATE INDEX IF NOT EXISTS idx_hytk_store_date_month
  46 +ON lq_hytk_hytk(md, F_IsEffective, tksj);
  47 +
  48 +-- ============================================
  49 +-- 4. 验证索引创建
  50 +-- ============================================
  51 +-- 验证索引是否创建成功
  52 +-- SELECT
  53 +-- TABLE_NAME,
  54 +-- INDEX_NAME,
  55 +-- COLUMN_NAME,
  56 +-- SEQ_IN_INDEX
  57 +-- FROM INFORMATION_SCHEMA.STATISTICS
  58 +-- WHERE TABLE_SCHEMA = DATABASE()
  59 +-- AND TABLE_NAME IN ('lq_kd_kdjlb', 'lq_xh_hyhk', 'lq_hytk_hytk')
  60 +-- AND INDEX_NAME LIKE 'idx_%'
  61 +-- ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
  62 +
... ...