Commit 7d89becc85df33f45b7d8a245c2c9f82dec97ece

Authored by “wangming”
1 parent 297c68d7

feat: 退款明细列表优化 - 支持显示多个健康师及业绩

- 修改退款明细列表查询逻辑,从退款健康师业绩表查询(而非开单健康师业绩表)
- 支持一个退款明细显示多个健康师,格式:姓名(业绩)、姓名(业绩)
- 新增实际退款金额字段,显示所有健康师业绩之和
- 优化前端显示,健康师列显示每个健康师及其业绩
- 添加测试脚本test_refund_detail.sh
antis-ncc-admin/src/views/statisticsList/form21.vue
... ... @@ -270,10 +270,10 @@
270 270 </el-tag>
271 271 </template>
272 272 </el-table-column>
273   - <el-table-column prop="healthCoachName" label="健康师" min-width="120" sortable="custom">
  273 + <el-table-column prop="healthCoachName" label="健康师及业绩" min-width="200" sortable="custom">
274 274 <template slot-scope="scope">
275 275 <i class="el-icon-user-solid" style="margin-right: 4px; color: #F56C6C;"></i>
276   - <span>{{ scope.row.healthCoachName || '无' }}</span>
  276 + <span :title="scope.row.healthCoachName || '无'" style="white-space: normal; word-break: break-all;">{{ scope.row.healthCoachName || '无' }}</span>
277 277 </template>
278 278 </el-table-column>
279 279 <el-table-column prop="healthCoachPerformance" label="健康师业绩" min-width="130" sortable="custom">
... ... @@ -281,6 +281,12 @@
281 281 <span class="amount-value">¥{{ formatMoney(scope.row.healthCoachPerformance) }}</span>
282 282 </template>
283 283 </el-table-column>
  284 + <el-table-column prop="actualRefundAmount" label="实际退款金额" min-width="140" sortable="custom">
  285 + <template slot-scope="scope">
  286 + <i class="el-icon-money" style="margin-right: 4px; color: #67C23A;"></i>
  287 + <span class="amount-value">¥{{ formatMoney(scope.row.actualRefundAmount) }}</span>
  288 + </template>
  289 + </el-table-column>
284 290 </NCC-table>
285 291 <pagination
286 292 v-show="total > 0"
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqHytkHytk/RefundDetailListOutput.cs
... ... @@ -83,19 +83,24 @@ namespace NCC.Extend.Entitys.Dto.LqHytkHytk
83 83 public string itemCategory { get; set; }
84 84  
85 85 /// <summary>
86   - /// 健康师账号
  86 + /// 健康师账号(保留字段,兼容旧版本,显示第一个健康师)
87 87 /// </summary>
88 88 public string healthCoachId { get; set; }
89 89  
90 90 /// <summary>
91   - /// 健康师姓名
  91 + /// 健康师姓名(多个健康师用顿号分隔)
92 92 /// </summary>
93 93 public string healthCoachName { get; set; }
94 94  
95 95 /// <summary>
96   - /// 健康师业绩
  96 + /// 健康师业绩(保留字段,兼容旧版本,显示第一个健康师业绩)
97 97 /// </summary>
98 98 public decimal healthCoachPerformance { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 实际退款金额(所有健康师业绩之和)
  102 + /// </summary>
  103 + public decimal actualRefundAmount { get; set; }
99 104 }
100 105 }
101 106  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs
... ... @@ -916,39 +916,36 @@ namespace NCC.Extend.LqHytkHytk
916 916 memberDict = members.ToDictionary(x => x.Id, x => x.Sjh ?? "");
917 917 }
918 918  
919   - // 5. 批量查询健康师业绩信息
920   - var billingItemIds = mxList.Select(x => x.BillingItemId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
921   - var healthCoachDict = new Dictionary<string, (string Id, string Name, decimal Performance)>();
922   - if (billingItemIds.Any())
  919 + // 5. 批量查询退款健康师业绩信息(从退款健康师业绩表查询,而不是开单健康师业绩表)
  920 + var mxIds = mxList.Select(x => x.Id).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  921 + // 使用字典存储每个退款明细对应的多个健康师信息
  922 + var healthCoachDict = new Dictionary<string, List<(string Id, string Name, decimal Performance)>>();
  923 + var actualRefundAmountDict = new Dictionary<string, decimal>();
  924 +
  925 + if (mxIds.Any())
923 926 {
924   - // 通过开单品项明细ID查询开单品项明细,获取开单编号和品项明细ID
925   - var pxmxList = await _db.Queryable<LqKdPxmxEntity>()
926   - .Where(x => billingItemIds.Contains(x.Id))
927   - .Select(x => new { x.Id, x.Glkdbh })
  927 + // 通过退卡品相表ID(F_CardReturn)查询退款健康师业绩
  928 + var jksyjList = await _db.Queryable<LqHytkJksyjEntity>()
  929 + .Where(x => mxIds.Contains(x.CardReturn) && x.IsEffective == StatusEnum.有效.GetHashCode())
  930 + .Select(x => new { x.CardReturn, x.Jkszh, x.Jksxm, x.Jksyj })
928 931 .ToListAsync();
929 932  
930   - if (pxmxList.Any())
  933 + // 按退款明细ID分组,建立映射
  934 + foreach (var jksyj in jksyjList)
931 935 {
932   - var glkdbhList = pxmxList.Select(x => x.Glkdbh).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
933   - var pxmxIdList = pxmxList.Select(x => x.Id).Distinct().ToList();
  936 + if (string.IsNullOrEmpty(jksyj.CardReturn))
  937 + continue;
934 938  
935   - // 通过开单编号和品项明细ID查询健康师业绩
936   - var jksyjList = await _db.Queryable<LqKdJksyjEntity>()
937   - .Where(x => glkdbhList.Contains(x.Glkdbh) && pxmxIdList.Contains(x.Kdpxid) && x.IsEffective == 1)
938   - .Select(x => new { x.Glkdbh, x.Kdpxid, x.Jkszh, x.Jksxm, x.Jksyj })
939   - .ToListAsync();
  939 + var jksyjValue = jksyj.Jksyj ?? 0m;
940 940  
941   - // 建立开单品项明细ID到健康师信息的映射
942   - foreach (var jksyj in jksyjList)
  941 + if (!healthCoachDict.ContainsKey(jksyj.CardReturn))
943 942 {
944   - // 找到对应的开单品项明细ID
945   - var pxmxId = pxmxList.FirstOrDefault(x => x.Glkdbh == jksyj.Glkdbh && x.Id == jksyj.Kdpxid)?.Id;
946   - if (!string.IsNullOrEmpty(pxmxId))
947   - {
948   - var jksyjValue = decimal.TryParse(jksyj.Jksyj, out var perf) ? perf : 0;
949   - healthCoachDict[pxmxId] = (jksyj.Jkszh ?? "", jksyj.Jksxm ?? "", jksyjValue);
950   - }
  943 + healthCoachDict[jksyj.CardReturn] = new List<(string Id, string Name, decimal Performance)>();
  944 + actualRefundAmountDict[jksyj.CardReturn] = 0m;
951 945 }
  946 +
  947 + healthCoachDict[jksyj.CardReturn].Add((jksyj.Jkszh ?? "", jksyj.Jksxm ?? "", jksyjValue));
  948 + actualRefundAmountDict[jksyj.CardReturn] += jksyjValue;
952 949 }
953 950 }
954 951  
... ... @@ -957,7 +954,7 @@ namespace NCC.Extend.LqHytkHytk
957 954 foreach (var mx in mxList)
958 955 {
959 956 var refundInfo = refundInfoDict.ContainsKey(mx.RefundInfoId) ? refundInfoDict[mx.RefundInfoId] : null;
960   -
  957 +
961 958 // 应用筛选条件
962 959 if (input.storeIds != null && input.storeIds.Any())
963 960 {
... ... @@ -973,10 +970,29 @@ namespace NCC.Extend.LqHytkHytk
973 970 var memberId = mx.MemberId ?? (refundInfo?.Hy);
974 971 var refundTime = mx.Tksj ?? refundInfo?.Tksj;
975 972  
976   - // 获取健康师信息
977   - var healthCoachInfo = !string.IsNullOrEmpty(mx.BillingItemId) && healthCoachDict.ContainsKey(mx.BillingItemId)
978   - ? healthCoachDict[mx.BillingItemId]
979   - : (Id: "", Name: "", Performance: 0m);
  973 + // 获取健康师信息(支持多个健康师)
  974 + var healthCoachList = healthCoachDict.ContainsKey(mx.Id)
  975 + ? healthCoachDict[mx.Id]
  976 + : new List<(string Id, string Name, decimal Performance)>();
  977 +
  978 + // 合并多个健康师姓名和业绩(格式:姓名(业绩),用顿号分隔)
  979 + var healthCoachNames = "";
  980 + if (healthCoachList.Any())
  981 + {
  982 + var healthCoachItems = healthCoachList
  983 + .Where(h => !string.IsNullOrEmpty(h.Name))
  984 + .Select(h => $"{h.Name}({h.Performance:F2})")
  985 + .ToList();
  986 + healthCoachNames = string.Join("、", healthCoachItems);
  987 + }
  988 +
  989 + // 获取第一个健康师信息(兼容旧版本)
  990 + var firstHealthCoach = healthCoachList.FirstOrDefault();
  991 +
  992 + // 获取实际退款金额(所有健康师业绩之和)
  993 + var actualRefundAmount = actualRefundAmountDict.ContainsKey(mx.Id)
  994 + ? actualRefundAmountDict[mx.Id]
  995 + : 0m;
980 996  
981 997 resultList.Add(new RefundDetailListOutput
982 998 {
... ... @@ -995,16 +1011,17 @@ namespace NCC.Extend.LqHytkHytk
995 1011 beautyType = mx.BeautyType,
996 1012 sourceType = mx.SourceType,
997 1013 itemCategory = mx.ItemCategory,
998   - healthCoachId = healthCoachInfo.Id,
999   - healthCoachName = healthCoachInfo.Name,
1000   - healthCoachPerformance = healthCoachInfo.Performance
  1014 + healthCoachId = firstHealthCoach.Id,
  1015 + healthCoachName = healthCoachNames,
  1016 + healthCoachPerformance = firstHealthCoach.Performance,
  1017 + actualRefundAmount = actualRefundAmount
1001 1018 });
1002 1019 }
1003 1020  
1004 1021 // 7. 排序
1005 1022 var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
1006 1023 var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
1007   -
  1024 +
1008 1025 if (sort.ToLower() == "desc")
1009 1026 {
1010 1027 resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
... ... @@ -1114,39 +1131,36 @@ namespace NCC.Extend.LqHytkHytk
1114 1131 memberDict = members.ToDictionary(x => x.Id, x => x.Sjh ?? "");
1115 1132 }
1116 1133  
1117   - // 5. 批量查询健康师业绩信息
1118   - var billingItemIds = mxList.Select(x => x.BillingItemId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
1119   - var healthCoachDict = new Dictionary<string, (string Id, string Name, decimal Performance)>();
1120   - if (billingItemIds.Any())
  1134 + // 5. 批量查询退款健康师业绩信息(从退款健康师业绩表查询,而不是开单健康师业绩表)
  1135 + var mxIds = mxList.Select(x => x.Id).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  1136 + // 使用字典存储每个退款明细对应的多个健康师信息
  1137 + var healthCoachDict = new Dictionary<string, List<(string Id, string Name, decimal Performance)>>();
  1138 + var actualRefundAmountDict = new Dictionary<string, decimal>();
  1139 +
  1140 + if (mxIds.Any())
1121 1141 {
1122   - // 通过开单品项明细ID查询开单品项明细,获取开单编号和品项明细ID
1123   - var pxmxList = await _db.Queryable<LqKdPxmxEntity>()
1124   - .Where(x => billingItemIds.Contains(x.Id))
1125   - .Select(x => new { x.Id, x.Glkdbh })
  1142 + // 通过退卡品相表ID(F_CardReturn)查询退款健康师业绩
  1143 + var jksyjList = await _db.Queryable<LqHytkJksyjEntity>()
  1144 + .Where(x => mxIds.Contains(x.CardReturn) && x.IsEffective == StatusEnum.有效.GetHashCode())
  1145 + .Select(x => new { x.CardReturn, x.Jkszh, x.Jksxm, x.Jksyj })
1126 1146 .ToListAsync();
1127 1147  
1128   - if (pxmxList.Any())
  1148 + // 按退款明细ID分组,建立映射
  1149 + foreach (var jksyj in jksyjList)
1129 1150 {
1130   - var glkdbhList = pxmxList.Select(x => x.Glkdbh).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
1131   - var pxmxIdList = pxmxList.Select(x => x.Id).Distinct().ToList();
  1151 + if (string.IsNullOrEmpty(jksyj.CardReturn))
  1152 + continue;
1132 1153  
1133   - // 通过开单编号和品项明细ID查询健康师业绩
1134   - var jksyjList = await _db.Queryable<LqKdJksyjEntity>()
1135   - .Where(x => glkdbhList.Contains(x.Glkdbh) && pxmxIdList.Contains(x.Kdpxid) && x.IsEffective == 1)
1136   - .Select(x => new { x.Glkdbh, x.Kdpxid, x.Jkszh, x.Jksxm, x.Jksyj })
1137   - .ToListAsync();
  1154 + var jksyjValue = jksyj.Jksyj ?? 0m;
1138 1155  
1139   - // 建立开单品项明细ID到健康师信息的映射
1140   - foreach (var jksyj in jksyjList)
  1156 + if (!healthCoachDict.ContainsKey(jksyj.CardReturn))
1141 1157 {
1142   - // 找到对应的开单品项明细ID
1143   - var pxmxId = pxmxList.FirstOrDefault(x => x.Glkdbh == jksyj.Glkdbh && x.Id == jksyj.Kdpxid)?.Id;
1144   - if (!string.IsNullOrEmpty(pxmxId))
1145   - {
1146   - var jksyjValue = decimal.TryParse(jksyj.Jksyj, out var perf) ? perf : 0;
1147   - healthCoachDict[pxmxId] = (jksyj.Jkszh ?? "", jksyj.Jksxm ?? "", jksyjValue);
1148   - }
  1158 + healthCoachDict[jksyj.CardReturn] = new List<(string Id, string Name, decimal Performance)>();
  1159 + actualRefundAmountDict[jksyj.CardReturn] = 0m;
1149 1160 }
  1161 +
  1162 + healthCoachDict[jksyj.CardReturn].Add((jksyj.Jkszh ?? "", jksyj.Jksxm ?? "", jksyjValue));
  1163 + actualRefundAmountDict[jksyj.CardReturn] += jksyjValue;
1150 1164 }
1151 1165 }
1152 1166  
... ... @@ -1155,7 +1169,7 @@ namespace NCC.Extend.LqHytkHytk
1155 1169 foreach (var mx in mxList)
1156 1170 {
1157 1171 var refundInfo = refundInfoDict.ContainsKey(mx.RefundInfoId) ? refundInfoDict[mx.RefundInfoId] : null;
1158   -
  1172 +
1159 1173 // 应用筛选条件
1160 1174 if (input.storeIds != null && input.storeIds.Any())
1161 1175 {
... ... @@ -1171,10 +1185,29 @@ namespace NCC.Extend.LqHytkHytk
1171 1185 var memberId = mx.MemberId ?? (refundInfo?.Hy);
1172 1186 var refundTime = mx.Tksj ?? refundInfo?.Tksj;
1173 1187  
1174   - // 获取健康师信息
1175   - var healthCoachInfo = !string.IsNullOrEmpty(mx.BillingItemId) && healthCoachDict.ContainsKey(mx.BillingItemId)
1176   - ? healthCoachDict[mx.BillingItemId]
1177   - : (Id: "", Name: "", Performance: 0m);
  1188 + // 获取健康师信息(支持多个健康师)
  1189 + var healthCoachList = healthCoachDict.ContainsKey(mx.Id)
  1190 + ? healthCoachDict[mx.Id]
  1191 + : new List<(string Id, string Name, decimal Performance)>();
  1192 +
  1193 + // 合并多个健康师姓名和业绩(格式:姓名(业绩),用顿号分隔)
  1194 + var healthCoachNames = "";
  1195 + if (healthCoachList.Any())
  1196 + {
  1197 + var healthCoachItems = healthCoachList
  1198 + .Where(h => !string.IsNullOrEmpty(h.Name))
  1199 + .Select(h => $"{h.Name}({h.Performance:F2})")
  1200 + .ToList();
  1201 + healthCoachNames = string.Join("、", healthCoachItems);
  1202 + }
  1203 +
  1204 + // 获取第一个健康师信息(兼容旧版本)
  1205 + var firstHealthCoach = healthCoachList.FirstOrDefault();
  1206 +
  1207 + // 获取实际退款金额(所有健康师业绩之和)
  1208 + var actualRefundAmount = actualRefundAmountDict.ContainsKey(mx.Id)
  1209 + ? actualRefundAmountDict[mx.Id]
  1210 + : 0m;
1178 1211  
1179 1212 resultList.Add(new RefundDetailListOutput
1180 1213 {
... ... @@ -1193,16 +1226,17 @@ namespace NCC.Extend.LqHytkHytk
1193 1226 beautyType = mx.BeautyType,
1194 1227 sourceType = mx.SourceType,
1195 1228 itemCategory = mx.ItemCategory,
1196   - healthCoachId = healthCoachInfo.Id,
1197   - healthCoachName = healthCoachInfo.Name,
1198   - healthCoachPerformance = healthCoachInfo.Performance
  1229 + healthCoachId = firstHealthCoach.Id,
  1230 + healthCoachName = healthCoachNames,
  1231 + healthCoachPerformance = firstHealthCoach.Performance,
  1232 + actualRefundAmount = actualRefundAmount
1199 1233 });
1200 1234 }
1201 1235  
1202 1236 // 7. 排序
1203 1237 var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
1204 1238 var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
1205   -
  1239 +
1206 1240 if (sort.ToLower() == "desc")
1207 1241 {
1208 1242 resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
... ... @@ -1264,7 +1298,7 @@ namespace NCC.Extend.LqHytkHytk
1264 1298  
1265 1299 // 定义导出字段
1266 1300 List<ParamsModel> paramList =
1267   - "[{\"value\":\"门店\",\"field\":\"storeName\"},{\"value\":\"会员ID\",\"field\":\"memberId\"},{\"value\":\"会员名称\",\"field\":\"memberName\"},{\"value\":\"会员电话\",\"field\":\"memberPhone\"},{\"value\":\"退卡时间\",\"field\":\"refundTime\"},{\"value\":\"退卡品项ID\",\"field\":\"itemId\"},{\"value\":\"退卡品项名称\",\"field\":\"itemName\"},{\"value\":\"退卡数量\",\"field\":\"refundQuantity\"},{\"value\":\"单价\",\"field\":\"unitPrice\"},{\"value\":\"总价\",\"field\":\"totalPrice\"},{\"value\":\"业绩类型\",\"field\":\"performanceType\"},{\"value\":\"科美类型\",\"field\":\"beautyType\"},{\"value\":\"来源类型\",\"field\":\"sourceType\"},{\"value\":\"品项分类\",\"field\":\"itemCategory\"},]".ToList<ParamsModel>();
  1301 + "[{\"value\":\"门店\",\"field\":\"storeName\"},{\"value\":\"会员ID\",\"field\":\"memberId\"},{\"value\":\"会员名称\",\"field\":\"memberName\"},{\"value\":\"会员电话\",\"field\":\"memberPhone\"},{\"value\":\"退卡时间\",\"field\":\"refundTime\"},{\"value\":\"退卡品项ID\",\"field\":\"itemId\"},{\"value\":\"退卡品项名称\",\"field\":\"itemName\"},{\"value\":\"退卡数量\",\"field\":\"refundQuantity\"},{\"value\":\"单价\",\"field\":\"unitPrice\"},{\"value\":\"总价\",\"field\":\"totalPrice\"},{\"value\":\"业绩类型\",\"field\":\"performanceType\"},{\"value\":\"科美类型\",\"field\":\"beautyType\"},{\"value\":\"来源类型\",\"field\":\"sourceType\"},{\"value\":\"品项分类\",\"field\":\"itemCategory\"},{\"value\":\"健康师\",\"field\":\"healthCoachName\"},{\"value\":\"健康师业绩\",\"field\":\"healthCoachPerformance\"},]".ToList<ParamsModel>();
1268 1302  
1269 1303 ExcelConfig excelconfig = new ExcelConfig();
1270 1304 excelconfig.FileName = "退卡明细_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xls";
... ...
test_refund_detail.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试退款明细列表接口
  4 +
  5 +echo "=== 测试退款明细列表接口 ==="
  6 +echo ""
  7 +
  8 +# 步骤1: 获取Token
  9 +echo "步骤1: 获取Token..."
  10 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  11 + -H "Content-Type: application/x-www-form-urlencoded" \
  12 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  13 +
  14 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ Token获取失败"
  18 + exit 1
  19 +fi
  20 +
  21 +echo "✅ Token获取成功"
  22 +echo ""
  23 +
  24 +# 步骤2: 调用退款明细列表接口(2025年11月数据)
  25 +echo "步骤2: 调用退款明细列表接口(2025年11月数据)..."
  26 +echo "接口: POST /api/Extend/LqHytkHytk/refund-detail-list"
  27 +
  28 +RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqHytkHytk/refund-detail-list" \
  29 + -H "Authorization: Bearer $TOKEN" \
  30 + -H "Content-Type: application/json" \
  31 + -d '{
  32 + "currentPage": 1,
  33 + "pageSize": 5,
  34 + "startTime": "2025-11-01T00:00:00",
  35 + "endTime": "2025-11-30T23:59:59"
  36 + }')
  37 +
  38 +echo "$RESPONSE" | python3 -c "
  39 +import sys, json
  40 +try:
  41 + data = json.load(sys.stdin)
  42 + print('状态码:', data.get('code'))
  43 + print('消息:', data.get('msg'))
  44 + items = data.get('data', {}).get('list', [])
  45 + print('返回数据条数:', len(items))
  46 + if items:
  47 + print('\\n前3条数据详情:')
  48 + for i, item in enumerate(items[:3], 1):
  49 + print(f'\\n第{i}条:')
  50 + print(f' 门店名称: {item.get(\"storeName\", \"N/A\")}')
  51 + print(f' 会员名称: {item.get(\"memberName\", \"N/A\")}')
  52 + print(f' 品项名称: {item.get(\"itemName\", \"N/A\")}')
  53 + print(f' 健康师姓名: {item.get(\"healthCoachName\", \"N/A\")}')
  54 + print(f' 健康师业绩: {item.get(\"healthCoachPerformance\", \"N/A\")}')
  55 + print(f' 实际退款金额: {item.get(\"actualRefundAmount\", \"N/A\")}')
  56 +except Exception as e:
  57 + print('解析错误:', str(e))
  58 + print('原始响应:')
  59 + print(sys.stdin.read())
  60 +" 2>/dev/null
  61 +
  62 +echo ""
  63 +echo "=== 测试完成 ==="
  64 +
... ...