diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Config/MemberInfoUpdateConfig.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Config/MemberInfoUpdateConfig.cs new file mode 100644 index 0000000..7b981c0 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Config/MemberInfoUpdateConfig.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; + +namespace NCC.Extend.Entitys.Config +{ + /// + /// 会员信息更新配置 + /// + public static class MemberInfoUpdateConfig + { + /// + /// 生美会员判断金额阈值(元) + /// + public static decimal BeautyMemberAmountThreshold { get; set; } = 500m; + + /// + /// 医美会员判断金额阈值(元) + /// + public static decimal MedicalMemberAmountThreshold { get; set; } = 3000m; + + /// + /// 科技部会员判断金额阈值(元) + /// + public static decimal TechMemberAmountThreshold { get; set; } = 3800m; + + /// + /// 沉睡天数阈值列表(天) + /// + public static List SleepDaysThresholds { get; set; } = new List { 60, 90, 180, 360 }; + + /// + /// 消费等级配置(金额阈值,单位:元) + /// Key: 等级编号(0-普通, 1-银卡, 2-金卡, 3-钻石等) + /// Value: 金额阈值 + /// + public static Dictionary ConsumeLevelThresholds { get; set; } = new Dictionary + { + { 0, 0m }, // D: 小于 1 万 + { 1, 10000m }, // C: 1 万 + { 2, 30000m }, // B: 3 万 + { 3, 50000m }, // A: 5 万 + { 4, 100000m }, // A+: 10 万 + { 5, 200000m } // A++: 20 万 + }; + + /// + /// 消费等级名称映射 + /// + public static Dictionary ConsumeLevelNames { get; set; } = new Dictionary + { + { 0, "D" }, + { 1, "C" }, + { 2, "B" }, + { 3, "A" }, + { 4, "A+" }, + { 5, "A++" } + }; + + /// + /// 根据消费金额获取消费等级 + /// + /// 消费金额 + /// 消费等级编号 + public static int GetConsumeLevel(decimal amount) + { + // 从高到低遍历,找到第一个满足条件的等级 + for (int i = ConsumeLevelThresholds.Count - 1; i >= 0; i--) + { + if (amount >= ConsumeLevelThresholds[i]) + { + return i; + } + } + return 0; // 默认返回最低等级 + } + + /// + /// 根据消费金额获取消费等级名称 + /// + /// 消费金额 + /// 消费等级名称 + public static string GetConsumeLevelName(decimal amount) + { + var level = GetConsumeLevel(amount); + return ConsumeLevelNames.ContainsKey(level) ? ConsumeLevelNames[level] : "D"; + } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs index 821309e..e8762af 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxInfoOutput.cs @@ -135,6 +135,85 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx /// public string subHealthUserName { get; set; } + /// + /// 是否生美会员(0-否,1-是) + /// + public int isBeautyMember { get; set; } + + /// + /// 是否医美会员(0-否,1-是) + /// + public int isMedicalMember { get; set; } + + /// + /// 是否科技部会员(0-否,1-是) + /// + public int isTechMember { get; set; } + + /// + /// 成为生美会员时间 + /// + public DateTime? beautyMemberTime { get; set; } + + /// + /// 成为医美会员时间 + /// + public DateTime? medicalMemberTime { get; set; } + + /// + /// 成为科技部会员时间 + /// + public DateTime? techMemberTime { get; set; } + + /// + /// 首次到店时间 + /// + public DateTime? firstVisitTime { get; set; } + + /// + /// 最后到店时间 + /// + public DateTime? lastVisitTime { get; set; } + + /// + /// 到店天数 + /// + public int visitDays { get; set; } + + /// + /// 沉睡开始时间 + /// + public DateTime? sleepStartTime { get; set; } + + /// + /// 沉睡天数 + /// + public int sleepDays { get; set; } + + /// + /// 耗卡总金额(累计所有耗卡的金额总和) + /// + public decimal totalConsumeAmount { get; set; } + + /// + /// 开卡总金额(累计所有开单的实付业绩总和) + /// + public decimal totalBillingAmount { get; set; } + + /// + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额) + /// + public decimal remainingRightsAmount { get; set; } + + /// + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等) + /// + public int consumeLevel { get; set; } + + /// + /// 消费等级更新时间 + /// + public DateTime? consumeLevelUpdateTime { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs index 1991964..924fde8 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListOutput.cs @@ -161,5 +161,85 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx /// 副健康师名称 /// public string subHealthUserName { get; set; } + + /// + /// 是否生美会员(0-否,1-是) + /// + public int isBeautyMember { get; set; } + + /// + /// 是否医美会员(0-否,1-是) + /// + public int isMedicalMember { get; set; } + + /// + /// 是否科技部会员(0-否,1-是) + /// + public int isTechMember { get; set; } + + /// + /// 成为生美会员时间 + /// + public DateTime? beautyMemberTime { get; set; } + + /// + /// 成为医美会员时间 + /// + public DateTime? medicalMemberTime { get; set; } + + /// + /// 成为科技部会员时间 + /// + public DateTime? techMemberTime { get; set; } + + /// + /// 首次到店时间 + /// + public DateTime? firstVisitTime { get; set; } + + /// + /// 最后到店时间 + /// + public DateTime? lastVisitTime { get; set; } + + /// + /// 到店天数 + /// + public int visitDays { get; set; } + + /// + /// 沉睡开始时间 + /// + public DateTime? sleepStartTime { get; set; } + + /// + /// 沉睡天数 + /// + public int sleepDays { get; set; } + + /// + /// 耗卡总金额(累计所有耗卡的金额总和) + /// + public decimal totalConsumeAmount { get; set; } + + /// + /// 开卡总金额(累计所有开单的实付业绩总和) + /// + public decimal totalBillingAmount { get; set; } + + /// + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额) + /// + public decimal remainingRightsAmount { get; set; } + + /// + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等) + /// + public int consumeLevel { get; set; } + + /// + /// 消费等级更新时间 + /// + public DateTime? consumeLevelUpdateTime { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs index a79029e..c60cd12 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs @@ -48,8 +48,6 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx /// public string xb { get; set; } - - /// /// 归属门店 /// diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoOutput.cs index 645b424..a5b8638 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoOutput.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using NCC.Code; +using NCC.Common.Extension; +using NCC.Extend.Entitys.Enum; namespace NCC.Extend.Entitys.Dto.LqKhxx { @@ -29,6 +32,21 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx public string MemberPhone { get; set; } /// + /// 会员类型 + /// + public int MemberType { get; set; } + + /// + /// 会员类型名称 + /// + public string MemberTypeName + { + get + { + return EnumHelper.GetEnumDesc(MemberType); + } + } + /// /// 所属门店ID /// public string BelongStoreId { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoQueryInput.cs index 6f3b19d..2fa2ac7 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoQueryInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/MemberItemInfoQueryInput.cs @@ -24,6 +24,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx public string BillingStoreId { get; set; } /// + /// 会员类型 + /// + public int MemberType { get; set; } = -1; + + /// /// 开单人ID /// public string BillingUserId { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordCrInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordCrInput.cs index 6f6b509..b67dddc 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordCrInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordCrInput.cs @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord public string businessType { get; set; } /// + /// 是否有开单(0-否,1-是) + /// + public int hasBilling { get; set; } = 0; + + /// /// 人员类型(健康师/科技老师) /// public string personType { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordInfoOutput.cs index 20563cd..cee2dc9 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordInfoOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordInfoOutput.cs @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord public string businessType { get; set; } /// + /// 是否有开单(0-否,1-是) + /// + public int hasBilling { get; set; } + + /// /// 人员类型(健康师/科技老师) /// public string personType { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordListOutput.cs index ef3d419..567fd96 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordListOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqPersonTimesRecord/LqPersonTimesRecordListOutput.cs @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqPersonTimesRecord public string businessType { get; set; } /// + /// 是否有开单(0-否,1-是) + /// + public int hasBilling { get; set; } + + /// /// 人员类型(健康师/科技老师) /// public string personType { get; set; } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListOutput.cs new file mode 100644 index 0000000..0a15b43 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListOutput.cs @@ -0,0 +1,81 @@ +using System; + +namespace NCC.Extend.Entitys.Dto.LqXhPxmx +{ + /// + /// 耗卡品项明细记录输出 + /// + public class ConsumeItemDetailListOutput + { + /// + /// 品项明细ID + /// + public string id { get; set; } + + /// + /// 耗卡记录ID + /// + public string consumeId { get; set; } + + /// + /// 耗卡时间 + /// + public DateTime? consumeTime { get; set; } + + /// + /// 会员名称 + /// + public string memberName { get; set; } + + /// + /// 会员手机 + /// + public string memberPhone { get; set; } + + /// + /// 品项名称 + /// + public string itemName { get; set; } + + /// + /// 品项类型(分类④-统计品项用) + /// + public string itemType { get; set; } + + /// + /// 品项价格 + /// + public decimal itemPrice { get; set; } + + /// + /// 项目次数 + /// + public decimal projectNumber { get; set; } + + /// + /// 原始项目次数 + /// + public decimal? originalProjectNumber { get; set; } + + /// + /// 合计金额 + /// + public decimal totalPrice { get; set; } + + /// + /// 来源类型(购买/赠送/体验) + /// + public string sourceType { get; set; } + + /// + /// 门店ID + /// + public string storeId { get; set; } + + /// + /// 门店名称 + /// + public string storeName { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListQueryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListQueryInput.cs new file mode 100644 index 0000000..43db838 --- /dev/null +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqXhPxmx/ConsumeItemDetailListQueryInput.cs @@ -0,0 +1,81 @@ +using NCC.Common.Filter; + +namespace NCC.Extend.Entitys.Dto.LqXhPxmx +{ + /// + /// 耗卡品项明细记录查询输入参数 + /// + public class ConsumeItemDetailListQueryInput : PageInputBase + { + /// + /// 品项明细ID + /// + public string Id { get; set; } + + /// + /// 耗卡记录ID + /// + public string ConsumeId { get; set; } + + /// + /// 耗卡时间(开始) + /// + public string StartConsumeTime { get; set; } + + /// + /// 耗卡时间(结束) + /// + public string EndConsumeTime { get; set; } + + /// + /// 开始时间(兼容参数名) + /// + public string startTime { get; set; } + + /// + /// 结束时间(兼容参数名) + /// + public string endTime { get; set; } + + /// + /// 会员ID + /// + public string MemberId { get; set; } + + /// + /// 会员名称(模糊查询) + /// + public string MemberName { get; set; } + + /// + /// 会员手机号 + /// + public string MemberPhone { get; set; } + + /// + /// 品项ID + /// + public string ItemId { get; set; } + + /// + /// 品项名称(模糊查询) + /// + public string ItemName { get; set; } + + /// + /// 品项类型(分类④-统计品项用) + /// + public string ItemType { get; set; } + + /// + /// 来源类型(购买/赠送/体验) + /// + public string SourceType { get; set; } + + /// + /// 门店ID + /// + public string StoreId { get; set; } + } +} + diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_khxx/LqKhxxEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_khxx/LqKhxxEntity.cs index dd4f453..64c4b33 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_khxx/LqKhxxEntity.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_khxx/LqKhxxEntity.cs @@ -142,6 +142,108 @@ namespace NCC.Extend.Entitys.lq_khxx public string SubHealthUser { get; set; } /// + /// 最后消费时间 + /// + [SugarColumn(ColumnName = "F_LastConsumeTime")] + public DateTime? LastConsumeTime { get; set; } + + /// + /// 是否生美会员(0-否,1-是) + /// + [SugarColumn(ColumnName = "F_IsBeautyMember")] + public int IsBeautyMember { get; set; } = 0; + + /// + /// 是否医美会员(0-否,1-是) + /// + [SugarColumn(ColumnName = "F_IsMedicalMember")] + public int IsMedicalMember { get; set; } = 0; + + /// + /// 是否科技部会员(0-否,1-是) + /// + [SugarColumn(ColumnName = "F_IsTechMember")] + public int IsTechMember { get; set; } = 0; + + /// + /// 成为生美会员时间 + /// + [SugarColumn(ColumnName = "F_BeautyMemberTime")] + public DateTime? BeautyMemberTime { get; set; } + + /// + /// 成为医美会员时间 + /// + [SugarColumn(ColumnName = "F_MedicalMemberTime")] + public DateTime? MedicalMemberTime { get; set; } + + /// + /// 成为科技部会员时间 + /// + [SugarColumn(ColumnName = "F_TechMemberTime")] + public DateTime? TechMemberTime { get; set; } + + /// + /// 首次到店时间 + /// + [SugarColumn(ColumnName = "F_FirstVisitTime")] + public DateTime? FirstVisitTime { get; set; } + + /// + /// 最后到店时间 + /// + [SugarColumn(ColumnName = "F_LastVisitTime")] + public DateTime? LastVisitTime { get; set; } + + /// + /// 到店天数 + /// + [SugarColumn(ColumnName = "F_VisitDays")] + public int VisitDays { get; set; } = 0; + + /// + /// 沉睡开始时间 + /// + [SugarColumn(ColumnName = "F_SleepStartTime")] + public DateTime? SleepStartTime { get; set; } + + /// + /// 沉睡天数 + /// + [SugarColumn(ColumnName = "F_SleepDays")] + public int SleepDays { get; set; } = 0; + + /// + /// 耗卡总金额(累计所有耗卡的金额总和) + /// + [SugarColumn(ColumnName = "F_TotalConsumeAmount")] + public decimal TotalConsumeAmount { get; set; } = 0m; + + /// + /// 开卡总金额(累计所有开单的实付业绩总和) + /// + [SugarColumn(ColumnName = "F_TotalBillingAmount")] + public decimal TotalBillingAmount { get; set; } = 0m; + + /// + /// 剩余权益总金额(开卡总金额-耗卡总金额-退款金额) + /// + [SugarColumn(ColumnName = "F_RemainingRightsAmount")] + public decimal RemainingRightsAmount { get; set; } = 0m; + + /// + /// 消费等级(0-普通,1-银卡,2-金卡,3-钻石等) + /// + [SugarColumn(ColumnName = "F_ConsumeLevel")] + public int ConsumeLevel { get; set; } = 0; + + /// + /// 消费等级更新时间 + /// + [SugarColumn(ColumnName = "F_ConsumeLevelUpdateTime")] + public DateTime? ConsumeLevelUpdateTime { get; set; } + + /// /// 是否有效 /// [SugarColumn(ColumnName = "F_IsEffective")] diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_person_times_record/LqPersonTimesRecordEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_person_times_record/LqPersonTimesRecordEntity.cs index c75e06a..a12b00f 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_person_times_record/LqPersonTimesRecordEntity.cs +++ b/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 public string BusinessType { get; set; } /// + /// 是否有开单(0-否,1-是) + /// + [SugarColumn(ColumnName = "F_HasBilling")] + public int HasBilling { get; set; } = 0; + + /// /// 人员类型(健康师/科技老师) /// [SugarColumn(ColumnName = "F_PersonType")] diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/MemberTypeEnum.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/MemberTypeEnum.cs index fcd3a66..b760212 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/MemberTypeEnum.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/MemberTypeEnum.cs @@ -31,5 +31,10 @@ namespace NCC.Extend.Entitys.Enum [Description("会员")] 会员 = 3, + /// + /// 归档 + /// + [Description("归档")] + 归档 = 4, } } \ No newline at end of file diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs index e068112..ac43793 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Mapper/LqKhxxMapper.cs @@ -12,6 +12,25 @@ namespace NCC.Extend.Entitys.Mapper.LqKhxx { // 由于khxf字段现在是string类型,不需要特殊的映射处理 // 使用默认的映射即可 + + // 配置实体到详情输出的映射(确保新字段正确映射) + config.ForType() + .Map(dest => dest.isBeautyMember, src => src.IsBeautyMember) + .Map(dest => dest.isMedicalMember, src => src.IsMedicalMember) + .Map(dest => dest.isTechMember, src => src.IsTechMember) + .Map(dest => dest.beautyMemberTime, src => src.BeautyMemberTime) + .Map(dest => dest.medicalMemberTime, src => src.MedicalMemberTime) + .Map(dest => dest.techMemberTime, src => src.TechMemberTime) + .Map(dest => dest.firstVisitTime, src => src.FirstVisitTime) + .Map(dest => dest.lastVisitTime, src => src.LastVisitTime) + .Map(dest => dest.visitDays, src => src.VisitDays) + .Map(dest => dest.sleepStartTime, src => src.SleepStartTime) + .Map(dest => dest.sleepDays, src => src.SleepDays) + .Map(dest => dest.totalConsumeAmount, src => src.TotalConsumeAmount) + .Map(dest => dest.totalBillingAmount, src => src.TotalBillingAmount) + .Map(dest => dest.remainingRightsAmount, src => src.RemainingRightsAmount) + .Map(dest => dest.consumeLevel, src => src.ConsumeLevel) + .Map(dest => dest.consumeLevelUpdateTime, src => src.ConsumeLevelUpdateTime); } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs index 238389a..44e29ff 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs @@ -30,12 +30,14 @@ using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_xh_pxmx; using NCC.Extend.Entitys.lq_xmzl; using NCC.Extend.Interfaces.LqKhxx; +using NCC.Extend.Entitys.Config; using NCC.System.Entitys.Permission; using NCC.FriendlyException; using NCC.JsonSerialization; using SqlSugar; using Yitter.IdGenerator; using NCC.Extend.Entitys.lq_kd_deductinfo; +using Microsoft.AspNetCore.Authorization; namespace NCC.Extend.LqKhxx { @@ -109,6 +111,9 @@ namespace NCC.Extend.LqKhxx public async Task GetList([FromQuery] LqKhxxListQueryInput input) { var sidx = input.sidx == null ? "id" : input.sidx; + List queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject>() : null; + DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null; + DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null; var data = await _db.Queryable() .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord)) .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) @@ -125,6 +130,8 @@ namespace NCC.Extend.LqKhxx .WhereIF(!string.IsNullOrEmpty(input.jdqd), p => p.Jdqd.Contains(input.jdqd)) .WhereIF(!string.IsNullOrEmpty(input.lxdz), p => p.Lxdz.Contains(input.lxdz)) .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) + .WhereIF(queryZcsj != null, p => p.CreateTime >= new DateTime(startZcsj.ToDate().Year, startZcsj.ToDate().Month, startZcsj.ToDate().Day, 0, 0, 0)) + .WhereIF(queryZcsj != null, p => p.CreateTime <= new DateTime(endZcsj.ToDate().Year, endZcsj.ToDate().Month, endZcsj.ToDate().Day, 23, 59, 59)) .Select(it => new LqKhxxListOutput { id = it.Id, @@ -153,6 +160,22 @@ namespace NCC.Extend.LqKhxx mainHealthUserName = SqlFunc.Subqueryable().Where(u => u.Id == it.MainHealthUser).Select(u => u.RealName), subHealthUserName = SqlFunc.Subqueryable().Where(u => u.Id == it.SubHealthUser).Select(u => u.RealName), tjrName = SqlFunc.Subqueryable().Where(u => u.Id == it.Tjr).Select(u => u.Khmc), + isBeautyMember = it.IsBeautyMember, + isMedicalMember = it.IsMedicalMember, + isTechMember = it.IsTechMember, + beautyMemberTime = it.BeautyMemberTime, + medicalMemberTime = it.MedicalMemberTime, + techMemberTime = it.TechMemberTime, + firstVisitTime = it.FirstVisitTime, + lastVisitTime = it.LastVisitTime, + visitDays = it.VisitDays, + sleepStartTime = it.SleepStartTime, + sleepDays = it.SleepDays, + totalConsumeAmount = it.TotalConsumeAmount, + totalBillingAmount = it.TotalBillingAmount, + remainingRightsAmount = it.RemainingRightsAmount, + consumeLevel = it.ConsumeLevel, + consumeLevelUpdateTime = it.ConsumeLevelUpdateTime }) .MergeTable() .OrderBy(sidx + " " + input.sort) @@ -180,6 +203,7 @@ namespace NCC.Extend.LqKhxx var entity = input.Adapt(); entity.Id = YitIdHelper.NextId().ToString(); entity.Dah = "LQ" + DateTime.Now.ToString("yyyyMMddHHmmssfff"); + entity.Zcsj = DateTime.Now; // 处理客户消费字段:将数组转换为逗号分隔的字符串 if (input.khxf != null && input.khxf.Count > 0) { @@ -1048,6 +1072,11 @@ namespace NCC.Extend.LqKhxx memberQuery = memberQuery.Where(x => x.Gsmd == input.BelongStoreId); } + if (input.MemberType >= 0) + { + memberQuery = memberQuery.Where(x => x.Khlx == input.MemberType.GetHashCode().ToString()); + } + // 如果指定了开单门店、开单人、品项ID等条件,需要通过开单表进行筛选 if (!string.IsNullOrEmpty(input.BillingStoreId) || !string.IsNullOrEmpty(input.BillingUserId) || @@ -1070,13 +1099,9 @@ namespace NCC.Extend.LqKhxx } // 如果指定了品项ID,需要通过品项明细表进行筛选 - if (!string.IsNullOrEmpty(input.PurchaseItemId) || - !string.IsNullOrEmpty(input.GiftItemId) || - !string.IsNullOrEmpty(input.ExperienceItemId)) + if (!string.IsNullOrEmpty(input.PurchaseItemId) || !string.IsNullOrEmpty(input.GiftItemId) || !string.IsNullOrEmpty(input.ExperienceItemId)) { - var itemQuery = _db.Queryable() - .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()); - + var itemQuery = _db.Queryable().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()); if (!string.IsNullOrEmpty(input.PurchaseItemId)) { itemQuery = itemQuery.Where(x => x.Px == input.PurchaseItemId && x.SourceType == "购买"); @@ -1147,7 +1172,8 @@ namespace NCC.Extend.LqKhxx x.Dah, x.Khmc, x.Sjh, - x.Gsmd + x.Gsmd, + x.Khlx }) .ToListAsync(); @@ -1328,6 +1354,7 @@ namespace NCC.Extend.LqKhxx MemberCode = member.Dah ?? "", MemberName = member.Khmc ?? "", MemberPhone = member.Sjh ?? "", + MemberType = member.Khlx.ToInt(), BelongStoreId = member.Gsmd ?? "", BelongStoreName = stores.ContainsKey(member.Gsmd ?? "") ? stores[member.Gsmd] : "" }; @@ -1531,5 +1558,507 @@ namespace NCC.Extend.LqKhxx } #endregion + #region 会员信息更新 + /// + /// 更新会员信息(单个会员) + /// + /// 会员ID + /// + [HttpPost("UpdateMemberInfo")] + public async Task UpdateMemberInfo([FromBody] string memberId) + { + try + { + if (string.IsNullOrEmpty(memberId)) + { + throw NCCException.Oh("会员ID不能为空"); + } + + var member = await _db.Queryable() + .Where(x => x.Id == memberId && x.IsEffective == StatusEnum.有效.GetHashCode()) + .FirstAsync(); + + if (member == null) + { + throw NCCException.Oh("会员不存在"); + } + + await UpdateMemberInfoInternal(member); + + return new { success = true, message = "更新成功" }; + } + catch (Exception ex) + { + _logger.LogError(ex, "更新会员信息失败,会员ID:{MemberId}", memberId); + throw NCCException.Oh($"更新会员信息失败:{ex.Message}"); + } + } + + /// + /// 批量更新所有会员信息(高性能版:使用SQL批量更新) + /// + /// + [HttpPost("BatchUpdateMemberInfo")] + [AllowAnonymous] + public async Task BatchUpdateMemberInfo() + { + try + { + var startTime = DateTime.Now; + _logger.LogInformation("开始批量更新会员信息(高性能SQL版)"); + + // 1. 获取所有有效会员ID + var memberIds = await _db.Queryable() + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => x.Id) + .ToListAsync(); + + var totalCount = memberIds.Count; + + if (totalCount == 0) + { + return new + { + success = true, + totalCount = 0, + successCount = 0, + failCount = 0, + duration = "0秒" + }; + } + + _logger.LogInformation("开始使用SQL批量更新,会员总数:{Total}", totalCount); + var sqlStartTime = DateTime.Now; + + // 使用原生SQL一次性更新所有会员信息 + var beautyThreshold = MemberInfoUpdateConfig.BeautyMemberAmountThreshold; + var medicalThreshold = MemberInfoUpdateConfig.MedicalMemberAmountThreshold; + var techThreshold = MemberInfoUpdateConfig.TechMemberAmountThreshold; + var minSleepThreshold = MemberInfoUpdateConfig.SleepDaysThresholds.Min(); + var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + // 构建SQL更新语句 + var updateSql = $@" +UPDATE lq_khxx kh +LEFT JOIN ( + -- 生美会员判断 + SELECT + pxmx.F_MemberId as MemberId, + CASE WHEN SUM(CASE WHEN pxmx.F_ItemCategory = '生美' THEN pxmx.F_ActualPrice ELSE 0 END) > {beautyThreshold} THEN 1 ELSE 0 END as IsBeautyMember, + MIN(CASE WHEN pxmx.F_ItemCategory = '生美' AND kd.kdrq IS NOT NULL THEN kd.kdrq ELSE NULL END) as BeautyMemberTime + FROM lq_kd_pxmx pxmx + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id + WHERE pxmx.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND pxmx.F_ActualPrice > 0 + AND pxmx.px != '61' + GROUP BY pxmx.F_MemberId +) beauty ON kh.F_Id = beauty.MemberId +LEFT JOIN ( + -- 医美会员判断 + SELECT + pxmx.F_MemberId as MemberId, + CASE WHEN SUM(CASE WHEN pxmx.F_ItemCategory = '医美' THEN pxmx.F_ActualPrice ELSE 0 END) > {medicalThreshold} THEN 1 ELSE 0 END as IsMedicalMember, + MIN(CASE WHEN pxmx.F_ItemCategory = '医美' AND kd.kdrq IS NOT NULL THEN kd.kdrq ELSE NULL END) as MedicalMemberTime + FROM lq_kd_pxmx pxmx + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id + WHERE pxmx.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND pxmx.F_ActualPrice > 0 + AND pxmx.px != '61' + GROUP BY pxmx.F_MemberId +) medical ON kh.F_Id = medical.MemberId +LEFT JOIN ( + -- 科技部会员判断 + SELECT + pxmx.F_MemberId as MemberId, + 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, + 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 + FROM lq_kd_pxmx pxmx + INNER JOIN lq_kd_kdjlb kd ON pxmx.glkdbh = kd.F_Id + WHERE pxmx.F_IsEffective = 1 + AND kd.F_IsEffective = 1 + AND pxmx.F_ActualPrice > 0 + AND pxmx.px != '61' + GROUP BY pxmx.F_MemberId +) tech ON kh.F_Id = tech.MemberId +LEFT JOIN ( + -- 到店天数、首次到店时间、最后到店时间 + SELECT + xh.hy as MemberId, + COUNT(DISTINCT DATE(xh.hksj)) as VisitDays, + MIN(xh.hksj) as FirstVisitTime, + MAX(xh.hksj) as LastVisitTime + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + GROUP BY xh.hy +) visit ON kh.F_Id = visit.MemberId +LEFT JOIN ( + -- 开卡总金额 + SELECT + kd.kdhy as MemberId, + COALESCE(SUM(kd.sfyj), 0) as TotalBillingAmount + FROM lq_kd_kdjlb kd + WHERE kd.F_IsEffective = 1 + GROUP BY kd.kdhy +) billing ON kh.F_Id = billing.MemberId +LEFT JOIN ( + -- 耗卡总金额 + SELECT + xh.hy as MemberId, + COALESCE(SUM(xh.xfje), 0) as TotalConsumeAmount + FROM lq_xh_hyhk xh + WHERE xh.F_IsEffective = 1 + GROUP BY xh.hy +) consume ON kh.F_Id = consume.MemberId +LEFT JOIN ( + -- 退款总金额 + SELECT + tk.hy as MemberId, + COALESCE(SUM(tk.F_ActualRefundAmount), 0) as TotalRefundAmount + FROM lq_hytk_hytk tk + WHERE tk.F_IsEffective = 1 + GROUP BY tk.hy +) refund ON kh.F_Id = refund.MemberId +SET + kh.F_IsBeautyMember = COALESCE(beauty.IsBeautyMember, 0), + kh.F_IsMedicalMember = COALESCE(medical.IsMedicalMember, 0), + kh.F_IsTechMember = COALESCE(tech.IsTechMember, 0), + kh.F_BeautyMemberTime = CASE WHEN kh.F_BeautyMemberTime IS NULL THEN beauty.BeautyMemberTime ELSE kh.F_BeautyMemberTime END, + kh.F_MedicalMemberTime = CASE WHEN kh.F_MedicalMemberTime IS NULL THEN medical.MedicalMemberTime ELSE kh.F_MedicalMemberTime END, + kh.F_TechMemberTime = CASE WHEN kh.F_TechMemberTime IS NULL THEN tech.TechMemberTime ELSE kh.F_TechMemberTime END, + kh.F_FirstVisitTime = visit.FirstVisitTime, + kh.F_LastVisitTime = visit.LastVisitTime, + kh.F_VisitDays = COALESCE(visit.VisitDays, 0), + kh.F_TotalBillingAmount = COALESCE(billing.TotalBillingAmount, 0), + kh.F_TotalConsumeAmount = COALESCE(consume.TotalConsumeAmount, 0), + kh.F_RemainingRightsAmount = GREATEST(0, COALESCE(billing.TotalBillingAmount, 0) - COALESCE(consume.TotalConsumeAmount, 0) - COALESCE(refund.TotalRefundAmount, 0)), + kh.F_SleepDays = GREATEST(0, DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime))), + kh.F_SleepStartTime = CASE + WHEN DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime)) >= {minSleepThreshold} + AND kh.F_SleepStartTime IS NULL + THEN DATE(COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime)) + WHEN DATEDIFF(CURDATE(), COALESCE(kh.F_LastConsumeTime, visit.LastVisitTime, kh.F_CreateTime)) < {minSleepThreshold} + THEN NULL + ELSE kh.F_SleepStartTime + END, + kh.F_ConsumeLevel = CASE + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1 + ELSE 0 + END, + kh.F_ConsumeLevelUpdateTime = CASE + WHEN kh.F_ConsumeLevel != CASE + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1 + ELSE 0 + END THEN NOW() + WHEN kh.F_ConsumeLevelUpdateTime IS NULL AND CASE + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 200000 THEN 5 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 100000 THEN 4 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 50000 THEN 3 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 30000 THEN 2 + WHEN COALESCE(consume.TotalConsumeAmount, 0) >= 10000 THEN 1 + ELSE 0 + END > 0 THEN NOW() + ELSE kh.F_ConsumeLevelUpdateTime + END +WHERE kh.F_IsEffective = 1"; + + var affectedRows = await _db.Ado.ExecuteCommandAsync(updateSql); + var sqlEndTime = DateTime.Now; + var duration = (sqlEndTime - sqlStartTime).TotalSeconds; + + _logger.LogInformation("SQL批量更新完成,影响行数:{Rows},耗时:{Duration}秒", affectedRows, duration); + + var endTime = DateTime.Now; + var totalDuration = (endTime - startTime).TotalSeconds; + + _logger.LogInformation("批量更新会员信息完成,总数量:{Total},影响行数:{Rows},耗时:{Duration}秒", + totalCount, affectedRows, totalDuration); + + return new + { + success = true, + totalCount = totalCount, + successCount = affectedRows, + failCount = totalCount - affectedRows, + duration = $"{totalDuration:F2}秒", + sqlDuration = $"{duration:F2}秒" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "批量更新会员信息失败"); + throw NCCException.Oh($"批量更新会员信息失败:{ex.Message}"); + } + } + + /// + /// 更新会员信息内部方法 + /// + /// 会员实体 + private async Task UpdateMemberInfoInternal(LqKhxxEntity member) + { + // 1. 判断生美/医美/科技部会员 + await UpdateMemberTypeFlags(member); + + // 2. 计算到店天数 + await UpdateVisitDays(member); + + // 3. 计算沉睡天数 + await UpdateSleepDays(member); + + // 4. 计算消费金额和等级 + await UpdateConsumeInfo(member); + + // 5. 更新到数据库 + await _db.Updateable(member) + .UpdateColumns(x => new + { + x.IsBeautyMember, + x.IsMedicalMember, + x.IsTechMember, + x.BeautyMemberTime, + x.MedicalMemberTime, + x.TechMemberTime, + x.FirstVisitTime, + x.LastVisitTime, + x.VisitDays, + x.SleepStartTime, + x.SleepDays, + x.TotalConsumeAmount, + x.TotalBillingAmount, + x.RemainingRightsAmount, + x.ConsumeLevel, + x.ConsumeLevelUpdateTime + }) + .ExecuteCommandAsync(); + } + + /// + /// 更新会员类型标识(生美/医美/科技部会员) + /// + private async Task UpdateMemberTypeFlags(LqKhxxEntity member) + { + // 查询该会员的所有有效开单品项明细 + var billingItems = await _db.Queryable() + .InnerJoin((pxmx, kd) => pxmx.Glkdbh == kd.Id) + .Where((pxmx, kd) => + pxmx.MemberId == member.Id + && pxmx.IsEffective == StatusEnum.有效.GetHashCode() + && kd.IsEffective == StatusEnum.有效.GetHashCode() + && pxmx.ActualPrice > 0 + && pxmx.Px != "61") // 排除女神卡 + .Select((pxmx, kd) => new + { + pxmx.ItemCategory, + pxmx.ActualPrice, + pxmx.Pxjg, + kd.Kdrq + }) + .ToListAsync(); + + // 判断生美会员:开卡品项为生美并且金额大于阈值 + var beautyAmount = billingItems + .Where(x => x.ItemCategory == "生美") + .Sum(x => x.ActualPrice); + + if (beautyAmount > MemberInfoUpdateConfig.BeautyMemberAmountThreshold) + { + member.IsBeautyMember = 1; + if (!member.BeautyMemberTime.HasValue) + { + var firstBeautyBilling = billingItems + .Where(x => x.ItemCategory == "生美") + .OrderBy(x => x.Kdrq) + .FirstOrDefault(); + member.BeautyMemberTime = firstBeautyBilling?.Kdrq; + } + } + else + { + member.IsBeautyMember = 0; + member.BeautyMemberTime = null; + } + + // 判断医美会员:开卡品项为医美并且金额大于阈值 + var medicalAmount = billingItems + .Where(x => x.ItemCategory == "医美") + .Sum(x => x.ActualPrice); + + if (medicalAmount > MemberInfoUpdateConfig.MedicalMemberAmountThreshold) + { + member.IsMedicalMember = 1; + if (!member.MedicalMemberTime.HasValue) + { + var firstMedicalBilling = billingItems + .Where(x => x.ItemCategory == "医美") + .OrderBy(x => x.Kdrq) + .FirstOrDefault(); + member.MedicalMemberTime = firstMedicalBilling?.Kdrq; + } + } + else + { + member.IsMedicalMember = 0; + member.MedicalMemberTime = null; + } + + // 判断科技部会员:科美单品项金额大于等于阈值 + var hasTechMember = billingItems + .Where(x => x.ItemCategory == "科美") + .Any(x => x.ActualPrice >= MemberInfoUpdateConfig.TechMemberAmountThreshold); + + if (hasTechMember) + { + member.IsTechMember = 1; + if (!member.TechMemberTime.HasValue) + { + var firstTechBilling = billingItems + .Where(x => x.ItemCategory == "科美" && x.ActualPrice >= MemberInfoUpdateConfig.TechMemberAmountThreshold) + .OrderBy(x => x.Kdrq) + .FirstOrDefault(); + member.TechMemberTime = firstTechBilling?.Kdrq; + } + } + else + { + member.IsTechMember = 0; + member.TechMemberTime = null; + } + } + + /// + /// 更新到店天数 + /// + private async Task UpdateVisitDays(LqKhxxEntity member) + { + // 查询该会员的所有有效耗卡记录,按天去重 + var visitDates = await _db.Queryable() + .Where(x => + x.Hy == member.Id + && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Select(x => new + { + VisitDate = SqlFunc.ToDate(x.Hksj) + }) + .Distinct() + .ToListAsync(); + + member.VisitDays = visitDates.Count; + + // 更新首次到店时间和最后到店时间 + if (visitDates.Any()) + { + var firstVisit = await _db.Queryable() + .Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) + .OrderBy(x => x.Hksj) + .Select(x => x.Hksj) + .FirstAsync(); + member.FirstVisitTime = firstVisit; + + var lastVisit = await _db.Queryable() + .Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) + .OrderBy(x => x.Hksj, OrderByType.Desc) + .Select(x => x.Hksj) + .FirstAsync(); + member.LastVisitTime = lastVisit; + } + else + { + member.FirstVisitTime = null; + member.LastVisitTime = null; + } + } + + /// + /// 更新沉睡天数 + /// + private Task UpdateSleepDays(LqKhxxEntity member) + { + var now = DateTime.Now; + var lastConsumeTime = member.LastConsumeTime; + + if (!lastConsumeTime.HasValue) + { + // 如果没有最后消费时间,使用最后到店时间 + lastConsumeTime = member.LastVisitTime; + } + + if (lastConsumeTime.HasValue) + { + var daysSinceLastConsume = (now.Date - lastConsumeTime.Value.Date).Days; + member.SleepDays = Math.Max(0, daysSinceLastConsume); + + // 如果超过最小沉睡阈值,设置沉睡开始时间 + var minSleepThreshold = MemberInfoUpdateConfig.SleepDaysThresholds.Min(); + if (member.SleepDays >= minSleepThreshold) + { + if (!member.SleepStartTime.HasValue) + { + member.SleepStartTime = lastConsumeTime.Value.Date; + } + } + else + { + member.SleepStartTime = null; + } + } + else + { + member.SleepDays = 0; + member.SleepStartTime = null; + } + + return Task.CompletedTask; + } + + /// + /// 更新消费信息(开卡总金额、耗卡总金额、剩余权益总金额、消费等级) + /// + private async Task UpdateConsumeInfo(LqKhxxEntity member) + { + // 1. 计算开卡总金额(累计所有开单的实付业绩总和) + var totalBillingAmount = await _db.Queryable().Where(x => x.Kdhy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.Sfyj) ?? 0m; + member.TotalBillingAmount = totalBillingAmount; + + // 2. 计算耗卡总金额(累计所有耗卡的金额总和) + var totalConsumeAmount = await _db.Queryable().Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.Xfje) ?? 0m; + member.TotalConsumeAmount = totalConsumeAmount; + + // 3. 计算退款总金额 + var totalRefundAmount = await _db.Queryable().Where(x => x.Hy == member.Id && x.IsEffective == StatusEnum.有效.GetHashCode()).SumAsync(x => (decimal?)x.ActualRefundAmount) ?? 0m; + + // 4. 计算剩余权益总金额 + member.RemainingRightsAmount = totalBillingAmount - totalConsumeAmount - totalRefundAmount; + if (member.RemainingRightsAmount < 0) + { + member.RemainingRightsAmount = 0m; + } + + // 5. 计算消费等级(根据耗卡总金额) + var oldLevel = member.ConsumeLevel; + member.ConsumeLevel = MemberInfoUpdateConfig.GetConsumeLevel(totalConsumeAmount); + + // 如果等级发生变化,更新等级更新时间 + if (oldLevel != member.ConsumeLevel) + { + member.ConsumeLevelUpdateTime = DateTime.Now; + } + else if (!member.ConsumeLevelUpdateTime.HasValue && member.ConsumeLevel > 0) + { + // 如果之前没有等级更新时间,但当前有等级,设置当前时间 + member.ConsumeLevelUpdateTime = DateTime.Now; + } + } + #endregion + } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs index d43be6e..5c106cc 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs @@ -192,6 +192,7 @@ namespace NCC.Extend.LqTkjlb MemberInfo.Khlx = MemberTypeEnum.线索.GetHashCode().ToString(); MemberInfo.Dah = MemberNumber; MemberInfo.Jdqd = "19.9卡"; + MemberInfo.Zcsj = DateTime.Now; //找到input.expansionUserId的用户信息 var userInfo = await _db.Queryable().Where(u => u.Id == input.expansionUserId).FirstAsync(); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs index df2e9a2..781dce3 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs @@ -32,6 +32,7 @@ using NCC.Extend.Entitys.lq_xh_jksyj; using NCC.Extend.Entitys.lq_xh_kjbsyj; using NCC.Extend.Entitys.lq_xh_pxmx; using NCC.Extend.Entitys.lq_xmzl; +using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Interfaces.LqXhHyhk; using NCC.FriendlyException; using NCC.JsonSerialization; @@ -899,6 +900,11 @@ namespace NCC.Extend.LqXhHyhk { memberInfo.Khlx = MemberTypeEnum.新客.GetHashCode().ToString(); } + //判断消费金额是否大于0 + if (entity.Sgfy > 0) + { + memberInfo.LastConsumeTime = DateTime.Now; + } //保存会员信息 await _db.Updateable(memberInfo).ExecuteCommandAsync(); // 新增会员耗卡记录 @@ -1003,6 +1009,8 @@ namespace NCC.Extend.LqXhHyhk } } } + //查询开单记录 + var billingRecord = await _db.Queryable().Where(x => x.Kdhy == entity.Hy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Sfyj > 0).AnyAsync(); // 剔除所有T区健康师(姓名中包含"T区"的健康师) var NoTallJksyjEntities = allJksyjEntities.Where(x => x.Jks != null && x.Jks != "" && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList(); //获取这次耗卡有多少个健康师 @@ -1025,6 +1033,7 @@ namespace NCC.Extend.LqXhHyhk Quantity = 1 / (decimal)jksCount, CreateTime = DateTime.Now, IsEffective = StatusEnum.有效.GetHashCode(), + HasBilling = billingRecord ? 1 : 0, }; allPersonTimesRecordEntities.Add(personTimesRecordEntity); } @@ -1050,6 +1059,7 @@ namespace NCC.Extend.LqXhHyhk Quantity = 1 / (decimal)kjbCount, CreateTime = DateTime.Now, IsEffective = StatusEnum.有效.GetHashCode(), + HasBilling = billingRecord ? 1 : 0, }; allPersonTimesRecordEntities.Add(personTimesRecordEntity); } @@ -1894,5 +1904,180 @@ namespace NCC.Extend.LqXhHyhk } #endregion + #region 耗卡品项明细列表 + /// + /// 获取耗卡品项明细记录列表 + /// + /// + /// 根据多种条件查询耗卡品项明细记录列表,支持分页、排序、筛选等功能 + /// + /// 示例请求: + /// GET /api/Extend/LqXhHyhk/consume-item-detail-list?currentPage=1&pageSize=10&startTime=2025-01-01&endTime=2025-01-31 + /// + /// 传入参数说明: + /// - currentPage: 当前页码(必填,默认1) + /// - pageSize: 每页数量(必填,默认10) + /// - sidx: 排序字段(可选,默认"yjsj") + /// - sort: 排序方式(可选,ASC/DESC,默认DESC) + /// - Id: 品项明细ID(可选,精确匹配) + /// - ConsumeId: 耗卡记录ID(可选,精确匹配) + /// - StartConsumeTime/EndConsumeTime: 耗卡时间范围(可选,格式:yyyy-MM-dd) + /// - startTime/endTime: 耗卡时间范围(兼容参数名,格式:yyyy-MM-dd) + /// - MemberId: 会员ID(可选,精确匹配) + /// - MemberName: 会员名称(可选,模糊查询) + /// - MemberPhone: 会员手机号(可选,精确匹配) + /// - ItemId: 品项ID(可选,精确匹配) + /// - ItemName: 品项名称(可选,模糊查询) + /// - ItemType: 品项类型(可选,精确匹配) + /// - SourceType: 来源类型(可选,精确匹配) + /// - StoreId: 门店ID(可选,精确匹配) + /// + /// 返回字段说明: + /// - id: 品项明细ID + /// - consumeId: 耗卡记录ID + /// - consumeTime: 耗卡时间(DateTime格式) + /// - memberName: 会员名称(关联会员表) + /// - memberPhone: 会员手机号(关联会员表) + /// - itemName: 品项名称(品项明细表字段) + /// - itemType: 品项类型(关联项目资料表的qt2字段) + /// - itemPrice: 品项价格(decimal类型) + /// - projectNumber: 项目次数(decimal类型,支持小数) + /// - originalProjectNumber: 原始项目次数(decimal类型) + /// - totalPrice: 合计金额(decimal类型) + /// - sourceType: 来源类型(字符串,如:购买、赠送、体验) + /// - storeId: 门店ID(关联耗卡记录表的门店ID) + /// - storeName: 门店名称(关联门店表的店名字段) + /// + /// 查询参数 + /// 耗卡品项明细记录列表(分页) + /// 查询成功,返回耗卡品项明细记录列表 + /// 参数错误,如页码或页大小无效 + /// 服务器错误,查询过程中发生异常 + [HttpGet("consume-item-detail-list")] + public async Task GetConsumeItemDetailList([FromQuery] ConsumeItemDetailListQueryInput input) + { + try + { + var sidx = input.sidx == null ? "yjsj" : input.sidx; + var sort = string.IsNullOrEmpty(input.sort) ? "DESC" : input.sort; + + // 处理耗卡时间范围(兼容 StartConsumeTime/EndConsumeTime 和 startTime/endTime 两种参数名) + DateTime? startConsumeTime = null; + DateTime? endConsumeTime = null; + + // 优先使用 StartConsumeTime/EndConsumeTime,如果没有则使用 startTime/endTime + string startTimeStr = !string.IsNullOrEmpty(input.StartConsumeTime) ? input.StartConsumeTime : input.startTime; + string endTimeStr = !string.IsNullOrEmpty(input.EndConsumeTime) ? input.EndConsumeTime : input.endTime; + + if (!string.IsNullOrEmpty(startTimeStr) && !string.IsNullOrEmpty(endTimeStr)) + { + // 尝试解析日期字符串(支持多种格式) + if (DateTime.TryParse(startTimeStr, out DateTime startDate)) + { + startConsumeTime = startDate; + } + else + { + throw NCCException.Oh($"开始时间格式错误:{startTimeStr}"); + } + + if (DateTime.TryParse(endTimeStr, out DateTime endDate)) + { + endConsumeTime = endDate; + } + else + { + throw NCCException.Oh($"结束时间格式错误:{endTimeStr}"); + } + } + + // 优化查询:先分页查询主表,再批量查询关联数据,避免子查询性能问题 + // 1. 先构建基础查询条件 + var baseQuery = _db.Queryable() + .Where(pxmx => pxmx.IsEffective == StatusEnum.有效.GetHashCode()) + .WhereIF(!string.IsNullOrEmpty(input.Id), pxmx => pxmx.Id == input.Id) + .WhereIF(!string.IsNullOrEmpty(input.ConsumeId), pxmx => pxmx.ConsumeInfoId == input.ConsumeId) + .WhereIF(startConsumeTime.HasValue, pxmx => pxmx.Yjsj >= new DateTime(startConsumeTime.Value.Year, startConsumeTime.Value.Month, startConsumeTime.Value.Day, 0, 0, 0)) + .WhereIF(endConsumeTime.HasValue, pxmx => pxmx.Yjsj <= new DateTime(endConsumeTime.Value.Year, endConsumeTime.Value.Month, endConsumeTime.Value.Day, 23, 59, 59)) + .WhereIF(!string.IsNullOrEmpty(input.MemberId), pxmx => pxmx.MemberId == input.MemberId) + .WhereIF(!string.IsNullOrEmpty(input.ItemId), pxmx => pxmx.Px == input.ItemId) + .WhereIF(!string.IsNullOrEmpty(input.ItemName), pxmx => pxmx.Pxmc != null && pxmx.Pxmc.Contains(input.ItemName)) + .WhereIF(!string.IsNullOrEmpty(input.SourceType), pxmx => pxmx.SourceType == input.SourceType) + .WhereIF(!string.IsNullOrEmpty(input.ItemType), pxmx => pxmx.ItemCategory == input.ItemType); + + // 2. 通过 EXISTS 子查询筛选关联字段(在分页前筛选,确保分页准确) + baseQuery = baseQuery.WhereIF(!string.IsNullOrEmpty(input.MemberName), pxmx => SqlFunc.Subqueryable().Where(x => x.Id == pxmx.MemberId && x.Khmc != null && x.Khmc.Contains(input.MemberName)).Any()) + .WhereIF(!string.IsNullOrEmpty(input.MemberPhone), pxmx => SqlFunc.Subqueryable().Where(x => x.Id == pxmx.MemberId && x.Sjh == input.MemberPhone).Any()) + .WhereIF(!string.IsNullOrEmpty(input.StoreId), pxmx => SqlFunc.Subqueryable().Where(x => x.Id == pxmx.ConsumeInfoId && x.Md == input.StoreId).Any()); + + // 3. 先分页查询主表数据(查询实体类,提高性能) + var pagedData = await baseQuery.OrderBy(sidx + " " + sort).ToPagedListAsync(input.currentPage, input.pageSize); + + // 4. 批量查询关联数据 + var itemIds = pagedData.list.Select(x => x.Id).ToList(); + var memberIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.MemberId)).Select(x => x.MemberId).Distinct().ToList(); + var consumeIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.ConsumeInfoId)).Select(x => x.ConsumeInfoId).Distinct().ToList(); + + // 批量查询会员信息 + var memberDict = new Dictionary(); + if (memberIds.Any()) + { + var members = await _db.Queryable().Where(x => memberIds.Contains(x.Id)).Select(x => new { x.Id, x.Khmc, x.Sjh }).ToListAsync(); + memberDict = members.ToDictionary(x => x.Id, x => (x.Khmc ?? "", x.Sjh ?? "")); + } + + // 批量查询耗卡记录,获取门店ID和耗卡时间 + var consumeDict = new Dictionary(); + if (consumeIds.Any()) + { + var consumes = await _db.Queryable().Where(x => consumeIds.Contains(x.Id)).Select(x => new { x.Id, x.Md, x.Hksj }).ToListAsync(); + consumeDict = consumes.ToDictionary(x => x.Id, x => (x.Md ?? "", x.Hksj)); + } + + // 批量查询门店信息 + var storeDict = new Dictionary(); + var storeIds = consumeDict.Values.Where(x => !string.IsNullOrEmpty(x.StoreId)).Select(x => x.StoreId).Distinct().ToList(); + if (storeIds.Any()) + { + var stores = await _db.Queryable().Where(x => storeIds.Contains(x.Id)).Select(x => new { x.Id, x.Dm }).ToListAsync(); + storeDict = stores.ToDictionary(x => x.Id, x => x.Dm ?? ""); + } + + // 5. 组装返回数据 + var resultList = pagedData.list.Select(pxmx => new ConsumeItemDetailListOutput + { + id = pxmx.Id, + consumeId = pxmx.ConsumeInfoId, + consumeTime = pxmx.ConsumeInfoId != null && consumeDict.ContainsKey(pxmx.ConsumeInfoId) ? consumeDict[pxmx.ConsumeInfoId].ConsumeTime : pxmx.Yjsj, + memberName = pxmx.MemberId != null && memberDict.ContainsKey(pxmx.MemberId) ? memberDict[pxmx.MemberId].Name : "", + memberPhone = pxmx.MemberId != null && memberDict.ContainsKey(pxmx.MemberId) ? memberDict[pxmx.MemberId].Phone : "", + itemName = pxmx.Pxmc, + itemType = pxmx.ItemCategory, + itemPrice = pxmx.Pxjg, + projectNumber = pxmx.ProjectNumber, + originalProjectNumber = pxmx.OriginalProjectNumber, + totalPrice = pxmx.TotalPrice, + sourceType = pxmx.SourceType, + storeId = pxmx.ConsumeInfoId != null && consumeDict.ContainsKey(pxmx.ConsumeInfoId) ? consumeDict[pxmx.ConsumeInfoId].StoreId : "", + 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] : "" + }).ToList(); + + // 6. 返回分页结果 + var result = new SqlSugarPagedList + { + list = resultList, + pagination = pagedData.pagination + }; + + return PageResult.SqlSugarPageResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex, $"获取耗卡品项明细记录列表失败: {ex.ToString()}"); + throw NCCException.Oh(ErrorCode.COM1005, $"获取耗卡品项明细记录列表失败: {ex.Message}"); + } + } + #endregion + } } diff --git a/sql/添加人次记录表字段.sql b/sql/添加人次记录表字段.sql new file mode 100644 index 0000000..fdbe586 --- /dev/null +++ b/sql/添加人次记录表字段.sql @@ -0,0 +1,18 @@ +-- ============================================ +-- 为人次记录表(lq_person_times_record)添加字段 +-- ============================================ +-- 说明:添加"是否有开单"字段,用于标识该人次记录是否关联开单业务 +-- ============================================ + +-- 添加"是否有开单"字段 (0-否, 1-是) +ALTER TABLE `lq_person_times_record` +ADD COLUMN `F_HasBilling` INT(11) DEFAULT 0 COMMENT '是否有开单(0-否,1-是)' AFTER `F_BusinessType`; + +-- ============================================ +-- 验证字段添加 +-- ============================================ +-- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, COLUMN_COMMENT +-- FROM INFORMATION_SCHEMA.COLUMNS +-- WHERE TABLE_NAME = 'lq_person_times_record' +-- AND COLUMN_NAME = 'F_HasBilling'; + diff --git a/sql/添加会员信息表字段.sql b/sql/添加会员信息表字段.sql new file mode 100644 index 0000000..ef6721c --- /dev/null +++ b/sql/添加会员信息表字段.sql @@ -0,0 +1,87 @@ +-- 为会员信息表(lq_khxx)添加新字段 +-- 执行时间:2025-01-XX + +-- ============================================ +-- 一、会员类型标识字段 +-- ============================================ + +-- 1. 是否生美会员 (0-否, 1-是) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_IsBeautyMember` INT(11) DEFAULT 0 COMMENT '是否生美会员(0-否,1-是)' AFTER `F_LastConsumeTime`; + +-- 2. 是否医美会员 (0-否, 1-是) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_IsMedicalMember` INT(11) DEFAULT 0 COMMENT '是否医美会员(0-否,1-是)' AFTER `F_IsBeautyMember`; + +-- 3. 是否科技部会员 (0-否, 1-是) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_IsTechMember` INT(11) DEFAULT 0 COMMENT '是否科技部会员(0-否,1-是)' AFTER `F_IsMedicalMember`; + +-- ============================================ +-- 二、会员类型成为时间字段(记录成为各类会员的时间) +-- ============================================ + +-- 4. 成为生美会员时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_BeautyMemberTime` DATETIME DEFAULT NULL COMMENT '成为生美会员时间' AFTER `F_IsTechMember`; + +-- 5. 成为医美会员时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_MedicalMemberTime` DATETIME DEFAULT NULL COMMENT '成为医美会员时间' AFTER `F_BeautyMemberTime`; + +-- 6. 成为科技部会员时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_TechMemberTime` DATETIME DEFAULT NULL COMMENT '成为科技部会员时间' AFTER `F_MedicalMemberTime`; + +-- ============================================ +-- 三、到店相关字段(用于计算到店天数) +-- ============================================ + +-- 7. 首次到店时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_FirstVisitTime` DATETIME DEFAULT NULL COMMENT '首次到店时间' AFTER `F_TechMemberTime`; + +-- 8. 最后到店时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_LastVisitTime` DATETIME DEFAULT NULL COMMENT '最后到店时间' AFTER `F_FirstVisitTime`; + +-- 9. 到店天数 (整数,记录会员到店的天数) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_VisitDays` INT(11) DEFAULT 0 COMMENT '到店天数' AFTER `F_LastVisitTime`; + +-- ============================================ +-- 四、沉睡相关字段(用于计算沉睡天数) +-- ============================================ + +-- 10. 沉睡开始时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_SleepStartTime` DATETIME DEFAULT NULL COMMENT '沉睡开始时间' AFTER `F_VisitDays`; + +-- 11. 沉睡天数 (整数,记录会员沉睡的天数) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_SleepDays` INT(11) DEFAULT 0 COMMENT '沉睡天数' AFTER `F_SleepStartTime`; + +-- ============================================ +-- 五、消费相关字段(用于计算消费等级) +-- ============================================ + +-- 12. 耗卡总金额 (累计所有耗卡的金额总和,从lq_xh_hyhk表的xfje字段统计) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_TotalConsumeAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '耗卡总金额(累计所有耗卡的金额总和)' AFTER `F_SleepDays`; + +-- 13. 开卡总金额 (累计所有开单的实付业绩总和,从lq_kd_kdjlb表的sfyj字段统计) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_TotalBillingAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '开卡总金额(累计所有开单的实付业绩总和)' AFTER `F_TotalConsumeAmount`; + +-- 14. 剩余权益总金额 (开卡总金额 - 耗卡总金额 - 退款金额) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_RemainingRightsAmount` DECIMAL(18,2) DEFAULT 0.00 COMMENT '剩余权益总金额(开卡总金额-耗卡总金额-退款金额)' AFTER `F_TotalBillingAmount`; + +-- 15. 消费等级 (整数,记录会员的消费等级,如:0-普通, 1-银卡, 2-金卡, 3-钻石等) +ALTER TABLE `lq_khxx` +ADD COLUMN `F_ConsumeLevel` INT(11) DEFAULT 0 COMMENT '消费等级(0-普通,1-银卡,2-金卡,3-钻石等)' AFTER `F_RemainingRightsAmount`; + +-- 16. 消费等级更新时间 +ALTER TABLE `lq_khxx` +ADD COLUMN `F_ConsumeLevelUpdateTime` DATETIME DEFAULT NULL COMMENT '消费等级更新时间' AFTER `F_ConsumeLevel`; +