using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Mapster; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.ClayObject; using NCC.Common.Configuration; using NCC.Common.Core.Manager; using NCC.Common.Enum; using NCC.Common.Extension; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Common.Model.NPOI; using NCC.DataEncryption; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.Common; using NCC.Extend.Entitys.Dto.LqKdDeductinfo; using NCC.Extend.Entitys.Dto.LqKdKdjlb; using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_hytk_hytk; using NCC.Extend.Entitys.lq_hytk_mx; using NCC.Extend.Entitys.lq_hytk_jksyj; using NCC.Extend.Entitys.lq_hytk_kjbsyj; using NCC.Extend.Entitys.lq_jinsanjiao_user; using NCC.Extend.Entitys.lq_kd_deductinfo; using NCC.Extend.Entitys.lq_xh_hyhk; using NCC.Extend.Entitys.lq_xh_pxmx; using NCC.Extend.Entitys.lq_kd_jksyj; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.Extend.Entitys.lq_kd_kjbsyj; using NCC.Extend.Entitys.lq_kd_pxmx; using NCC.Extend.Entitys.lq_khxx; using NCC.Extend.Entitys.lq_xmzl; using NCC.Extend.Interfaces.LqKdKdjlb; using NCC.Extend.Utils; using NCC.FriendlyException; using NCC.JsonSerialization; using NCC.System.Entitys.Permission; using SqlSugar; using Yitter.IdGenerator; using NCC.Extend.Entitys.lq_package_info; using NCC.Extend.Entitys.lq_mdxx; using NCC.Extend.Entitys.lq_card_transfer_log; using NCC.Extend.Entitys.Dto.LqDailyReport; using NCC.Extend.Entitys.lq_yyjl; using NCC.Extend.Entitys.lq_hzf; namespace NCC.Extend.LqKdKdjlb { /// /// 开单记录表服务 /// [ApiDescriptionSettings(Tag = "绿纤开单记录表服务", Name = "LqKdKdjlb", Order = 200)] [Route("api/Extend/[controller]")] public class LqKdKdjlbService : ILqKdKdjlbService, IDynamicApiController, ITransient { private readonly ISqlSugarRepository _lqKdKdjlbRepository; private readonly ISqlSugarRepository _lqKdJksyjRepository; private readonly ISqlSugarRepository _lqKdKjbsyjRepository; private readonly ISqlSugarRepository _lqKdPxmxRepository; private readonly SqlSugarScope _db; private readonly IUserManager _userManager; private readonly WeChatBotService _weChatBotService; private readonly LqKdKdjlbStringGenerator _stringGenerator; private readonly ILogger _logger; private readonly LqDailyReportService _dailyReportService; /// /// 初始化一个类型的新实例 /// public LqKdKdjlbService( ISqlSugarRepository lqKdKdjlbRepository, ISqlSugarRepository lqKdJksyjRepository, ISqlSugarRepository lqKdKjbsyjRepository, ISqlSugarRepository lqKdPxmxRepository, IUserManager userManager, WeChatBotService weChatBotService, LqKdKdjlbStringGenerator stringGenerator, ILogger logger, LqDailyReportService dailyReportService ) { _lqKdKdjlbRepository = lqKdKdjlbRepository; _db = _lqKdKdjlbRepository.Context; _lqKdJksyjRepository = lqKdJksyjRepository; _lqKdKjbsyjRepository = lqKdKjbsyjRepository; _lqKdPxmxRepository = lqKdPxmxRepository; _userManager = userManager; _weChatBotService = weChatBotService; _stringGenerator = stringGenerator; _logger = logger; _dailyReportService = dailyReportService; } #region 获取开单记录表 /// /// 获取开单记录表 /// /// /// 获取开单记录及其关联的品项明细、健康师业绩、科技部老师业绩信息 /// 按照开单的完整格式返回数据,不包含汇总信息 /// /// 返回数据结构: /// - 主表信息:开单基础信息、业绩信息、支付信息、会员信息等 /// - 品项明细列表:每个品项包含完整的项目信息(项目次数、是否有效、来源类型等) /// - 健康师业绩列表:按品项关联的健康师业绩信息 /// - 科技部老师业绩列表:按品项关联的科技部老师业绩信息 /// /// 开单记录主键ID /// 开单记录完整信息 /// 查询成功 /// 开单记录不存在 /// 服务器内部错误 [HttpGet("{id}")] public async Task GetInfo(string id) { try { // 1. 查询主表信息 var entity = await _db.Queryable().FirstAsync(p => p.Id == id); if (entity == null) { throw NCCException.Oh(ErrorCode.COM1005, "开单记录不存在"); } var output = entity.Adapt(); output.upgradeLifeBeauty = entity.UpgradeLifeBeauty; output.upgradeTechBeauty = entity.UpgradeTechBeauty; output.upgradeMedicalBeauty = entity.UpgradeMedicalBeauty; output.fileUrl = entity.F_FIleUrl; if (!string.IsNullOrEmpty(entity.AppointmentId)) { output.appointmentTime = await _db.Queryable().Where(x => x.Id == entity.AppointmentId).Select(x => x.Yysj).FirstAsync(); } if (output.activityId != null) { output.activityName = await _db.Queryable().Where(x => x.Id == entity.ActivityId).Select(x => x.ActivityName).FirstAsync(); } //获取会员信息 var MemberInfo = await _db.Queryable().FirstAsync(p => p.Id == entity.Kdhy); output.kdhyc = MemberInfo.Khmc; output.kdhysjh = MemberInfo.Sjh; if (!string.IsNullOrEmpty(entity.ActivityId)) { output.activityName = await _db.Queryable().Where(x => x.Id == entity.ActivityId).Select(x => x.ActivityName).FirstAsync(); } // 2. 查询品项明细列表 var lqKdPxmxList = await _db.Queryable().Where(w => w.Glkdbh == entity.Id).ToListAsync(); // 3. 查询健康师业绩列表 var lqKdJksyjList = await _db.Queryable().Where(w => w.Glkdbh == entity.Id).ToListAsync(); // 4. 查询科技部老师业绩列表 var lqKdKjbsyjList = await _db.Queryable().Where(w => w.Glkdbh == entity.Id).ToListAsync(); // 5. 查询扣款信息列表 var lqKdDeductList = await _db.Queryable().Where(w => w.BillingId == entity.Id).ToListAsync(); // 5. 构建品项明细输出,每个品项关联对应的业绩信息 var pxmxOutputList = new List(); foreach (var pxmx in lqKdPxmxList) { var pxmxOutput = new LqKdPxmxInfoOutput { id = pxmx.Id, glkdbh = pxmx.Glkdbh, px = pxmx.Px, pxmc = pxmx.Pxmc, pxjg = pxmx.Pxjg, projectNumber = pxmx.ProjectNumber, isEnabled = pxmx.IsEnabled, sourceType = pxmx.SourceType, memberId = pxmx.MemberId, createTime = pxmx.CreateTIme, totalPrice = pxmx.TotalPrice, actualPrice = pxmx.ActualPrice, remark = pxmx.Remark, isEffective = pxmx.IsEffective, }; // 关联该品项的健康师业绩 var jksyjForPx = lqKdJksyjList.Where(j => j.Kdpxid == pxmx.Id).ToList(); pxmxOutput.lqKdJksyjList = jksyjForPx.Adapt>(); // 关联该品项的科技部老师业绩 var kjbsyjForPx = lqKdKjbsyjList.Where(k => k.Kdpxid == pxmx.Id).ToList(); pxmxOutput.lqKdKjbsyjList = kjbsyjForPx.Adapt>(); pxmxOutputList.Add(pxmxOutput); } // 6. 设置输出结果 output.lqKdPxmxList = pxmxOutputList; // 7. 设置全局业绩列表(用于兼容性,但主要使用品项关联的业绩) output.lqKdJksyjList = lqKdJksyjList.Adapt>(); output.lqKdKjbsyjList = lqKdKjbsyjList.Adapt>(); // 8. 设置扣款信息列表 output.lqKdDeductList = lqKdDeductList.Adapt>(); return output; } catch (Exception ex) { Console.WriteLine($"获取开单记录失败: {ex.Message}"); throw NCCException.Oh(ErrorCode.COM1000, "获取开单记录失败"); } } #endregion #region 获取开单记录表列表 /// /// 获取开单记录表列表 /// /// 请求参数 /// [HttpGet("")] public async Task GetList([FromQuery] LqKdKdjlbListQueryInput input) { var sidx = input.sidx == null ? "kdrq" : input.sidx; List queryKdrq = input.kdrq != null ? input.kdrq.Split(',').ToObeject>() : null; DateTime? startKdrq = queryKdrq != null ? Ext.GetDateTime(queryKdrq.First()) : null; DateTime? endKdrq = queryKdrq != null ? Ext.GetDateTime(queryKdrq.Last()) : null; // 根据是否传入健康师ID或科技部老师ID,动态构建查询 ISugarQueryable baseQuery = null; if (!string.IsNullOrEmpty(input.jksId) && !string.IsNullOrEmpty(input.kjblsId)) { // 同时传入健康师ID和科技部老师ID,需要同时关联两个业绩表 baseQuery = _db.Queryable( (jksyj, kjbsyj, kdjlb) => jksyj.Glkdbh == kdjlb.Id && kjbsyj.Glkdbh == kdjlb.Id) .Where((jksyj, kjbsyj, kdjlb) => jksyj.Jkszh == input.jksId && jksyj.IsEffective == StatusEnum.有效.GetHashCode()) .Where((jksyj, kjbsyj, kdjlb) => kjbsyj.Kjbls == input.kjblsId && kjbsyj.IsEffective == StatusEnum.有效.GetHashCode()) .Select((jksyj, kjbsyj, kdjlb) => kdjlb) .Distinct() .MergeTable(); } else if (!string.IsNullOrEmpty(input.jksId)) { // 只传入健康师ID,关联健康师业绩表 baseQuery = _db.Queryable( (jksyj, kdjlb) => jksyj.Glkdbh == kdjlb.Id) .Where((jksyj, kdjlb) => jksyj.Jkszh == input.jksId && jksyj.IsEffective == StatusEnum.有效.GetHashCode()) .Select((jksyj, kdjlb) => kdjlb) .Distinct() .MergeTable(); } else if (!string.IsNullOrEmpty(input.kjblsId)) { // 只传入科技部老师ID,关联科技部老师业绩表 baseQuery = _db.Queryable( (kjbsyj, kdjlb) => kjbsyj.Glkdbh == kdjlb.Id) .Where((kjbsyj, kdjlb) => kjbsyj.Kjbls == input.kjblsId && kjbsyj.IsEffective == StatusEnum.有效.GetHashCode()) .Select((kjbsyj, kdjlb) => kdjlb) .Distinct() .MergeTable(); } else { // 没有传入健康师ID或科技部老师ID,使用原来的查询逻辑 baseQuery = _db.Queryable(); } var data = await baseQuery .WhereIF(!string.IsNullOrEmpty(input.keyword), p => p.Kdhyc.Contains(input.keyword) || p.Kdhysjh.Contains(input.keyword)) .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) .WhereIF(!string.IsNullOrEmpty(input.djmd), p => p.Djmd.Equals(input.djmd)) .WhereIF(!string.IsNullOrEmpty(input.jsj), p => p.Jsj.Equals(input.jsj)) .WhereIF(queryKdrq != null, p => p.Kdrq >= new DateTime(startKdrq.ToDate().Year, startKdrq.ToDate().Month, startKdrq.ToDate().Day, 0, 0, 0)) .WhereIF(queryKdrq != null, p => p.Kdrq <= new DateTime(endKdrq.ToDate().Year, endKdrq.ToDate().Month, endKdrq.ToDate().Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.gjlx), p => p.Gjlx.Equals(input.gjlx)) .WhereIF(!string.IsNullOrEmpty(input.hgjg), p => p.Hgjg.Equals(input.hgjg)) .WhereIF(input.zdyj != null, p => p.Zdyj > input.zdyj) .WhereIF(input.sfyj != null, p => p.Sfyj > input.sfyj) .WhereIF(input.qk != null, p => p.Qk > input.qk) .WhereIF(!string.IsNullOrEmpty(input.ckfs), p => p.Ckfs.Equals(input.ckfs)) .WhereIF(!string.IsNullOrEmpty(input.fkfs), p => p.Fkfs.Equals(input.fkfs)) .WhereIF(!string.IsNullOrEmpty(input.fkyy), p => p.Fkyy.Equals(input.fkyy)) .WhereIF(!string.IsNullOrEmpty(input.fkpd), p => p.Fkpd.Contains(input.fkpd)) .WhereIF(!string.IsNullOrEmpty(input.khly), p => p.Khly.Equals(input.khly)) .WhereIF(!string.IsNullOrEmpty(input.tjr), p => p.Tjr.Contains(input.tjr)) .WhereIF(!string.IsNullOrEmpty(input.sfskdd), p => p.Sfskdd.Equals(input.sfskdd)) .WhereIF(!string.IsNullOrEmpty(input.jj), p => p.Jj.Contains(input.jj)) .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) .WhereIF(!string.IsNullOrEmpty(input.kdhy), p => p.Kdhy.Equals(input.kdhy)) .WhereIF(!string.IsNullOrEmpty(input.kdhyc), p => p.Kdhyc.Contains(input.kdhyc)) .WhereIF(!string.IsNullOrEmpty(input.kdhysjh), p => p.Kdhysjh.Contains(input.kdhysjh)) .WhereIF(!string.IsNullOrEmpty(input.F_FIleUrl), p => p.F_FIleUrl.Contains(input.F_FIleUrl)) .WhereIF(!string.IsNullOrEmpty(input.CreateUser), p => p.CreateUser.Equals(input.CreateUser)) .WhereIF(input.isEffective != 0, p => p.IsEffective == input.isEffective) .Select(it => new LqKdKdjlbListOutput { id = it.Id, djmd = it.Djmd, jsj = it.Jsj, kdrq = it.Kdrq, gjlx = it.Gjlx, hgjg = it.Hgjg, zdyj = it.Zdyj, sfyj = it.Sfyj, qk = it.Qk, ckfs = it.Ckfs, fkfs = it.Fkfs, fkyy = it.Fkyy, fkpd = it.Fkpd, khly = it.Khly, tjr = it.Tjr, deductAmount = it.DeductAmount, paidDebt = it.PaidDebt, supplementBillingId = it.SupplementBillingId, sfskdd = it.Sfskdd, jj = it.Jj, bz = it.Bz, kdhy = it.Kdhy, kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), isEffective = it.IsEffective, createUser = it.CreateUser, createUserName = SqlFunc.Subqueryable().Where(x => x.Id == it.CreateUser).Select(x => x.RealName), activityId = it.ActivityId, activityName = SqlFunc.Subqueryable().Where(x => x.Id == it.ActivityId).Select(x => x.ActivityName), appointmentId = it.AppointmentId, appointmentTime = it.AppointmentId == null ? (DateTime?)null : SqlFunc.Subqueryable().Where(x => x.Id == it.AppointmentId).Select(x => SqlFunc.ToDate(x.Yysj)), upgradeMedicalBeauty = it.UpgradeMedicalBeauty, upgradeTechBeauty = it.UpgradeTechBeauty, upgradeLifeBeauty = it.UpgradeLifeBeauty, }) .MergeTable() .OrderBy(sidx + " " + input.sort) .ToPagedListAsync(input.currentPage, input.pageSize); // 获取当前页的开单记录ID列表 var billingIds = data.list.Select(x => x.id).ToList(); // 批量查询品项明细 var itemDetails = new List(); if (billingIds.Any()) { itemDetails = await _db.Queryable().Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqKdPxmxInfoOutput { id = x.Id, glkdbh = x.Glkdbh, px = x.Px, pxmc = x.Pxmc, pxjg = x.Pxjg, projectNumber = x.ProjectNumber, isEnabled = x.IsEnabled, sourceType = x.SourceType, memberId = x.MemberId, createTime = x.CreateTIme, totalPrice = x.TotalPrice, actualPrice = x.ActualPrice, remark = x.Remark, isEffective = x.IsEffective }) .ToListAsync(); } // 按开单ID分组品项明细 var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh) .ToDictionary(g => g.Key, g => g.ToList()); // 批量查询健康师业绩(性能优化:一次性查询所有开单的健康师业绩) var jksyjList = new List(); if (billingIds.Any()) { jksyjList = await _db.Queryable().Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => new LqKdJksyjInfoOutput { id = x.Id, glkdbh = x.Glkdbh, kdpxid = x.Kdpxid, jks = x.Jks, jksxm = x.Jksxm, jkszh = x.Jkszh, jksyj = x.Jksyj, yjsj = x.Yjsj, jsj_id = x.Jsj_id, isEffective = x.IsEffective }).ToListAsync(); } // 批量查询科技部老师业绩(性能优化:一次性查询所有开单的科技部老师业绩) var kjbsyjList = new List(); if (billingIds.Any()) { kjbsyjList = await _db.Queryable().Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => new LqKdKjbsyjInfoOutput { id = x.Id, glkdbh = x.Glkdbh, kdpxid = x.Kdpxid, kjbls = x.Kjbls, kjblsxm = x.Kjblsxm, kjblszh = x.Kjblszh, kjblsyj = x.Kjblsyj, yjsj = x.Yjsj, isEffective = x.IsEffective }) .ToListAsync(); } // 批量查询储扣扣款信息(性能优化:一次性查询所有开单的储扣扣款信息) var deductinfoList = new List(); if (billingIds.Any()) { deductinfoList = await _db.Queryable().Where(x => billingIds.Contains(x.BillingId) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqKdDeductinfoInfoOutput { id = x.Id, deductType = x.DeductType, deductId = x.DeductId, billingId = x.BillingId, amount = x.Amount, isEffective = x.IsEffective, unitPrice = x.UnitPrice, itemName = x.ItemName, itemId = x.ItemId, createTime = x.CreateTime, projectNumber = x.ProjectNumber, }) .ToListAsync(); } // 按开单ID分组储扣扣款信息 var deductinfoGrouped = deductinfoList.GroupBy(x => x.billingId).ToDictionary(g => g.Key, g => g.ToList()); // 按开单ID分组健康师业绩 var jksyjGrouped = jksyjList.GroupBy(x => x.glkdbh).ToDictionary(g => g.Key, g => g.ToList()); // 按开单ID分组科技部老师业绩 var kjbsyjGrouped = kjbsyjList.GroupBy(x => x.glkdbh).ToDictionary(g => g.Key, g => g.ToList()); // 为每个开单记录分配品项明细、健康师业绩和科技部老师业绩 foreach (var item in data.list) { item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id) ? itemDetailsGrouped[item.id] : new List(); item.lqKdJksyjList = jksyjGrouped.ContainsKey(item.id) ? jksyjGrouped[item.id] : new List(); item.lqKdKjbsyjList = kjbsyjGrouped.ContainsKey(item.id) ? kjbsyjGrouped[item.id] : new List(); item.lqKdDeductList = deductinfoGrouped.ContainsKey(item.id) ? deductinfoGrouped[item.id] : new List(); } return PageResult.SqlSugarPageResult(data); } #endregion #region 根据健康师ID获取开单列表 /// /// 根据健康师ID获取开单列表 /// /// /// 根据健康师ID查询该健康师参与的所有开单记录,支持时间周期查询和分页 /// /// 示例请求: /// GET /api/Extend/LqKdKdjlb/GetListByJksId?jksId=健康师ID&startTime=2025-01-01&endTime=2025-01-31&currentPage=1&pageSize=10 /// /// 参数说明: /// - jksId: 健康师ID(必填) /// - startTime: 开始时间(可选,格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) /// - endTime: 结束时间(可选,格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) /// - djmd: 门店ID(可选) /// - isEffective: 是否有效(可选,0=全部,1=有效,2=无效,默认1) /// - currentPage: 当前页码(必填) /// - pageSize: 每页数量(必填) /// /// 返回说明: /// - 返回格式与GetList接口相同,包含开单基本信息及品项明细列表 /// /// 查询参数 /// 开单列表(分页) /// 查询成功 /// 参数错误 /// 服务器内部错误 [HttpGet("GetListByJksId")] public async Task GetListByJksId([FromQuery] LqKdKdjlbListByJksQueryInput input) { try { if (string.IsNullOrEmpty(input.jksId)) { throw NCCException.Oh("健康师ID不能为空"); } var sidx = input.sidx == null ? "kdrq" : input.sidx; var sort = string.IsNullOrEmpty(input.sort) ? "DESC" : input.sort; // 解析时间范围 DateTime? startDate = null; DateTime? endDate = null; if (!string.IsNullOrEmpty(input.startTime)) { startDate = Ext.GetDateTime(input.startTime); } if (!string.IsNullOrEmpty(input.endTime)) { endDate = Ext.GetDateTime(input.endTime); // 如果只传了日期,则设置为当天的23:59:59 if (endDate.HasValue && !input.endTime.Contains(":")) { endDate = new DateTime(endDate.Value.Year, endDate.Value.Month, endDate.Value.Day, 23, 59, 59); } } // 通过健康师业绩表关联查询开单记录 var data = await _db.Queryable((jksyj, kdjlb) => jksyj.Glkdbh == kdjlb.Id) .Where((jksyj, kdjlb) => jksyj.Jkszh == input.jksId) .WhereIF(input.isEffective != 0, (jksyj, kdjlb) => jksyj.IsEffective == input.isEffective && kdjlb.IsEffective == input.isEffective) .WhereIF(input.isEffective == 0, (jksyj, kdjlb) => jksyj.IsEffective == StatusEnum.有效.GetHashCode() && kdjlb.IsEffective == StatusEnum.有效.GetHashCode()) .WhereIF(startDate.HasValue, (jksyj, kdjlb) => kdjlb.Kdrq >= startDate.Value) .WhereIF(endDate.HasValue, (jksyj, kdjlb) => kdjlb.Kdrq <= endDate.Value) .WhereIF(!string.IsNullOrEmpty(input.djmd), (jksyj, kdjlb) => kdjlb.Djmd == input.djmd) .Select((jksyj, kdjlb) => new LqKdKdjlbListOutput { id = kdjlb.Id, djmd = kdjlb.Djmd, jsj = kdjlb.Jsj, kdrq = kdjlb.Kdrq, gjlx = kdjlb.Gjlx, hgjg = kdjlb.Hgjg, zdyj = kdjlb.Zdyj, sfyj = kdjlb.Sfyj, qk = kdjlb.Qk, ckfs = kdjlb.Ckfs, fkfs = kdjlb.Fkfs, fkyy = kdjlb.Fkyy, fkpd = kdjlb.Fkpd, khly = kdjlb.Khly, tjr = kdjlb.Tjr, deductAmount = kdjlb.DeductAmount, paidDebt = kdjlb.PaidDebt, supplementBillingId = kdjlb.SupplementBillingId, sfskdd = kdjlb.Sfskdd, jj = kdjlb.Jj, bz = kdjlb.Bz, kdhy = kdjlb.Kdhy, kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.Kdhy).Select(x => x.Sjh), isEffective = kdjlb.IsEffective, createUser = kdjlb.CreateUser, createUserName = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.CreateUser).Select(x => x.RealName), activityId = kdjlb.ActivityId, activityName = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.ActivityId).Select(x => x.ActivityName), appointmentId = kdjlb.AppointmentId, appointmentTime = kdjlb.AppointmentId == null ? (DateTime?)null : SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.AppointmentId).Select(x => SqlFunc.ToDate(x.Yysj)), }) .MergeTable() .Distinct() // 去重,因为一个开单可能对应多个健康师业绩记录 .OrderBy($"{sidx} {sort}") .ToPagedListAsync(input.currentPage, input.pageSize); // 获取当前页的开单记录ID列表 var billingIds = data.list.Select(x => x.id).ToList(); // 批量查询品项明细 var itemDetails = new List(); if (billingIds.Any()) { itemDetails = await _db.Queryable() .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqKdPxmxInfoOutput { id = x.Id, glkdbh = x.Glkdbh, px = x.Px, pxmc = x.Pxmc, pxjg = x.Pxjg, projectNumber = x.ProjectNumber, isEnabled = x.IsEnabled, sourceType = x.SourceType, memberId = x.MemberId, createTime = x.CreateTIme, totalPrice = x.TotalPrice, actualPrice = x.ActualPrice, remark = x.Remark, isEffective = x.IsEffective }) .ToListAsync(); } // 按开单ID分组品项明细 var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh) .ToDictionary(g => g.Key, g => g.ToList()); // 为每个开单记录分配品项明细 foreach (var item in data.list) { item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id) ? itemDetailsGrouped[item.id] : new List(); } return PageResult.SqlSugarPageResult(data); } catch (Exception ex) { _logger.LogError(ex, $"根据健康师ID获取开单列表失败 - 健康师ID: {input?.jksId}, 开始时间: {input?.startTime}, 结束时间: {input?.endTime}"); throw NCCException.Oh($"根据健康师ID获取开单列表失败: {ex.Message}"); } } #endregion #region 根据科技部老师ID获取开单列表 /// /// 根据科技部老师ID获取开单列表 /// /// /// 根据科技部老师ID查询该老师参与的所有开单记录,支持时间周期查询和分页 /// /// 示例请求: /// GET /api/Extend/LqKdKdjlb/GetListByKjbId?kjblsId=科技部老师ID&startTime=2025-01-01&endTime=2025-01-31&currentPage=1&pageSize=10 /// /// 参数说明: /// - kjblsId: 科技部老师ID(必填) /// - startTime: 开始时间(可选,格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) /// - endTime: 结束时间(可选,格式:yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss) /// - djmd: 门店ID(可选) /// - isEffective: 是否有效(可选,0=全部,1=有效,2=无效,默认1) /// - currentPage: 当前页码(必填) /// - pageSize: 每页数量(必填) /// /// 返回说明: /// - 返回格式与GetList接口相同,包含开单基本信息及品项明细列表 /// /// 查询参数 /// 开单列表(分页) /// 查询成功 /// 参数错误 /// 服务器内部错误 [HttpGet("GetListByKjbId")] public async Task GetListByKjbId([FromQuery] LqKdKdjlbListByKjbQueryInput input) { try { if (string.IsNullOrEmpty(input.kjblsId)) { throw NCCException.Oh("科技部老师ID不能为空"); } var sidx = input.sidx == null ? "kdrq" : input.sidx; var sort = string.IsNullOrEmpty(input.sort) ? "DESC" : input.sort; // 解析时间范围 DateTime? startDate = null; DateTime? endDate = null; if (!string.IsNullOrEmpty(input.startTime)) { startDate = Ext.GetDateTime(input.startTime); } if (!string.IsNullOrEmpty(input.endTime)) { endDate = Ext.GetDateTime(input.endTime); // 如果只传了日期,则设置为当天的23:59:59 if (endDate.HasValue && !input.endTime.Contains(":")) { endDate = new DateTime(endDate.Value.Year, endDate.Value.Month, endDate.Value.Day, 23, 59, 59); } } // 通过科技部老师业绩表关联查询开单记录 var data = await _db.Queryable( (kjbsyj, kdjlb) => kjbsyj.Glkdbh == kdjlb.Id) .Where((kjbsyj, kdjlb) => kjbsyj.Kjbls == input.kjblsId) .WhereIF(input.isEffective != 0, (kjbsyj, kdjlb) => kjbsyj.IsEffective == input.isEffective && kdjlb.IsEffective == input.isEffective) .WhereIF(input.isEffective == 0, (kjbsyj, kdjlb) => kjbsyj.IsEffective == StatusEnum.有效.GetHashCode() && kdjlb.IsEffective == StatusEnum.有效.GetHashCode()) .WhereIF(startDate.HasValue, (kjbsyj, kdjlb) => kdjlb.Kdrq >= startDate.Value) .WhereIF(endDate.HasValue, (kjbsyj, kdjlb) => kdjlb.Kdrq <= endDate.Value) .WhereIF(!string.IsNullOrEmpty(input.djmd), (kjbsyj, kdjlb) => kdjlb.Djmd == input.djmd) .Select((kjbsyj, kdjlb) => new LqKdKdjlbListOutput { id = kdjlb.Id, djmd = kdjlb.Djmd, jsj = kdjlb.Jsj, kdrq = kdjlb.Kdrq, gjlx = kdjlb.Gjlx, hgjg = kdjlb.Hgjg, zdyj = kdjlb.Zdyj, sfyj = kdjlb.Sfyj, qk = kdjlb.Qk, ckfs = kdjlb.Ckfs, fkfs = kdjlb.Fkfs, fkyy = kdjlb.Fkyy, fkpd = kdjlb.Fkpd, khly = kdjlb.Khly, tjr = kdjlb.Tjr, deductAmount = kdjlb.DeductAmount, paidDebt = kdjlb.PaidDebt, supplementBillingId = kdjlb.SupplementBillingId, sfskdd = kdjlb.Sfskdd, jj = kdjlb.Jj, bz = kdjlb.Bz, kdhy = kdjlb.Kdhy, kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.Kdhy).Select(x => x.Sjh), isEffective = kdjlb.IsEffective, createUser = kdjlb.CreateUser, createUserName = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.CreateUser).Select(x => x.RealName), activityId = kdjlb.ActivityId, activityName = SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.ActivityId).Select(x => x.ActivityName), appointmentId = kdjlb.AppointmentId, appointmentTime = kdjlb.AppointmentId == null ? (DateTime?)null : SqlFunc.Subqueryable().Where(x => x.Id == kdjlb.AppointmentId).Select(x => SqlFunc.ToDate(x.Yysj)), }) .MergeTable() .Distinct() // 去重,因为一个开单可能对应多个科技部老师业绩记录 .OrderBy($"{sidx} {sort}") .ToPagedListAsync(input.currentPage, input.pageSize); // 获取当前页的开单记录ID列表 var billingIds = data.list.Select(x => x.id).ToList(); // 批量查询品项明细 var itemDetails = new List(); if (billingIds.Any()) { itemDetails = await _db.Queryable() .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqKdPxmxInfoOutput { id = x.Id, glkdbh = x.Glkdbh, px = x.Px, pxmc = x.Pxmc, pxjg = x.Pxjg, projectNumber = x.ProjectNumber, isEnabled = x.IsEnabled, sourceType = x.SourceType, memberId = x.MemberId, createTime = x.CreateTIme, totalPrice = x.TotalPrice, actualPrice = x.ActualPrice, remark = x.Remark, isEffective = x.IsEffective }) .ToListAsync(); } // 按开单ID分组品项明细 var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh) .ToDictionary(g => g.Key, g => g.ToList()); // 为每个开单记录分配品项明细 foreach (var item in data.list) { item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id) ? itemDetailsGrouped[item.id] : new List(); } return PageResult.SqlSugarPageResult(data); } catch (Exception ex) { _logger.LogError(ex, $"根据科技部老师ID获取开单列表失败 - 科技部老师ID: {input?.kjblsId}, 开始时间: {input?.startTime}, 结束时间: {input?.endTime}"); throw NCCException.Oh($"根据科技部老师ID获取开单列表失败: {ex.Message}"); } } #endregion #region 新建开单记录表 /// /// 新建开单记录表 /// /// 参数 /// [HttpPost("")] public async Task Create([FromBody] LqKdKdjlbCrInput input) { var userInfo = await _userManager.GetUserInfo(); var entity = input.Adapt(); var HealthInstructorNames = ""; entity.Id = YitIdHelper.NextId().ToString(); entity.CreateTime = DateTime.Now; entity.UpdateTime = DateTime.Now; entity.IsEffective = StatusEnum.有效.GetHashCode(); try { //开启事务 _db.BeginTran(); //判断是否有补缴开单ID if (!string.IsNullOrEmpty(input.supplementBillingId)) { //查询补缴开单ID var supplementBillingEntity = await _db.Queryable().FirstAsync(p => p.Id == input.supplementBillingId); if (supplementBillingEntity == null || supplementBillingEntity.IsEffective == StatusEnum.无效.GetHashCode()) { throw NCCException.Oh("补缴开单记录不存在或已作废"); } //然后对之前的开单表的补缴金额进行累加 //需要判断补缴金额是否超过欠款金额,只记录应缴金额 var supplementAmount = supplementBillingEntity.Qk - supplementBillingEntity.PaidDebt; supplementBillingEntity.PaidDebt += input.supplementAmount > supplementAmount ? supplementAmount : input.supplementAmount; await _db.Updateable(supplementBillingEntity).ExecuteCommandAsync(); } //新增开单记录表记录 entity.CreateUser = userInfo.userId; //计算储扣总金额 entity.DeductAmount = input.lqKdKdjlbDeductList.Sum(x => x.Amount ?? 0); //判断升单类型:需要同时满足两个条件 //1. 当前开单包含对应类型的品项 //2. 该会员之前有开单记录(升单的前提) //批量查询当前开单所有品项的分类(性能优化) var itemIds = input.lqKdPxmxList?.Where(x => !string.IsNullOrEmpty(x.px)).Select(x => x.px).Distinct().ToList() ?? new List(); var itemCategoryDict = new Dictionary(); if (itemIds.Any()) { var itemCategories = await _db.Queryable() .Where(x => itemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new { x.Id, x.Qt2 }) .ToListAsync(); itemCategoryDict = itemCategories.ToDictionary(k => k.Id, v => v.Qt2 ?? ""); } //判断当前开单是否包含医美品项 var hasMedicalItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "医美"); var MedicalItemInCurrentBillingAmount = input.lqKdPxmxList.Where(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "医美").Sum(x => x.actualPrice); //判断当前开单是否包含科美品项 var hasTechItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "科美"); //判断当前开单是否包含生美品项 var hasLifeItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "生美"); //判断该会员之前是否有医美类型的开单记录(升医美的前提) var hasPreviousMedicalBilling = await _db.Queryable() .Where(pxmx => pxmx.MemberId == entity.Kdhy && pxmx.IsEffective == StatusEnum.有效.GetHashCode() && pxmx.ItemCategory == "医美" && pxmx.ActualPrice > 0 && pxmx.Px != "61") .AnyAsync(); //判断该会员之前是否有科美类型的开单记录(升科美的前提) var hasPreviousTechBilling = await _db.Queryable() .Where(pxmx => pxmx.MemberId == entity.Kdhy && pxmx.IsEffective == StatusEnum.有效.GetHashCode() && pxmx.ItemCategory == "科美" && pxmx.ActualPrice > 0 && pxmx.Px != "61") .AnyAsync(); //判断该会员之前是否有生美类型的开单记录(升生美的前提) var hasPreviousLifeBilling = await _db.Queryable() .Where(pxmx => pxmx.MemberId == entity.Kdhy && pxmx.IsEffective == StatusEnum.有效.GetHashCode() && pxmx.ItemCategory == "生美" && pxmx.ActualPrice > 0 && pxmx.Px != "61") .AnyAsync(); //判断升医美:当前开单包含医美品项 + 之前有医美类型的开单记录 + 实付业绩>=1000 if (hasMedicalItemInCurrentBilling && hasPreviousMedicalBilling && MedicalItemInCurrentBillingAmount >= 1000) { entity.UpgradeMedicalBeauty = "是"; } else { entity.UpgradeMedicalBeauty = "否"; } //判断升科美:当前开单包含科美品项 + 之前有科美类型的开单记录 if (hasTechItemInCurrentBilling && hasPreviousTechBilling) { entity.UpgradeTechBeauty = "是"; } else { entity.UpgradeTechBeauty = "否"; } //判断升生美:当前开单包含生美品项 + 之前有生美类型的开单记录 if (hasLifeItemInCurrentBilling && hasPreviousLifeBilling) { entity.UpgradeLifeBeauty = "是"; } else { entity.UpgradeLifeBeauty = "否"; } //保存开单记录 var newEntity = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteReturnEntityAsync(); //循环品相信息 // 收集所有需要插入的实体,然后批量插入 var allPxmxEntities = new List(); var allJksyjEntities = new List(); var allKjbsyjEntities = new List(); var allDeductEntities = new List(); // 处理扣款信息列表 foreach (var item in input.lqKdKdjlbDeductList) { var lqKdDeductEntity = new LqKdDeductinfoEntity { Id = YitIdHelper.NextId().ToString(), BillingId = newEntity.Id, DeductId = item.DeductId, DeductType = item.DeductType, Amount = item.Amount, ProjectNumber = item.ProjectNumber, UnitPrice = item.UnitPrice, ItemName = item.ItemName, ItemId = item.ItemId, IsEffective = StatusEnum.有效.GetHashCode(), // 设置为有效 CreateTime = DateTime.Now, // 设置创建时间 ItemCategory = await _db.Queryable().Where(x => x.Id == item.ItemId).Select(x => x.Qt2).FirstAsync(), }; allDeductEntities.Add(lqKdDeductEntity); } // 处理品项明细列表 foreach (var item in input.lqKdPxmxList) { // 创建品项明细实体 var lqKdPxmxEntity = new LqKdPxmxEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = newEntity.Id, Yjsj = input.kdrq, CreateTIme = DateTime.Now, MemberId = entity.Kdhy, IsEnabled = StatusEnum.有效.GetHashCode(), ProjectNumber = item.projectNumber, TotalPrice = (decimal)(item.pxjg * item.projectNumber), Px = item.px, Pxmc = item.pxmc, Pxjg = item.pxjg, SourceType = item.sourceType, ActualPrice = item.actualPrice, Remark = item.remark, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.Qt2).FirstAsync(), PerformanceType = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.Fl3).FirstAsync() ?? "", BeautyType = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.BeautyType).FirstAsync(), }; allPxmxEntities.Add(lqKdPxmxEntity); // 收集该品项关联的健康师业绩 if (item.lqKdJksyjList != null && item.lqKdJksyjList.Any()) { //把jksxm保存到HealthInstructorNames foreach (var ijks_tem in item.lqKdJksyjList) { HealthInstructorNames += ijks_tem.jksxm + ","; allJksyjEntities.Add(new LqKdJksyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = newEntity.Id, Jks = ijks_tem.jks, Jksxm = ijks_tem.jksxm, Jkszh = ijks_tem.jkszh, Jksyj = ijks_tem.jksyj, Yjsj = input.kdrq, Jsj_id = ijks_tem.jsj_id, Kdpxid = lqKdPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = lqKdPxmxEntity.ItemCategory, ItemId = lqKdPxmxEntity.Px, ItemName = lqKdPxmxEntity.Pxmc, StoreId = entity.Djmd, PerformanceType = lqKdPxmxEntity.PerformanceType, BeautyType = lqKdPxmxEntity.BeautyType, }); } } // 收集该品项关联的科技部老师业绩 if (item.lqKdKjbsyjList != null && item.lqKdKjbsyjList.Any()) { foreach (var ikjbs_tem in item.lqKdKjbsyjList) { allKjbsyjEntities.Add(new LqKdKjbsyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = newEntity.Id, Kjbls = ikjbs_tem.kjbls, Kjblsxm = ikjbs_tem.kjblsxm, Kjblszh = ikjbs_tem.kjblszh, Kjblsyj = ikjbs_tem.kjblsyj, Yjsj = input.kdrq, Kdpxid = lqKdPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = lqKdPxmxEntity.ItemCategory, ItemId = lqKdPxmxEntity.Px, ItemName = lqKdPxmxEntity.Pxmc, StoreId = entity.Djmd, PerformanceType = lqKdPxmxEntity.PerformanceType, BeautyType = lqKdPxmxEntity.BeautyType, } ); } } } //通过会员id查询会员信息 var memberInfo = await _db.Queryable().Where(u => u.Id == entity.Kdhy).FirstAsync(); //通过开单记录表查询这个会员开单金额 var kdAmount = await _db.Queryable().Where(u => u.Kdhy == entity.Kdhy).SumAsync(u => u.Sfyj); //如果开单金额小于500,为散客,如果大于500,为会员 if (kdAmount < 500 && kdAmount > 0) { memberInfo.Khlx = MemberTypeEnum.散客.GetHashCode().ToString(); } else { memberInfo.Khlx = MemberTypeEnum.会员.GetHashCode().ToString(); } await _db.Updateable(memberInfo).ExecuteCommandAsync(); // 批量插入扣款信息 if (allDeductEntities.Any()) { await _db.Insertable(allDeductEntities).ExecuteCommandAsync(); } // 批量插入品项明细 if (allPxmxEntities.Any()) { await _db.Insertable(allPxmxEntities).ExecuteCommandAsync(); } // 批量插入健康师业绩 if (allJksyjEntities.Any()) { await _db.Insertable(allJksyjEntities).ExecuteCommandAsync(); } // 批量插入科技部老师业绩 if (allKjbsyjEntities.Any()) { await _db.Insertable(allKjbsyjEntities).ExecuteCommandAsync(); } //关闭事务 _db.CommitTran(); // 生成开单记录字符串并发送到企业微信 try { //判断如果开单金额大于0,则发送开单记录字符串到企业微信 if (newEntity.Sfyj > 0) { var entityInfo = await GetInfo(newEntity.Id); if (entityInfo != null) { var orderRecordString = _stringGenerator.GenerateOrderRecordString(entityInfo, HealthInstructorNames); Console.WriteLine("开单记录字符串生成成功:"); Console.WriteLine(orderRecordString); // 发送到企业微信群 try { var sendResult = await _weChatBotService.SendOrderRecordMessage(orderRecordString); if (sendResult) { Console.WriteLine("开单记录已成功发送到企业微信群"); } else { Console.WriteLine("开单记录发送到企业微信群失败"); } } catch (Exception wechatEx) { Console.WriteLine($"发送企业微信消息异常: {wechatEx.Message}"); } } } } catch (Exception ex) { // 字符串生成失败不影响主流程,只记录日志 Console.WriteLine($"生成开单记录字符串失败: {ex.Message}"); } // 播报事业部开单统计数据 try { //判断如果开单金额大于0,则播报事业部开单统计数据 if (newEntity.Sfyj > 0 && newEntity.Kdrq.HasValue) { var billingDate = newEntity.Kdrq.Value.ToString("yyyy-MM-dd"); var statisticsInput = new BusinessUnitBillingStatisticsInput { Date = billingDate }; var statisticsText = await _dailyReportService.GetBusinessUnitBillingStatisticsText(statisticsInput); if (!string.IsNullOrEmpty(statisticsText) && !statisticsText.Contains("暂无开单数据")) { // 发送到企业微信群 try { var sendResult = await _weChatBotService.SendTextMessage(statisticsText); if (sendResult) { Console.WriteLine("事业部开单统计数据已成功发送到企业微信群"); } else { Console.WriteLine("事业部开单统计数据发送到企业微信群失败"); } } catch (Exception wechatEx) { Console.WriteLine($"发送事业部开单统计数据异常: {wechatEx.Message}"); } } } } catch (Exception ex) { // 播报失败不影响主流程,只记录日志 Console.WriteLine($"播报事业部开单统计数据失败: {ex.Message}"); } } catch (Exception ex) { //回滚事务 _db.RollbackTran(); Console.WriteLine($"开单创建失败: {ex.Message}"); Console.WriteLine($"异常堆栈: {ex.StackTrace}"); throw NCCException.Oh(ErrorCode.COM1000); } } #endregion #region 修改开单记录及其关联的品项明细、健康师业绩、科技部老师业绩信息 /// /// 修改开单记录及其关联的品项明细、健康师业绩、科技部老师业绩信息 /// /// /// 参数说明: /// - id: 开单记录主键ID /// - input: 开单记录更新参数,包含品项明细和业绩信息 /// /// 开单记录主键ID /// 开单记录更新参数 /// 无返回值 /// 更新成功 /// 参数错误或数据验证失败 /// 服务器内部错误 [HttpPut("UpdateForNoDelete/{id}")] public async Task UpdateForNoDelete(string id, [FromBody] LqKdKdjlbUpInput input) { var entity = input.Adapt(); entity.Id = id; // 确保ID正确设置 try { //检查开单记录是否可以操作 var (canCancel, errorMessage) = await CheckBillingCanCancelAsync(id); if (!canCancel) { throw NCCException.Oh(errorMessage); } //开启事务 _db.BeginTran(); //查询是否有对应的补缴开单ID if (!string.IsNullOrEmpty(entity.SupplementBillingId)) { //查询补缴开单ID var supplementBillingEntity = await _db.Queryable().FirstAsync(p => p.Id == entity.SupplementBillingId);//900,900 if (supplementBillingEntity == null || supplementBillingEntity.IsEffective == StatusEnum.无效.GetHashCode()) { throw NCCException.Oh("补缴开单记录不存在或已作废"); } //查询当前开单已经补缴金额 var OldSupplementAmount = await _db.Queryable().Where(p => p.Id == id).SumAsync(p => p.SupplementAmount);//900,0 supplementBillingEntity.PaidDebt = supplementBillingEntity.PaidDebt - OldSupplementAmount + input.supplementAmount; await _db.Updateable(supplementBillingEntity).ExecuteCommandAsync(); } //批量查询当前开单所有品项的分类(性能优化) var itemIds = input.lqKdPxmxList?.Where(x => !string.IsNullOrEmpty(x.px)).Select(x => x.px).Distinct().ToList() ?? new List(); var itemCategoryDict = new Dictionary(); if (itemIds.Any()) { var itemCategories = await _db.Queryable() .Where(x => itemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new { x.Id, x.Qt2 }) .ToListAsync(); itemCategoryDict = itemCategories.ToDictionary(k => k.Id, v => v.Qt2 ?? ""); } //判断当前开单是否包含医美品项 var hasMedicalItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "医美"); var MedicalItemInCurrentBillingAmount = input.lqKdPxmxList.Where(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "医美").Sum(x => x.actualPrice); //判断当前开单是否包含科美品项 var hasTechItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "科美"); //判断当前开单是否包含生美品项 var hasLifeItemInCurrentBilling = input.lqKdPxmxList != null && input.lqKdPxmxList.Any(x => !string.IsNullOrEmpty(x.px) && itemCategoryDict.ContainsKey(x.px) && itemCategoryDict[x.px] == "生美"); //获取该会员之前开单品项里面是否有医美项目 var isMedicalProject = hasMedicalItemInCurrentBilling && await _db.Queryable().Where(x => x.MemberId == entity.Kdhy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.ItemCategory == "医美" && x.ActualPrice > 0 && x.Px != "61" && x.Glkdbh != id).AnyAsync(); if (isMedicalProject && MedicalItemInCurrentBillingAmount >= 1000) { entity.UpgradeLifeBeauty = "是"; } else { entity.UpgradeLifeBeauty = "否"; } //获取该会员之前开单品项里面是否有科美项目 var isTechProject = hasTechItemInCurrentBilling && await _db.Queryable().Where(x => x.MemberId == entity.Kdhy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.ItemCategory == "科美" && x.ActualPrice > 0 && x.Px != "61" && x.Glkdbh != id).AnyAsync(); if (isTechProject) { entity.UpgradeTechBeauty = "是"; } else { entity.UpgradeTechBeauty = "否"; } //获取该会员之前开单品项里面是否有生美项目 var isLifeProject = hasLifeItemInCurrentBilling && await _db.Queryable().Where(x => x.MemberId == entity.Kdhy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.ItemCategory == "生美" && x.ActualPrice > 0 && x.Px != "61" && x.Glkdbh != id).AnyAsync(); if (isLifeProject) { entity.UpgradeMedicalBeauty = "是"; } else { entity.UpgradeMedicalBeauty = "否"; } //计算储扣总金额 entity.DeductAmount = input.lqKdKdjlbDeductList.Sum(x => x.Amount ?? 0); entity.UpdateTime = DateTime.Now; // 更新开单记录主表 await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).IgnoreColumns(x => x.CreateTime).ExecuteCommandAsync(); //清空原有品项明细 await _db.Deleteable().Where(x => x.Glkdbh == id).ExecuteCommandAsync(); //清空原有健康师业绩 await _db.Deleteable().Where(x => x.Glkdbh == id).ExecuteCommandAsync(); //清空原有科技部老师业绩 await _db.Deleteable().Where(x => x.Glkdbh == id).ExecuteCommandAsync(); //清空原有扣款信息 await _db.Deleteable().Where(x => x.BillingId == id).ExecuteCommandAsync(); //循环品相信息 // 收集所有需要插入的实体,然后批量插入 var allPxmxEntities = new List(); var allJksyjEntities = new List(); var allKjbsyjEntities = new List(); var allDeductEntities = new List(); // 处理扣款信息列表 foreach (var item in input.lqKdKdjlbDeductList) { var lqKdDeductEntity = new LqKdDeductinfoEntity { Id = YitIdHelper.NextId().ToString(), BillingId = id, DeductId = item.DeductId, DeductType = item.DeductType, Amount = item.Amount, ProjectNumber = item.ProjectNumber, UnitPrice = item.UnitPrice, ItemName = item.ItemName, ItemId = item.ItemId, IsEffective = StatusEnum.有效.GetHashCode(), // 设置为有效 CreateTime = DateTime.Now, // 设置创建时间 ItemCategory = await _db.Queryable().Where(x => x.Id == item.DeductId).Select(x => x.Qt2).FirstAsync() }; allDeductEntities.Add(lqKdDeductEntity); } // 处理品项明细列表 foreach (var item in input.lqKdPxmxList) { // 创建品项明细实体 var lqKdPxmxEntity = new LqKdPxmxEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = id, Yjsj = input.kdrq, CreateTIme = DateTime.Now, MemberId = entity.Kdhy, IsEnabled = StatusEnum.有效.GetHashCode(), ProjectNumber = item.projectNumber, TotalPrice = (decimal)(item.pxjg * item.projectNumber), Px = item.px, Pxmc = item.pxmc, Pxjg = item.pxjg, SourceType = item.sourceType, ActualPrice = item.actualPrice, Remark = item.remark, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.Qt2).FirstAsync(), PerformanceType = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.Fl3).FirstAsync() ?? "", BeautyType = await _db.Queryable().Where(x => x.Id == item.px).Select(x => x.BeautyType).FirstAsync(), }; allPxmxEntities.Add(lqKdPxmxEntity); // 收集该品项关联的健康师业绩 if (item.lqKdJksyjList != null && item.lqKdJksyjList.Any()) { //把jksxm保存到HealthInstructorNames foreach (var ijks_tem in item.lqKdJksyjList) { allJksyjEntities.Add(new LqKdJksyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = id, Jks = ijks_tem.jks, Jksxm = ijks_tem.jksxm, Jkszh = ijks_tem.jkszh, Jksyj = ijks_tem.jksyj, Yjsj = input.kdrq, Jsj_id = ijks_tem.jsj_id, Kdpxid = lqKdPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = lqKdPxmxEntity.ItemCategory, ItemId = lqKdPxmxEntity.Px, StoreId = entity.Djmd, ItemName = lqKdPxmxEntity.Pxmc, PerformanceType = lqKdPxmxEntity.PerformanceType, BeautyType = lqKdPxmxEntity.BeautyType, }); } } // 收集该品项关联的科技部老师业绩 if (item.lqKdKjbsyjList != null && item.lqKdKjbsyjList.Any()) { foreach (var ikjbs_tem in item.lqKdKjbsyjList) { allKjbsyjEntities.Add(new LqKdKjbsyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = id, Kjbls = ikjbs_tem.kjbls, Kjblsxm = ikjbs_tem.kjblsxm, Kjblszh = ikjbs_tem.kjblszh, Kjblsyj = ikjbs_tem.kjblsyj, Yjsj = input.kdrq, Kdpxid = lqKdPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ActivityId = input.activityId, ItemCategory = lqKdPxmxEntity.ItemCategory, ItemId = lqKdPxmxEntity.Px, StoreId = entity.Djmd, ItemName = lqKdPxmxEntity.Pxmc, PerformanceType = lqKdPxmxEntity.PerformanceType, BeautyType = lqKdPxmxEntity.BeautyType, }); } } } //通过会员id查询会员信息 var memberInfo = await _db.Queryable().Where(u => u.Id == entity.Kdhy).FirstAsync(); //通过开单记录表查询这个会员开单金额 var kdAmount = await _db.Queryable().Where(u => u.Kdhy == entity.Kdhy).SumAsync(u => u.Sfyj); //如果开单金额小于500,为散客,如果大于500,为会员 if (kdAmount < 500) { memberInfo.Khlx = MemberTypeEnum.散客.GetHashCode().ToString(); } else { memberInfo.Khlx = MemberTypeEnum.会员.GetHashCode().ToString(); } await _db.Updateable(memberInfo).ExecuteCommandAsync(); // 批量插入扣款信息 if (allDeductEntities.Any()) { await _db.Insertable(allDeductEntities).ExecuteCommandAsync(); } // 批量插入品项明细 if (allPxmxEntities.Any()) { await _db.Insertable(allPxmxEntities).ExecuteCommandAsync(); } // 批量插入健康师业绩 if (allJksyjEntities.Any()) { await _db.Insertable(allJksyjEntities).ExecuteCommandAsync(); } // 批量插入科技部老师业绩 if (allKjbsyjEntities.Any()) { await _db.Insertable(allKjbsyjEntities).ExecuteCommandAsync(); } //关闭事务 _db.CommitTran(); } catch (Exception ex) { //回滚事务 _db.RollbackTran(); throw NCCException.Oh($"创建开单记录失败: {ex.Message}"); } } #endregion #region 获取开单记录表无分页列表 /// /// 获取开单记录表无分页列表 /// /// 请求参数 /// [NonAction] public async Task GetNoPagingList([FromQuery] LqKdKdjlbListQueryInput input) { var sidx = input.sidx == null ? "id" : input.sidx; List queryKdrq = input.kdrq != null ? input.kdrq.Split(',').ToObeject>() : null; DateTime? startKdrq = queryKdrq != null ? Ext.GetDateTime(queryKdrq.First()) : null; DateTime? endKdrq = queryKdrq != null ? Ext.GetDateTime(queryKdrq.Last()) : null; var data = await _db.Queryable() .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) .WhereIF(!string.IsNullOrEmpty(input.djmd), p => p.Djmd.Equals(input.djmd)) .WhereIF(!string.IsNullOrEmpty(input.jsj), p => p.Jsj.Equals(input.jsj)) .WhereIF(queryKdrq != null, p => p.Kdrq >= new DateTime(startKdrq.ToDate().Year, startKdrq.ToDate().Month, startKdrq.ToDate().Day, 0, 0, 0)) .WhereIF(queryKdrq != null, p => p.Kdrq <= new DateTime(endKdrq.ToDate().Year, endKdrq.ToDate().Month, endKdrq.ToDate().Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.gjlx), p => p.Gjlx.Equals(input.gjlx)) .WhereIF(!string.IsNullOrEmpty(input.hgjg), p => p.Hgjg.Equals(input.hgjg)) .WhereIF(input.zdyj != null, p => p.Zdyj > input.zdyj) .WhereIF(input.sfyj != null, p => p.Sfyj > input.sfyj) .WhereIF(input.qk != null, p => p.Qk > input.qk) .WhereIF(!string.IsNullOrEmpty(input.ckfs), p => p.Ckfs.Equals(input.ckfs)) .WhereIF(!string.IsNullOrEmpty(input.fkfs), p => p.Fkfs.Equals(input.fkfs)) .WhereIF(!string.IsNullOrEmpty(input.fkyy), p => p.Fkyy.Equals(input.fkyy)) .WhereIF(!string.IsNullOrEmpty(input.fkpd), p => p.Fkpd.Contains(input.fkpd)) .WhereIF(!string.IsNullOrEmpty(input.khly), p => p.Khly.Equals(input.khly)) .WhereIF(!string.IsNullOrEmpty(input.tjr), p => p.Tjr.Contains(input.tjr)) .WhereIF(!string.IsNullOrEmpty(input.sfskdd), p => p.Sfskdd.Equals(input.sfskdd)) .WhereIF(!string.IsNullOrEmpty(input.jj), p => p.Jj.Contains(input.jj)) .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) .WhereIF(!string.IsNullOrEmpty(input.kdhy), p => p.Kdhy.Equals(input.kdhy)) .WhereIF(!string.IsNullOrEmpty(input.kdhyc), p => p.Kdhyc.Contains(input.kdhyc)) .WhereIF(!string.IsNullOrEmpty(input.kdhysjh), p => p.Kdhysjh.Contains(input.kdhysjh)) .WhereIF(!string.IsNullOrEmpty(input.F_FIleUrl), p => p.F_FIleUrl.Contains(input.F_FIleUrl)) .WhereIF(input.isEffective != 0, p => p.IsEffective == input.isEffective) .Select(it => new LqKdKdjlbListOutput { id = it.Id, djmd = it.Djmd, jsj = it.Jsj, kdrq = it.Kdrq, gjlx = it.Gjlx, hgjg = it.Hgjg, zdyj = it.Zdyj, sfyj = it.Sfyj, qk = it.Qk, ckfs = it.Ckfs, deductAmount = it.DeductAmount, fkfs = it.Fkfs, fkyy = it.Fkyy, fkpd = it.Fkpd, khly = it.Khly, tjr = it.Tjr, sfskdd = it.Sfskdd, jj = it.Jj, bz = it.Bz, kdhy = it.Kdhy, kdhyc = it.Kdhyc, kdhysjh = it.Kdhysjh, fileUrl = it.F_FIleUrl, }) .MergeTable() .OrderBy(sidx + " " + input.sort) .ToListAsync(); return data; } #endregion #region 导出开单记录表 /// /// 导出开单记录表 /// /// 请求参数 /// [HttpGet("Actions/Export")] public async Task Export([FromQuery] LqKdKdjlbListQueryInput input) { var userInfo = await _userManager.GetUserInfo(); var exportData = new List(); if (input.dataType == 0) { var data = Clay.Object(await this.GetList(input)); exportData = data.Solidify>().list; } else { exportData = await this.GetNoPagingList(input); } List paramList = "[{\"value\":\"开单编号\",\"field\":\"id\"},{\"value\":\"开单会员\",\"field\":\"kdhy\"},{\"value\":\"开单会员名称\",\"field\":\"kdhyc\"},{\"value\":\"会员手机号\",\"field\":\"kdhysjh\"},{\"value\":\"单据门店\",\"field\":\"djmd\"},{\"value\":\"金三角\",\"field\":\"jsj\"},{\"value\":\"开单日期\",\"field\":\"kdrq\"},{\"value\":\"顾客类型\",\"field\":\"gjlx\"},{\"value\":\"合作机构\",\"field\":\"hgjg\"},{\"value\":\"整单业绩\",\"field\":\"zdyj\"},{\"value\":\"实付业绩\",\"field\":\"sfyj\"},{\"value\":\"欠款\",\"field\":\"qk\"},{\"value\":\"储扣方式\",\"field\":\"ckfs\"},{\"value\":\"储扣明细\",\"field\":\"ckmx\"},{\"value\":\"付款方式\",\"field\":\"fkfs\"},{\"value\":\"付款医院\",\"field\":\"fkyy\"},{\"value\":\"付款判断\",\"field\":\"fkpd\"},{\"value\":\"客户来源\",\"field\":\"khly\"},{\"value\":\"推荐人\",\"field\":\"tjr\"},{\"value\":\"是否首开订单\",\"field\":\"sfskdd\"},{\"value\":\"简介\",\"field\":\"jj\"},{\"value\":\"备注\",\"field\":\"bz\"},{\"value\":\"健康师业绩\",\"field\":\"jksyj\"},{\"value\":\"科技部老师业绩\",\"field\":\"kjblsyj\"},{\"value\":\"品项信息\",\"field\":\"pxxx\"},{\"value\":\"方案其他\",\"field\":\"F_FIleUrl\"},]".ToList(); ExcelConfig excelconfig = new ExcelConfig(); excelconfig.FileName = "开单记录表.xls"; excelconfig.HeadFont = "微软雅黑"; excelconfig.HeadPoint = 10; excelconfig.IsAllSizeColumn = true; excelconfig.ColumnModel = new List(); List selectKeyList = input.selectKey.Split(',').ToList(); foreach (var item in selectKeyList) { var isExist = paramList.Find(p => p.field == item); if (isExist != null) { excelconfig.ColumnModel.Add(new ExcelColumnModel() { Column = isExist.field, ExcelColumn = isExist.value }); } } var addPath = FileVariable.TemporaryFilePath + excelconfig.FileName; ExcelExportHelper.Export(exportData, excelconfig, addPath); var fileName = _userManager.UserId + "|" + addPath + "|xls"; var output = new { name = excelconfig.FileName, url = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC") }; return output; } #endregion #region 批量删除开单记录表 /// /// 批量删除开单记录表 /// /// 主键数组 /// [HttpPost("batchRemove")] public async Task BatchRemove([FromBody] List ids) { var entitys = await _db.Queryable().In(it => it.Id, ids).ToListAsync(); if (entitys.Count > 0) { try { //开启事务 _db.BeginTran(); // 检查是否有消耗记录 var billingItemIds = await _db.Queryable().Where(p => ids.Contains(p.Glkdbh)).Select(p => p.Id).ToListAsync(); if (billingItemIds.Any()) { var consumeRecords = await _db.Queryable().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()).Where(x => billingItemIds.Contains(x.BillingItemId)).AnyAsync(); if (consumeRecords) { throw NCCException.Oh("选中的开单记录中有已消耗的记录,不能删除"); } } // 检查是否有储扣记录 var deductRecords = await _db.Queryable().Where(x => ids.Contains(x.BillingId) && x.IsEffective == StatusEnum.有效.GetHashCode()).AnyAsync(); if (deductRecords) { throw NCCException.Oh("选中的开单记录中有储扣记录,不能删除"); } //批量删除开单记录表 await _db.Deleteable().In(d => d.Id, ids).ExecuteCommandAsync(); //清空健康师业绩 await _db.Deleteable().In(u => u.Glkdbh, ids).ExecuteCommandAsync(); //清空科技部老师业绩 await _db.Deleteable().In(u => u.Glkdbh, ids).ExecuteCommandAsync(); //清空品项明细 await _db.Deleteable().In(u => u.Glkdbh, ids).ExecuteCommandAsync(); //清空扣款信息 await _db.Deleteable().In(u => u.BillingId, ids).ExecuteCommandAsync(); //关闭事务 _db.CommitTran(); } catch (Exception ex) { //回滚事务 _db.RollbackTran(); throw NCCException.Oh($"批量删除开单记录表失败: {ex.Message}"); } } } #endregion #region 删除开单记录表 /// /// 删除开单记录表 /// /// [HttpDelete("{id}")] public async Task Delete(string id) { var entity = await _db.Queryable().FirstAsync(p => p.Id == id); _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005); try { //开启事务 _db.BeginTran(); //先查询开单品项明细 var lqPxmxList = _db.Queryable().Where(x => x.Glkdbh == id).ToList(); //判断是否有对应的消耗记录 var consumeRecords = await _db.Queryable().Where(x => lqPxmxList.Select(y => y.Id).Contains(x.BillingItemId)).AnyAsync(); if (consumeRecords) { throw NCCException.Oh("选中的开单记录中有已消耗的记录,不能删除"); } //判断是否有对应的退卡记录 var refundRecords = await _db.Queryable().Where(x => lqPxmxList.Select(y => y.Id).Contains(x.BillingItemId)).AnyAsync(); if (refundRecords) { throw NCCException.Oh("选中的开单记录中有退卡记录,不能删除"); } //判断是否有对应的储扣记录 var deductRecords = await _db.Queryable().Where(x => lqPxmxList.Select(y => y.Id).Contains(x.DeductId)).AnyAsync(); if (deductRecords) { throw NCCException.Oh("选中的开单记录中有储扣记录,不能删除"); } //删除开单记录表记录 await _db.Deleteable().Where(d => d.Id == id).ExecuteCommandAsync(); //清空健康师业绩 await _db.Deleteable().Where(u => u.Glkdbh == id).ExecuteCommandAsync(); //清空科技部老师业绩 await _db.Deleteable().Where(u => u.Glkdbh == id).ExecuteCommandAsync(); //清空品项明细 await _db.Deleteable().Where(u => u.Glkdbh == id).ExecuteCommandAsync(); //清空扣款信息 await _db.Deleteable().Where(u => u.BillingId == id).ExecuteCommandAsync(); //关闭事务 _db.CommitTran(); } catch (Exception ex) { //回滚事务 _db.RollbackTran(); throw NCCException.Oh($"删除开单记录表失败: {ex.Message}"); } } #endregion #region 获取扣款类型枚举内容 /// /// 获取扣款类型枚举内容 /// /// 扣款类型枚举列表 [HttpGet("deduct-types")] public List GetDeductTypes() { return Enum.GetValues() .Select(e => new EnumOutput { Value = (int)e, Name = e.ToString(), Description = e.GetDescription(), }) .ToList(); } #endregion #region 获取状态枚举内容(所有的状态通用) /// /// 获取状态枚举内容 /// /// 状态枚举列表 [HttpGet("status-enum")] public List GetStatusEnum() { return Enum.GetValues() .Select(e => new EnumOutput { Value = (int)e, Name = e.ToString(), Description = e.GetDescription(), }) .ToList(); } #endregion #region 作废开单记录 /// /// 作废开单记录 /// /// 作废开单记录输入 /// 无返回值 /// /// 作废指定的开单记录,包括相关的品项明细、业绩记录等 /// /// 示例请求: /// ```json /// { /// "id": "123456789", /// "remarks": "客户要求作废此订单" /// } /// ``` /// /// 参数说明: /// - id: 开单记录主键ID(必填) /// - remarks: 作废备注说明 /// /// 作废成功 /// 参数错误,开单记录ID不能为空 /// 开单记录不存在 /// 服务器内部错误 [HttpPut("Cancel")] public async Task Cancel(CancelBillingInput input) { if (string.IsNullOrEmpty(input.Id)) { throw NCCException.Oh("开单记录ID不能为空"); } try { //开启事务 _db.BeginTran(); // 检查开单记录是否可以作废 var (canCancel, errorMessage) = await CheckBillingCanCancelAsync(input.Id); if (!canCancel) { throw NCCException.Oh(errorMessage); } // 查询开单记录 var entity = await _db.Queryable().FirstAsync(p => p.Id == input.Id); if (entity == null) { throw NCCException.Oh("开单记录不存在"); } // 标记开单记录为无效,并添加作废备注 entity.IsEffective = StatusEnum.无效.GetHashCode(); entity.CancelRefRemarks = input.Remarks; entity.UpdateTime = DateTime.Now; await _db.Updateable(entity).ExecuteCommandAsync(); // 标记对应开单明细表为无效 await _db.Updateable().SetColumns(it => new LqKdPxmxEntity { IsEffective = StatusEnum.无效.GetHashCode() }).Where(it => it.Glkdbh == input.Id).ExecuteCommandAsync(); // 标记健康师业绩为无效 await _db.Updateable().SetColumns(it => new LqKdJksyjEntity { IsEffective = StatusEnum.无效.GetHashCode() }).Where(it => it.Glkdbh == input.Id).ExecuteCommandAsync(); // 标记科技部老师业绩为无效 await _db.Updateable().SetColumns(it => new LqKdKjbsyjEntity { IsEffective = StatusEnum.无效.GetHashCode() }).Where(it => it.Glkdbh == input.Id).ExecuteCommandAsync(); // 标记开单_储扣详细表为无效 await _db.Updateable().SetColumns(it => new LqKdDeductinfoEntity { IsEffective = StatusEnum.无效.GetHashCode() }).Where(it => it.BillingId == input.Id).ExecuteCommandAsync(); //如果存在补缴开单ID,则将补缴开单ID的IsEffective设置为无效 if (!string.IsNullOrEmpty(entity.SupplementBillingId)) { var supplementBillingEntity = await _db.Queryable().FirstAsync(p => p.Id == entity.SupplementBillingId); if (supplementBillingEntity != null && supplementBillingEntity.IsEffective == StatusEnum.有效.GetHashCode()) { supplementBillingEntity.PaidDebt = supplementBillingEntity.PaidDebt - entity.SupplementAmount; await _db.Updateable(supplementBillingEntity).ExecuteCommandAsync(); } } //关闭事务 _db.CommitTran(); } catch (Exception ex) { //回滚事务 _db.RollbackTran(); throw NCCException.Oh($"作废开单记录失败: {ex.Message}"); } } #endregion #region 历史会员权益数据导入 /// /// 导入历史会员权益数据到开单记录表和开单品项明细表 /// /// /// 将历史会员权益表的数据导入到开单记录表和开单品项明细表中 /// /// 导入规则: /// 1. 逐条处理历史会员权益数据,每条记录生成一个开单记录和一个品项明细 /// 2. 开单时间和创建时间统一设置为 2025-06-01 00:00:00 /// 3. 使用YitIdHelper生成所有ID /// 4. 直接使用F_StoreId作为门店ID /// /// 字段映射: /// - F_MemberId/会员编号 -> kdhy (开单会员,优先使用会员ID) /// - 会员名称 -> kdhyc (开单会员名称) /// - 会员电话 -> kdhysjh (开单会员手机号) /// - F_StoreId -> djmd (单据门店ID) /// - 整单业绩 -> zdyj (整单业绩) /// - 实付业绩 -> sfyj (实付业绩) /// - 欠款 -> qk (欠款) /// - 来源 -> khly (客户来源) /// - 品项名称 -> pxmc (品项名称) /// - 品项价格 -> pxjg (品项价格) /// - 品项次数 -> F_ProjectNumber (项目次数) /// - F_MemberId/会员编号 -> F_MemberId (品项明细会员ID,优先使用会员ID) /// /// 导入结果统计 /// 导入成功 /// 服务器内部错误 [HttpPost("import-history-member-rights")] public async Task ImportHistoryMemberRights() { try { _db.BeginTran(); // 设置固定的时间 var fixedDateTime = new DateTime(2025, 6, 1, 0, 0, 0); // 1. 查询历史会员权益数据 var historyData = await _db.Ado.SqlQueryAsync(@" SELECT 会员编号, 会员名称, 会员电话, 开单门店, 开单时间, 整单业绩, 实付业绩, 欠款, 来源, 品项名称, 品项价格, 品项次数, F_StoreId, F_ProjectId, F_MemberId FROM `历史会员权益`"); int successCount = 0; int errorCount = 0; var errorMessages = new List(); // 2. 批量处理数据,每批1000条 const int batchSize = 1000; var totalBatches = (int)Math.Ceiling((double)historyData.Count / batchSize); for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { var batchData = historyData.Skip(batchIndex * batchSize).Take(batchSize).ToList(); var kdjlbEntities = new List(); var pxmxEntities = new List(); foreach (var item in batchData) { try { // 3. 生成开单记录ID string kdjlbId = YitIdHelper.NextId().ToString(); // 4. 创建开单记录 var kdjlbEntity = new LqKdKdjlbEntity { Id = kdjlbId, Djmd = (string)item.F_StoreId, // 直接使用F_StoreId Kdhy = (string)item.F_MemberId, Kdhyc = (string)item.会员名称, Jsj = "暂无", Kdhysjh = (string)item.会员电话, Kdrq = fixedDateTime, Zdyj = Convert.ToDecimal(item.整单业绩 ?? 0), Sfyj = Convert.ToDecimal(item.实付业绩 ?? 0), Qk = Convert.ToDecimal(item.欠款 ?? 0), CreateTime = fixedDateTime, UpdateTime = fixedDateTime, Sfskdd = "否", IsEffective = 1, Bz = "导入历史会员权益数据" }; kdjlbEntities.Add(kdjlbEntity); // 5. 创建品项明细 string pxmxId = YitIdHelper.NextId().ToString(); var pxmxEntity = new LqKdPxmxEntity { Id = pxmxId, Glkdbh = kdjlbId, Px = (string)item.F_ProjectId, Pxmc = (string)item.品项名称, Pxjg = Convert.ToDecimal(item.品项价格 ?? 0) / Convert.ToDecimal(item.品项次数 ?? 0), ProjectNumber = Convert.ToDecimal(item.品项次数 ?? 0), SourceType = (string)item.来源, MemberId = (string)item.F_MemberId, Yjsj = fixedDateTime, CreateTIme = fixedDateTime, IsEffective = 1, IsEnabled = 0, TotalPrice = Convert.ToDecimal(item.品项价格 ?? 0), ActualPrice = Convert.ToDecimal(item.品项价格 ?? 0) }; pxmxEntities.Add(pxmxEntity); successCount++; } catch (Exception ex) { errorCount++; errorMessages.Add($"处理记录 {(string)item.会员编号}-{(string)item.品项名称} 时出错: {ex.Message}"); } } // 6. 批量插入开单记录 if (kdjlbEntities.Any()) { await _db.Insertable(kdjlbEntities).ExecuteCommandAsync(); } // 7. 批量插入品项明细 if (pxmxEntities.Any()) { await _db.Insertable(pxmxEntities).ExecuteCommandAsync(); } // 8. 显示进度 if (batchIndex % 10 == 0) { Console.WriteLine($"已处理 {batchIndex + 1}/{totalBatches} 批次,成功 {successCount} 条,失败 {errorCount} 条"); } } _db.CommitTran(); return new { success = true, message = "历史会员权益数据导入完成", totalRecords = historyData.Count, successCount = successCount, errorCount = errorCount, errorMessages = errorMessages.Take(10).ToList(), // 只返回前10个错误信息 importTime = DateTime.Now }; } catch (Exception ex) { _db.RollbackTran(); throw NCCException.Oh($"导入历史会员权益数据失败: {ex.Message}"); } } #endregion #region 根据门店获取该门店的欠款记录 /// /// 根据门店获取该门店的欠款记录 /// /// 查询参数 /// 分页的欠款记录 /// /// 获取指定门店的欠款记录,考虑已缴欠款情况,支持分页 /// /// 示例请求: /// ```json /// { /// "storeId": "门店ID", /// "currentPage": 1, /// "pageSize": 10, /// "sidx": "Kdrq", /// "sort": "desc" /// } /// ``` /// /// 参数说明: /// - storeId: 门店ID,必填 /// - currentPage: 当前页码 /// - pageSize: 每页大小 /// - sidx: 排序字段,默认按开单日期 /// - sort: 排序方式,默认降序 /// /// 返回说明: /// - 只返回仍有欠款的记录(总欠款 > 已缴欠款) /// - 包含开单编号、客户信息、欠款金额、已缴金额等 /// /// 成功返回分页的欠款记录列表 /// 门店ID不能为空 /// 服务器错误 [HttpGet("GetDebtRecordByStoreId")] public async Task GetDebtRecordByStoreId([FromQuery] DebtRecordQueryInput input) { try { if (string.IsNullOrEmpty(input.StoreId)) { throw NCCException.Oh("门店ID不能为空"); } var sidx = string.IsNullOrEmpty(input.sidx) ? "Kdrq" : input.sidx; var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort; // 获取该门店的欠款记录,考虑已缴欠款情况 // 只返回仍有欠款的记录:总欠款 > 已缴欠款 var debtRecords = await _db.Queryable() .Where(p => p.Djmd == input.StoreId && p.Qk > p.PaidDebt && p.IsEffective == StatusEnum.有效.GetHashCode()) .Select(it => new LqKdKdjlbListOutput { id = it.Id, djmd = it.Djmd, jsj = it.Jsj, kdrq = it.Kdrq, gjlx = it.Gjlx, hgjg = it.Hgjg, zdyj = it.Zdyj, sfyj = it.Sfyj, qk = it.Qk, ckfs = it.Ckfs, paidDebt = it.PaidDebt, fkfs = it.Fkfs, fkyy = it.Fkyy, fkpd = it.Fkpd, khly = it.Khly, tjr = it.Tjr, deductAmount = it.DeductAmount, sfskdd = it.Sfskdd, jj = it.Jj, bz = it.Bz, kdhy = it.Kdhy, kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), isEffective = it.IsEffective, createUser = it.CreateUser, createUserName = SqlFunc.Subqueryable().Where(x => x.Id == it.CreateUser).Select(x => x.RealName), }) .MergeTable() .OrderBy(sidx + " " + sort) .ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(debtRecords); } catch (Exception ex) { throw NCCException.Oh($"获取门店欠款记录失败: {ex.Message}"); } } #endregion #region 获取某个会员的开单记录 /// /// 获取某个会员的开单记录 /// /// /// 根据会员ID查询该会员的所有开单记录,支持分页和时间筛选 /// /// 示例请求: /// ```json /// { /// "memberId": "会员ID", /// "storeId": "门店ID", /// "startTime": "2025-01-01", /// "endTime": "2025-12-31", /// "currentPage": 1, /// "pageSize": 20 /// } /// ``` /// /// 参数说明: /// - memberId: 会员ID(必填) /// - storeId: 门店ID(可选) /// - startTime: 开始时间(可选) /// - endTime: 结束时间(可选) /// - currentPage: 当前页码 /// - pageSize: 每页大小 /// /// 查询参数 /// 分页的开单记录列表 /// 成功返回会员开单记录列表 /// 参数错误 /// 服务器错误 [HttpGet("GetBillingRecordByMemberId")] public async Task GetBillingRecordByMemberId([FromQuery] BillingRecordQueryInput input) { try { var sidx = input.sidx == null ? "kdrq" : input.sidx; var data = await _db.Queryable() .Where(p => p.Kdhy == input.MemberId) .WhereIF(input.StartTime.HasValue, p => p.Kdrq >= input.StartTime.Value) .WhereIF(input.EndTime.HasValue, p => p.Kdrq <= input.EndTime.Value) .Select(it => new LqKdKdjlbListOutput { id = it.Id, djmd = it.Djmd, jsj = it.Jsj, kdrq = it.Kdrq, gjlx = it.Gjlx, hgjg = it.Hgjg, zdyj = it.Zdyj, sfyj = it.Sfyj, qk = it.Qk, ckfs = it.Ckfs, fkfs = it.Fkfs, fkyy = it.Fkyy, fkpd = it.Fkpd, khly = it.Khly, tjr = it.Tjr, deductAmount = it.DeductAmount, sfskdd = it.Sfskdd, jj = it.Jj, bz = it.Bz, kdhy = it.Kdhy, kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), isEffective = it.IsEffective, createUser = it.CreateUser, createUserName = SqlFunc.Subqueryable().Where(x => x.Id == it.CreateUser).Select(x => x.RealName), appointmentId = it.AppointmentId, }) .MergeTable() .OrderBy(sidx + " " + input.sort) .ToPagedListAsync(input.currentPage, input.pageSize); // 获取当前页的开单记录ID列表 var billingIds = data.list.Select(x => x.id).ToList(); // 批量查询品项明细 var itemDetails = new List(); if (billingIds.Any()) { itemDetails = await _db.Queryable() .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqKdPxmxInfoOutput { id = x.Id, glkdbh = x.Glkdbh, px = x.Px, pxmc = x.Pxmc, pxjg = x.Pxjg, projectNumber = x.ProjectNumber, isEnabled = x.IsEnabled, sourceType = x.SourceType, memberId = x.MemberId, createTime = x.CreateTIme, totalPrice = x.TotalPrice, actualPrice = x.ActualPrice, remark = x.Remark, isEffective = x.IsEffective }) .ToListAsync(); } // 按开单ID分组品项明细 var itemDetailsGrouped = itemDetails.GroupBy(x => x.glkdbh) .ToDictionary(g => g.Key, g => g.ToList()); // 为每个开单记录分配品项明细 foreach (var item in data.list) { item.ItemDetails = itemDetailsGrouped.ContainsKey(item.id) ? itemDetailsGrouped[item.id] : new List(); } return PageResult.SqlSugarPageResult(data); } catch (Exception ex) { throw NCCException.Oh($"获取会员开单记录失败: {ex.Message}"); } } #endregion #region 根据会员id获取会员的开单品项列表 /// /// 根据会员id获取会员的开单品项列表 /// /// 查询参数 /// 会员的开单品项列表 [HttpPost("GetBillingItemsByMemberId")] public async Task GetBillingItemsByMemberId([FromBody] BillingItemsByMemberIdQueryInput input) { try { var query = _db.Queryable() .Where(p => p.MemberId == input.MemberId) .WhereIF(!string.IsNullOrEmpty(input.ItemCategory), p => p.ItemCategory == input.ItemCategory) .WhereIF(!string.IsNullOrEmpty(input.BillingId), p => p.Glkdbh == input.BillingId) .WhereIF(input.StartTime.HasValue, p => p.Yjsj >= input.StartTime.Value) .WhereIF(input.EndTime.HasValue, p => p.Yjsj <= input.EndTime.Value) .WhereIF(input.MinActualPrice.HasValue, p => p.ActualPrice >= input.MinActualPrice.Value) .WhereIF(input.MaxActualPrice.HasValue, p => p.ActualPrice <= input.MaxActualPrice.Value) .OrderBy(p => p.Yjsj, OrderByType.Desc); var list = await query.ToPageListAsync(input.currentPage, input.pageSize); var totalCount = await query.CountAsync(); return new { list = list, pagination = new { pageIndex = input.currentPage, pageSize = input.pageSize, totalCount = totalCount } }; } catch (Exception ex) { throw NCCException.Oh($"获取会员开单品项列表失败: {ex.Message}"); } } #endregion #region 根据开单id获取当前开单欠款信息 /// /// 根据开单id获取当前开单欠款信息 /// /// 开单记录ID /// 当前开单欠款信息 [HttpGet("GetBillingDebtInfo/{billingId}")] public async Task GetBillingDebtInfoByBillingId(string billingId) { //返回 整单金额、实付金额、欠款金额、已缴欠款金额 var entity = await _db.Queryable().FirstAsync(p => p.Id == billingId); if (entity == null) { throw NCCException.Oh("开单记录不存在"); } return new { zdyj = entity.Zdyj, sfyj = entity.Sfyj, qk = entity.Qk, PaidDebt = entity.PaidDebt, }; } #endregion #region 修改开单金额 /// /// 修改开单金额 /// /// 修改开单金额输入 /// 修改结果 /// /// 修改开单记录表的金额以及品项明细表里面的金额 /// /// 示例请求: /// ```json /// { /// "billingId": "123456789", /// "zdyj": 1000.00, /// "sfyj": 800.00, /// "deductAmount": 200.00, /// "qk": 0.00, /// "itemDetails": [ /// { /// "itemDetailId": "item001", /// "pxjg": 500.00, /// "totalPrice": 500.00, /// "actualPrice": 400.00 /// } /// ], /// "remark": "金额调整" /// } /// ``` /// /// 参数说明: /// - billingId: 开单记录ID(必填) /// - zdyj: 整单业绩 /// - sfyj: 实付业绩 /// - deductAmount: 储扣总金额 /// - qk: 欠款 /// - itemDetails: 品项明细金额列表 /// - remark: 修改备注 /// /// 成功修改开单金额 /// 请求参数错误 /// 开单记录不存在 /// 服务器内部错误 [HttpPut("UpdateBillingAmount")] public async Task UpdateBillingAmount(LqKdKdjlbUpdateAmountInput input) { try { // 验证输入参数 if (string.IsNullOrEmpty(input.BillingId)) { throw NCCException.Oh("开单记录ID不能为空"); } // 检查开单记录是否存在且有效 var billingEntity = await _db.Queryable().Where(w => w.Id == input.BillingId && w.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync(); if (billingEntity == null) { throw NCCException.Oh("开单记录不存在或已作废"); } // 验证品项明细 if (input.ItemDetails != null && input.ItemDetails.Any()) { var itemDetailIds = input.ItemDetails.Select(x => x.ItemDetailId).ToList(); var existingItems = await _db.Queryable().Where(w => w.Glkdbh == input.BillingId && itemDetailIds.Contains(w.Id)).ToListAsync(); if (existingItems.Count != input.ItemDetails.Count) { throw NCCException.Oh("部分品项明细不存在或不属于该开单记录"); } } // 开始事务 _db.BeginTran(); try { // 更新开单记录金额 var updateResult = await _db.Updateable().SetColumns(it => new LqKdKdjlbEntity { Zdyj = input.Zdyj, Sfyj = input.Sfyj, DeductAmount = input.DeductAmount, Qk = input.Qk, UpdateTime = DateTime.Now }).Where(w => w.Id == input.BillingId).ExecuteCommandAsync(); if (updateResult <= 0) { throw NCCException.Oh("更新开单记录金额失败"); } // 更新品项明细金额 if (input.ItemDetails != null && input.ItemDetails.Any()) { foreach (var itemDetail in input.ItemDetails) { var itemUpdateResult = await _db.Updateable().SetColumns(it => new LqKdPxmxEntity { Pxjg = itemDetail.Pxjg, TotalPrice = itemDetail.TotalPrice, ActualPrice = itemDetail.ActualPrice }).Where(w => w.Id == itemDetail.ItemDetailId && w.Glkdbh == input.BillingId).ExecuteCommandAsync(); if (itemUpdateResult <= 0) { throw NCCException.Oh($"更新品项明细金额失败,品项ID: {itemDetail.ItemDetailId}"); } } } // 提交事务 _db.CommitTran(); return "修改开单金额成功"; } catch (Exception ex) { // 回滚事务 _db.RollbackTran(); throw NCCException.Oh($"修改开单金额失败: {ex.Message}"); } } catch (Exception ex) { throw NCCException.Oh($"修改开单金额时发生错误: {ex.Message}"); } } #endregion #region 获取门店某个时间段的开单记录汇总信息(台账) /// /// 获取门店某个时间段的开单记录汇总信息(台账) /// /// 查询参数 /// 开单记录汇总信息 [HttpGet("GetBillingRecordSummaryByStoreId")] public async Task GetBillingRecordSummaryByStoreId([FromQuery] BillingRecordSummaryQueryInput input) { try { // 验证参数 if (string.IsNullOrEmpty(input.StoreId)) { throw NCCException.Oh("门店ID不能为空"); } // 如果开始时间和结束时间为空,就默认是当月 if (input.StartTime == null || input.EndTime == null) { input.StartTime = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); input.EndTime = DateTime.Now.AddDays(DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month)); } // 构建开单记录查询条件 var billingQuery = _db.Queryable() .Where(w => w.Djmd == input.StoreId && w.Kdrq >= input.StartTime && w.Kdrq <= input.EndTime && w.IsEffective == StatusEnum.有效.GetHashCode()); // 客户筛选 if (!string.IsNullOrEmpty(input.MemberId)) { billingQuery = billingQuery.Where(w => w.Kdhy == input.MemberId); } // 查询开单记录 var billingRecords = await billingQuery .Select(it => new { id = it.Id, djmd = it.Djmd, jsj = it.Jsj, kdrq = it.Kdrq, gjlx = it.Gjlx, zdyj = it.Zdyj, sfyj = it.Sfyj, qk = it.Qk, hgjg = it.Hgjg, hgjgName = SqlFunc.Subqueryable().Where(x => x.Id == it.Hgjg).Select(x => x.Hzmc), kdhyc = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Khmc), kdhysjh = SqlFunc.Subqueryable().Where(x => x.Id == it.Kdhy).Select(x => x.Sjh), fkfs = it.Fkfs, // 付款方式 fkyy = it.Fkyy, // 付款医院ID fkyyName = SqlFunc.Subqueryable().Where(x => x.Id == it.Fkyy).Select(x => x.Hzmc), // 付款医院名称 activityId = it.ActivityId, // 营销活动ID activityName = SqlFunc.Subqueryable().Where(x => x.Id == it.ActivityId).Select(x => x.ActivityName), // 营销活动名称 khly = it.Khly, // 客户来源 bz = it.Bz, // 备注 createTime = it.CreateTime }) .ToListAsync(); if (!billingRecords.Any()) { return new { success = true, data = new { billingSummary = new { totalCount = 0, totalAmount = 0, totalPaidAmount = 0, totalDebt = 0, billingRecords = new List() }, itemSummary = new { purchased = new { count = 0, totalAmount = 0, items = new List() }, gifted = new { count = 0, totalAmount = 0, items = new List() }, experience = new { count = 0, totalAmount = 0, items = new List() } }, healthTeacherSummary = new { totalCount = 0, teachers = new List() } }, message = "该时间段内无开单记录" }; } var billingIds = billingRecords.Select(x => x.id).ToList(); // 构建品项明细查询条件 var itemDetailsQuery = _db.Queryable() .Where(w => billingIds.Contains(w.Glkdbh) && w.IsEffective == StatusEnum.有效.GetHashCode()); // 品项分类筛选 if (!string.IsNullOrEmpty(input.ItemCategory)) { itemDetailsQuery = itemDetailsQuery.Where(w => w.ItemCategory == input.ItemCategory); } // 品项筛选 if (!string.IsNullOrEmpty(input.ItemId)) { itemDetailsQuery = itemDetailsQuery.Where(w => w.Px == input.ItemId); } // 查询品项明细,按F_SourceType分类 var itemDetails = await itemDetailsQuery .Select(it => new { id = it.Id, glkdbh = it.Glkdbh, px = it.Px, pxmc = it.Pxmc, pxjg = it.Pxjg, sourceType = it.SourceType, totalPrice = it.TotalPrice, actualPrice = it.ActualPrice, projectNumber = it.ProjectNumber, remark = it.Remark, itemCategory = it.ItemCategory, }) .ToListAsync(); // 按来源类型分类品项 var purchasedItems = itemDetails.Where(x => x.sourceType == "购买").ToList(); var giftedItems = itemDetails.Where(x => x.sourceType == "赠送").ToList(); var experienceItems = itemDetails.Where(x => x.sourceType == "体验").ToList(); // 构建健康师业绩查询条件 var healthTeacherQuery = _db.Queryable() .Where(w => billingIds.Contains(w.Glkdbh) && w.IsEffective == StatusEnum.有效.GetHashCode()); // 健康师筛选 if (!string.IsNullOrEmpty(input.HealthCoachId)) { healthTeacherQuery = healthTeacherQuery.Where(w => w.Jks == input.HealthCoachId || w.Jkszh == input.HealthCoachId); } // 品项分类筛选(健康师业绩表) if (!string.IsNullOrEmpty(input.ItemCategory)) { healthTeacherQuery = healthTeacherQuery.Where(w => w.ItemCategory == input.ItemCategory); } // 品项筛选(健康师业绩表) if (!string.IsNullOrEmpty(input.ItemId)) { healthTeacherQuery = healthTeacherQuery.Where(w => w.ItemId == input.ItemId); } // 查询健康师业绩数据 var healthTeacherData = await healthTeacherQuery .Select(it => new { id = it.Id, glkdbh = it.Glkdbh, jks = it.Jks, jksxm = it.Jksxm, jkszh = it.Jkszh, jksyj = it.Jksyj, kdpxid = it.Kdpxid, yjsj = it.Yjsj, jsj_id = it.Jsj_id, itemCategory = it.ItemCategory, storeId = it.StoreId, itemId = it.ItemId, itemName = it.ItemName, }) .ToListAsync(); // 根据筛选条件过滤开单记录 // 收集所有需要匹配的开单ID(取交集) var matchedBillingIds = new List(); var hasFilterCondition = false; // 标记是否传入了筛选条件 // 如果传入了品项分类或品项筛选条件,收集匹配的开单ID if (!string.IsNullOrEmpty(input.ItemCategory) || !string.IsNullOrEmpty(input.ItemId)) { hasFilterCondition = true; var itemMatchedIds = itemDetails.Select(x => x.glkdbh).Distinct().ToList(); if (matchedBillingIds.Any()) { matchedBillingIds = matchedBillingIds.Intersect(itemMatchedIds).ToList(); } else { matchedBillingIds = itemMatchedIds; } } // 如果传入了健康师筛选条件,收集匹配的开单ID if (!string.IsNullOrEmpty(input.HealthCoachId)) { hasFilterCondition = true; var healthTeacherMatchedIds = healthTeacherData.Select(x => x.glkdbh).Distinct().ToList(); if (matchedBillingIds.Any()) { matchedBillingIds = matchedBillingIds.Intersect(healthTeacherMatchedIds).ToList(); } else { matchedBillingIds = healthTeacherMatchedIds; } } // 如果传入了筛选条件,必须过滤开单记录(即使没有匹配数据也要过滤,返回空结果) if (hasFilterCondition) { if (matchedBillingIds.Any()) { billingRecords = billingRecords.Where(x => matchedBillingIds.Contains(x.id)).ToList(); } else { // 传入了筛选条件但没有匹配数据,返回空结果 billingRecords = billingRecords.Where(x => false).ToList(); } billingIds = billingRecords.Select(x => x.id).ToList(); } // 构建按开单记录分组的详细数据 var detailedRecords = billingRecords.Select(billing => new { // 基本信息 date = billing.kdrq?.ToString("yyyy-MM-dd"), customerName = billing.kdhyc, customerPhone = billing.kdhysjh, storeId = billing.djmd, goldTriangle = billing.jsj, customerType = billing.gjlx, cooperationInstitution = billing.hgjg, cooperationInstitutionName = billing.hgjgName, paymentHospital = billing.fkyy, // 付款医院ID paymentHospitalName = billing.fkyyName, // 付款医院名称 activityId = billing.activityId, // 营销活动ID activityName = billing.activityName, // 营销活动名称 // 品项分类 purchasedItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "购买").Select(x => new { id = x.id, itemName = x.pxmc, itemCode = x.px, price = x.pxjg, totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, remark = x.remark, itemCategory = x.itemCategory, }).ToList(), giftedItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "赠送").Select(x => new { id = x.id, itemName = x.pxmc, itemCode = x.px, price = x.pxjg, totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, remark = x.remark, itemCategory = x.itemCategory, }).ToList(), experienceItems = itemDetails.Where(x => x.glkdbh == billing.id && x.sourceType == "体验").Select(x => new { id = x.id, itemName = x.pxmc, itemCode = x.px, price = x.pxjg, totalPrice = x.totalPrice, actualPrice = x.actualPrice, projectNumber = x.projectNumber, remark = x.remark, itemCategory = x.itemCategory, }).ToList(), // 金额信息 paidAmount = billing.sfyj, debtAmount = billing.qk, totalAmount = billing.zdyj, // 健康师信息 healthTeachers = healthTeacherData.Where(x => x.glkdbh == billing.id).Select(x => new { teacherId = x.jks, teacherName = x.jksxm, teacherAccount = x.jkszh, performance = x.jksyj, performanceTime = x.yjsj, itemDetailId = x.kdpxid, goldTriangleId = x.jsj_id, itemCategory = x.itemCategory, itemId = x.itemId, storeId = x.storeId, itemName = x.itemName }).ToList(), // 其他信息 source = billing.khly, // 客户来源 paymentMethod = billing.fkfs, // 支付方式 remark = billing.bz, // 备注 createTime = billing.createTime }).OrderByDescending(x => x.date).ToList(); // 构建返回结果 var result = new { success = true, data = new { // 汇总统计 summary = new { totalCount = billingRecords.Count, totalAmount = billingRecords.Sum(x => x.zdyj), totalPaidAmount = billingRecords.Sum(x => x.sfyj), totalDebt = billingRecords.Sum(x => x.qk), totalPurchasedItems = purchasedItems.Count, totalGiftedItems = giftedItems.Count, totalExperienceItems = experienceItems.Count, totalHealthTeachers = healthTeacherData.GroupBy(x => x.jks).Count() }, // 详细记录列表 records = detailedRecords }, message = "获取开单记录汇总信息成功" }; return result; } catch (Exception ex) { throw NCCException.Oh($"获取开单记录汇总信息失败:{ex.Message}"); } } #endregion #region 私有方法 /// /// 检查开单记录是否可以操作 /// /// 开单记录ID /// 是否可以作废 private async Task<(bool canCancel, string errorMessage)> CheckBillingCanCancelAsync(string billingId) { try { // 查询开单记录 var entity = await _db.Queryable().FirstAsync(p => p.Id == billingId); if (entity == null) { return (false, "开单记录不存在"); } // 检查是否已经作废 if (entity.IsEffective == StatusEnum.无效.GetHashCode()) { return (false, "该开单记录已经作废"); } // 判断是否有对应的补缴记录 var qkbjList = await _db.Queryable().Where(p => p.SupplementBillingId == billingId && p.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); if (qkbjList.Any()) { return (false, "该开单记录有对应的补缴记录,不能进行操作"); } // 查询开单记录下的品项明细ID列表 var pxmxIdList = await _db.Queryable().Where(p => p.Glkdbh == billingId && p.IsEffective == StatusEnum.有效.GetHashCode()).Select(p => p.Id).Distinct().ToListAsync(); // 判断是否有对应的消耗记录 var xhPxmxList = await _db.Queryable().Where(p => pxmxIdList.Contains(p.BillingItemId) && p.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); if (xhPxmxList.Any()) { return (false, "该开单记录有对应的消耗记录,不能进行操作"); } // 判断是否有退卡记录 var hytkMxList = await _db.Queryable().Where(p => pxmxIdList.Contains(p.BillingItemId) && p.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); if (hytkMxList.Any()) { return (false, "该开单记录有对应的退卡记录,不能进行操作"); } // 判断是否已经有储扣记录 var deductInfoList = await _db.Queryable().Where(p => pxmxIdList.Contains(p.DeductId) && p.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); if (deductInfoList.Any()) { return (false, "该开单记录有对应的储扣记录,不能进行操作"); } return (true, string.Empty); } catch (Exception ex) { return (false, $"检查开单记录状态失败: {ex.Message}"); } } #endregion #region 会员转卡操作 /// /// 会员转卡操作 /// /// /// 转卡是指一个会员把自己剩余的某些品项,转给另外一个会员 /// 转卡会员转出的品项,就做退卡操作 /// 被转卡会员收到的品项,做开卡操作 /// 系统会自动从原开单记录中获取健康师和科技部老师的业绩数据 /// /// 示例请求: /// ```json /// { /// "fromMemberId": "member001", /// "toMemberId": "member002", /// "storeId": "store001", /// "signatureFile": "base64签名数据", /// "remarks": "转卡备注信息", /// "transferItems": [ /// { /// "billingItemId": "pxmx001", /// "transferQuantity": 5, /// "itemName": "美容项目A", /// "itemPrice": 100.00 /// } /// ] /// } /// ``` /// /// 参数说明: /// - fromMemberId: 转出会员ID(必填) /// - toMemberId: 转入会员ID(必填) /// - storeId: 门店ID(必填) /// - signatureFile: 转出会员签名文件(必填) /// - remarks: 转卡备注信息(可选) /// - transferItems: 转出品项列表(必填) /// - billingItemId: 开单品项明细ID(必填) /// - transferQuantity: 转出数量(必填,大于0) /// - itemName: 品项名称(必填) /// - itemPrice: 品项价格(必填,大于等于0) /// /// 业务规则: /// - 转出和转入会员必须存在且有效 /// - 转出品项的剩余数量必须足够 /// - 系统会自动从原开单记录中获取健康师和科技部老师业绩数据 /// - 转卡操作会同时创建退卡记录和开卡记录 /// - 所有操作在事务中执行,确保数据一致性 /// /// 转卡输入参数 /// 转卡结果 /// 转卡成功 /// 参数错误或业务规则验证失败 /// 服务器错误 [HttpPost("TransferCard")] public async Task TransferCard([FromBody] TransferCardInput input) { if (input == null) { throw NCCException.Oh("转卡参数不能为空"); } var userInfo = await _userManager.GetUserInfo(); var transferTime = DateTime.Now; try { // 开启事务 _db.BeginTran(); // 1. 验证转出和转入会员是否存在 var fromMember = await _db.Queryable().Where(x => x.Id == input.FromMemberId && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync(); if (fromMember == null) { throw NCCException.Oh("转出会员不存在或已失效"); } var toMember = await _db.Queryable().Where(x => x.Id == input.ToMemberId && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync(); if (toMember == null) { throw NCCException.Oh("转入会员不存在或已失效"); } // 2. 验证转出品项的余额是否足够 foreach (var item in input.TransferItems) { var billingItem = await _db.Queryable().Where(x => x.Id == item.BillingItemId && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync(); if (billingItem == null) { throw NCCException.Oh($"品项明细不存在:{item.ItemName}"); } // 查询剩余数量 var remainingCount = await GetItemRemainingCount(item.BillingItemId); if (remainingCount < item.TransferQuantity) { throw NCCException.Oh($"品项 {item.ItemName} 剩余数量不足,当前剩余:{remainingCount},需要转出:{item.TransferQuantity}"); } } // 3. 创建退卡记录(转出方) var refundId = YitIdHelper.NextId().ToString(); var totalRefundAmount = input.TransferItems.Sum(x => x.ItemPrice * x.TransferQuantity); var StoreEntity = await _db.Queryable().Where(p => p.Id == input.StoreId).FirstAsync(); var refundEntity = new LqHytkHytkEntity { Id = refundId, F_CreateTime = transferTime, F_CreateUser = userInfo.userId, IsEffective = StatusEnum.有效.GetHashCode(), Czry = userInfo.userId, Hy = input.FromMemberId, Hymc = fromMember.Khmc, Md = input.StoreId, Mdmc = StoreEntity.Dm, Mdbh = StoreEntity.Mdbm, SignatureFile = input.SignatureFile, Hyzh = input.FromMemberId, Gklx = fromMember.Khlx, Tksj = DateTime.Now, Tkje = totalRefundAmount, ActualRefundAmount = totalRefundAmount, // 转卡时实退金额等于退卡总金额 Tkyy = "转卡", Bz = $"转卡给会员:{toMember.Khmc},{input.Remarks}" }; await _db.Insertable(refundEntity).ExecuteCommandAsync(); // 4. 创建退卡品项明细和业绩记录 var refundMxEntities = new List(); var refundJksyjEntities = new List(); var refundKjbsyjEntities = new List(); foreach (var item in input.TransferItems) { var refundPxmxEntity = await _db.Queryable().Where(p => p.Id == item.BillingItemId).FirstAsync(); //计算品项扣除总金额 var totalItemDeduction = item.ItemPrice * item.TransferQuantity; //保存退卡明细表 var refundMxEntity = new LqHytkMxEntity { Id = YitIdHelper.NextId().ToString(), RefundInfoId = refundId, BillingItemId = item.BillingItemId, MemberId = input.FromMemberId, // 转卡时使用转出方会员ID CreateTime = transferTime, CreateUser = userInfo.userId, Px = refundPxmxEntity.Px, Pxmc = item.ItemName, Pxjg = item.ItemPrice, Tkje = totalItemDeduction, ProjectNumber = item.TransferQuantity, SourceType = refundPxmxEntity.SourceType, TotalPrice = totalItemDeduction, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.Qt2).FirstAsync(), PerformanceType = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.Fl3).FirstAsync() ?? "", BeautyType = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.BeautyType).FirstAsync(), }; refundMxEntities.Add(refundMxEntity); var refundKdyjEntities = _db.Queryable().Where(p => p.Kdpxid == item.BillingItemId).ToList(); //保存退卡健康师业绩表 foreach (var jks in refundKdyjEntities) { //获取健康师当月金三角id var monthString = DateTime.Now.ToString("yyyyMM"); var GetMonTeamResult = await _db.Queryable().Where(p => p.UserId == jks.Jkszh && p.Month == monthString).ToListAsync(); var GetMonTeam = GetMonTeamResult.FirstOrDefault(); if (GetMonTeam == null) { throw NCCException.Oh($"健康师 {jks.Jksxm} 在 {monthString} 月份的金三角团队中不存在,请先配置金三角团队信息"); } //获取本月 refundJksyjEntities.Add(new LqHytkJksyjEntity { Id = YitIdHelper.NextId().ToString(), Gltkbh = refundId, Jks = jks.Jkszh, Jksxm = jks.Jksxm, Jkszh = jks.Jkszh, Jksyj = totalItemDeduction / refundKdyjEntities.Count(), Tksj = DateTime.Now, F_jsjid = GetMonTeam.JsjId, F_tkpxid = refundMxEntity.Id, F_tkpxNumber = item.TransferQuantity, F_CreateTime = transferTime, F_CreateUser = userInfo.userId, CardReturn = refundMxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = refundMxEntity.ItemCategory, ItemId = refundMxEntity.Px, StoreId = refundEntity.Md, ItemName = refundMxEntity.Pxmc, PerformanceType = refundMxEntity.PerformanceType, BeautyType = refundMxEntity.BeautyType, }); } //查询科技部老师的业绩 var TechTeacherEntities = _db.Queryable().Where(p => p.Kdpxid == item.BillingItemId).ToList(); // 创建退卡科技部老师业绩记录 foreach (var kjbs in TechTeacherEntities) { refundKjbsyjEntities.Add(new LqHytkKjbsyjEntity { Id = YitIdHelper.NextId().ToString(), Gltkbh = refundId, Kjbls = kjbs.Kjbls, Kjblsxm = kjbs.Kjblsxm, Kjblszh = kjbs.Kjblszh, Kjblsyj = (totalItemDeduction / TechTeacherEntities.Count()), Tksj = transferTime, F_tkpxid = refundMxEntity.Id, F_LaborCost = kjbs.LaborCost, F_tkpxNumber = item.TransferQuantity, F_CreateTime = transferTime, F_CreateUser = userInfo.userId, CardReturn = refundMxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = refundMxEntity.ItemCategory, ItemId = refundMxEntity.Px, StoreId = refundEntity.Md, ItemName = refundMxEntity.Pxmc, PerformanceType = refundMxEntity.PerformanceType, BeautyType = refundMxEntity.BeautyType, }); } } await _db.Insertable(refundMxEntities).ExecuteCommandAsync(); if (refundJksyjEntities.Any()) { await _db.Insertable(refundJksyjEntities).ExecuteCommandAsync(); } if (refundKjbsyjEntities.Any()) { await _db.Insertable(refundKjbsyjEntities).ExecuteCommandAsync(); } // 5. 创建开卡记录(转入方) var billingId = YitIdHelper.NextId().ToString(); var billingEntity = new LqKdKdjlbEntity { Id = billingId, CreateTime = transferTime, UpdateTime = transferTime, IsEffective = StatusEnum.有效.GetHashCode(), CreateUser = userInfo.userId, Kdhy = input.ToMemberId, Djmd = input.StoreId, Kdrq = DateTime.Now, Gjlx = toMember.Khlx, Fkfs = "转卡", Sfskdd = "否", Zdyj = input.TransferItems.Sum(x => x.ItemPrice * x.TransferQuantity), Sfyj = input.TransferItems.Sum(x => x.ItemPrice * x.TransferQuantity), Bz = $"从会员 {fromMember.Khmc} 转入,{input.Remarks}" }; await _db.Insertable(billingEntity).ExecuteCommandAsync(); // 6. 创建开单品项明细和业绩记录 var billingPxmxEntities = new List(); var billingJksyjEntities = new List(); var billingKjbsyjEntities = new List(); foreach (var item in input.TransferItems) { var refundPxmxEntity = await _db.Queryable().Where(p => p.Id == item.BillingItemId).FirstAsync(); // 计算转卡金额和数量 var transferAmount = item.ItemPrice * item.TransferQuantity; var transferQuantity = item.TransferQuantity; // 获取原开单品项的总金额和总数量 var originalTotalPrice = refundPxmxEntity.TotalPrice > 0 ? refundPxmxEntity.TotalPrice : refundPxmxEntity.ActualPrice; var originalProjectNumber = refundPxmxEntity.ProjectNumber > 0 ? refundPxmxEntity.ProjectNumber : 1; // 计算转卡比例(优先使用金额比例,如果金额为0则使用数量比例) decimal transferRatio = 0; if (originalTotalPrice > 0) { transferRatio = transferAmount / originalTotalPrice; } else if (originalProjectNumber > 0) { transferRatio = (decimal)transferQuantity / originalProjectNumber; } else { transferRatio = 1; // 如果原数据异常,默认按100%计算 } var billingPxmxEntity = new LqKdPxmxEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = billingId, Px = refundPxmxEntity.Px, Pxmc = item.ItemName, Pxjg = item.ItemPrice, MemberId = input.ToMemberId, CreateTIme = transferTime, ProjectNumber = item.TransferQuantity, IsEnabled = StatusEnum.有效.GetHashCode(), SourceType = refundPxmxEntity.SourceType, TotalPrice = transferAmount, ActualPrice = transferAmount, IsEffective = StatusEnum.有效.GetHashCode(), Remark = $"从会员 {fromMember.Khmc} 转入", ItemCategory = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.Qt2).FirstAsync(), PerformanceType = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.Fl3).FirstAsync() ?? "", BeautyType = await _db.Queryable().Where(x => x.Id == refundPxmxEntity.Px).Select(x => x.BeautyType).FirstAsync(), }; billingPxmxEntities.Add(billingPxmxEntity); // 查询原开单记录的健康师业绩 var billingKdyjEntitiesList = _db.Queryable().Where(p => p.Kdpxid == item.BillingItemId).ToList(); if (billingKdyjEntitiesList.Any()) { // 计算原开单所有健康师的总业绩 var originalTotalJksyj = billingKdyjEntitiesList.Sum(x => decimal.Parse(x.Jksyj ?? "0")); // 如果原开单总业绩为0,则按转卡金额均分 if (originalTotalJksyj == 0) { var avgJksyj = transferAmount / billingKdyjEntitiesList.Count; foreach (var jks in billingKdyjEntitiesList) { billingJksyjEntities.Add(new LqKdJksyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = billingId, Jks = jks.Jkszh, Jksxm = jks.Jksxm, Jkszh = jks.Jkszh, Jksyj = avgJksyj.ToString("F2"), Yjsj = transferTime, Jsj_id = jks.Jsj_id, Kdpxid = billingPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = billingPxmxEntity.ItemCategory, ItemId = billingPxmxEntity.Px, StoreId = billingEntity.Djmd, ItemName = billingPxmxEntity.Pxmc, PerformanceType = billingPxmxEntity.PerformanceType, BeautyType = billingPxmxEntity.BeautyType, }); } } else { // 按原业绩比例计算转卡后的业绩 foreach (var jks in billingKdyjEntitiesList) { var originalJksyj = decimal.Parse(jks.Jksyj ?? "0"); // 计算该健康师在原开单中的业绩占比 var jksRatio = originalTotalJksyj > 0 ? originalJksyj / originalTotalJksyj : 0; // 转卡后的业绩 = 转卡金额 × 该健康师的业绩占比 var transferJksyj = transferAmount * jksRatio; billingJksyjEntities.Add(new LqKdJksyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = billingId, Jks = jks.Jkszh, Jksxm = jks.Jksxm, Jkszh = jks.Jkszh, Jksyj = transferJksyj.ToString("F2"), Yjsj = transferTime, Jsj_id = jks.Jsj_id, Kdpxid = billingPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = billingPxmxEntity.ItemCategory, ItemId = billingPxmxEntity.Px, StoreId = billingEntity.Djmd, ItemName = billingPxmxEntity.Pxmc, PerformanceType = billingPxmxEntity.PerformanceType, BeautyType = billingPxmxEntity.BeautyType, }); } } } // 查询原开单记录的科技部老师业绩 var billingKjbsyjEntitiesList = _db.Queryable().Where(p => p.Kdpxid == item.BillingItemId).ToList(); if (billingKjbsyjEntitiesList.Any()) { // 计算原开单所有科技部老师的总业绩 var originalTotalKjbsyj = billingKjbsyjEntitiesList.Sum(x => decimal.Parse(x.Kjblsyj ?? "0")); // 如果原开单总业绩为0,则按转卡金额均分 if (originalTotalKjbsyj == 0) { var avgKjbsyj = transferAmount / billingKjbsyjEntitiesList.Count; foreach (var kjbs in billingKjbsyjEntitiesList) { billingKjbsyjEntities.Add(new LqKdKjbsyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = billingId, Kjbls = kjbs.Kjbls, Kjblsxm = kjbs.Kjblsxm, Kjblszh = kjbs.Kjblszh, Kjblsyj = avgKjbsyj.ToString("F2"), Yjsj = transferTime, Kdpxid = billingPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = billingPxmxEntity.ItemCategory, ItemId = billingPxmxEntity.Px, StoreId = billingEntity.Djmd, ItemName = billingPxmxEntity.Pxmc, PerformanceType = billingPxmxEntity.PerformanceType }); } } else { // 按原业绩比例计算转卡后的业绩 foreach (var kjbs in billingKjbsyjEntitiesList) { var originalKjbsyj = decimal.Parse(kjbs.Kjblsyj ?? "0"); // 计算该科技部老师在原开单中的业绩占比 var kjbsRatio = originalTotalKjbsyj > 0 ? originalKjbsyj / originalTotalKjbsyj : 0; // 转卡后的业绩 = 转卡金额 × 该科技部老师的业绩占比 var transferKjbsyj = transferAmount * kjbsRatio; billingKjbsyjEntities.Add(new LqKdKjbsyjEntity { Id = YitIdHelper.NextId().ToString(), Glkdbh = billingId, Kjbls = kjbs.Kjbls, Kjblsxm = kjbs.Kjblsxm, Kjblszh = kjbs.Kjblszh, Kjblsyj = transferKjbsyj.ToString("F2"), Yjsj = transferTime, Kdpxid = billingPxmxEntity.Id, IsEffective = StatusEnum.有效.GetHashCode(), ItemCategory = billingPxmxEntity.ItemCategory, ItemId = billingPxmxEntity.Px, StoreId = billingEntity.Djmd, ItemName = billingPxmxEntity.Pxmc, PerformanceType = billingPxmxEntity.PerformanceType }); } } } } //记录转卡日志 var transferLogEntity = new LqCardTransferLogEntity { Id = YitIdHelper.NextId().ToString(), RefundCardId = refundId, BillingCardId = billingId, TransferFromMemberId = input.FromMemberId, TransferToMemberId = input.ToMemberId, TransferAmount = input.TransferItems.Sum(x => x.ItemPrice * x.TransferQuantity), TransferTime = transferTime, OperatorId = userInfo.userId, Remarks = input.Remarks, CreateTime = DateTime.Now, IsEffective = StatusEnum.有效.GetHashCode() }; await _db.Insertable(transferLogEntity).ExecuteCommandAsync(); await _db.Insertable(billingPxmxEntities).ExecuteCommandAsync(); if (billingJksyjEntities.Any()) { await _db.Insertable(billingJksyjEntities).ExecuteCommandAsync(); } if (billingKjbsyjEntities.Any()) { await _db.Insertable(billingKjbsyjEntities).ExecuteCommandAsync(); } // 提交事务 _db.CommitTran(); return new TransferCardOutput { Success = true, TransferId = refundId, RefundId = refundId, BillingId = billingId, TotalAmount = input.TransferItems.Sum(x => x.ItemPrice * x.TransferQuantity), TotalQuantity = input.TransferItems.Sum(x => x.TransferQuantity), FromMemberName = fromMember.Khmc, ToMemberName = toMember.Khmc, TransferTime = transferTime, Message = "转卡操作成功" }; } catch (Exception ex) { _db.RollbackTran(); _logger.LogError(ex, "转卡操作失败:{Message}", ex.Message); throw NCCException.Oh($"转卡操作失败:{ex.Message}"); } } #endregion #region 获取品项剩余数量 /// /// 获取品项剩余数量 /// /// 开单品项明细ID /// 剩余数量 private async Task GetItemRemainingCount(string billingItemId) { try { // 查询购买数量 var purchasedCount = await _db.Queryable() .Where(x => x.Id == billingItemId && x.IsEffective == StatusEnum.有效.GetHashCode()) .SumAsync(x => (decimal?)x.ProjectNumber) ?? 0m; // 查询消费数量 var consumedCount = await _db.Queryable() .Where(x => x.BillingItemId == billingItemId && x.IsEffective == StatusEnum.有效.GetHashCode()) .SumAsync(x => x.OriginalProjectNumber) ?? 0m; // 查询退卡数量 var refundedCount = await _db.Queryable() .Where(x => x.BillingItemId == billingItemId && x.IsEffective == StatusEnum.有效.GetHashCode()) .SumAsync(x => (decimal?)x.ProjectNumber) ?? 0m; // 查询储扣数量 var deductCount = await _db.Queryable() .Where(x => x.DeductId == billingItemId && x.IsEffective == StatusEnum.有效.GetHashCode()) .SumAsync(x => (decimal?)x.ProjectNumber) ?? 0m; // 计算剩余数量 var remainingCount = (int)(purchasedCount - consumedCount - refundedCount - deductCount); return Math.Max(0, remainingCount); // 确保不为负数 } catch (Exception ex) { _logger.LogError(ex, "获取品项剩余数量失败,品项ID:{BillingItemId}", billingItemId); throw NCCException.Oh($"获取品项剩余数量失败:{ex.Message}"); } } #endregion #region 门店整体统计表 /// /// 门店整体统计表 /// /// /// 统计每个健康师在指定时间周期内的各项数据指标 /// 包括:邀约人数、预约人数、到店人数、开单人数、开单金额、消耗金额、人头、人次、消耗项目数 /// /// 示例请求: /// ```json /// { /// "startTime": "2025-10-01", /// "endTime": "2025-10-31", /// "departmentId": "部门ID", /// "storeId": "门店ID", /// "employeeName": "健康师姓名" /// } /// ``` /// /// 参数说明: /// - startTime: 开始时间(可选,默认为当月1号) /// - endTime: 结束时间(可选,默认为当前时间) /// - departmentId: 事业部ID(可选) /// - storeId: 门店ID(可选) /// - employeeName: 健康师姓名(可选) /// /// 返回字段说明: /// - EmployeeId: 健康师ID /// - EmployeeName: 健康师姓名 /// - StoreId: 门店ID /// - StoreName: 门店名称 /// - DepartmentId: 事业部ID /// - DepartmentName: 事业部名称 /// - InviteCount: 邀约人数(按客户去重) /// - AppointmentCount: 预约人数(按客户去重,无论预约状态) /// - VisitCount: 到店人数(按客户去重,仅统计状态为'已确认'的预约) /// - BillingCount: 开单人数(按开单记录去重) /// - BillingAmount: 开单金额(开单业绩总金额) /// - ConsumeAmount: 消耗金额(消耗业绩总金额) /// - HeadCount: 人头(按客户去重) /// - PersonCount: 人次(按客户+日期去重,同一客户不同天算多次) /// - ProjectCount: 消耗项目数(项目总次数) /// - RefundAmount: 退卡金额(健康师退卡业绩总金额) /// - LaborCost: 手工费(消耗时的手工费总金额) /// - OriginalLaborCost: 原始手工费(消耗时的原始手工费总金额) /// - OvertimeLaborCost: 加班手工费(消耗时的加班手工费总金额) /// /// 查询参数 /// 健康师统计数据列表 /// 成功返回统计数据 /// 参数错误 /// 服务器错误 [HttpGet("get-health-coach-statistics")] public async Task GetHealthCoachStatistics([FromQuery] HealthCoachStatisticsQueryInput input) { try { // 设置默认时间范围(如果未提供,默认为当月) var startTime = input.StartTime ?? new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); var endTime = input.EndTime ?? DateTime.Now; // 构建SQL查询 var sql = $@" SELECT u.F_Id as EmployeeId, u.F_REALNAME as EmployeeName, u.F_MDID as StoreId, md.dm as StoreName, md.syb as DepartmentId, dept.F_FullName as DepartmentName, -- 邀约人数 COALESCE(invite_stats.InviteCount, 0) as InviteCount, -- 预约人数(无论状态) COALESCE(appointment_stats.AppointmentCount, 0) as AppointmentCount, -- 到店人数(已确认状态) COALESCE(visit_stats.VisitCount, 0) as VisitCount, -- 开单人数和金额 COALESCE(billing_stats.BillingCount, 0) as BillingCount, COALESCE(billing_stats.BillingAmount, 0) as BillingAmount, -- 消耗相关统计 COALESCE(consume_stats.ConsumeAmount, 0) as ConsumeAmount, CAST(COALESCE(headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as HeadCount, CAST(COALESCE(personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as PersonCount, CAST(COALESCE(invalid_headcount_stats.HeadCount, 0) AS DECIMAL(18,2)) as InvalidHeadCount, CAST(COALESCE(invalid_personcount_stats.PersonCount, 0) AS DECIMAL(18,2)) as InvalidPersonCount, CAST(COALESCE(consume_stats.ProjectCount, 0) AS DECIMAL(18,2)) as ProjectCount, -- 退卡金额 COALESCE(refund_stats.RefundAmount, 0) as RefundAmount, -- 手工费相关统计 COALESCE(consume_stats.LaborCost, 0) as LaborCost, COALESCE(consume_stats.OriginalLaborCost, 0) as OriginalLaborCost, COALESCE(consume_stats.OvertimeLaborCost, 0) as OvertimeLaborCost FROM BASE_USER u LEFT JOIN lq_mdxx md ON u.F_MDID = md.F_Id LEFT JOIN base_organize dept ON md.syb = dept.F_Id -- 邀约统计子查询 LEFT JOIN ( SELECT yyr as EmployeeId, COUNT(DISTINCT yykh) as InviteCount FROM lq_yaoyjl WHERE yyr IS NOT NULL AND F_CreateTime >= @startTime AND F_CreateTime <= @endTime GROUP BY yyr ) invite_stats ON u.F_Id = invite_stats.EmployeeId -- 预约统计子查询 LEFT JOIN ( SELECT yyr as EmployeeId, COUNT(DISTINCT gk) as AppointmentCount FROM lq_yyjl WHERE yyr IS NOT NULL AND F_CreateTime >= @startTime AND F_CreateTime <= @endTime GROUP BY yyr ) appointment_stats ON u.F_Id = appointment_stats.EmployeeId -- 到店统计子查询 LEFT JOIN ( SELECT yyr as EmployeeId, COUNT(DISTINCT gk) as VisitCount FROM lq_yyjl WHERE yyr IS NOT NULL AND F_Status = '已确认' AND F_CreateTime >= @startTime AND F_CreateTime <= @endTime GROUP BY yyr ) visit_stats ON u.F_Id = visit_stats.EmployeeId -- 开单统计子查询 LEFT JOIN ( SELECT jkszh as EmployeeId, COUNT(DISTINCT glkdbh) as BillingCount, SUM(CAST(jksyj AS DECIMAL(18,2))) as BillingAmount FROM lq_kd_jksyj WHERE jkszh IS NOT NULL AND F_IsEffective = 1 AND yjsj >= @startTime AND yjsj <= @endTime GROUP BY jkszh ) billing_stats ON u.F_Id = billing_stats.EmployeeId -- 消耗统计子查询 LEFT JOIN ( SELECT jksyj.jks as EmployeeId, SUM(jksyj.jksyj) as ConsumeAmount, CAST(SUM(jksyj.F_kdpxNumber) AS DECIMAL(18,2)) as ProjectCount, COALESCE(SUM(jksyj.F_LaborCost), 0) as LaborCost, COALESCE(SUM(jksyj.F_OriginalLaborCost), 0) as OriginalLaborCost, COALESCE(SUM(jksyj.F_OvertimeLaborCost), 0) as OvertimeLaborCost FROM lq_xh_jksyj jksyj INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id WHERE jksyj.jks IS NOT NULL AND jksyj.F_IsEffective = 1 AND hyhk.F_IsEffective = 1 AND hyhk.hksj >= @startTime AND hyhk.hksj <= @endTime GROUP BY jksyj.jks ) consume_stats ON u.F_Id = consume_stats.EmployeeId -- 有效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=1) LEFT JOIN ( SELECT F_PersonId as EmployeeId, CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount FROM ( SELECT F_PersonId, F_WorkMonth, F_MemberId, F_Quantity FROM lq_person_times_record WHERE F_PersonId IS NOT NULL AND F_IsEffective = 1 AND F_PersonType = '健康师' AND F_HasBilling = 1 AND F_WorkDate >= DATE(@startTime) AND F_WorkDate <= DATE(@endTime) GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity ) as distinct_headcount GROUP BY F_PersonId ) headcount_stats ON u.F_Id = headcount_stats.EmployeeId -- 有效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=1) LEFT JOIN ( SELECT F_PersonId as EmployeeId, CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount FROM ( SELECT F_PersonId, F_WorkDate, F_MemberId, F_Quantity FROM lq_person_times_record WHERE F_PersonId IS NOT NULL AND F_IsEffective = 1 AND F_PersonType = '健康师' AND F_HasBilling = 1 AND F_WorkDate >= DATE(@startTime) AND F_WorkDate <= DATE(@endTime) GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity ) as distinct_personcount GROUP BY F_PersonId ) personcount_stats ON u.F_Id = personcount_stats.EmployeeId -- 无效人头统计子查询(从人次记录表获取,按月份+客户+数量去重后累加数量,F_HasBilling=0) LEFT JOIN ( SELECT F_PersonId as EmployeeId, CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as HeadCount FROM ( SELECT F_PersonId, F_WorkMonth, F_MemberId, F_Quantity FROM lq_person_times_record WHERE F_PersonId IS NOT NULL AND F_IsEffective = 1 AND F_PersonType = '健康师' AND F_HasBilling = 0 AND F_WorkDate >= DATE(@startTime) AND F_WorkDate <= DATE(@endTime) GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity ) as distinct_headcount GROUP BY F_PersonId ) invalid_headcount_stats ON u.F_Id = invalid_headcount_stats.EmployeeId -- 无效人次统计子查询(从人次记录表获取,按日期+客户+数量去重后累加数量,F_HasBilling=0) LEFT JOIN ( SELECT F_PersonId as EmployeeId, CAST(COALESCE(SUM(F_Quantity), 0) AS DECIMAL(18,2)) as PersonCount FROM ( SELECT F_PersonId, F_WorkDate, F_MemberId, F_Quantity FROM lq_person_times_record WHERE F_PersonId IS NOT NULL AND F_IsEffective = 1 AND F_PersonType = '健康师' AND F_HasBilling = 0 AND F_WorkDate >= DATE(@startTime) AND F_WorkDate <= DATE(@endTime) GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity ) as distinct_personcount GROUP BY F_PersonId ) invalid_personcount_stats ON u.F_Id = invalid_personcount_stats.EmployeeId -- 退卡统计子查询 LEFT JOIN ( SELECT jkszh as EmployeeId, SUM(CAST(jksyj AS DECIMAL(18,2))) as RefundAmount FROM lq_hytk_jksyj WHERE jkszh IS NOT NULL AND F_IsEffective = 1 AND tksj >= @startTime AND tksj <= @endTime GROUP BY jkszh ) refund_stats ON u.F_Id = refund_stats.EmployeeId WHERE u.F_GW = '健康师' "; // 添加条件过滤 var conditions = new List(); var parameters = new List { new SugarParameter("@startTime", startTime), new SugarParameter("@endTime", endTime) }; if (!string.IsNullOrEmpty(input.DepartmentId)) { conditions.Add("md.syb = @departmentId"); parameters.Add(new SugarParameter("@departmentId", input.DepartmentId)); } if (!string.IsNullOrEmpty(input.StoreId)) { conditions.Add("u.F_MDID = @storeId"); parameters.Add(new SugarParameter("@storeId", input.StoreId)); } if (!string.IsNullOrEmpty(input.EmployeeName)) { conditions.Add("u.F_REALNAME LIKE @employeeName"); parameters.Add(new SugarParameter("@employeeName", $"%{input.EmployeeName}%")); } if (conditions.Any()) { sql += " AND " + string.Join(" AND ", conditions); } sql += " ORDER BY u.F_REALNAME"; // 执行查询 var allData = await _db.Ado.SqlQueryAsync(sql, parameters); // 手动分页 var totalCount = allData.Count; var pagedData = allData .Skip((input.currentPage - 1) * input.pageSize) .Take(input.pageSize) .ToList(); // 直接返回分页结果 return new { list = pagedData, pagination = new { pageIndex = input.currentPage, pageSize = input.pageSize, totalCount = totalCount } }; } catch (Exception ex) { _logger.LogError(ex, "获取健康师统计数据失败"); throw NCCException.Oh($"获取健康师统计数据失败:{ex.Message}"); } } #endregion #region 获取开单品项明细记录列表 /// /// 获取开单品项明细记录列表(分页) /// /// /// 查询开单品项明细记录,支持多条件筛选和分页查询。采用先分页后关联的查询方式,确保分页准确性和查询性能。 /// /// 示例请求: /// ```json /// { /// "currentPage": 1, /// "pageSize": 10, /// "sidx": "yjsj", /// "sort": "DESC", /// "Id": "品项明细ID", /// "BillingId": "开单ID", /// "StartBillingTime": "2025-01-01", /// "EndBillingTime": "2025-12-31", /// "ActivityId": "营销活动ID", /// "MemberId": "会员ID", /// "MemberName": "会员名称", /// "MemberPhone": "会员手机号", /// "ItemId": "品项ID", /// "ItemName": "品项名称", /// "ItemType": "品项类型", /// "SourceType": "来源类型", /// "StoreId": "门店ID" /// } /// ``` /// /// 查询参数说明: /// - currentPage: 当前页码(必填,从1开始) /// - pageSize: 每页数量(必填) /// - sidx: 排序字段(可选,默认:yjsj,支持:id、billingId、billingTime、itemName、actualPrice、projectNumber等) /// - sort: 排序方式(可选,默认:DESC,支持:ASC、DESC) /// - Id: 品项明细ID(可选,精确匹配) /// - BillingId: 开单ID(可选,精确匹配) /// - StartBillingTime: 开单时间开始(可选,格式:yyyy-MM-dd,与EndBillingTime同时使用) /// - EndBillingTime: 开单时间结束(可选,格式:yyyy-MM-dd,与StartBillingTime同时使用) /// - ActivityId: 营销活动ID(可选,精确匹配) /// - MemberId: 会员ID(可选,精确匹配) /// - MemberName: 会员名称(可选,模糊查询) /// - MemberPhone: 会员手机号(可选,精确匹配) /// - ItemId: 品项ID(可选,精确匹配) /// - ItemName: 品项名称(可选,模糊查询) /// - ItemType: 品项类型(可选,精确匹配,对应项目资料表的qt2字段) /// - SourceType: 来源类型(可选,精确匹配,如:购买、赠送、体验) /// - StoreId: 门店ID(可选,精确匹配,根据开单记录表的门店ID筛选) /// /// 返回数据结构: /// ```json /// { /// "pagination": { /// "pageIndex": 1, /// "pageSize": 10, /// "total": 100 /// }, /// "list": [ /// { /// "id": "品项明细ID", /// "billingId": "开单ID", /// "billingTime": "2025-11-15T10:00:00", /// "activityName": "营销活动名称", /// "memberName": "会员名称", /// "memberPhone": "会员手机号", /// "itemName": "品项名称", /// "itemType": "品项类型", /// "actualPrice": 500.00, /// "projectNumber": 5.0, /// "sourceType": "购买", /// "remark": "备注", /// "storeId": "门店ID", /// "storeName": "门店名称" /// } /// ] /// } /// ``` /// /// 返回字段说明: /// - id: 品项明细ID(主键) /// - billingId: 开单ID(关联开单记录表) /// - billingTime: 开单时间(业绩时间yjsj,DateTime格式) /// - activityName: 营销活动名称(关联营销活动表) /// - memberName: 会员名称(关联会员表) /// - memberPhone: 会员手机号(关联会员表) /// - itemName: 品项名称(品项明细表字段) /// - itemType: 品项类型(关联项目资料表的qt2字段) /// - actualPrice: 实付金额(decimal类型) /// - projectNumber: 项目次数(decimal类型,支持小数) /// - sourceType: 来源类型(字符串,如:购买、赠送、体验) /// - remark: 备注(字符串) /// - storeId: 门店ID(关联开单记录表的门店ID) /// - storeName: 门店名称(关联门店表的店名字段) /// /// 查询参数 /// 开单品项明细记录列表(分页) /// 查询成功,返回开单品项明细记录列表 /// 参数错误,如页码或页大小无效 /// 服务器错误,查询过程中发生异常 [HttpGet("billing-item-detail-list")] public async Task GetBillingItemDetailList([FromQuery] BillingItemDetailListQueryInput input) { try { var sidx = input.sidx == null ? "yjsj" : input.sidx; var sort = string.IsNullOrEmpty(input.sort) ? "DESC" : input.sort; // 处理开单时间范围(兼容 StartBillingTime/EndBillingTime 和 startTime/endTime 两种参数名) DateTime? startBillingTime = null; DateTime? endBillingTime = null; // 优先使用 StartBillingTime/EndBillingTime,如果没有则使用 startTime/endTime string startTimeStr = !string.IsNullOrEmpty(input.StartBillingTime) ? input.StartBillingTime : input.startTime; string endTimeStr = !string.IsNullOrEmpty(input.EndBillingTime) ? input.EndBillingTime : input.endTime; if (!string.IsNullOrEmpty(startTimeStr) && !string.IsNullOrEmpty(endTimeStr)) { // 尝试解析日期字符串(支持多种格式) if (DateTime.TryParse(startTimeStr, out DateTime startDate)) { startBillingTime = startDate; } else { throw NCCException.Oh($"开始时间格式错误:{startTimeStr}"); } if (DateTime.TryParse(endTimeStr, out DateTime endDate)) { endBillingTime = 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.BillingId), pxmx => pxmx.Glkdbh == input.BillingId) .WhereIF(startBillingTime.HasValue, pxmx => pxmx.Yjsj >= new DateTime(startBillingTime.Value.Year, startBillingTime.Value.Month, startBillingTime.Value.Day, 0, 0, 0)) .WhereIF(endBillingTime.HasValue, pxmx => pxmx.Yjsj <= new DateTime(endBillingTime.Value.Year, endBillingTime.Value.Month, endBillingTime.Value.Day, 23, 59, 59)) .WhereIF(!string.IsNullOrEmpty(input.ActivityId), pxmx => pxmx.ActivityId == input.ActivityId) .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.Glkdbh && x.Djmd == 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 activityIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.ActivityId)).Select(x => x.ActivityId).Distinct().ToList(); var billingIds = pagedData.list.Where(x => !string.IsNullOrEmpty(x.Glkdbh)).Select(x => x.Glkdbh).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 ?? "")); } // 批量查询活动信息 var activityDict = new Dictionary(); if (activityIds.Any()) { var activities = await _db.Queryable().Where(x => activityIds.Contains(x.Id)).Select(x => new { x.Id, x.ActivityName }).ToListAsync(); activityDict = activities.ToDictionary(x => x.Id, x => x.ActivityName ?? ""); } // 批量查询开单记录,获取门店ID var billingStoreDict = new Dictionary(); if (billingIds.Any()) { var billings = await _db.Queryable().Where(x => billingIds.Contains(x.Id)).Select(x => new { x.Id, x.Djmd }).ToListAsync(); billingStoreDict = billings.ToDictionary(x => x.Id, x => x.Djmd ?? ""); } // 批量查询门店信息 var storeDict = new Dictionary(); var storeIds = billingStoreDict.Values.Where(x => !string.IsNullOrEmpty(x)).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 BillingItemDetailListOutput { id = pxmx.Id, billingId = pxmx.Glkdbh, billingTime = pxmx.Yjsj, activityName = pxmx.ActivityId != null && activityDict.ContainsKey(pxmx.ActivityId) ? activityDict[pxmx.ActivityId] : "", 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, actualPrice = pxmx.ActualPrice, projectNumber = pxmx.ProjectNumber, sourceType = pxmx.SourceType, remark = pxmx.Remark, storeId = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) ? billingStoreDict[pxmx.Glkdbh] : "", storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : "" }).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 #region 返回储扣列表 /// /// 获取储扣列表 /// /// /// 获取开单扣减信息(储扣)列表,支持多种条件筛选和分页查询 /// /// 示例请求: /// ```json /// { /// "currentPage": 1, /// "pageSize": 10, /// "sidx": "CreateTime", /// "sort": "desc", /// "DeductType": "储值", /// "BillingId": "开单ID", /// "ItemName": "品项名称", /// "MinAmount": 100, /// "MaxAmount": 1000 /// } /// ``` /// /// 返回字段说明: /// - Id: 储扣记录ID /// - DeductType: 扣减类型 /// - DeductTypeName: 扣减类型名称 /// - BillingId: 开单ID /// - Amount: 合计金额 /// - ItemName: 品项名称 /// - ItemCategory: 品项分类(医美/科美/生美) /// - BillingDate: 开单日期 /// - MemberName: 客户名称 /// - MemberPhone: 客户手机号 /// - StoreName: 门店名称 /// /// 查询参数 /// 分页的储扣列表 /// 成功返回分页的储扣列表 /// 服务器错误 [HttpGet("deduct-list")] public async Task GetDeductList([FromQuery] LqKdDeductinfoListQueryInput input) { try { var sidx = string.IsNullOrEmpty(input.sidx) ? "CreateTime" : input.sidx; // 构建基础查询:储扣信息 JOIN 开单记录 JOIN 客户信息 JOIN 门店信息 var baseQuery = _db.Queryable( (deduct, billing, member, store) => deduct.BillingId == billing.Id && billing.Kdhy == member.Id && billing.Djmd == store.Id) .WhereIF(!string.IsNullOrEmpty(input.DeductType), (deduct, billing, member, store) => deduct.DeductType == input.DeductType) .WhereIF(!string.IsNullOrEmpty(input.DeductId), (deduct, billing, member, store) => deduct.DeductId == input.DeductId) .WhereIF(!string.IsNullOrEmpty(input.BillingId), (deduct, billing, member, store) => deduct.BillingId == input.BillingId) .WhereIF(input.MinAmount.HasValue, (deduct, billing, member, store) => deduct.Amount >= input.MinAmount.Value) .WhereIF(input.MaxAmount.HasValue, (deduct, billing, member, store) => deduct.Amount <= input.MaxAmount.Value) .WhereIF(input.IsEffective.HasValue, (deduct, billing, member, store) => deduct.IsEffective == input.IsEffective.Value) .WhereIF(!string.IsNullOrEmpty(input.ItemName), (deduct, billing, member, store) => deduct.ItemName != null && deduct.ItemName.Contains(input.ItemName)) .WhereIF(!string.IsNullOrEmpty(input.ItemId), (deduct, billing, member, store) => deduct.ItemId == input.ItemId) .WhereIF(input.MinUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice >= input.MinUnitPrice.Value) .WhereIF(input.MaxUnitPrice.HasValue, (deduct, billing, member, store) => deduct.UnitPrice <= input.MaxUnitPrice.Value) .WhereIF(input.StartCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime >= input.StartCreateTime.Value) .WhereIF(input.EndCreateTime.HasValue, (deduct, billing, member, store) => deduct.CreateTime <= input.EndCreateTime.Value) .WhereIF(!string.IsNullOrEmpty(input.keyword), (deduct, billing, member, store) => (deduct.ItemName != null && deduct.ItemName.Contains(input.keyword)) || (member.Khmc != null && member.Khmc.Contains(input.keyword)) || (member.Sjh != null && member.Sjh.Contains(input.keyword)) || (billing.Id != null && billing.Id.Contains(input.keyword))); // 查询并分页,使用子查询获取开单类型 var data = await baseQuery.Select((deduct, billing, member, store) => new LqKdDeductinfoListOutput { Id = deduct.Id ?? "", DeductType = deduct.DeductType ?? "", DeductId = deduct.DeductId ?? "", BillingId = deduct.BillingId ?? "", Amount = deduct.Amount, IsEffective = deduct.IsEffective, UnitPrice = deduct.UnitPrice, ItemName = deduct.ItemName ?? "", ItemId = deduct.ItemId ?? "", CreateTime = deduct.CreateTime, ProjectNumber = deduct.ProjectNumber, ItemCategory = deduct.ItemCategory ?? "", BillingDate = billing.Kdrq, MemberId = billing.Kdhy ?? "", MemberName = member.Khmc ?? "", MemberPhone = member.Sjh ?? "", StoreId = billing.Djmd ?? "", StoreName = store.Dm ?? "", TimePeriod = billing.Kdrq, BillingType = SqlFunc.Subqueryable() .Where(pxmx => pxmx.Id == deduct.DeductId && pxmx.Px == deduct.ItemId) .Select(pxmx => pxmx.SourceType), CooperationInstitution = billing.Hgjg ?? "" }).MergeTable().OrderBy(sidx + " " + input.sort).ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(data); } catch (Exception ex) { _logger.LogError(ex, $"获取储扣列表失败: {ex.ToString()}"); throw NCCException.Oh(ErrorCode.COM1005, $"获取储扣列表失败: {ex.Message}"); } } #endregion #region 批量处理历史开单数据的升单类型 /// /// 批量处理历史开单数据的升单类型 /// /// /// 批量更新历史开单记录的升单类型字段(升生美、升科美、升医美) /// /// 处理逻辑: /// 1. 查询所有有效的开单记录 /// 2. 对于每条开单记录,应用升单判断逻辑: /// - 判断当前开单是否包含医美/科美/生美品项 /// - 判断该会员在当前开单日期之前是否有对应类型的开单记录(重要:只看该开单之前的记录) /// - 更新升单类型字段 /// 3. 批量更新开单记录 /// /// 升单判断规则: /// - 升医美:当前开单包含医美品项 + 该会员在当前开单日期之前有医美类型的开单记录 + 当前开单医美品项金额>=1000 /// - 升科美:当前开单包含科美品项 + 该会员在当前开单日期之前有科美类型的开单记录 /// - 升生美:当前开单包含生美品项 + 该会员在当前开单日期之前有生美类型的开单记录 /// /// 处理结果,包含处理的记录数和成功数 /// 处理成功 /// 服务器错误 [HttpPost("batch-update-upgrade-type")] public async Task BatchUpdateUpgradeType() { try { // 1. 查询所有有效的开单记录 var billingList = await _db.Queryable().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); if (!billingList.Any()) { return new { success = true, message = "没有需要处理的开单记录", totalCount = 0, successCount = 0 }; } _logger.LogInformation($"找到 {billingList.Count} 条开单记录需要处理"); // 2. 批量查询关联数据 var billingIds = billingList.Select(x => x.Id).ToList(); var memberIds = billingList.Select(x => x.Kdhy).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); // 查询所有品项明细 var pxmxList = await _db.Queryable().Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync(); // 查询所有项目资料(用于获取品项分类) var itemIds = pxmxList.Where(x => !string.IsNullOrEmpty(x.Px)).Select(x => x.Px).Distinct().ToList(); var itemCategoryDict = new Dictionary(); if (itemIds.Any()) { var itemCategories = await _db.Queryable().Where(x => itemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => new { x.Id, x.Qt2 }).ToListAsync(); itemCategoryDict = itemCategories.ToDictionary(k => k.Id, v => v.Qt2 ?? ""); } // 按开单ID分组品项明细 var pxmxGrouped = pxmxList.GroupBy(x => x.Glkdbh).ToDictionary(g => g.Key, g => g.ToList()); // 3. 批量处理,每批500条 const int batchSize = 500; var totalBatches = (int)Math.Ceiling((double)billingList.Count / batchSize); var successCount = 0; var updateList = new List(); for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { var batchBillingList = billingList.Skip(batchIndex * batchSize).Take(batchSize).ToList(); foreach (var billing in batchBillingList) { try { // 获取该开单的品项明细 var currentPxmxList = pxmxGrouped.ContainsKey(billing.Id) ? pxmxGrouped[billing.Id] : new List(); if (!currentPxmxList.Any()) { // 如果没有品项明细,设置为"否" billing.UpgradeMedicalBeauty = "否"; billing.UpgradeTechBeauty = "否"; billing.UpgradeLifeBeauty = "否"; updateList.Add(billing); continue; } // 判断当前开单是否包含医美/科美/生美品项 var hasMedicalItem = currentPxmxList.Any(x => !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "医美"); var medicalItemAmount = currentPxmxList .Where(x => !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "医美") .Sum(x => x.ActualPrice); var hasTechItem = currentPxmxList.Any(x => !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "科美"); var hasLifeItem = currentPxmxList.Any(x => !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "生美"); // 判断该会员在当前开单日期之前是否有对应类型的开单记录 if (!string.IsNullOrEmpty(billing.Kdhy) && billing.Kdrq != null) { var currentBillingDate = billing.Kdrq.Value; // 查询该会员在当前开单日期之前的开单记录 var previousBillingIds = await _db.Queryable() .Where(x => x.Kdhy == billing.Kdhy && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Kdrq < currentBillingDate) .Select(x => x.Id) .ToListAsync(); if (previousBillingIds.Any()) { // 查询这些开单的品项明细 var previousPxmxList = await _db.Queryable() .Where(x => previousBillingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode() && x.ActualPrice > 0 && x.Px != "61") .Select(x => x.ItemCategory) .Distinct() .ToListAsync(); var hasPreviousMedical = previousPxmxList.Contains("医美"); var hasPreviousTech = previousPxmxList.Contains("科美"); var hasPreviousLife = previousPxmxList.Contains("生美"); // 判断升医美 billing.UpgradeMedicalBeauty = (hasMedicalItem && hasPreviousMedical && medicalItemAmount >= 1000) ? "是" : "否"; // 判断升科美 billing.UpgradeTechBeauty = (hasTechItem && hasPreviousTech) ? "是" : "否"; // 判断升生美 billing.UpgradeLifeBeauty = (hasLifeItem && hasPreviousLife) ? "是" : "否"; } else { billing.UpgradeMedicalBeauty = "否"; billing.UpgradeTechBeauty = "否"; billing.UpgradeLifeBeauty = "否"; } } else { billing.UpgradeMedicalBeauty = "否"; billing.UpgradeTechBeauty = "否"; billing.UpgradeLifeBeauty = "否"; } updateList.Add(billing); } catch (Exception ex) { _logger.LogError(ex, $"处理开单记录 {billing.Id} 失败: {ex.Message}"); } } // 批量更新 if (updateList.Any()) { await _db.Updateable(updateList) .UpdateColumns(it => new { it.UpgradeMedicalBeauty, it.UpgradeTechBeauty, it.UpgradeLifeBeauty }) .ExecuteCommandAsync(); successCount += updateList.Count; updateList.Clear(); } _logger.LogInformation($"已处理 {Math.Min((batchIndex + 1) * batchSize, billingList.Count)} / {billingList.Count} 条记录"); } _logger.LogInformation($"批量处理完成,共处理 {billingList.Count} 条记录,成功 {successCount} 条"); return new { success = true, message = "批量处理完成", totalCount = billingList.Count, successCount = successCount }; } catch (Exception ex) { _logger.LogError(ex, $"批量处理历史开单数据的升单类型失败: {ex.ToString()}"); throw NCCException.Oh(ErrorCode.COM1005, $"批量处理历史开单数据的升单类型失败: {ex.Message}"); } } #endregion #region 清空欠款 /// /// 清空欠款(减免剩余欠款) /// /// /// 返回参数说明: /// - billingId: 开单ID /// - original: 原始数据(zdyj-整单业绩, qk-欠款, paidDebt-已交欠款, remainingDebt-剩余欠款) /// - updated: 更新后数据(zdyj-整单业绩, qk-欠款, paidDebt-已交欠款, remainingDebt-剩余欠款) /// - reducedAmount: 减免金额 /// /// 开单记录ID /// 操作结果 /// 成功清空欠款 /// 参数错误或无剩余欠款 /// 开单记录不存在 /// 服务器错误 [HttpPost("clear-debt/{id}")] public async Task ClearDebt(string id) { try { // 1. 参数验证 if (string.IsNullOrEmpty(id)) { throw NCCException.Oh("开单ID不能为空"); } // 2. 查询开单记录 var entity = await _db.Queryable().FirstAsync(p => p.Id == id); if (entity == null) { throw NCCException.Oh("开单记录不存在"); } // 3. 验证开单是否有效 if (entity.IsEffective != StatusEnum.有效.GetHashCode()) { throw NCCException.Oh("开单记录已作废,无法清空欠款"); } // 4. 计算剩余欠款 var remainingDebt = entity.Qk - entity.PaidDebt; // 5. 验证是否有剩余欠款 if (remainingDebt <= 0) { return new { billingId = id, totalDebt = entity.Qk, paidDebt = entity.PaidDebt, remainingDebt = remainingDebt, message = "该开单无剩余欠款,无需清空" }; } // 6. 记录原始数据(用于返回) var originalZdyj = entity.Zdyj; var originalQk = entity.Qk; // 7. 执行欠款减免 // 从整单业绩中减去剩余欠款 entity.Zdyj = entity.Zdyj - remainingDebt; // 欠款金额调整为已交金额(让剩余欠款归零) entity.Qk = entity.PaidDebt; // 更新时间 entity.UpdateTime = DateTime.Now; // 8. 更新数据库 await _db.Updateable(entity).UpdateColumns(it => new { it.Zdyj, it.Qk, it.UpdateTime }).ExecuteCommandAsync(); // 9. 返回结果 return new { billingId = id, original = new { zdyj = originalZdyj, qk = originalQk, paidDebt = entity.PaidDebt, remainingDebt = remainingDebt }, updated = new { zdyj = entity.Zdyj, qk = entity.Qk, paidDebt = entity.PaidDebt, remainingDebt = entity.Qk - entity.PaidDebt }, reducedAmount = remainingDebt }; } catch (Exception ex) { _logger.LogError(ex, $"清空欠款失败 - 开单ID: {id}"); throw NCCException.Oh($"清空欠款失败: {ex.Message}"); } } #endregion } }