Commit d64f9fbdca5a77c3e94b21166fff22e0991fb268

Authored by “wangming”
1 parent b1146e00

feat: 添加退卡明细查询和导出功能

- 新增退卡明细查询接口
  - 支持多门店、会员、时间范围、品项、业绩类型、科美类型、来源类型、品项分类等筛选
  - 返回字段:门店、会员信息、退卡时间、品项信息、金额、类型等
  - 支持分页和排序

- 新增退卡明细导出接口
  - 导出Excel文件,包含所有筛选条件
  - 文件保存到项目根目录ExportFiles文件夹

- 优化退卡穿透统计接口
  - 所有金额字段改为使用实退金额(F_ActualRefundAmount)
  - 包括门店分布、总计、转卡总计、金额最大/次数最多的人等统计

- 优化客户资料导出接口
  - 添加消费等级、开单总金额、剩余权益总金额字段
  - 添加首次到店时间、最后到店时间、到店天数、沉睡天数字段
  - 消费等级显示为名称(D/C/B/A/A+/A++)
ExportFiles/退卡明细_20251228193529.xls 0 → 100644
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqHytkHytk/RefundDetailListOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqHytkHytk
  4 +{
  5 + /// <summary>
  6 + /// 退卡明细列表输出参数
  7 + /// </summary>
  8 + public class RefundDetailListOutput
  9 + {
  10 + /// <summary>
  11 + /// 门店ID
  12 + /// </summary>
  13 + public string storeId { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 门店名称
  17 + /// </summary>
  18 + public string storeName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 会员ID
  22 + /// </summary>
  23 + public string memberId { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 会员名称
  27 + /// </summary>
  28 + public string memberName { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 会员电话
  32 + /// </summary>
  33 + public string memberPhone { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 退卡时间
  37 + /// </summary>
  38 + public DateTime? refundTime { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 退卡品项ID
  42 + /// </summary>
  43 + public string itemId { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 退卡品项名称
  47 + /// </summary>
  48 + public string itemName { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 退卡数量
  52 + /// </summary>
  53 + public decimal refundQuantity { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 单价
  57 + /// </summary>
  58 + public decimal unitPrice { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 总价
  62 + /// </summary>
  63 + public decimal totalPrice { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 业绩类型
  67 + /// </summary>
  68 + public string performanceType { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 科美类型
  72 + /// </summary>
  73 + public string beautyType { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 来源类型
  77 + /// </summary>
  78 + public string sourceType { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 品项分类
  82 + /// </summary>
  83 + public string itemCategory { get; set; }
  84 + }
  85 +}
  86 +
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqHytkHytk/RefundDetailListQueryInput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqHytkHytk
  5 +{
  6 + /// <summary>
  7 + /// 退卡明细列表查询输入参数
  8 + /// </summary>
  9 + public class RefundDetailListQueryInput
  10 + {
  11 + /// <summary>
  12 + /// 当前页码
  13 + /// </summary>
  14 + public int currentPage { get; set; } = 1;
  15 +
  16 + /// <summary>
  17 + /// 每页数量
  18 + /// </summary>
  19 + public int pageSize { get; set; } = 20;
  20 +
  21 + /// <summary>
  22 + /// 排序字段
  23 + /// </summary>
  24 + public string sidx { get; set; } = "tksj";
  25 +
  26 + /// <summary>
  27 + /// 排序方式(asc/desc)
  28 + /// </summary>
  29 + public string sort { get; set; } = "desc";
  30 +
  31 + /// <summary>
  32 + /// 门店ID列表(多门店筛选)
  33 + /// </summary>
  34 + public List<string> storeIds { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 会员ID(会员筛选)
  38 + /// </summary>
  39 + public string memberId { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 开始时间(时间范围筛选)
  43 + /// </summary>
  44 + public DateTime? startTime { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 结束时间(时间范围筛选)
  48 + /// </summary>
  49 + public DateTime? endTime { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 品项ID(品项筛选)
  53 + /// </summary>
  54 + public string itemId { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 业绩类型(业绩类型筛选)
  58 + /// </summary>
  59 + public string performanceType { get; set; }
  60 +
  61 + /// <summary>
  62 + /// 科美类型(科美类型筛选)
  63 + /// </summary>
  64 + public string beautyType { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 来源类型(来源类型筛选)
  68 + /// </summary>
  69 + public string sourceType { get; set; }
  70 +
  71 + /// <summary>
  72 + /// 品项分类(品项分类筛选)
  73 + /// </summary>
  74 + public string itemCategory { get; set; }
  75 + }
  76 +}
  77 +
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs
1 using System; 1 using System;
  2 +using System.Collections.Generic;
2 using NCC.Code; 3 using NCC.Code;
3 using NCC.Extend.Entitys.Enum; 4 using NCC.Extend.Entitys.Enum;
4 5
@@ -253,6 +254,26 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx @@ -253,6 +254,26 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
253 public int consumeLevel { get; set; } 254 public int consumeLevel { get; set; }
254 255
255 /// <summary> 256 /// <summary>
  257 + /// 消费等级名称
  258 + /// </summary>
  259 + public string consumeLevelName
  260 + {
  261 + get
  262 + {
  263 + var levelMap = new Dictionary<int, string>
  264 + {
  265 + { 0, "D" },
  266 + { 1, "C" },
  267 + { 2, "B" },
  268 + { 3, "A" },
  269 + { 4, "A+" },
  270 + { 5, "A++" }
  271 + };
  272 + return levelMap.ContainsKey(consumeLevel) ? levelMap[consumeLevel] : "D";
  273 + }
  274 + }
  275 +
  276 + /// <summary>
256 /// 消费等级更新时间 277 /// 消费等级更新时间
257 /// </summary> 278 /// </summary>
258 public DateTime? consumeLevelUpdateTime { get; set; } 279 public DateTime? consumeLevelUpdateTime { get; set; }
netcore/src/Modularity/Extend/NCC.Extend/LqHytkHytkService.cs
1 using System; 1 using System;
2 using System.Collections.Generic; 2 using System.Collections.Generic;
  3 +using System.IO;
3 using System.Linq; 4 using System.Linq;
  5 +using System.Reflection;
4 using System.Threading.Tasks; 6 using System.Threading.Tasks;
5 using Mapster; 7 using Mapster;
6 using Microsoft.AspNetCore.Mvc; 8 using Microsoft.AspNetCore.Mvc;
@@ -26,6 +28,8 @@ using NCC.Extend.Entitys.lq_hytk_kjbsyj; @@ -26,6 +28,8 @@ using NCC.Extend.Entitys.lq_hytk_kjbsyj;
26 using NCC.Extend.Entitys.lq_hytk_mx; 28 using NCC.Extend.Entitys.lq_hytk_mx;
27 using NCC.Extend.Entitys.lq_kd_pxmx; 29 using NCC.Extend.Entitys.lq_kd_pxmx;
28 using NCC.Extend.Entitys.lq_xmzl; 30 using NCC.Extend.Entitys.lq_xmzl;
  31 +using NCC.Extend.Entitys.lq_khxx;
  32 +using NCC.Extend.Entitys.lq_mdxx;
29 using NCC.Extend.Interfaces.LqHytkHytk; 33 using NCC.Extend.Interfaces.LqHytkHytk;
30 using NCC.FriendlyException; 34 using NCC.FriendlyException;
31 using NCC.JsonSerialization; 35 using NCC.JsonSerialization;
@@ -810,5 +814,445 @@ namespace NCC.Extend.LqHytkHytk @@ -810,5 +814,445 @@ namespace NCC.Extend.LqHytkHytk
810 } 814 }
811 } 815 }
812 #endregion 816 #endregion
  817 +
  818 + #region 退卡明细查询
  819 + /// <summary>
  820 + /// 获取退卡明细列表
  821 + /// </summary>
  822 + /// <remarks>
  823 + /// 获取退卡明细列表,支持多门店、会员、时间范围、品项、业绩类型、科美类型、来源类型、品项分类等筛选
  824 + ///
  825 + /// 示例请求:
  826 + /// ```json
  827 + /// GET /api/Extend/LqHytkHytk/refund-detail-list?storeIds=门店ID1,门店ID2&amp;memberId=会员ID&amp;startTime=2025-01-01&amp;endTime=2025-12-31&amp;currentPage=1&amp;pageSize=20
  828 + /// ```
  829 + ///
  830 + /// 参数说明:
  831 + /// - storeIds: 门店ID列表(可选,多门店筛选)
  832 + /// - memberId: 会员ID(可选)
  833 + /// - startTime: 开始时间(可选,时间范围筛选)
  834 + /// - endTime: 结束时间(可选,时间范围筛选)
  835 + /// - itemId: 品项ID(可选)
  836 + /// - performanceType: 业绩类型(可选)
  837 + /// - beautyType: 科美类型(可选)
  838 + /// - sourceType: 来源类型(可选)
  839 + /// - itemCategory: 品项分类(可选)
  840 + /// - currentPage: 当前页码(必填)
  841 + /// - pageSize: 每页数量(必填)
  842 + /// </remarks>
  843 + /// <param name="input">查询参数</param>
  844 + /// <returns>分页的退卡明细列表</returns>
  845 + /// <response code="200">成功返回退卡明细列表</response>
  846 + /// <response code="400">参数错误</response>
  847 + /// <response code="500">服务器内部错误</response>
  848 + [HttpPost("refund-detail-list")]
  849 + public async Task<dynamic> GetRefundDetailList([FromBody] RefundDetailListQueryInput input)
  850 + {
  851 + try
  852 + {
  853 + if (input == null)
  854 + {
  855 + throw NCCException.Oh("参数不能为空");
  856 + }
  857 +
  858 + // 1. 先查询退卡明细表
  859 + var mxQuery = _db.Queryable<LqHytkMxEntity>()
  860 + .Where(mx => mx.IsEffective == StatusEnum.有效.GetHashCode())
  861 + .WhereIF(!string.IsNullOrEmpty(input.itemId), mx => mx.Px == input.itemId)
  862 + .WhereIF(!string.IsNullOrEmpty(input.performanceType), mx => mx.PerformanceType == input.performanceType)
  863 + .WhereIF(!string.IsNullOrEmpty(input.beautyType), mx => mx.BeautyType == input.beautyType)
  864 + .WhereIF(!string.IsNullOrEmpty(input.sourceType), mx => mx.SourceType == input.sourceType)
  865 + .WhereIF(!string.IsNullOrEmpty(input.itemCategory), mx => mx.ItemCategory == input.itemCategory)
  866 + .WhereIF(input.startTime.HasValue, mx => mx.Tksj.HasValue && mx.Tksj >= input.startTime.Value)
  867 + .WhereIF(input.endTime.HasValue, mx => mx.Tksj.HasValue && mx.Tksj <= input.endTime.Value);
  868 +
  869 + var mxList = await mxQuery.ToListAsync();
  870 +
  871 + if (!mxList.Any())
  872 + {
  873 + return PageResult<RefundDetailListOutput>.SqlSugarPageResult(new SqlSugarPagedList<RefundDetailListOutput>());
  874 + }
  875 +
  876 + // 2. 批量查询退卡信息表
  877 + var refundInfoIds = mxList.Select(x => x.RefundInfoId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  878 + var refundInfoList = new List<LqHytkHytkEntity>();
  879 + if (refundInfoIds.Any())
  880 + {
  881 + refundInfoList = await _db.Queryable<LqHytkHytkEntity>()
  882 + .Where(x => refundInfoIds.Contains(x.Id))
  883 + .WhereIF(input.storeIds != null && input.storeIds.Any(), x => input.storeIds.Contains(x.Md))
  884 + .WhereIF(!string.IsNullOrEmpty(input.memberId), x => x.Hy == input.memberId)
  885 + .WhereIF(input.startTime.HasValue && !mxList.Any(m => m.Tksj.HasValue), x => x.Tksj.HasValue && x.Tksj >= input.startTime.Value)
  886 + .WhereIF(input.endTime.HasValue && !mxList.Any(m => m.Tksj.HasValue), x => x.Tksj.HasValue && x.Tksj <= input.endTime.Value)
  887 + .ToListAsync();
  888 + }
  889 +
  890 + var refundInfoDict = refundInfoList.ToDictionary(x => x.Id, x => x);
  891 +
  892 + // 3. 批量查询门店信息
  893 + var storeIds = refundInfoList.Select(x => x.Md).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  894 + var storeDict = new Dictionary<string, string>();
  895 + if (storeIds.Any())
  896 + {
  897 + var stores = await _db.Queryable<LqMdxxEntity>()
  898 + .Where(x => storeIds.Contains(x.Id))
  899 + .Select(x => new { x.Id, x.Dm })
  900 + .ToListAsync();
  901 + storeDict = stores.ToDictionary(x => x.Id, x => x.Dm ?? "");
  902 + }
  903 +
  904 + // 4. 批量查询会员信息
  905 + var memberIds = mxList.Select(x => x.MemberId).Where(x => !string.IsNullOrEmpty(x))
  906 + .Concat(refundInfoList.Select(x => x.Hy).Where(x => !string.IsNullOrEmpty(x)))
  907 + .Distinct().ToList();
  908 + var memberDict = new Dictionary<string, string>();
  909 + if (memberIds.Any())
  910 + {
  911 + var members = await _db.Queryable<LqKhxxEntity>()
  912 + .Where(x => memberIds.Contains(x.Id))
  913 + .Select(x => new { x.Id, x.Sjh })
  914 + .ToListAsync();
  915 + memberDict = members.ToDictionary(x => x.Id, x => x.Sjh ?? "");
  916 + }
  917 +
  918 + // 5. 组装数据
  919 + var resultList = new List<RefundDetailListOutput>();
  920 + foreach (var mx in mxList)
  921 + {
  922 + var refundInfo = refundInfoDict.ContainsKey(mx.RefundInfoId) ? refundInfoDict[mx.RefundInfoId] : null;
  923 +
  924 + // 应用筛选条件
  925 + if (input.storeIds != null && input.storeIds.Any())
  926 + {
  927 + if (refundInfo == null || !input.storeIds.Contains(refundInfo.Md))
  928 + continue;
  929 + }
  930 + if (!string.IsNullOrEmpty(input.memberId))
  931 + {
  932 + if (mx.MemberId != input.memberId && (refundInfo == null || refundInfo.Hy != input.memberId))
  933 + continue;
  934 + }
  935 +
  936 + var memberId = mx.MemberId ?? (refundInfo?.Hy);
  937 + var refundTime = mx.Tksj ?? refundInfo?.Tksj;
  938 +
  939 + resultList.Add(new RefundDetailListOutput
  940 + {
  941 + storeId = refundInfo?.Md,
  942 + storeName = refundInfo != null && storeDict.ContainsKey(refundInfo.Md) ? storeDict[refundInfo.Md] : "",
  943 + memberId = memberId,
  944 + memberName = refundInfo?.Hymc,
  945 + memberPhone = !string.IsNullOrEmpty(memberId) && memberDict.ContainsKey(memberId) ? memberDict[memberId] : "",
  946 + refundTime = refundTime,
  947 + itemId = mx.Px,
  948 + itemName = mx.Pxmc,
  949 + refundQuantity = mx.ProjectNumber,
  950 + unitPrice = mx.Pxjg ?? 0,
  951 + totalPrice = mx.Tkje ?? mx.TotalPrice ?? 0,
  952 + performanceType = mx.PerformanceType,
  953 + beautyType = mx.BeautyType,
  954 + sourceType = mx.SourceType,
  955 + itemCategory = mx.ItemCategory
  956 + });
  957 + }
  958 +
  959 + // 6. 排序
  960 + var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
  961 + var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
  962 +
  963 + if (sort.ToLower() == "desc")
  964 + {
  965 + resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
  966 + }
  967 + else
  968 + {
  969 + resultList = resultList.OrderBy(x => GetPropertyValue(x, sidx)).ToList();
  970 + }
  971 +
  972 + // 7. 分页
  973 + var total = resultList.Count;
  974 + var skip = (input.currentPage - 1) * input.pageSize;
  975 + var pagedList = resultList.Skip(skip).Take(input.pageSize).ToList();
  976 +
  977 + var pageList = new SqlSugarPagedList<RefundDetailListOutput>
  978 + {
  979 + list = pagedList,
  980 + pagination = new PagedModel
  981 + {
  982 + PageIndex = input.currentPage,
  983 + PageSize = input.pageSize,
  984 + Total = total
  985 + }
  986 + };
  987 +
  988 + return PageResult<RefundDetailListOutput>.SqlSugarPageResult(pageList);
  989 + }
  990 + catch (Exception ex)
  991 + {
  992 + throw NCCException.Oh($"获取退卡明细列表失败: {ex.Message}");
  993 + }
  994 + }
  995 +
  996 + private object GetPropertyValue(RefundDetailListOutput obj, string propertyName)
  997 + {
  998 + var prop = typeof(RefundDetailListOutput).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
  999 + return prop?.GetValue(obj) ?? "";
  1000 + }
  1001 +
  1002 + /// <summary>
  1003 + /// 获取退卡明细列表(无分页,用于导出)
  1004 + /// </summary>
  1005 + /// <param name="input">查询参数</param>
  1006 + /// <returns>退卡明细列表</returns>
  1007 + [NonAction]
  1008 + public async Task<List<RefundDetailListOutput>> GetRefundDetailListNoPaging(RefundDetailListQueryInput input)
  1009 + {
  1010 + try
  1011 + {
  1012 + // 1. 先查询退卡明细表
  1013 + var mxQuery = _db.Queryable<LqHytkMxEntity>()
  1014 + .Where(mx => mx.IsEffective == StatusEnum.有效.GetHashCode())
  1015 + .WhereIF(!string.IsNullOrEmpty(input.itemId), mx => mx.Px == input.itemId)
  1016 + .WhereIF(!string.IsNullOrEmpty(input.performanceType), mx => mx.PerformanceType == input.performanceType)
  1017 + .WhereIF(!string.IsNullOrEmpty(input.beautyType), mx => mx.BeautyType == input.beautyType)
  1018 + .WhereIF(!string.IsNullOrEmpty(input.sourceType), mx => mx.SourceType == input.sourceType)
  1019 + .WhereIF(!string.IsNullOrEmpty(input.itemCategory), mx => mx.ItemCategory == input.itemCategory)
  1020 + .WhereIF(input.startTime.HasValue, mx => mx.Tksj.HasValue && mx.Tksj >= input.startTime.Value)
  1021 + .WhereIF(input.endTime.HasValue, mx => mx.Tksj.HasValue && mx.Tksj <= input.endTime.Value);
  1022 +
  1023 + var mxList = await mxQuery.ToListAsync();
  1024 +
  1025 + if (!mxList.Any())
  1026 + {
  1027 + return new List<RefundDetailListOutput>();
  1028 + }
  1029 +
  1030 + // 2. 批量查询退卡信息表
  1031 + var refundInfoIds = mxList.Select(x => x.RefundInfoId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  1032 + var refundInfoList = new List<LqHytkHytkEntity>();
  1033 + if (refundInfoIds.Any())
  1034 + {
  1035 + refundInfoList = await _db.Queryable<LqHytkHytkEntity>()
  1036 + .Where(x => refundInfoIds.Contains(x.Id))
  1037 + .WhereIF(input.storeIds != null && input.storeIds.Any(), x => input.storeIds.Contains(x.Md))
  1038 + .WhereIF(!string.IsNullOrEmpty(input.memberId), x => x.Hy == input.memberId)
  1039 + .WhereIF(input.startTime.HasValue && !mxList.Any(m => m.Tksj.HasValue), x => x.Tksj.HasValue && x.Tksj >= input.startTime.Value)
  1040 + .WhereIF(input.endTime.HasValue && !mxList.Any(m => m.Tksj.HasValue), x => x.Tksj.HasValue && x.Tksj <= input.endTime.Value)
  1041 + .ToListAsync();
  1042 + }
  1043 +
  1044 + var refundInfoDict = refundInfoList.ToDictionary(x => x.Id, x => x);
  1045 +
  1046 + // 3. 批量查询门店信息
  1047 + var storeIds = refundInfoList.Select(x => x.Md).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  1048 + var storeDict = new Dictionary<string, string>();
  1049 + if (storeIds.Any())
  1050 + {
  1051 + var stores = await _db.Queryable<LqMdxxEntity>()
  1052 + .Where(x => storeIds.Contains(x.Id))
  1053 + .Select(x => new { x.Id, x.Dm })
  1054 + .ToListAsync();
  1055 + storeDict = stores.ToDictionary(x => x.Id, x => x.Dm ?? "");
  1056 + }
  1057 +
  1058 + // 4. 批量查询会员信息
  1059 + var memberIds = mxList.Select(x => x.MemberId).Where(x => !string.IsNullOrEmpty(x))
  1060 + .Concat(refundInfoList.Select(x => x.Hy).Where(x => !string.IsNullOrEmpty(x)))
  1061 + .Distinct().ToList();
  1062 + var memberDict = new Dictionary<string, string>();
  1063 + if (memberIds.Any())
  1064 + {
  1065 + var members = await _db.Queryable<LqKhxxEntity>()
  1066 + .Where(x => memberIds.Contains(x.Id))
  1067 + .Select(x => new { x.Id, x.Sjh })
  1068 + .ToListAsync();
  1069 + memberDict = members.ToDictionary(x => x.Id, x => x.Sjh ?? "");
  1070 + }
  1071 +
  1072 + // 5. 组装数据
  1073 + var resultList = new List<RefundDetailListOutput>();
  1074 + foreach (var mx in mxList)
  1075 + {
  1076 + var refundInfo = refundInfoDict.ContainsKey(mx.RefundInfoId) ? refundInfoDict[mx.RefundInfoId] : null;
  1077 +
  1078 + // 应用筛选条件
  1079 + if (input.storeIds != null && input.storeIds.Any())
  1080 + {
  1081 + if (refundInfo == null || !input.storeIds.Contains(refundInfo.Md))
  1082 + continue;
  1083 + }
  1084 + if (!string.IsNullOrEmpty(input.memberId))
  1085 + {
  1086 + if (mx.MemberId != input.memberId && (refundInfo == null || refundInfo.Hy != input.memberId))
  1087 + continue;
  1088 + }
  1089 +
  1090 + var memberId = mx.MemberId ?? (refundInfo?.Hy);
  1091 + var refundTime = mx.Tksj ?? refundInfo?.Tksj;
  1092 +
  1093 + resultList.Add(new RefundDetailListOutput
  1094 + {
  1095 + storeId = refundInfo?.Md,
  1096 + storeName = refundInfo != null && storeDict.ContainsKey(refundInfo.Md) ? storeDict[refundInfo.Md] : "",
  1097 + memberId = memberId,
  1098 + memberName = refundInfo?.Hymc,
  1099 + memberPhone = !string.IsNullOrEmpty(memberId) && memberDict.ContainsKey(memberId) ? memberDict[memberId] : "",
  1100 + refundTime = refundTime,
  1101 + itemId = mx.Px,
  1102 + itemName = mx.Pxmc,
  1103 + refundQuantity = mx.ProjectNumber,
  1104 + unitPrice = mx.Pxjg ?? 0,
  1105 + totalPrice = mx.Tkje ?? mx.TotalPrice ?? 0,
  1106 + performanceType = mx.PerformanceType,
  1107 + beautyType = mx.BeautyType,
  1108 + sourceType = mx.SourceType,
  1109 + itemCategory = mx.ItemCategory
  1110 + });
  1111 + }
  1112 +
  1113 + // 6. 排序
  1114 + var sidx = string.IsNullOrEmpty(input.sidx) ? "refundTime" : input.sidx;
  1115 + var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
  1116 +
  1117 + if (sort.ToLower() == "desc")
  1118 + {
  1119 + resultList = resultList.OrderByDescending(x => GetPropertyValue(x, sidx)).ToList();
  1120 + }
  1121 + else
  1122 + {
  1123 + resultList = resultList.OrderBy(x => GetPropertyValue(x, sidx)).ToList();
  1124 + }
  1125 +
  1126 + return resultList;
  1127 + }
  1128 + catch (Exception ex)
  1129 + {
  1130 + throw NCCException.Oh($"获取退卡明细列表失败: {ex.Message}");
  1131 + }
  1132 + }
  1133 + #endregion
  1134 +
  1135 + #region 退卡明细导出
  1136 + /// <summary>
  1137 + /// 导出退卡明细
  1138 + /// </summary>
  1139 + /// <remarks>
  1140 + /// 导出退卡明细到Excel,支持多门店、会员、时间范围、品项、业绩类型、科美类型、来源类型、品项分类等筛选
  1141 + ///
  1142 + /// 示例请求:
  1143 + /// ```json
  1144 + /// POST /api/Extend/LqHytkHytk/refund-detail-export
  1145 + /// {
  1146 + /// "storeIds": ["门店ID1", "门店ID2"],
  1147 + /// "memberId": "会员ID",
  1148 + /// "startTime": "2025-01-01T00:00:00",
  1149 + /// "endTime": "2025-12-31T23:59:59",
  1150 + /// "itemId": "品项ID",
  1151 + /// "performanceType": "业绩类型",
  1152 + /// "beautyType": "科美类型",
  1153 + /// "sourceType": "来源类型",
  1154 + /// "itemCategory": "品项分类"
  1155 + /// }
  1156 + /// ```
  1157 + /// </remarks>
  1158 + /// <param name="input">查询参数</param>
  1159 + /// <returns>Excel文件下载信息</returns>
  1160 + /// <response code="200">成功返回Excel文件下载链接</response>
  1161 + /// <response code="400">参数错误</response>
  1162 + /// <response code="500">服务器内部错误</response>
  1163 + [HttpPost("refund-detail-export")]
  1164 + public async Task<dynamic> ExportRefundDetail([FromBody] RefundDetailListQueryInput input)
  1165 + {
  1166 + try
  1167 + {
  1168 + var userInfo = await _userManager.GetUserInfo();
  1169 + var exportData = await GetRefundDetailListNoPaging(input);
  1170 +
  1171 + if (exportData == null || !exportData.Any())
  1172 + {
  1173 + return new { name = "退卡明细.xls", url = "", message = "没有找到符合条件的数据" };
  1174 + }
  1175 +
  1176 + // 定义导出字段
  1177 + List<ParamsModel> paramList =
  1178 + "[{\"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>();
  1179 +
  1180 + ExcelConfig excelconfig = new ExcelConfig();
  1181 + excelconfig.FileName = "退卡明细_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xls";
  1182 + excelconfig.HeadFont = "微软雅黑";
  1183 + excelconfig.HeadPoint = 10;
  1184 + excelconfig.IsAllSizeColumn = true;
  1185 + excelconfig.ColumnModel = new List<ExcelColumnModel>();
  1186 +
  1187 + // 添加所有字段到导出列
  1188 + foreach (var param in paramList)
  1189 + {
  1190 + excelconfig.ColumnModel.Add(new ExcelColumnModel() { Column = param.field, ExcelColumn = param.value });
  1191 + }
  1192 +
  1193 + // 查找项目根目录
  1194 + var baseDir = AppContext.BaseDirectory;
  1195 + var projectRoot = baseDir;
  1196 + var dir = new DirectoryInfo(baseDir);
  1197 +
  1198 + // 优先查找包含 .git 目录的目录(真正的项目根目录)
  1199 + while (dir != null && dir.Parent != null)
  1200 + {
  1201 + try
  1202 + {
  1203 + if (dir.GetDirectories(".git").Any())
  1204 + {
  1205 + projectRoot = dir.FullName;
  1206 + break;
  1207 + }
  1208 + }
  1209 + catch
  1210 + {
  1211 + // 忽略访问错误,继续向上查找
  1212 + }
  1213 + dir = dir.Parent;
  1214 + }
  1215 +
  1216 + // 如果没找到 .git 目录,再查找包含 .sln 文件的目录
  1217 + if (projectRoot == baseDir)
  1218 + {
  1219 + dir = new DirectoryInfo(baseDir);
  1220 + while (dir != null && dir.Parent != null)
  1221 + {
  1222 + try
  1223 + {
  1224 + if (dir.GetFiles("*.sln").Any())
  1225 + {
  1226 + projectRoot = dir.FullName;
  1227 + break;
  1228 + }
  1229 + }
  1230 + catch
  1231 + {
  1232 + // 忽略访问错误,继续向上查找
  1233 + }
  1234 + dir = dir.Parent;
  1235 + }
  1236 + }
  1237 +
  1238 + // 在项目根目录下创建 ExportFiles 文件夹
  1239 + var exportFilesPath = Path.Combine(projectRoot, "ExportFiles");
  1240 + if (!Directory.Exists(exportFilesPath))
  1241 + {
  1242 + Directory.CreateDirectory(exportFilesPath);
  1243 + }
  1244 + var addPath = Path.Combine(exportFilesPath, excelconfig.FileName);
  1245 +
  1246 + ExcelExportHelper<RefundDetailListOutput>.Export(exportData, excelconfig, addPath);
  1247 + var fileName = _userManager.UserId + "|" + addPath + "|xls";
  1248 + var output = new { name = excelconfig.FileName, url = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC") };
  1249 + return output;
  1250 + }
  1251 + catch (Exception ex)
  1252 + {
  1253 + throw NCCException.Oh($"导出退卡明细失败: {ex.Message}");
  1254 + }
  1255 + }
  1256 + #endregion
813 } 1257 }
814 } 1258 }
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
@@ -562,7 +562,7 @@ namespace NCC.Extend.LqKhxx @@ -562,7 +562,7 @@ namespace NCC.Extend.LqKhxx
562 exportData = await this.GetNoPagingList(input); 562 exportData = await this.GetNoPagingList(input);
563 } 563 }
564 List<ParamsModel> paramList = 564 List<ParamsModel> paramList =
565 - "[{\"value\":\"客户编码\",\"field\":\"id\"},{\"value\":\"客户名称\",\"field\":\"khmc\"},{\"value\":\"手机号\",\"field\":\"sjh\"},{\"value\":\"档案号\",\"field\":\"dah\"},{\"value\":\"性别\",\"field\":\"xb\"},{\"value\":\"公众号状态\",\"field\":\"gzhzt\"},{\"value\":\"微信昵称\",\"field\":\"wxnc\"},{\"value\":\"小程序状态\",\"field\":\"wxxcxzt\"},{\"value\":\"最近登录时间\",\"field\":\"zjdlsj\"},{\"value\":\"客户目前归属\",\"field\":\"khmqgs\"},{\"value\":\"归属门店\",\"field\":\"gsmd\"},{\"value\":\"注册时间\",\"field\":\"zcsj\"},{\"value\":\"客户类型\",\"field\":\"khlx\"},{\"value\":\"客户阶段\",\"field\":\"khjd\"},{\"value\":\"客户消费\",\"field\":\"khxf\"},{\"value\":\"消费频次\",\"field\":\"xfpc\"},{\"value\":\"推荐人\",\"field\":\"tjr\"},{\"value\":\"负责顾问\",\"field\":\"fzgw\"},{\"value\":\"美容师\",\"field\":\"mrs\"},{\"value\":\"进店渠道\",\"field\":\"jdqd\"},{\"value\":\"联系地址\",\"field\":\"lxdz\"},{\"value\":\"备注\",\"field\":\"bz\"},{\"value\":\"阳历生日\",\"field\":\"yanglsr\"},{\"value\":\"阴历生日\",\"field\":\"yinlsr\"},{\"value\":\"年龄\",\"field\":\"ml\"},]".ToList<ParamsModel>(); 565 + "[{\"value\":\"客户编码\",\"field\":\"id\"},{\"value\":\"客户名称\",\"field\":\"khmc\"},{\"value\":\"手机号\",\"field\":\"sjh\"},{\"value\":\"档案号\",\"field\":\"dah\"},{\"value\":\"性别\",\"field\":\"xb\"},{\"value\":\"公众号状态\",\"field\":\"gzhzt\"},{\"value\":\"微信昵称\",\"field\":\"wxnc\"},{\"value\":\"小程序状态\",\"field\":\"wxxcxzt\"},{\"value\":\"最近登录时间\",\"field\":\"zjdlsj\"},{\"value\":\"客户目前归属\",\"field\":\"khmqgs\"},{\"value\":\"归属门店\",\"field\":\"gsmd\"},{\"value\":\"注册时间\",\"field\":\"zcsj\"},{\"value\":\"客户类型\",\"field\":\"khlx\"},{\"value\":\"客户阶段\",\"field\":\"khjd\"},{\"value\":\"客户消费\",\"field\":\"khxf\"},{\"value\":\"消费频次\",\"field\":\"xfpc\"},{\"value\":\"推荐人\",\"field\":\"tjr\"},{\"value\":\"负责顾问\",\"field\":\"fzgw\"},{\"value\":\"美容师\",\"field\":\"mrs\"},{\"value\":\"进店渠道\",\"field\":\"jdqd\"},{\"value\":\"联系地址\",\"field\":\"lxdz\"},{\"value\":\"备注\",\"field\":\"bz\"},{\"value\":\"阳历生日\",\"field\":\"yanglsr\"},{\"value\":\"阴历生日\",\"field\":\"yinlsr\"},{\"value\":\"年龄\",\"field\":\"ml\"},{\"value\":\"消费等级\",\"field\":\"consumeLevelName\"},{\"value\":\"开单总金额\",\"field\":\"totalBillingAmount\"},{\"value\":\"剩余权益总金额\",\"field\":\"remainingRightsAmount\"},{\"value\":\"首次到店时间\",\"field\":\"firstVisitTime\"},{\"value\":\"最后到店时间\",\"field\":\"lastVisitTime\"},{\"value\":\"到店天数\",\"field\":\"visitDays\"},{\"value\":\"沉睡天数\",\"field\":\"sleepDays\"},]".ToList<ParamsModel>();
566 ExcelConfig excelconfig = new ExcelConfig(); 566 ExcelConfig excelconfig = new ExcelConfig();
567 excelconfig.FileName = "客户资料.xls"; 567 excelconfig.FileName = "客户资料.xls";
568 excelconfig.HeadFont = "微软雅黑"; 568 excelconfig.HeadFont = "微软雅黑";
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
@@ -4351,11 +4351,11 @@ namespace NCC.Extend @@ -4351,11 +4351,11 @@ namespace NCC.Extend
4351 storeFilter = $" AND hytk.md IN ('{storeIdsStr}')"; 4351 storeFilter = $" AND hytk.md IN ('{storeIdsStr}')";
4352 } 4352 }
4353 4353
4354 - // 1. 各门店退卡金额分布(不包含转卡)- 使用原生SQL确保正确 4354 + // 1. 各门店退卡金额分布(不包含转卡)- 使用原生SQL确保正确,使用实退金额
4355 var storeDistributionSql = $@" 4355 var storeDistributionSql = $@"
4356 SELECT 4356 SELECT
4357 hytk.md as StoreId, 4357 hytk.md as StoreId,
4358 - COALESCE(SUM(CAST(hytk.tkje AS DECIMAL(18,2))), 0) as RefundAmount, 4358 + COALESCE(SUM(CAST(COALESCE(hytk.F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as RefundAmount,
4359 COUNT(hytk.F_Id) as RefundCount 4359 COUNT(hytk.F_Id) as RefundCount
4360 FROM lq_hytk_hytk hytk 4360 FROM lq_hytk_hytk hytk
4361 WHERE hytk.F_IsEffective = 1 4361 WHERE hytk.F_IsEffective = 1
@@ -4390,10 +4390,10 @@ namespace NCC.Extend @@ -4390,10 +4390,10 @@ namespace NCC.Extend
4390 RefundCount = Convert.ToInt32(x.RefundCount ?? 0) 4390 RefundCount = Convert.ToInt32(x.RefundCount ?? 0)
4391 }).OrderByDescending(x => x.RefundAmount).ToList(); 4391 }).OrderByDescending(x => x.RefundAmount).ToList();
4392 4392
4393 - // 2. 总计数据 - 使用原生SQL确保正确(包含转卡,与驾驶舱保持一致) 4393 + // 2. 总计数据 - 使用原生SQL确保正确(包含转卡,与驾驶舱保持一致),使用实退金额
4394 var totalStatsSql = $@" 4394 var totalStatsSql = $@"
4395 SELECT 4395 SELECT
4396 - COALESCE(SUM(CAST(tkje AS DECIMAL(18,2))), 0) as TotalRefundAmount, 4396 + COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalRefundAmount,
4397 COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalActualRefundAmount, 4397 COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalActualRefundAmount,
4398 COUNT(F_Id) as TotalRefundCount 4398 COUNT(F_Id) as TotalRefundCount
4399 FROM lq_hytk_hytk 4399 FROM lq_hytk_hytk
@@ -4409,10 +4409,10 @@ namespace NCC.Extend @@ -4409,10 +4409,10 @@ namespace NCC.Extend
4409 var totalActualRefundAmount = totalStats != null ? Convert.ToDecimal(totalStats.TotalActualRefundAmount ?? 0) : 0m; 4409 var totalActualRefundAmount = totalStats != null ? Convert.ToDecimal(totalStats.TotalActualRefundAmount ?? 0) : 0m;
4410 var totalRefundCount = totalStats != null ? Convert.ToInt32(totalStats.TotalRefundCount ?? 0) : 0; 4410 var totalRefundCount = totalStats != null ? Convert.ToInt32(totalStats.TotalRefundCount ?? 0) : 0;
4411 4411
4412 - // 转卡总计(单独统计)- 使用原生SQL 4412 + // 转卡总计(单独统计)- 使用原生SQL,使用实退金额
4413 var transferCardSql = $@" 4413 var transferCardSql = $@"
4414 SELECT 4414 SELECT
4415 - COALESCE(SUM(CAST(tkje AS DECIMAL(18,2))), 0) as TotalTransferAmount, 4415 + COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalTransferAmount,
4416 COUNT(F_Id) as TotalTransferCount 4416 COUNT(F_Id) as TotalTransferCount
4417 FROM lq_hytk_hytk 4417 FROM lq_hytk_hytk
4418 WHERE F_IsEffective = 1 4418 WHERE F_IsEffective = 1
@@ -4427,13 +4427,13 @@ namespace NCC.Extend @@ -4427,13 +4427,13 @@ namespace NCC.Extend
4427 var totalTransferAmount = transferCardStats != null ? Convert.ToDecimal(transferCardStats.TotalTransferAmount ?? 0) : 0m; 4427 var totalTransferAmount = transferCardStats != null ? Convert.ToDecimal(transferCardStats.TotalTransferAmount ?? 0) : 0m;
4428 var totalTransferCount = transferCardStats != null ? Convert.ToInt32(transferCardStats.TotalTransferCount ?? 0) : 0; 4428 var totalTransferCount = transferCardStats != null ? Convert.ToInt32(transferCardStats.TotalTransferCount ?? 0) : 0;
4429 4429
4430 - // 3. 退卡金额最大的人 - 使用原生SQL 4430 + // 3. 退卡金额最大的人 - 使用原生SQL,使用实退金额
4431 // 注意:按 hymc(会员名称)分组,因为同一个会员可能有不同的 hy(会员ID) 4431 // 注意:按 hymc(会员名称)分组,因为同一个会员可能有不同的 hy(会员ID)
4432 var maxAmountPersonSql = $@" 4432 var maxAmountPersonSql = $@"
4433 SELECT 4433 SELECT
4434 MAX(hy) as MemberId, 4434 MAX(hy) as MemberId,
4435 hymc as MemberName, 4435 hymc as MemberName,
4436 - COALESCE(SUM(CAST(tkje AS DECIMAL(18,2))), 0) as TotalRefundAmount, 4436 + COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalRefundAmount,
4437 COUNT(F_Id) as RefundCount 4437 COUNT(F_Id) as RefundCount
4438 FROM lq_hytk_hytk 4438 FROM lq_hytk_hytk
4439 WHERE F_IsEffective = 1 4439 WHERE F_IsEffective = 1
@@ -4451,13 +4451,13 @@ namespace NCC.Extend @@ -4451,13 +4451,13 @@ namespace NCC.Extend
4451 var maxAmountPersonResult = await _db.Ado.SqlQueryAsync<dynamic>(maxAmountPersonSql); 4451 var maxAmountPersonResult = await _db.Ado.SqlQueryAsync<dynamic>(maxAmountPersonSql);
4452 var maxAmountPerson = maxAmountPersonResult?.FirstOrDefault(); 4452 var maxAmountPerson = maxAmountPersonResult?.FirstOrDefault();
4453 4453
4454 - // 4. 退卡次数最多的人 - 使用原生SQL(按退卡单数统计,一个退卡单算一次) 4454 + // 4. 退卡次数最多的人 - 使用原生SQL(按退卡单数统计,一个退卡单算一次),使用实退金额
4455 // 注意:按 hymc(会员名称)分组,因为同一个会员可能有不同的 hy(会员ID) 4455 // 注意:按 hymc(会员名称)分组,因为同一个会员可能有不同的 hy(会员ID)
4456 var maxCountPersonSql = $@" 4456 var maxCountPersonSql = $@"
4457 SELECT 4457 SELECT
4458 MAX(hy) as MemberId, 4458 MAX(hy) as MemberId,
4459 hymc as MemberName, 4459 hymc as MemberName,
4460 - COALESCE(SUM(CAST(tkje AS DECIMAL(18,2))), 0) as TotalRefundAmount, 4460 + COALESCE(SUM(CAST(COALESCE(F_ActualRefundAmount, 0) AS DECIMAL(18,2))), 0) as TotalRefundAmount,
4461 COUNT(F_Id) as RefundCount 4461 COUNT(F_Id) as RefundCount
4462 FROM lq_hytk_hytk 4462 FROM lq_hytk_hytk
4463 WHERE F_IsEffective = 1 4463 WHERE F_IsEffective = 1