Commit b26a469f7479eb0cc7064902101f98c975b3131e

Authored by “wangming”
1 parent b5ed92da

feat: 添加耗卡品项明细查询接口和人次记录表字段

- 新增耗卡品项明细查询接口(GetConsumeItemDetailList),支持分页和多条件筛选
- 为人次记录表(lq_person_times_record)添加'是否有开单'字段(F_HasBilling)
- 更新相关实体类和DTO,支持新字段
- 添加SQL脚本用于数据库字段添加
Showing 20 changed files with 1406 additions and 9 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Config/MemberInfoUpdateConfig.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +
  3 +namespace NCC.Extend.Entitys.Config
  4 +{
  5 + /// <summary>
  6 + /// 会员信息更新配置
  7 + /// </summary>
  8 + public static class MemberInfoUpdateConfig
  9 + {
  10 + /// <summary>
  11 + /// 生美会员判断金额阈值(元)
  12 + /// </summary>
  13 + public static decimal BeautyMemberAmountThreshold { get; set; } = 500m;
  14 +
  15 + /// <summary>
  16 + /// 医美会员判断金额阈值(元)
  17 + /// </summary>
  18 + public static decimal MedicalMemberAmountThreshold { get; set; } = 3000m;
  19 +
  20 + /// <summary>
  21 + /// 科技部会员判断金额阈值(元)
  22 + /// </summary>
  23 + public static decimal TechMemberAmountThreshold { get; set; } = 3800m;
  24 +
  25 + /// <summary>
  26 + /// 沉睡天数阈值列表(天)
  27 + /// </summary>
  28 + public static List<int> SleepDaysThresholds { get; set; } = new List<int> { 60, 90, 180, 360 };
  29 +
  30 + /// <summary>
  31 + /// 消费等级配置(金额阈值,单位:元)
  32 + /// Key: 等级编号(0-普通, 1-银卡, 2-金卡, 3-钻石等)
  33 + /// Value: 金额阈值
  34 + /// </summary>
  35 + public static Dictionary<int, decimal> ConsumeLevelThresholds { get; set; } = new Dictionary<int, decimal>
  36 + {
  37 + { 0, 0m }, // D: 小于 1 万
  38 + { 1, 10000m }, // C: 1 万
  39 + { 2, 30000m }, // B: 3 万
  40 + { 3, 50000m }, // A: 5 万
  41 + { 4, 100000m }, // A+: 10 万
  42 + { 5, 200000m } // A++: 20 万
  43 + };
  44 +
  45 + /// <summary>
  46 + /// 消费等级名称映射
  47 + /// </summary>
  48 + public static Dictionary<int, string> ConsumeLevelNames { get; set; } = new Dictionary<int, string>
  49 + {
  50 + { 0, "D" },
  51 + { 1, "C" },
  52 + { 2, "B" },
  53 + { 3, "A" },
  54 + { 4, "A+" },
  55 + { 5, "A++" }
  56 + };
  57 +
  58 + /// <summary>
  59 + /// 根据消费金额获取消费等级
  60 + /// </summary>
  61 + /// <param name="amount">消费金额</param>
  62 + /// <returns>消费等级编号</returns>
  63 + public static int GetConsumeLevel(decimal amount)
  64 + {
  65 + // 从高到低遍历,找到第一个满足条件的等级
  66 + for (int i = ConsumeLevelThresholds.Count - 1; i >= 0; i--)
  67 + {
  68 + if (amount >= ConsumeLevelThresholds[i])
  69 + {
  70 + return i;
  71 + }
  72 + }
  73 + return 0; // 默认返回最低等级
  74 + }
  75 +
  76 + /// <summary>
  77 + /// 根据消费金额获取消费等级名称
  78 + /// </summary>
  79 + /// <param name="amount">消费金额</param>
  80 + /// <returns>消费等级名称</returns>
  81 + public static string GetConsumeLevelName(decimal amount)
  82 + {
  83 + var level = GetConsumeLevel(amount);
  84 + return ConsumeLevelNames.ContainsKey(level) ? ConsumeLevelNames[level] : "D";
  85 + }
  86 + }
  87 +}
  88 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs
... ... @@ -135,6 +135,85 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
135 135 /// </summary>
136 136 public string subHealthUserName { get; set; }
137 137  
  138 + /// <summary>
  139 + /// 是否生美会员(0-否,1-是)
  140 + /// </summary>
  141 + public int isBeautyMember { get; set; }
  142 +
  143 + /// <summary>
  144 + /// 是否医美会员(0-否,1-是)
  145 + /// </summary>
  146 + public int isMedicalMember { get; set; }
  147 +
  148 + /// <summary>
  149 + /// 是否科技部会员(0-否,1-是)
  150 + /// </summary>
  151 + public int isTechMember { get; set; }
  152 +
  153 + /// <summary>
  154 + /// 成为生美会员时间
  155 + /// </summary>
  156 + public DateTime? beautyMemberTime { get; set; }
  157 +
  158 + /// <summary>
  159 + /// 成为医美会员时间
  160 + /// </summary>
  161 + public DateTime? medicalMemberTime { get; set; }
  162 +
  163 + /// <summary>
  164 + /// 成为科技部会员时间
  165 + /// </summary>
  166 + public DateTime? techMemberTime { get; set; }
  167 +
  168 + /// <summary>
  169 + /// 首次到店时间
  170 + /// </summary>
  171 + public DateTime? firstVisitTime { get; set; }
  172 +
  173 + /// <summary>
  174 + /// 最后到店时间
  175 + /// </summary>
  176 + public DateTime? lastVisitTime { get; set; }
  177 +
  178 + /// <summary>
  179 + /// 到店天数
  180 + /// </summary>
  181 + public int visitDays { get; set; }
  182 +
  183 + /// <summary>
  184 + /// 沉睡开始时间
  185 + /// </summary>
  186 + public DateTime? sleepStartTime { get; set; }
  187 +
  188 + /// <summary>
  189 + /// 沉睡天数
  190 + /// </summary>
  191 + public int sleepDays { get; set; }
  192 +
  193 + /// <summary>
  194 + /// 耗卡总金额(累计所有耗卡的金额总和)
  195 + /// </summary>
  196 + public decimal totalConsumeAmount { get; set; }
  197 +
  198 + /// <summary>
  199 + /// 开卡总金额(累计所有开单的实付业绩总和)
  200 + /// </summary>
  201 + public decimal totalBillingAmount { get; set; }
  202 +
  203 + /// <summary>
  204 + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额)
  205 + /// </summary>
  206 + public decimal remainingRightsAmount { get; set; }
  207 +
  208 + /// <summary>
  209 + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等)
  210 + /// </summary>
  211 + public int consumeLevel { get; set; }
  212 +
  213 + /// <summary>
  214 + /// 消费等级更新时间
  215 + /// </summary>
  216 + public DateTime? consumeLevelUpdateTime { get; set; }
138 217  
139 218 }
140 219 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs
... ... @@ -161,5 +161,85 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
161 161 /// 副健康师名称
162 162 /// </summary>
163 163 public string subHealthUserName { get; set; }
  164 +
  165 + /// <summary>
  166 + /// 是否生美会员(0-否,1-是)
  167 + /// </summary>
  168 + public int isBeautyMember { get; set; }
  169 +
  170 + /// <summary>
  171 + /// 是否医美会员(0-否,1-是)
  172 + /// </summary>
  173 + public int isMedicalMember { get; set; }
  174 +
  175 + /// <summary>
  176 + /// 是否科技部会员(0-否,1-是)
  177 + /// </summary>
  178 + public int isTechMember { get; set; }
  179 +
  180 + /// <summary>
  181 + /// 成为生美会员时间
  182 + /// </summary>
  183 + public DateTime? beautyMemberTime { get; set; }
  184 +
  185 + /// <summary>
  186 + /// 成为医美会员时间
  187 + /// </summary>
  188 + public DateTime? medicalMemberTime { get; set; }
  189 +
  190 + /// <summary>
  191 + /// 成为科技部会员时间
  192 + /// </summary>
  193 + public DateTime? techMemberTime { get; set; }
  194 +
  195 + /// <summary>
  196 + /// 首次到店时间
  197 + /// </summary>
  198 + public DateTime? firstVisitTime { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 最后到店时间
  202 + /// </summary>
  203 + public DateTime? lastVisitTime { get; set; }
  204 +
  205 + /// <summary>
  206 + /// 到店天数
  207 + /// </summary>
  208 + public int visitDays { get; set; }
  209 +
  210 + /// <summary>
  211 + /// 沉睡开始时间
  212 + /// </summary>
  213 + public DateTime? sleepStartTime { get; set; }
  214 +
  215 + /// <summary>
  216 + /// 沉睡天数
  217 + /// </summary>
  218 + public int sleepDays { get; set; }
  219 +
  220 + /// <summary>
  221 + /// 耗卡总金额(累计所有耗卡的金额总和)
  222 + /// </summary>
  223 + public decimal totalConsumeAmount { get; set; }
  224 +
  225 + /// <summary>
  226 + /// 开卡总金额(累计所有开单的实付业绩总和)
  227 + /// </summary>
  228 + public decimal totalBillingAmount { get; set; }
  229 +
  230 + /// <summary>
  231 + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额)
  232 + /// </summary>
  233 + public decimal remainingRightsAmount { get; set; }
  234 +
  235 + /// <summary>
  236 + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等)
  237 + /// </summary>
  238 + public int consumeLevel { get; set; }
  239 +
  240 + /// <summary>
  241 + /// 消费等级更新时间
  242 + /// </summary>
  243 + public DateTime? consumeLevelUpdateTime { get; set; }
164 244 }
165 245 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs
... ... @@ -48,8 +48,6 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
48 48 /// </summary>
49 49 public string xb { get; set; }
50 50  
51   -
52   -
53 51 /// <summary>
54 52 /// 归属门店
55 53 /// </summary>
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoOutput.cs
1 1 using System;
2 2 using System.Collections.Generic;
  3 +using NCC.Code;
  4 +using NCC.Common.Extension;
  5 +using NCC.Extend.Entitys.Enum;
3 6  
4 7 namespace NCC.Extend.Entitys.Dto.LqKhxx
5 8 {
... ... @@ -29,6 +32,21 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
29 32 public string MemberPhone { get; set; }
30 33  
31 34 /// <summary>
  35 + /// 会员类型
  36 + /// </summary>
  37 + public int MemberType { get; set; }
  38 +
  39 + /// <summary>
  40 + /// 会员类型名称
  41 + /// </summary>
  42 + public string MemberTypeName
  43 + {
  44 + get
  45 + {
  46 + return EnumHelper.GetEnumDesc<MemberTypeEnum>(MemberType);
  47 + }
  48 + }
  49 + /// <summary>
32 50 /// 所属门店ID
33 51 /// </summary>
34 52 public string BelongStoreId { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoQueryInput.cs
... ... @@ -24,6 +24,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
24 24 public string BillingStoreId { get; set; }
25 25  
26 26 /// <summary>
  27 + /// 会员类型
  28 + /// </summary>
  29 + public int MemberType { get; set; } = -1;
  30 +
  31 + /// <summary>
27 32 /// 开单人ID
28 33 /// </summary>
29 34 public string BillingUserId { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordCrInput.cs
... ... @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord
23 23 public string businessType { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 是否有开单(0-否,1-是)
  27 + /// </summary>
  28 + public int hasBilling { get; set; } = 0;
  29 +
  30 + /// <summary>
26 31 /// 人员类型(健康师/科技老师)
27 32 /// </summary>
28 33 public string personType { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordInfoOutput.cs
... ... @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord
23 23 public string businessType { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 是否有开单(0-否,1-是)
  27 + /// </summary>
  28 + public int hasBilling { get; set; }
  29 +
  30 + /// <summary>
26 31 /// 人员类型(健康师/科技老师)
27 32 /// </summary>
28 33 public string personType { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordListOutput.cs
... ... @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord
23 23 public string businessType { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 是否有开单(0-否,1-是)
  27 + /// </summary>
  28 + public int hasBilling { get; set; }
  29 +
  30 + /// <summary>
26 31 /// 人员类型(健康师/科技老师)
27 32 /// </summary>
28 33 public string personType { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqXhPxmx
  4 +{
  5 + /// <summary>
  6 + /// 耗卡品项明细记录输出
  7 + /// </summary>
  8 + public class ConsumeItemDetailListOutput
  9 + {
  10 + /// <summary>
  11 + /// 品项明细ID
  12 + /// </summary>
  13 + public string id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 耗卡记录ID
  17 + /// </summary>
  18 + public string consumeId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 耗卡时间
  22 + /// </summary>
  23 + public DateTime? consumeTime { 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 string itemName { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 品项类型(分类④-统计品项用)
  42 + /// </summary>
  43 + public string itemType { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 品项价格
  47 + /// </summary>
  48 + public decimal itemPrice { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 项目次数
  52 + /// </summary>
  53 + public decimal projectNumber { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 原始项目次数
  57 + /// </summary>
  58 + public decimal? originalProjectNumber { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 合计金额
  62 + /// </summary>
  63 + public decimal totalPrice { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 来源类型(购买/赠送/体验)
  67 + /// </summary>
  68 + public string sourceType { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 门店ID
  72 + /// </summary>
  73 + public string storeId { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 门店名称
  77 + /// </summary>
  78 + public string storeName { get; set; }
  79 + }
  80 +}
  81 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListQueryInput.cs 0 → 100644
  1 +using NCC.Common.Filter;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqXhPxmx
  4 +{
  5 + /// <summary>
  6 + /// 耗卡品项明细记录查询输入参数
  7 + /// </summary>
  8 + public class ConsumeItemDetailListQueryInput : PageInputBase
  9 + {
  10 + /// <summary>
  11 + /// 品项明细ID
  12 + /// </summary>
  13 + public string Id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 耗卡记录ID
  17 + /// </summary>
  18 + public string ConsumeId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 耗卡时间(开始)
  22 + /// </summary>
  23 + public string StartConsumeTime { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 耗卡时间(结束)
  27 + /// </summary>
  28 + public string EndConsumeTime { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 开始时间(兼容参数名)
  32 + /// </summary>
  33 + public string startTime { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 结束时间(兼容参数名)
  37 + /// </summary>
  38 + public string endTime { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 会员ID
  42 + /// </summary>
  43 + public string MemberId { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 会员名称(模糊查询)
  47 + /// </summary>
  48 + public string MemberName { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 会员手机号
  52 + /// </summary>
  53 + public string MemberPhone { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 品项ID
  57 + /// </summary>
  58 + public string ItemId { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 品项名称(模糊查询)
  62 + /// </summary>
  63 + public string ItemName { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 品项类型(分类④-统计品项用)
  67 + /// </summary>
  68 + public string ItemType { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 来源类型(购买/赠送/体验)
  72 + /// </summary>
  73 + public string SourceType { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 门店ID
  77 + /// </summary>
  78 + public string StoreId { get; set; }
  79 + }
  80 +}
  81 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_khxx/LqKhxxEntity.cs
... ... @@ -142,6 +142,108 @@ namespace NCC.Extend.Entitys.lq_khxx
142 142 public string SubHealthUser { get; set; }
143 143  
144 144 /// <summary>
  145 + /// 最后消费时间
  146 + /// </summary>
  147 + [SugarColumn(ColumnName = "F_LastConsumeTime")]
  148 + public DateTime? LastConsumeTime { get; set; }
  149 +
  150 + /// <summary>
  151 + /// 是否生美会员(0-否,1-是)
  152 + /// </summary>
  153 + [SugarColumn(ColumnName = "F_IsBeautyMember")]
  154 + public int IsBeautyMember { get; set; } = 0;
  155 +
  156 + /// <summary>
  157 + /// 是否医美会员(0-否,1-是)
  158 + /// </summary>
  159 + [SugarColumn(ColumnName = "F_IsMedicalMember")]
  160 + public int IsMedicalMember { get; set; } = 0;
  161 +
  162 + /// <summary>
  163 + /// 是否科技部会员(0-否,1-是)
  164 + /// </summary>
  165 + [SugarColumn(ColumnName = "F_IsTechMember")]
  166 + public int IsTechMember { get; set; } = 0;
  167 +
  168 + /// <summary>
  169 + /// 成为生美会员时间
  170 + /// </summary>
  171 + [SugarColumn(ColumnName = "F_BeautyMemberTime")]
  172 + public DateTime? BeautyMemberTime { get; set; }
  173 +
  174 + /// <summary>
  175 + /// 成为医美会员时间
  176 + /// </summary>
  177 + [SugarColumn(ColumnName = "F_MedicalMemberTime")]
  178 + public DateTime? MedicalMemberTime { get; set; }
  179 +
  180 + /// <summary>
  181 + /// 成为科技部会员时间
  182 + /// </summary>
  183 + [SugarColumn(ColumnName = "F_TechMemberTime")]
  184 + public DateTime? TechMemberTime { get; set; }
  185 +
  186 + /// <summary>
  187 + /// 首次到店时间
  188 + /// </summary>
  189 + [SugarColumn(ColumnName = "F_FirstVisitTime")]
  190 + public DateTime? FirstVisitTime { get; set; }
  191 +
  192 + /// <summary>
  193 + /// 最后到店时间
  194 + /// </summary>
  195 + [SugarColumn(ColumnName = "F_LastVisitTime")]
  196 + public DateTime? LastVisitTime { get; set; }
  197 +
  198 + /// <summary>
  199 + /// 到店天数
  200 + /// </summary>
  201 + [SugarColumn(ColumnName = "F_VisitDays")]
  202 + public int VisitDays { get; set; } = 0;
  203 +
  204 + /// <summary>
  205 + /// 沉睡开始时间
  206 + /// </summary>
  207 + [SugarColumn(ColumnName = "F_SleepStartTime")]
  208 + public DateTime? SleepStartTime { get; set; }
  209 +
  210 + /// <summary>
  211 + /// 沉睡天数
  212 + /// </summary>
  213 + [SugarColumn(ColumnName = "F_SleepDays")]
  214 + public int SleepDays { get; set; } = 0;
  215 +
  216 + /// <summary>
  217 + /// 耗卡总金额(累计所有耗卡的金额总和)
  218 + /// </summary>
  219 + [SugarColumn(ColumnName = "F_TotalConsumeAmount")]
  220 + public decimal TotalConsumeAmount { get; set; } = 0m;
  221 +
  222 + /// <summary>
  223 + /// 开卡总金额(累计所有开单的实付业绩总和)
  224 + /// </summary>
  225 + [SugarColumn(ColumnName = "F_TotalBillingAmount")]
  226 + public decimal TotalBillingAmount { get; set; } = 0m;
  227 +
  228 + /// <summary>
  229 + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额)
  230 + /// </summary>
  231 + [SugarColumn(ColumnName = "F_RemainingRightsAmount")]
  232 + public decimal RemainingRightsAmount { get; set; } = 0m;
  233 +
  234 + /// <summary>
  235 + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等)
  236 + /// </summary>
  237 + [SugarColumn(ColumnName = "F_ConsumeLevel")]
  238 + public int ConsumeLevel { get; set; } = 0;
  239 +
  240 + /// <summary>
  241 + /// 消费等级更新时间
  242 + /// </summary>
  243 + [SugarColumn(ColumnName = "F_ConsumeLevelUpdateTime")]
  244 + public DateTime? ConsumeLevelUpdateTime { get; set; }
  245 +
  246 + /// <summary>
145 247 /// 是否有效
146 248 /// </summary>
147 249 [SugarColumn(ColumnName = "F_IsEffective")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_person_times_record/LqPersonTimesRecordEntity.cs
... ... @@ -31,6 +31,12 @@ namespace NCC.Extend.Entitys.lq_person_times_record
31 31 public string BusinessType { get; set; }
32 32  
33 33 /// <summary>
  34 + /// 是否有开单(0-否,1-是)
  35 + /// </summary>
  36 + [SugarColumn(ColumnName = "F_HasBilling")]
  37 + public int HasBilling { get; set; } = 0;
  38 +
  39 + /// <summary>
34 40 /// 人员类型(健康师/科技老师)
35 41 /// </summary>
36 42 [SugarColumn(ColumnName = "F_PersonType")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/MemberTypeEnum.cs
... ... @@ -31,5 +31,10 @@ namespace NCC.Extend.Entitys.Enum
31 31 [Description("会员")]
32 32 会员 = 3,
33 33  
  34 + /// <summary>
  35 + /// 归档
  36 + /// </summary>
  37 + [Description("归档")]
  38 + 归档 = 4,
34 39 }
35 40 }
36 41 \ No newline at end of file
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs
... ... @@ -12,6 +12,25 @@ namespace NCC.Extend.Entitys.Mapper.LqKhxx
12 12 {
13 13 // 由于khxf字段现在是string类型,不需要特殊的映射处理
14 14 // 使用默认的映射即可
  15 +
  16 + // 配置实体到详情输出的映射(确保新字段正确映射)
  17 + config.ForType<LqKhxxEntity, LqKhxxInfoOutput>()
  18 + .Map(dest => dest.isBeautyMember, src => src.IsBeautyMember)
  19 + .Map(dest => dest.isMedicalMember, src => src.IsMedicalMember)
  20 + .Map(dest => dest.isTechMember, src => src.IsTechMember)
  21 + .Map(dest => dest.beautyMemberTime, src => src.BeautyMemberTime)
  22 + .Map(dest => dest.medicalMemberTime, src => src.MedicalMemberTime)
  23 + .Map(dest => dest.techMemberTime, src => src.TechMemberTime)
  24 + .Map(dest => dest.firstVisitTime, src => src.FirstVisitTime)
  25 + .Map(dest => dest.lastVisitTime, src => src.LastVisitTime)
  26 + .Map(dest => dest.visitDays, src => src.VisitDays)
  27 + .Map(dest => dest.sleepStartTime, src => src.SleepStartTime)
  28 + .Map(dest => dest.sleepDays, src => src.SleepDays)
  29 + .Map(dest => dest.totalConsumeAmount, src => src.TotalConsumeAmount)
  30 + .Map(dest => dest.totalBillingAmount, src => src.TotalBillingAmount)
  31 + .Map(dest => dest.remainingRightsAmount, src => src.RemainingRightsAmount)
  32 + .Map(dest => dest.consumeLevel, src => src.ConsumeLevel)
  33 + .Map(dest => dest.consumeLevelUpdateTime, src => src.ConsumeLevelUpdateTime);
15 34 }
16 35 }
17 36 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -30,12 +30,14 @@ using NCC.Extend.Entitys.lq_xh_hyhk;
30 30 using NCC.Extend.Entitys.lq_xh_pxmx;
31 31 using NCC.Extend.Entitys.lq_xmzl;
32 32 using NCC.Extend.Interfaces.LqKhxx;
  33 +using NCC.Extend.Entitys.Config;
33 34 using NCC.System.Entitys.Permission;
34 35 using NCC.FriendlyException;
35 36 using NCC.JsonSerialization;
36 37 using SqlSugar;
37 38 using Yitter.IdGenerator;
38 39 using NCC.Extend.Entitys.lq_kd_deductinfo;
  40 +using Microsoft.AspNetCore.Authorization;
39 41  
40 42 namespace NCC.Extend.LqKhxx
41 43 {
... ... @@ -109,6 +111,9 @@ namespace NCC.Extend.LqKhxx
109 111 public async Task<dynamic> GetList([FromQuery] LqKhxxListQueryInput input)
110 112 {
111 113 var sidx = input.sidx == null ? "id" : input.sidx;
  114 + List<string> queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject<List<string>>() : null;
  115 + DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null;
  116 + DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null;
112 117 var data = await _db.Queryable<LqKhxxEntity>()
113 118 .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord))
114 119 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
... ... @@ -125,6 +130,8 @@ namespace NCC.Extend.LqKhxx
125 130 .WhereIF(!string.IsNullOrEmpty(input.jdqd), p => p.Jdqd.Contains(input.jdqd))
126 131 .WhereIF(!string.IsNullOrEmpty(input.lxdz), p => p.Lxdz.Contains(input.lxdz))
127 132 .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz))
  133 + .WhereIF(queryZcsj != null, p => p.CreateTime >= new DateTime(startZcsj.ToDate().Year, startZcsj.ToDate().Month, startZcsj.ToDate().Day, 0, 0, 0))
  134 + .WhereIF(queryZcsj != null, p => p.CreateTime <= new DateTime(endZcsj.ToDate().Year, endZcsj.ToDate().Month, endZcsj.ToDate().Day, 23, 59, 59))
128 135 .Select(it => new LqKhxxListOutput
129 136 {
130 137 id = it.Id,
... ... @@ -153,6 +160,22 @@ namespace NCC.Extend.LqKhxx
153 160 mainHealthUserName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.MainHealthUser).Select(u => u.RealName),
154 161 subHealthUserName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.SubHealthUser).Select(u => u.RealName),
155 162 tjrName = SqlFunc.Subqueryable<LqKhxxEntity>().Where(u => u.Id == it.Tjr).Select(u => u.Khmc),
  163 + isBeautyMember = it.IsBeautyMember,
  164 + isMedicalMember = it.IsMedicalMember,
  165 + isTechMember = it.IsTechMember,
  166 + beautyMemberTime = it.BeautyMemberTime,
  167 + medicalMemberTime = it.MedicalMemberTime,
  168 + techMemberTime = it.TechMemberTime,
  169 + firstVisitTime = it.FirstVisitTime,
  170 + lastVisitTime = it.LastVisitTime,
  171 + visitDays = it.VisitDays,
  172 + sleepStartTime = it.SleepStartTime,
  173 + sleepDays = it.SleepDays,
  174 + totalConsumeAmount = it.TotalConsumeAmount,
  175 + totalBillingAmount = it.TotalBillingAmount,
  176 + remainingRightsAmount = it.RemainingRightsAmount,
  177 + consumeLevel = it.ConsumeLevel,
  178 + consumeLevelUpdateTime = it.ConsumeLevelUpdateTime
156 179 })
157 180 .MergeTable()
158 181 .OrderBy(sidx + " " + input.sort)
... ... @@ -180,6 +203,7 @@ namespace NCC.Extend.LqKhxx
180 203 var entity = input.Adapt<LqKhxxEntity>();
181 204 entity.Id = YitIdHelper.NextId().ToString();
182 205 entity.Dah = "LQ" + DateTime.Now.ToString("yyyyMMddHHmmssfff");
  206 + entity.Zcsj = DateTime.Now;
183 207 // 处理客户消费字段:将数组转换为逗号分隔的字符串
184 208 if (input.khxf != null && input.khxf.Count > 0)
185 209 {
... ... @@ -1048,6 +1072,11 @@ namespace NCC.Extend.LqKhxx
1048 1072 memberQuery = memberQuery.Where(x => x.Gsmd == input.BelongStoreId);
1049 1073 }
1050 1074  
  1075 + if (input.MemberType >= 0)
  1076 + {
  1077 + memberQuery = memberQuery.Where(x => x.Khlx == input.MemberType.GetHashCode().ToString());
  1078 + }
  1079 +
1051 1080 // 如果指定了开单门店、开单人、品项ID等条件,需要通过开单表进行筛选
1052 1081 if (!string.IsNullOrEmpty(input.BillingStoreId) ||
1053 1082 !string.IsNullOrEmpty(input.BillingUserId) ||
... ... @@ -1070,13 +1099,9 @@ namespace NCC.Extend.LqKhxx
1070 1099 }
1071 1100  
1072 1101 // 如果指定了品项ID,需要通过品项明细表进行筛选
1073   - if (!string.IsNullOrEmpty(input.PurchaseItemId) ||
1074   - !string.IsNullOrEmpty(input.GiftItemId) ||
1075   - !string.IsNullOrEmpty(input.ExperienceItemId))
  1102 + if (!string.IsNullOrEmpty(input.PurchaseItemId) || !string.IsNullOrEmpty(input.GiftItemId) || !string.IsNullOrEmpty(input.ExperienceItemId))
1076 1103 {
1077   - var itemQuery = _db.Queryable<LqKdPxmxEntity>()
1078   - .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
1079   -
  1104 + var itemQuery = _db.Queryable<LqKdPxmxEntity>().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
1080 1105 if (!string.IsNullOrEmpty(input.PurchaseItemId))
1081 1106 {
1082 1107 itemQuery = itemQuery.Where(x => x.Px == input.PurchaseItemId && x.SourceType == "购买");
... ... @@ -1147,7 +1172,8 @@ namespace NCC.Extend.LqKhxx
1147 1172 x.Dah,
1148 1173 x.Khmc,
1149 1174 x.Sjh,
1150   - x.Gsmd
  1175 + x.Gsmd,
  1176 + x.Khlx
1151 1177 })
1152 1178 .ToListAsync();
1153 1179  
... ... @@ -1328,6 +1354,7 @@ namespace NCC.Extend.LqKhxx
1328 1354 MemberCode = member.Dah ?? "",
1329 1355 MemberName = member.Khmc ?? "",
1330 1356 MemberPhone = member.Sjh ?? "",
  1357 + MemberType = member.Khlx.ToInt(),
1331 1358 BelongStoreId = member.Gsmd ?? "",
1332 1359 BelongStoreName = stores.ContainsKey(member.Gsmd ?? "") ? stores[member.Gsmd] : ""
1333 1360 };
... ... @@ -1531,5 +1558,507 @@ namespace NCC.Extend.LqKhxx
1531 1558 }
1532 1559 #endregion
1533 1560  
  1561 + #region 会员信息更新
  1562 + /// <summary>
  1563 + /// 更新会员信息(单个会员)
  1564 + /// </summary>
  1565 + /// <param name="memberId">会员ID</param>
  1566 + /// <returns></returns>
  1567 + [HttpPost("UpdateMemberInfo")]
  1568 + public async Task<dynamic> UpdateMemberInfo([FromBody] string memberId)
  1569 + {
  1570 + try
  1571 + {
  1572 + if (string.IsNullOrEmpty(memberId))
  1573 + {
  1574 + throw NCCException.Oh("会员ID不能为空");
  1575 + }
  1576 +
  1577 + var member = await _db.Queryable<LqKhxxEntity>()
  1578 + .Where(x => x.Id == memberId && x.IsEffective == StatusEnum.有效.GetHashCode())
  1579 + .FirstAsync();
  1580 +
  1581 + if (member == null)
  1582 + {
  1583 + throw NCCException.Oh("会员不存在");
  1584 + }
  1585 +
  1586 + await UpdateMemberInfoInternal(member);
  1587 +
  1588 + return new { success = true, message = "更新成功" };
  1589 + }
  1590 + catch (Exception ex)
  1591 + {
  1592 + _logger.LogError(ex, "更新会员信息失败,会员ID:{MemberId}", memberId);
  1593 + throw NCCException.Oh($"更新会员信息失败:{ex.Message}");
  1594 + }
  1595 + }
  1596 +
  1597 + /// <summary>
  1598 + /// 批量更新所有会员信息(高性能版:使用SQL批量更新)
  1599 + /// </summary>
  1600 + /// <returns></returns>
  1601 + [HttpPost("BatchUpdateMemberInfo")]
  1602 + [AllowAnonymous]
  1603 + public async Task<dynamic> BatchUpdateMemberInfo()
  1604 + {
  1605 + try
  1606 + {
  1607 + var startTime = DateTime.Now;
  1608 + _logger.LogInformation("开始批量更新会员信息(高性能SQL版)");
  1609 +
  1610 + // 1. 获取所有有效会员ID
  1611 + var memberIds = await _db.Queryable<LqKhxxEntity>()
  1612 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
  1613 + .Select(x => x.Id)
  1614 + .ToListAsync();
  1615 +
  1616 + var totalCount = memberIds.Count;
  1617 +
  1618 + if (totalCount == 0)
  1619 + {
  1620 + return new
  1621 + {
  1622 + success = true,
  1623 + totalCount = 0,
  1624 + successCount = 0,
  1625 + failCount = 0,
  1626 + duration = "0秒"
  1627 + };
  1628 + }
  1629 +
  1630 + _logger.LogInformation("开始使用SQL批量更新,会员总数:{Total}", totalCount);
  1631 + var sqlStartTime = DateTime.Now;
  1632 +
  1633 + // 使用原生SQL一次性更新所有会员信息
  1634 + var beautyThreshold = MemberInfoUpdateConfig.BeautyMemberAmountThreshold;
  1635 + var medicalThreshold = MemberInfoUpdateConfig.MedicalMemberAmountThreshold;
  1636 + var techThreshold = MemberInfoUpdateConfig.TechMemberAmountThreshold;
  1637 + var minSleepThreshold = MemberInfoUpdateConfig.SleepDaysThresholds.Min();
  1638 + var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  1639 +
  1640 + // 构建SQL更新语句
  1641 + var updateSql = $@"
  1642 +UPDATE lq_khxx kh
  1643 +LEFT JOIN (
  1644 + -- 生美会员判断
  1645 + SELECT
  1646 + pxmx.F_MemberId as MemberId,
  1647 + CASE WHEN SUM(CASE WHEN pxmx.F_ItemCategory = '生美' THEN pxmx.F_ActualPrice ELSE 0 END) > {beautyThreshold} THEN 1 ELSE 0 END as IsBeautyMember,
  1648 + MIN(CASE WHEN pxmx.F_ItemCategory = '生美' AND kd.kdrq IS NOT NULL THEN kd.kdrq ELSE NULL END) as BeautyMemberTime
  1649 + FROM lq_kd_pxmx pxmx
  1650 + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id
  1651 + WHERE pxmx.F_IsEffective = 1
  1652 + AND kd.F_IsEffective = 1
  1653 + AND pxmx.F_ActualPrice > 0
  1654 + AND pxmx.px != '61'
  1655 + GROUP BY pxmx.F_MemberId
  1656 +) beauty ON kh.F_Id = beauty.MemberId
  1657 +LEFT JOIN (
  1658 + -- 医美会员判断
  1659 + SELECT
  1660 + pxmx.F_MemberId as MemberId,
  1661 + CASE WHEN SUM(CASE WHEN pxmx.F_ItemCategory = '医美' THEN pxmx.F_ActualPrice ELSE 0 END) > {medicalThreshold} THEN 1 ELSE 0 END as IsMedicalMember,
  1662 + MIN(CASE WHEN pxmx.F_ItemCategory = '医美' AND kd.kdrq IS NOT NULL THEN kd.kdrq ELSE NULL END) as MedicalMemberTime
  1663 + FROM lq_kd_pxmx pxmx
  1664 + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id
  1665 + WHERE pxmx.F_IsEffective = 1
  1666 + AND kd.F_IsEffective = 1
  1667 + AND pxmx.F_ActualPrice > 0
  1668 + AND pxmx.px != '61'
  1669 + GROUP BY pxmx.F_MemberId
  1670 +) medical ON kh.F_Id = medical.MemberId
  1671 +LEFT JOIN (
  1672 + -- 科技部会员判断
  1673 + SELECT
  1674 + pxmx.F_MemberId as MemberId,
  1675 + CASE WHEN MAX(CASE WHEN pxmx.F_ItemCategory = '科美' AND pxmx.F_ActualPrice >= {techThreshold} THEN 1 ELSE 0 END) = 1 THEN 1 ELSE 0 END as IsTechMember,
  1676 + MIN(CASE WHEN pxmx.F_ItemCategory = '科美' AND pxmx.F_ActualPrice >= {techThreshold} AND kd.kdrq IS NOT NULL THEN kd.kdrq ELSE NULL END) as TechMemberTime
  1677 + FROM lq_kd_pxmx pxmx
  1678 + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id
  1679 + WHERE pxmx.F_IsEffective = 1
  1680 + AND kd.F_IsEffective = 1
  1681 + AND pxmx.F_ActualPrice > 0
  1682 + AND pxmx.px != '61'
  1683 + GROUP BY pxmx.F_MemberId
  1684 +) tech ON kh.F_Id = tech.MemberId
  1685 +LEFT JOIN (
  1686 + -- 到店天数、首次到店时间、最后到店时间
  1687 + SELECT
  1688 + xh.hy as MemberId,
  1689 + COUNT(DISTINCT DATE(xh.hksj)) as VisitDays,
  1690 + MIN(xh.hksj) as FirstVisitTime,
  1691 + MAX(xh.hksj) as LastVisitTime
  1692 + FROM lq_xh_hyhk xh
  1693 + WHERE xh.F_IsEffective = 1
  1694 + GROUP BY xh.hy
  1695 +) visit ON kh.F_Id = visit.MemberId
  1696 +LEFT JOIN (
  1697 + -- 开卡总金额
  1698 + SELECT
  1699 + kd.kdhy as MemberId,
  1700 + COALESCE(SUM(kd.sfyj), 0) as TotalBillingAmount
  1701 + FROM lq_kd_kdjlb kd
  1702 + WHERE kd.F_IsEffective = 1
  1703 + GROUP BY kd.kdhy
  1704 +) billing ON kh.F_Id = billing.MemberId
  1705 +LEFT JOIN (
  1706 + -- 耗卡总金额
  1707 + SELECT
  1708 + xh.hy as MemberId,
  1709 + COALESCE(SUM(xh.xfje), 0) as TotalConsumeAmount
  1710 + FROM lq_xh_hyhk xh
  1711 + WHERE xh.F_IsEffective = 1
  1712 + GROUP BY xh.hy
  1713 +) consume ON kh.F_Id = consume.MemberId
  1714 +LEFT JOIN (
  1715 + -- 退款总金额
  1716 + SELECT
  1717 + tk.hy as MemberId,
  1718 + COALESCE(SUM(tk.F_ActualRefundAmount), 0) as TotalRefundAmount
  1719 + FROM lq_hytk_hytk tk
  1720 + WHERE tk.F_IsEffective = 1
  1721 + GROUP BY tk.hy
  1722 +) refund ON kh.F_Id = refund.MemberId
  1723 +SET
  1724 + kh.F_IsBeautyMember = COALESCE(beauty.IsBeautyMember, 0),
  1725 + kh.F_IsMedicalMember = COALESCE(medical.IsMedicalMember, 0),
  1726 + kh.F_IsTechMember = COALESCE(tech.IsTechMember, 0),
  1727 + kh.F_BeautyMemberTime = CASE WHEN kh.F_BeautyMemberTime IS NULL THEN beauty.BeautyMemberTime ELSE kh.F_BeautyMemberTime END,
  1728 + kh.F_MedicalMemberTime = CASE WHEN kh.F_MedicalMemberTime IS NULL THEN medical.MedicalMemberTime ELSE kh.F_MedicalMemberTime END,
  1729 + kh.F_TechMemberTime = CASE WHEN kh.F_TechMemberTime IS NULL THEN tech.TechMemberTime ELSE kh.F_TechMemberTime END,
  1730 + kh.F_FirstVisitTime = visit.FirstVisitTime,
  1731 + kh.F_LastVisitTime = visit.LastVisitTime,
  1732 + kh.F_VisitDays = COALESCE(visit.VisitDays, 0),
  1733 + kh.F_TotalBillingAmount = COALESCE(billing.TotalBillingAmount, 0),
  1734 + kh.F_TotalConsumeAmount = COALESCE(consume.TotalConsumeAmount, 0),
  1735 + kh.F_RemainingRightsAmount = GREATEST(0, COALESCE(billing.TotalBillingAmount, 0) - COALESCE(consume.TotalConsumeAmount, 0) - COALESCE(refund.TotalRefundAmount, 0)),
  1736 + kh.F_SleepDays = GREATEST(0, DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime))),
  1737 + kh.F_SleepStartTime = CASE
  1738 + WHEN DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime)) >= {minSleepThreshold}
  1739 + AND kh.F_SleepStartTime IS NULL
  1740 + THEN DATE(COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime))
  1741 + WHEN DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime)) < {minSleepThreshold}
  1742 + THEN NULL
  1743 + ELSE kh.F_SleepStartTime
  1744 + END,
  1745 + kh.F_ConsumeLevel = CASE
  1746 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5
  1747 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4
  1748 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3
  1749 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2
  1750 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1
  1751 + ELSE 0
  1752 + END,
  1753 + kh.F_ConsumeLevelUpdateTime = CASE
  1754 + WHEN kh.F_ConsumeLevel != CASE
  1755 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5
  1756 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4
  1757 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3
  1758 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2
  1759 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1
  1760 + ELSE 0
  1761 + END THEN NOW()
  1762 + WHEN kh.F_ConsumeLevelUpdateTime IS NULL AND CASE
  1763 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5
  1764 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4
  1765 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3
  1766 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2
  1767 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1
  1768 + ELSE 0
  1769 + END > 0 THEN NOW()
  1770 + ELSE kh.F_ConsumeLevelUpdateTime
  1771 + END
  1772 +WHERE kh.F_IsEffective = 1";
  1773 +
  1774 + var affectedRows = await _db.Ado.ExecuteCommandAsync(updateSql);
  1775 + var sqlEndTime = DateTime.Now;
  1776 + var duration = (sqlEndTime - sqlStartTime).TotalSeconds;
  1777 +
  1778 + _logger.LogInformation("SQL批量更新完成,影响行数:{Rows},耗时:{Duration}秒", affectedRows, duration);
  1779 +
  1780 + var endTime = DateTime.Now;
  1781 + var totalDuration = (endTime - startTime).TotalSeconds;
  1782 +
  1783 + _logger.LogInformation("批量更新会员信息完成,总数量:{Total},影响行数:{Rows},耗时:{Duration}秒",
  1784 + totalCount, affectedRows, totalDuration);
  1785 +
  1786 + return new
  1787 + {
  1788 + success = true,
  1789 + totalCount = totalCount,
  1790 + successCount = affectedRows,
  1791 + failCount = totalCount - affectedRows,
  1792 + duration = $"{totalDuration:F2}秒",
  1793 + sqlDuration = $"{duration:F2}秒"
  1794 + };
  1795 + }
  1796 + catch (Exception ex)
  1797 + {
  1798 + _logger.LogError(ex, "批量更新会员信息失败");
  1799 + throw NCCException.Oh($"批量更新会员信息失败:{ex.Message}");
  1800 + }
  1801 + }
  1802 +
  1803 + /// <summary>
  1804 + /// 更新会员信息内部方法
  1805 + /// </summary>
  1806 + /// <param name="member">会员实体</param>
  1807 + private async Task UpdateMemberInfoInternal(LqKhxxEntity member)
  1808 + {
  1809 + // 1. 判断生美/医美/科技部会员
  1810 + await UpdateMemberTypeFlags(member);
  1811 +
  1812 + // 2. 计算到店天数
  1813 + await UpdateVisitDays(member);
  1814 +
  1815 + // 3. 计算沉睡天数
  1816 + await UpdateSleepDays(member);
  1817 +
  1818 + // 4. 计算消费金额和等级
  1819 + await UpdateConsumeInfo(member);
  1820 +
  1821 + // 5. 更新到数据库
  1822 + await _db.Updateable(member)
  1823 + .UpdateColumns(x => new
  1824 + {
  1825 + x.IsBeautyMember,
  1826 + x.IsMedicalMember,
  1827 + x.IsTechMember,
  1828 + x.BeautyMemberTime,
  1829 + x.MedicalMemberTime,
  1830 + x.TechMemberTime,
  1831 + x.FirstVisitTime,
  1832 + x.LastVisitTime,
  1833 + x.VisitDays,
  1834 + x.SleepStartTime,
  1835 + x.SleepDays,
  1836 + x.TotalConsumeAmount,
  1837 + x.TotalBillingAmount,
  1838 + x.RemainingRightsAmount,
  1839 + x.ConsumeLevel,
  1840 + x.ConsumeLevelUpdateTime
  1841 + })
  1842 + .ExecuteCommandAsync();
  1843 + }
  1844 +
  1845 + /// <summary>
  1846 + /// 更新会员类型标识(生美/医美/科技部会员)
  1847 + /// </summary>
  1848 + private async Task UpdateMemberTypeFlags(LqKhxxEntity member)
  1849 + {
  1850 + // 查询该会员的所有有效开单品项明细
  1851 + var billingItems = await _db.Queryable<LqKdPxmxEntity>()
  1852 + .InnerJoin<LqKdKdjlbEntity>((pxmx, kd) => pxmx.Glkdbh == kd.Id)
  1853 + .Where((pxmx, kd) =>
  1854 + pxmx.MemberId == member.Id
  1855 + && pxmx.IsEffective == StatusEnum.有效.GetHashCode()
  1856 + && kd.IsEffective == StatusEnum.有效.GetHashCode()
  1857 + && pxmx.ActualPrice > 0
  1858 + && pxmx.Px != "61") // 排除女神卡
  1859 + .Select((pxmx, kd) => new
  1860 + {
  1861 + pxmx.ItemCategory,
  1862 + pxmx.ActualPrice,
  1863 + pxmx.Pxjg,
  1864 + kd.Kdrq
  1865 + })
  1866 + .ToListAsync();
  1867 +
  1868 + // 判断生美会员:开卡品项为生美并且金额大于阈值
  1869 + var beautyAmount = billingItems
  1870 + .Where(x => x.ItemCategory == "生美")
  1871 + .Sum(x => x.ActualPrice);
  1872 +
  1873 + if (beautyAmount > MemberInfoUpdateConfig.BeautyMemberAmountThreshold)
  1874 + {
  1875 + member.IsBeautyMember = 1;
  1876 + if (!member.BeautyMemberTime.HasValue)
  1877 + {
  1878 + var firstBeautyBilling = billingItems
  1879 + .Where(x => x.ItemCategory == "生美")
  1880 + .OrderBy(x => x.Kdrq)
  1881 + .FirstOrDefault();
  1882 + member.BeautyMemberTime = firstBeautyBilling?.Kdrq;
  1883 + }
  1884 + }
  1885 + else
  1886 + {
  1887 + member.IsBeautyMember = 0;
  1888 + member.BeautyMemberTime = null;
  1889 + }
  1890 +
  1891 + // 判断医美会员:开卡品项为医美并且金额大于阈值
  1892 + var medicalAmount = billingItems
  1893 + .Where(x => x.ItemCategory == "医美")
  1894 + .Sum(x => x.ActualPrice);
  1895 +
  1896 + if (medicalAmount > MemberInfoUpdateConfig.MedicalMemberAmountThreshold)
  1897 + {
  1898 + member.IsMedicalMember = 1;
  1899 + if (!member.MedicalMemberTime.HasValue)
  1900 + {
  1901 + var firstMedicalBilling = billingItems
  1902 + .Where(x => x.ItemCategory == "医美")
  1903 + .OrderBy(x => x.Kdrq)
  1904 + .FirstOrDefault();
  1905 + member.MedicalMemberTime = firstMedicalBilling?.Kdrq;
  1906 + }
  1907 + }
  1908 + else
  1909 + {
  1910 + member.IsMedicalMember = 0;
  1911 + member.MedicalMemberTime = null;
  1912 + }
  1913 +
  1914 + // 判断科技部会员:科美单品项金额大于等于阈值
  1915 + var hasTechMember = billingItems
  1916 + .Where(x => x.ItemCategory == "科美")
  1917 + .Any(x => x.ActualPrice >= MemberInfoUpdateConfig.TechMemberAmountThreshold);
  1918 +
  1919 + if (hasTechMember)
  1920 + {
  1921 + member.IsTechMember = 1;
  1922 + if (!member.TechMemberTime.HasValue)
  1923 + {
  1924 + var firstTechBilling = billingItems
  1925 + .Where(x => x.ItemCategory == "科美" && x.ActualPrice >= MemberInfoUpdateConfig.TechMemberAmountThreshold)
  1926 + .OrderBy(x => x.Kdrq)
  1927 + .FirstOrDefault();
  1928 + member.TechMemberTime = firstTechBilling?.Kdrq;
  1929 + }
  1930 + }
  1931 + else
  1932 + {
  1933 + member.IsTechMember = 0;
  1934 + member.TechMemberTime = null;
  1935 + }
  1936 + }
  1937 +
  1938 + /// <summary>
  1939 + /// 更新到店天数
  1940 + /// </summary>
  1941 + private async Task UpdateVisitDays(LqKhxxEntity member)
  1942 + {
  1943 + // 查询该会员的所有有效耗卡记录,按天去重
  1944 + var visitDates = await _db.Queryable<LqXhHyhkEntity>()
  1945 + .Where(x =>
  1946 + x.Hy == member.Id
  1947 + && x.IsEffective == StatusEnum.有效.GetHashCode())
  1948 + .Select(x => new
  1949 + {
  1950 + VisitDate = SqlFunc.ToDate(x.Hksj)
  1951 + })
  1952 + .Distinct()
  1953 + .ToListAsync();
  1954 +
  1955 + member.VisitDays = visitDates.Count;
  1956 +
  1957 + // 更新首次到店时间和最后到店时间
  1958 + if (visitDates.Any())
  1959 + {
  1960 + var firstVisit = await _db.Queryable<LqXhHyhkEntity>()
  1961 + .Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  1962 + .OrderBy(x => x.Hksj)
  1963 + .Select(x => x.Hksj)
  1964 + .FirstAsync();
  1965 + member.FirstVisitTime = firstVisit;
  1966 +
  1967 + var lastVisit = await _db.Queryable<LqXhHyhkEntity>()
  1968 + .Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  1969 + .OrderBy(x => x.Hksj, OrderByType.Desc)
  1970 + .Select(x => x.Hksj)
  1971 + .FirstAsync();
  1972 + member.LastVisitTime = lastVisit;
  1973 + }
  1974 + else
  1975 + {
  1976 + member.FirstVisitTime = null;
  1977 + member.LastVisitTime = null;
  1978 + }
  1979 + }
  1980 +
  1981 + /// <summary>
  1982 + /// 更新沉睡天数
  1983 + /// </summary>
  1984 + private Task UpdateSleepDays(LqKhxxEntity member)
  1985 + {
  1986 + var now = DateTime.Now;
  1987 + var lastConsumeTime = member.LastConsumeTime;
  1988 +
  1989 + if (!lastConsumeTime.HasValue)
  1990 + {
  1991 + // 如果没有最后消费时间,使用最后到店时间
  1992 + lastConsumeTime = member.LastVisitTime;
  1993 + }
  1994 +
  1995 + if (lastConsumeTime.HasValue)
  1996 + {
  1997 + var daysSinceLastConsume = (now.Date - lastConsumeTime.Value.Date).Days;
  1998 + member.SleepDays = Math.Max(0, daysSinceLastConsume);
  1999 +
  2000 + // 如果超过最小沉睡阈值,设置沉睡开始时间
  2001 + var minSleepThreshold = MemberInfoUpdateConfig.SleepDaysThresholds.Min();
  2002 + if (member.SleepDays >= minSleepThreshold)
  2003 + {
  2004 + if (!member.SleepStartTime.HasValue)
  2005 + {
  2006 + member.SleepStartTime = lastConsumeTime.Value.Date;
  2007 + }
  2008 + }
  2009 + else
  2010 + {
  2011 + member.SleepStartTime = null;
  2012 + }
  2013 + }
  2014 + else
  2015 + {
  2016 + member.SleepDays = 0;
  2017 + member.SleepStartTime = null;
  2018 + }
  2019 +
  2020 + return Task.CompletedTask;
  2021 + }
  2022 +
  2023 + /// <summary>
  2024 + /// 更新消费信息(开卡总金额、耗卡总金额、剩余权益总金额、消费等级)
  2025 + /// </summary>
  2026 + private async Task UpdateConsumeInfo(LqKhxxEntity member)
  2027 + {
  2028 + // 1. 计算开卡总金额(累计所有开单的实付业绩总和)
  2029 + var totalBillingAmount = await _db.Queryable<LqKdKdjlbEntity>().Where(x => x.Kdhy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.Sfyj) ?? 0m;
  2030 + member.TotalBillingAmount = totalBillingAmount;
  2031 +
  2032 + // 2. 计算耗卡总金额(累计所有耗卡的金额总和)
  2033 + var totalConsumeAmount = await _db.Queryable<LqXhHyhkEntity>().Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.Xfje) ?? 0m;
  2034 + member.TotalConsumeAmount = totalConsumeAmount;
  2035 +
  2036 + // 3. 计算退款总金额
  2037 + var totalRefundAmount = await _db.Queryable<LqHytkHytkEntity>().Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.ActualRefundAmount) ?? 0m;
  2038 +
  2039 + // 4. 计算剩余权益总金额
  2040 + member.RemainingRightsAmount = totalBillingAmount - totalConsumeAmount - totalRefundAmount;
  2041 + if (member.RemainingRightsAmount < 0)
  2042 + {
  2043 + member.RemainingRightsAmount = 0m;
  2044 + }
  2045 +
  2046 + // 5. 计算消费等级(根据耗卡总金额)
  2047 + var oldLevel = member.ConsumeLevel;
  2048 + member.ConsumeLevel = MemberInfoUpdateConfig.GetConsumeLevel(totalConsumeAmount);
  2049 +
  2050 + // 如果等级发生变化,更新等级更新时间
  2051 + if (oldLevel != member.ConsumeLevel)
  2052 + {
  2053 + member.ConsumeLevelUpdateTime = DateTime.Now;
  2054 + }
  2055 + else if (!member.ConsumeLevelUpdateTime.HasValue && member.ConsumeLevel > 0)
  2056 + {
  2057 + // 如果之前没有等级更新时间,但当前有等级,设置当前时间
  2058 + member.ConsumeLevelUpdateTime = DateTime.Now;
  2059 + }
  2060 + }
  2061 + #endregion
  2062 +
1534 2063 }
1535 2064 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
... ... @@ -192,6 +192,7 @@ namespace NCC.Extend.LqTkjlb
192 192 MemberInfo.Khlx = MemberTypeEnum.线索.GetHashCode().ToString();
193 193 MemberInfo.Dah = MemberNumber;
194 194 MemberInfo.Jdqd = "19.9卡";
  195 + MemberInfo.Zcsj = DateTime.Now;
195 196  
196 197 //找到input.expansionUserId的用户信息
197 198 var userInfo = await _db.Queryable<UserEntity>().Where(u => u.Id == input.expansionUserId).FirstAsync();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
... ... @@ -32,6 +32,7 @@ using NCC.Extend.Entitys.lq_xh_jksyj;
32 32 using NCC.Extend.Entitys.lq_xh_kjbsyj;
33 33 using NCC.Extend.Entitys.lq_xh_pxmx;
34 34 using NCC.Extend.Entitys.lq_xmzl;
  35 +using NCC.Extend.Entitys.lq_mdxx;
35 36 using NCC.Extend.Interfaces.LqXhHyhk;
36 37 using NCC.FriendlyException;
37 38 using NCC.JsonSerialization;
... ... @@ -899,6 +900,11 @@ namespace NCC.Extend.LqXhHyhk
899 900 {
900 901 memberInfo.Khlx = MemberTypeEnum.新客.GetHashCode().ToString();
901 902 }
  903 + //判断消费金额是否大于0
  904 + if (entity.Sgfy > 0)
  905 + {
  906 + memberInfo.LastConsumeTime = DateTime.Now;
  907 + }
902 908 //保存会员信息
903 909 await _db.Updateable(memberInfo).ExecuteCommandAsync();
904 910 // 新增会员耗卡记录
... ... @@ -1003,6 +1009,8 @@ namespace NCC.Extend.LqXhHyhk
1003 1009 }
1004 1010 }
1005 1011 }
  1012 + //查询开单记录
  1013 + var billingRecord = await _db.Queryable<LqKdKdjlbEntity>().Where(x => x.Kdhy == entity.Hy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Sfyj > 0).AnyAsync();
1006 1014 // 剔除所有T区健康师(姓名中包含"T区"的健康师)
1007 1015 var NoTallJksyjEntities = allJksyjEntities.Where(x => x.Jks != null && x.Jks != "" && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
1008 1016 //获取这次耗卡有多少个健康师
... ... @@ -1025,6 +1033,7 @@ namespace NCC.Extend.LqXhHyhk
1025 1033 Quantity = 1 / (decimal)jksCount,
1026 1034 CreateTime = DateTime.Now,
1027 1035 IsEffective = StatusEnum.有效.GetHashCode(),
  1036 + HasBilling = billingRecord ? 1 : 0,
1028 1037 };
1029 1038 allPersonTimesRecordEntities.Add(personTimesRecordEntity);
1030 1039 }
... ... @@ -1050,6 +1059,7 @@ namespace NCC.Extend.LqXhHyhk
1050 1059 Quantity = 1 / (decimal)kjbCount,
1051 1060 CreateTime = DateTime.Now,
1052 1061 IsEffective = StatusEnum.有效.GetHashCode(),
  1062 + HasBilling = billingRecord ? 1 : 0,
1053 1063 };
1054 1064 allPersonTimesRecordEntities.Add(personTimesRecordEntity);
1055 1065 }
... ... @@ -1894,5 +1904,180 @@ namespace NCC.Extend.LqXhHyhk
1894 1904 }
1895 1905 #endregion
1896 1906  
  1907 + #region 耗卡品项明细列表
  1908 + /// <summary>
  1909 + /// 获取耗卡品项明细记录列表
  1910 + /// </summary>
  1911 + /// <remarks>
  1912 + /// 根据多种条件查询耗卡品项明细记录列表,支持分页、排序、筛选等功能
  1913 + ///
  1914 + /// 示例请求:
  1915 + /// GET /api/Extend/LqXhHyhk/consume-item-detail-list?currentPage=1&amp;pageSize=10&amp;startTime=2025-01-01&amp;endTime=2025-01-31
  1916 + ///
  1917 + /// 传入参数说明:
  1918 + /// - currentPage: 当前页码(必填,默认1)
  1919 + /// - pageSize: 每页数量(必填,默认10)
  1920 + /// - sidx: 排序字段(可选,默认"yjsj")
  1921 + /// - sort: 排序方式(可选,ASC/DESC,默认DESC)
  1922 + /// - Id: 品项明细ID(可选,精确匹配)
  1923 + /// - ConsumeId: 耗卡记录ID(可选,精确匹配)
  1924 + /// - StartConsumeTime/EndConsumeTime: 耗卡时间范围(可选,格式:yyyy-MM-dd)
  1925 + /// - startTime/endTime: 耗卡时间范围(兼容参数名,格式:yyyy-MM-dd)
  1926 + /// - MemberId: 会员ID(可选,精确匹配)
  1927 + /// - MemberName: 会员名称(可选,模糊查询)
  1928 + /// - MemberPhone: 会员手机号(可选,精确匹配)
  1929 + /// - ItemId: 品项ID(可选,精确匹配)
  1930 + /// - ItemName: 品项名称(可选,模糊查询)
  1931 + /// - ItemType: 品项类型(可选,精确匹配)
  1932 + /// - SourceType: 来源类型(可选,精确匹配)
  1933 + /// - StoreId: 门店ID(可选,精确匹配)
  1934 + ///
  1935 + /// 返回字段说明:
  1936 + /// - id: 品项明细ID
  1937 + /// - consumeId: 耗卡记录ID
  1938 + /// - consumeTime: 耗卡时间(DateTime格式)
  1939 + /// - memberName: 会员名称(关联会员表)
  1940 + /// - memberPhone: 会员手机号(关联会员表)
  1941 + /// - itemName: 品项名称(品项明细表字段)
  1942 + /// - itemType: 品项类型(关联项目资料表的qt2字段)
  1943 + /// - itemPrice: 品项价格(decimal类型)
  1944 + /// - projectNumber: 项目次数(decimal类型,支持小数)
  1945 + /// - originalProjectNumber: 原始项目次数(decimal类型)
  1946 + /// - totalPrice: 合计金额(decimal类型)
  1947 + /// - sourceType: 来源类型(字符串,如:购买、赠送、体验)
  1948 + /// - storeId: 门店ID(关联耗卡记录表的门店ID)
  1949 + /// - storeName: 门店名称(关联门店表的店名字段)
  1950 + /// </remarks>
  1951 + /// <param name="input">查询参数</param>
  1952 + /// <returns>耗卡品项明细记录列表(分页)</returns>
  1953 + /// <response code="200">查询成功,返回耗卡品项明细记录列表</response>
  1954 + /// <response code="400">参数错误,如页码或页大小无效</response>
  1955 + /// <response code="500">服务器错误,查询过程中发生异常</response>
  1956 + [HttpGet("consume-item-detail-list")]
  1957 + public async Task<dynamic> GetConsumeItemDetailList([FromQuery] ConsumeItemDetailListQueryInput input)
  1958 + {
  1959 + try
  1960 + {
  1961 + var sidx = input.sidx == null ? "yjsj" : input.sidx;
  1962 + var sort = string.IsNullOrEmpty(input.sort) ? "DESC" : input.sort;
  1963 +
  1964 + // 处理耗卡时间范围(兼容 StartConsumeTime/EndConsumeTime 和 startTime/endTime 两种参数名)
  1965 + DateTime? startConsumeTime = null;
  1966 + DateTime? endConsumeTime = null;
  1967 +
  1968 + // 优先使用 StartConsumeTime/EndConsumeTime,如果没有则使用 startTime/endTime
  1969 + string startTimeStr = !string.IsNullOrEmpty(input.StartConsumeTime) ? input.StartConsumeTime : input.startTime;
  1970 + string endTimeStr = !string.IsNullOrEmpty(input.EndConsumeTime) ? input.EndConsumeTime : input.endTime;
  1971 +
  1972 + if (!string.IsNullOrEmpty(startTimeStr) && !string.IsNullOrEmpty(endTimeStr))
  1973 + {
  1974 + // 尝试解析日期字符串(支持多种格式)
  1975 + if (DateTime.TryParse(startTimeStr, out DateTime startDate))
  1976 + {
  1977 + startConsumeTime = startDate;
  1978 + }
  1979 + else
  1980 + {
  1981 + throw NCCException.Oh($"开始时间格式错误:{startTimeStr}");
  1982 + }
  1983 +
  1984 + if (DateTime.TryParse(endTimeStr, out DateTime endDate))
  1985 + {
  1986 + endConsumeTime = endDate;
  1987 + }
  1988 + else
  1989 + {
  1990 + throw NCCException.Oh($"结束时间格式错误:{endTimeStr}");
  1991 + }
  1992 + }
  1993 +
  1994 + // 优化查询:先分页查询主表,再批量查询关联数据,避免子查询性能问题
  1995 + // 1. 先构建基础查询条件
  1996 + var baseQuery = _db.Queryable<LqXhPxmxEntity>()
  1997 + .Where(pxmx => pxmx.IsEffective == StatusEnum.有效.GetHashCode())
  1998 + .WhereIF(!string.IsNullOrEmpty(input.Id), pxmx => pxmx.Id == input.Id)
  1999 + .WhereIF(!string.IsNullOrEmpty(input.ConsumeId), pxmx => pxmx.ConsumeInfoId == input.ConsumeId)
  2000 + .WhereIF(startConsumeTime.HasValue, pxmx => pxmx.Yjsj >= new DateTime(startConsumeTime.Value.Year, startConsumeTime.Value.Month, startConsumeTime.Value.Day, 0, 0, 0))
  2001 + .WhereIF(endConsumeTime.HasValue, pxmx => pxmx.Yjsj <= new DateTime(endConsumeTime.Value.Year, endConsumeTime.Value.Month, endConsumeTime.Value.Day, 23, 59, 59))
  2002 + .WhereIF(!string.IsNullOrEmpty(input.MemberId), pxmx => pxmx.MemberId == input.MemberId)
  2003 + .WhereIF(!string.IsNullOrEmpty(input.ItemId), pxmx => pxmx.Px == input.ItemId)
  2004 + .WhereIF(!string.IsNullOrEmpty(input.ItemName), pxmx => pxmx.Pxmc != null && pxmx.Pxmc.Contains(input.ItemName))
  2005 + .WhereIF(!string.IsNullOrEmpty(input.SourceType), pxmx => pxmx.SourceType == input.SourceType)
  2006 + .WhereIF(!string.IsNullOrEmpty(input.ItemType), pxmx => pxmx.ItemCategory == input.ItemType);
  2007 +
  2008 + // 2. 通过 EXISTS 子查询筛选关联字段(在分页前筛选,确保分页准确)
  2009 + baseQuery = baseQuery.WhereIF(!string.IsNullOrEmpty(input.MemberName), pxmx => SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == pxmx.MemberId && x.Khmc != null && x.Khmc.Contains(input.MemberName)).Any())
  2010 + .WhereIF(!string.IsNullOrEmpty(input.MemberPhone), pxmx => SqlFunc.Subqueryable<LqKhxxEntity>().Where(x => x.Id == pxmx.MemberId && x.Sjh == input.MemberPhone).Any())
  2011 + .WhereIF(!string.IsNullOrEmpty(input.StoreId), pxmx => SqlFunc.Subqueryable<LqXhHyhkEntity>().Where(x => x.Id == pxmx.ConsumeInfoId && x.Md == input.StoreId).Any());
  2012 +
  2013 + // 3. 先分页查询主表数据(查询实体类,提高性能)
  2014 + var pagedData = await baseQuery.OrderBy(sidx + " " + sort).ToPagedListAsync(input.currentPage, input.pageSize);
  2015 +
  2016 + // 4. 批量查询关联数据
  2017 + var itemIds = pagedData.list.Select(x => x.Id).ToList();
  2018 + var memberIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.MemberId)).Select(x => x.MemberId).Distinct().ToList();
  2019 + var consumeIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.ConsumeInfoId)).Select(x => x.ConsumeInfoId).Distinct().ToList();
  2020 +
  2021 + // 批量查询会员信息
  2022 + var memberDict = new Dictionary<string, (string Name, string Phone)>();
  2023 + if (memberIds.Any())
  2024 + {
  2025 + var members = await _db.Queryable<LqKhxxEntity>().Where(x => memberIds.Contains(x.Id)).Select(x => new { x.Id, x.Khmc, x.Sjh }).ToListAsync();
  2026 + memberDict = members.ToDictionary(x => x.Id, x => (x.Khmc ?? "", x.Sjh ?? ""));
  2027 + }
  2028 +
  2029 + // 批量查询耗卡记录,获取门店ID和耗卡时间
  2030 + var consumeDict = new Dictionary<string, (string StoreId, DateTime? ConsumeTime)>();
  2031 + if (consumeIds.Any())
  2032 + {
  2033 + var consumes = await _db.Queryable<LqXhHyhkEntity>().Where(x => consumeIds.Contains(x.Id)).Select(x => new { x.Id, x.Md, x.Hksj }).ToListAsync();
  2034 + consumeDict = consumes.ToDictionary(x => x.Id, x => (x.Md ?? "", x.Hksj));
  2035 + }
  2036 +
  2037 + // 批量查询门店信息
  2038 + var storeDict = new Dictionary<string, string>();
  2039 + var storeIds = consumeDict.Values.Where(x => !string.IsNullOrEmpty(x.StoreId)).Select(x => x.StoreId).Distinct().ToList();
  2040 + if (storeIds.Any())
  2041 + {
  2042 + var stores = await _db.Queryable<LqMdxxEntity>().Where(x => storeIds.Contains(x.Id)).Select(x => new { x.Id, x.Dm }).ToListAsync();
  2043 + storeDict = stores.ToDictionary(x => x.Id, x => x.Dm ?? "");
  2044 + }
  2045 +
  2046 + // 5. 组装返回数据
  2047 + var resultList = pagedData.list.Select(pxmx => new ConsumeItemDetailListOutput
  2048 + {
  2049 + id = pxmx.Id,
  2050 + consumeId = pxmx.ConsumeInfoId,
  2051 + consumeTime = pxmx.ConsumeInfoId != null && consumeDict.ContainsKey(pxmx.ConsumeInfoId) ? consumeDict[pxmx.ConsumeInfoId].ConsumeTime : pxmx.Yjsj,
  2052 + memberName = pxmx.MemberId != null && memberDict.ContainsKey(pxmx.MemberId) ? memberDict[pxmx.MemberId].Name : "",
  2053 + memberPhone = pxmx.MemberId != null && memberDict.ContainsKey(pxmx.MemberId) ? memberDict[pxmx.MemberId].Phone : "",
  2054 + itemName = pxmx.Pxmc,
  2055 + itemType = pxmx.ItemCategory,
  2056 + itemPrice = pxmx.Pxjg,
  2057 + projectNumber = pxmx.ProjectNumber,
  2058 + originalProjectNumber = pxmx.OriginalProjectNumber,
  2059 + totalPrice = pxmx.TotalPrice,
  2060 + sourceType = pxmx.SourceType,
  2061 + storeId = pxmx.ConsumeInfoId != null && consumeDict.ContainsKey(pxmx.ConsumeInfoId) ? consumeDict[pxmx.ConsumeInfoId].StoreId : "",
  2062 + storeName = pxmx.ConsumeInfoId != null && consumeDict.ContainsKey(pxmx.ConsumeInfoId) && !string.IsNullOrEmpty(consumeDict[pxmx.ConsumeInfoId].StoreId) && storeDict.ContainsKey(consumeDict[pxmx.ConsumeInfoId].StoreId) ? storeDict[consumeDict[pxmx.ConsumeInfoId].StoreId] : ""
  2063 + }).ToList();
  2064 +
  2065 + // 6. 返回分页结果
  2066 + var result = new SqlSugarPagedList<ConsumeItemDetailListOutput>
  2067 + {
  2068 + list = resultList,
  2069 + pagination = pagedData.pagination
  2070 + };
  2071 +
  2072 + return PageResult<ConsumeItemDetailListOutput>.SqlSugarPageResult(result);
  2073 + }
  2074 + catch (Exception ex)
  2075 + {
  2076 + _logger.LogError(ex, $"获取耗卡品项明细记录列表失败: {ex.ToString()}");
  2077 + throw NCCException.Oh(ErrorCode.COM1005, $"获取耗卡品项明细记录列表失败: {ex.Message}");
  2078 + }
  2079 + }
  2080 + #endregion
  2081 +
1897 2082 }
1898 2083 }
... ...
sql/添加人次记录表字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 为人次记录表(lq_person_times_record)添加字段
  3 +-- ============================================
  4 +-- 说明:添加"是否有开单"字段,用于标识该人次记录是否关联开单业务
  5 +-- ============================================
  6 +
  7 +-- 添加"是否有开单"字段 (0-否, 1-是)
  8 +ALTER TABLE `lq_person_times_record`
  9 +ADD COLUMN `F_HasBilling` INT(11) DEFAULT 0 COMMENT '是否有开单(0-否,1-是)' AFTER `F_BusinessType`;
  10 +
  11 +-- ============================================
  12 +-- 验证字段添加
  13 +-- ============================================
  14 +-- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, COLUMN_COMMENT
  15 +-- FROM INFORMATION_SCHEMA.COLUMNS
  16 +-- WHERE TABLE_NAME = 'lq_person_times_record'
  17 +-- AND COLUMN_NAME = 'F_HasBilling';
  18 +
... ...
sql/添加会员信息表字段.sql 0 → 100644
  1 +-- 为会员信息表(lq_khxx)添加新字段
  2 +-- 执行时间:2025-01-XX
  3 +
  4 +-- ============================================
  5 +-- 一、会员类型标识字段
  6 +-- ============================================
  7 +
  8 +-- 1. 是否生美会员 (0-否, 1-是)
  9 +ALTER TABLE `lq_khxx`
  10 +ADD COLUMN `F_IsBeautyMember` INT(11) DEFAULT 0 COMMENT '是否生美会员(0-否,1-是)' AFTER `F_LastConsumeTime`;
  11 +
  12 +-- 2. 是否医美会员 (0-否, 1-是)
  13 +ALTER TABLE `lq_khxx`
  14 +ADD COLUMN `F_IsMedicalMember` INT(11) DEFAULT 0 COMMENT '是否医美会员(0-否,1-是)' AFTER `F_IsBeautyMember`;
  15 +
  16 +-- 3. 是否科技部会员 (0-否, 1-是)
  17 +ALTER TABLE `lq_khxx`
  18 +ADD COLUMN `F_IsTechMember` INT(11) DEFAULT 0 COMMENT '是否科技部会员(0-否,1-是)' AFTER `F_IsMedicalMember`;
  19 +
  20 +-- ============================================
  21 +-- 二、会员类型成为时间字段(记录成为各类会员的时间)
  22 +-- ============================================
  23 +
  24 +-- 4. 成为生美会员时间
  25 +ALTER TABLE `lq_khxx`
  26 +ADD COLUMN `F_BeautyMemberTime` DATETIME DEFAULT NULL COMMENT '成为生美会员时间' AFTER `F_IsTechMember`;
  27 +
  28 +-- 5. 成为医美会员时间
  29 +ALTER TABLE `lq_khxx`
  30 +ADD COLUMN `F_MedicalMemberTime` DATETIME DEFAULT NULL COMMENT '成为医美会员时间' AFTER `F_BeautyMemberTime`;
  31 +
  32 +-- 6. 成为科技部会员时间
  33 +ALTER TABLE `lq_khxx`
  34 +ADD COLUMN `F_TechMemberTime` DATETIME DEFAULT NULL COMMENT '成为科技部会员时间' AFTER `F_MedicalMemberTime`;
  35 +
  36 +-- ============================================
  37 +-- 三、到店相关字段(用于计算到店天数)
  38 +-- ============================================
  39 +
  40 +-- 7. 首次到店时间
  41 +ALTER TABLE `lq_khxx`
  42 +ADD COLUMN `F_FirstVisitTime` DATETIME DEFAULT NULL COMMENT '首次到店时间' AFTER `F_TechMemberTime`;
  43 +
  44 +-- 8. 最后到店时间
  45 +ALTER TABLE `lq_khxx`
  46 +ADD COLUMN `F_LastVisitTime` DATETIME DEFAULT NULL COMMENT '最后到店时间' AFTER `F_FirstVisitTime`;
  47 +
  48 +-- 9. 到店天数 (整数,记录会员到店的天数)
  49 +ALTER TABLE `lq_khxx`
  50 +ADD COLUMN `F_VisitDays` INT(11) DEFAULT 0 COMMENT '到店天数' AFTER `F_LastVisitTime`;
  51 +
  52 +-- ============================================
  53 +-- 四、沉睡相关字段(用于计算沉睡天数)
  54 +-- ============================================
  55 +
  56 +-- 10. 沉睡开始时间
  57 +ALTER TABLE `lq_khxx`
  58 +ADD COLUMN `F_SleepStartTime` DATETIME DEFAULT NULL COMMENT '沉睡开始时间' AFTER `F_VisitDays`;
  59 +
  60 +-- 11. 沉睡天数 (整数,记录会员沉睡的天数)
  61 +ALTER TABLE `lq_khxx`
  62 +ADD COLUMN `F_SleepDays` INT(11) DEFAULT 0 COMMENT '沉睡天数' AFTER `F_SleepStartTime`;
  63 +
  64 +-- ============================================
  65 +-- 五、消费相关字段(用于计算消费等级)
  66 +-- ============================================
  67 +
  68 +-- 12. 耗卡总金额 (累计所有耗卡的金额总和,从lq_xh_hyhk表的xfje字段统计)
  69 +ALTER TABLE `lq_khxx`
  70 +ADD COLUMN `F_TotalConsumeAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '耗卡总金额(累计所有耗卡的金额总和)' AFTER `F_SleepDays`;
  71 +
  72 +-- 13. 开卡总金额 (累计所有开单的实付业绩总和,从lq_kd_kdjlb表的sfyj字段统计)
  73 +ALTER TABLE `lq_khxx`
  74 +ADD COLUMN `F_TotalBillingAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '开卡总金额(累计所有开单的实付业绩总和)' AFTER `F_TotalConsumeAmount`;
  75 +
  76 +-- 14. 剩余权益总金额 (开卡总金额 - 耗卡总金额 - 退款金额)
  77 +ALTER TABLE `lq_khxx`
  78 +ADD COLUMN `F_RemainingRightsAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '剩余权益总金额(开卡总金额-耗卡总金额-退款金额)' AFTER `F_TotalBillingAmount`;
  79 +
  80 +-- 15. 消费等级 (整数,记录会员的消费等级,如:0-普通, 1-银卡, 2-金卡, 3-钻石等)
  81 +ALTER TABLE `lq_khxx`
  82 +ADD COLUMN `F_ConsumeLevel` INT(11) DEFAULT 0 COMMENT '消费等级(0-普通,1-银卡,2-金卡,3-钻石等)' AFTER `F_RemainingRightsAmount`;
  83 +
  84 +-- 16. 消费等级更新时间
  85 +ALTER TABLE `lq_khxx`
  86 +ADD COLUMN `F_ConsumeLevelUpdateTime` DATETIME DEFAULT NULL COMMENT '消费等级更新时间' AFTER `F_ConsumeLevel`;
  87 +
... ...