Commit 3d98506d0b49e987f193e03d09dbfe954d43eee4

Authored by “wangming”
1 parent c63026bc

feat: 增强合同管理功能

- 新增按月生成成本记录的功能,支持自动生成和更新合同的月度成本。
- 添加获取合同成本列表和按月统计的接口,便于查询和管理合同相关的财务数据。
- 优化合同更新逻辑,确保在修改合同信息时同步更新相关的成本记录。
- 增强错误处理,确保在获取和更新数据时提供清晰的错误信息。
Showing 35 changed files with 1789 additions and 79 deletions
excel/健康师额外数据模板_测试.xlsx 0 → 100644
No preview for this file type
excel/健康师额外数据模板_测试导入.xlsx 0 → 100644
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/LqInventoryUsageListOutput.cs
... ... @@ -33,6 +33,11 @@ namespace NCC.Extend.Entitys.Dto.LqInventoryUsage
33 33 public decimal productPrice { get; set; }
34 34  
35 35 /// <summary>
  36 + /// 产品归属仓库
  37 + /// </summary>
  38 + public string productWarehouse { get; set; }
  39 +
  40 + /// <summary>
36 41 /// 门店ID
37 42 /// </summary>
38 43 public string storeId { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs
... ... @@ -76,6 +76,16 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
76 76 /// 门店名称
77 77 /// </summary>
78 78 public string storeName { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 合作机构
  82 + /// </summary>
  83 + public string hgjg { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 付款医院
  87 + /// </summary>
  88 + public string fkyy { get; set; }
79 89 }
80 90 }
81 91  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowListOutput.cs
... ... @@ -109,6 +109,18 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
109 109 /// </summary>
110 110 [Display(Name = "创建时间")]
111 111 public DateTime createTime { get; set; }
  112 +
  113 + /// <summary>
  114 + /// 送出时间(流水类型为0时使用)
  115 + /// </summary>
  116 + [Display(Name = "送出时间")]
  117 + public DateTime? sendTime { get; set; }
  118 +
  119 + /// <summary>
  120 + /// 送回时间(流水类型为1时使用)
  121 + /// </summary>
  122 + [Display(Name = "送回时间")]
  123 + public DateTime? returnTime { get; set; }
112 124 }
113 125 }
114 126  
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowUpdateInput.cs 0 → 100644
  1 +using System;
  2 +using System.ComponentModel.DataAnnotations;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
  5 +{
  6 + /// <summary>
  7 + /// 清洗流水修改输入
  8 + /// </summary>
  9 + public class LqLaundryFlowUpdateInput
  10 + {
  11 + /// <summary>
  12 + /// 记录ID
  13 + /// </summary>
  14 + [Required(ErrorMessage = "记录ID不能为空")]
  15 + [StringLength(50, ErrorMessage = "记录ID长度不能超过50个字符")]
  16 + [Display(Name = "记录ID")]
  17 + public string Id { get; set; }
  18 +
  19 + /// <summary>
  20 + /// 数量
  21 + /// </summary>
  22 + [Range(0, int.MaxValue, ErrorMessage = "数量不能小于0")]
  23 + [Display(Name = "数量")]
  24 + public int? Quantity { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 送出时间(流水类型为0时使用)
  28 + /// </summary>
  29 + [Display(Name = "送出时间")]
  30 + public DateTime? SendTime { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 送回时间(流水类型为1时使用)
  34 + /// </summary>
  35 + [Display(Name = "送回时间")]
  36 + public DateTime? ReturnTime { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 备注
  40 + /// </summary>
  41 + [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
  42 + [Display(Name = "备注")]
  43 + public string Remark { get; set; }
  44 + }
  45 +}
  46 +
  47 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_contract_monthly_cost/LqContractMonthlyCostEntity.cs 0 → 100644
  1 +using System;
  2 +using NCC.Common.Const;
  3 +using SqlSugar;
  4 +
  5 +namespace NCC.Extend.Entitys.lq_contract_monthly_cost
  6 +{
  7 + /// <summary>
  8 + /// 合同成本按月统计表
  9 + /// </summary>
  10 + [SugarTable("lq_contract_monthly_cost")]
  11 + [Tenant(ClaimConst.TENANT_ID)]
  12 + public class LqContractMonthlyCostEntity
  13 + {
  14 + /// <summary>
  15 + /// 主键ID
  16 + /// </summary>
  17 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  18 + public string Id { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 合同ID(关联lq_contract.F_Id)
  22 + /// </summary>
  23 + [SugarColumn(ColumnName = "F_ContractId")]
  24 + public string ContractId { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 门店ID(关联lq_mdxx.F_Id,冗余字段便于查询)
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_StoreId")]
  30 + public string StoreId { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 店名(冗余字段,便于查询)
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_StoreName")]
  36 + public string StoreName { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 分类(冗余字段,便于按分类统计)
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_Category")]
  42 + public string Category { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 统计月份(格式:YYYY-MM-01,表示该月的第一天)
  46 + /// </summary>
  47 + [SugarColumn(ColumnName = "F_Month")]
  48 + public DateTime Month { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 该月的合同成本(缴租金额 / 交租周期)
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_MonthlyCost", DecimalDigits = 2)]
  54 + public decimal MonthlyCost { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 交租周期(冗余字段,便于查询)
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_PaymentCycle")]
  60 + public int PaymentCycle { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 缴租金额(冗余字段,便于查询)
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "F_PaymentAmount", DecimalDigits = 2)]
  66 + public decimal PaymentAmount { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 是否有效(1-有效,0-无效)
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "F_IsEffective")]
  72 + public int IsEffective { get; set; } = 1;
  73 +
  74 + /// <summary>
  75 + /// 创建人ID
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_CreateUser")]
  78 + public string CreateUser { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 创建时间
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_CreateTime")]
  84 + public DateTime CreateTime { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 更新时间
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_UpdateTime")]
  90 + public DateTime? UpdateTime { get; set; }
  91 + }
  92 +}
  93 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_kd_deductinfo/LqKdDeductinfoEntity.cs
... ... @@ -36,6 +36,12 @@ namespace NCC.Extend.Entitys.lq_kd_deductinfo
36 36 public string BillingId { get; set; }
37 37  
38 38 /// <summary>
  39 + /// 开单时间(来源:lq_kd_kdjlb.kdrq)
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_BillingTime")]
  42 + public DateTime? BillingTime { get; set; }
  43 +
  44 + /// <summary>
39 45 /// 合计金额
40 46 /// </summary>
41 47 [SugarColumn(ColumnName = "F_Amount")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_laundry_flow/LqLaundryFlowEntity.cs
... ... @@ -88,6 +88,18 @@ namespace NCC.Extend.Entitys.lq_laundry_flow
88 88 /// </summary>
89 89 [SugarColumn(ColumnName = "F_CreateTime")]
90 90 public DateTime CreateTime { get; set; }
  91 +
  92 + /// <summary>
  93 + /// 送出时间(流水类型为0时使用)
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_SendTime")]
  96 + public DateTime? SendTime { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 送回时间(流水类型为1时使用)
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_ReturnTime")]
  102 + public DateTime? ReturnTime { get; set; }
91 103 }
92 104 }
93 105  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqContractService.cs
1 1 using System;
2 2 using System.Collections.Generic;
  3 +using System.Globalization;
3 4 using System.Linq;
4 5 using System.Threading.Tasks;
5 6 using Microsoft.AspNetCore.Mvc;
... ... @@ -12,6 +13,7 @@ using NCC.Extend.Entitys.Dto.LqContract;
12 13 using NCC.Extend.Entitys.Enum;
13 14 using NCC.Extend.Entitys.lq_contract;
14 15 using NCC.Extend.Entitys.lq_contract_rent_detail;
  16 +using NCC.Extend.Entitys.lq_contract_monthly_cost;
15 17 using NCC.Extend.Entitys.lq_mdxx;
16 18 using NCC.FriendlyException;
17 19 using NCC.System.Entitys.Permission;
... ... @@ -147,6 +149,9 @@ namespace NCC.Extend
147 149 // 自动生成月租明细
148 150 await GenerateRentDetailsAsync(contractEntity.Id, contractEntity.ContractStartDate, contractEntity.ContractEndDate, contractEntity.PaymentCycle, contractEntity.PaymentAmount);
149 151  
  152 + // 自动生成按月成本记录
  153 + await GenerateMonthlyCostAsync(contractEntity.Id, contractEntity.StoreId, contractEntity.StoreName, contractEntity.Category, contractEntity.ContractStartDate, contractEntity.ContractEndDate, contractEntity.PaymentCycle, contractEntity.PaymentAmount);
  154 +
150 155 // 计算下次应交时间
151 156 await CalculateNextPaymentDate(contractEntity.Id);
152 157  
... ... @@ -249,6 +254,9 @@ namespace NCC.Extend
249 254 contract.PaymentCycle != input.PaymentCycle ||
250 255 contract.PaymentAmount != input.PaymentAmount;
251 256  
  257 + // 判断是否需要更新成本记录的分类(如果只修改了分类,不需要重新生成,只需更新分类字段)
  258 + bool needUpdateCategory = contract.Category != input.Category;
  259 +
252 260 // 更新合同信息
253 261 contract.StoreId = input.StoreId;
254 262 contract.StoreName = store.Dm ?? "";
... ... @@ -287,8 +295,33 @@ namespace NCC.Extend
287 295 .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
288 296 .ExecuteCommandAsync();
289 297  
  298 + // 删除旧的成本记录(逻辑删除)
  299 + await _db.Updateable<LqContractMonthlyCostEntity>()
  300 + .SetColumns(x => new LqContractMonthlyCostEntity
  301 + {
  302 + IsEffective = StatusEnum.无效.GetHashCode(),
  303 + UpdateTime = DateTime.Now
  304 + })
  305 + .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  306 + .ExecuteCommandAsync();
  307 +
290 308 // 生成新的明细
291 309 await GenerateRentDetailsAsync(contract.Id, contract.ContractStartDate, contract.ContractEndDate, contract.PaymentCycle, contract.PaymentAmount);
  310 +
  311 + // 生成新的成本记录
  312 + await GenerateMonthlyCostAsync(contract.Id, contract.StoreId, contract.StoreName, contract.Category, contract.ContractStartDate, contract.ContractEndDate, contract.PaymentCycle, contract.PaymentAmount);
  313 + }
  314 + else if (needUpdateCategory)
  315 + {
  316 + // 如果只修改了分类,只需更新成本记录的分类字段
  317 + await _db.Updateable<LqContractMonthlyCostEntity>()
  318 + .SetColumns(x => new LqContractMonthlyCostEntity
  319 + {
  320 + Category = contract.Category,
  321 + UpdateTime = DateTime.Now
  322 + })
  323 + .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  324 + .ExecuteCommandAsync();
292 325 }
293 326  
294 327 // 重新计算下次应交时间
... ... @@ -365,6 +398,16 @@ namespace NCC.Extend
365 398 .Where(x => x.ContractId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
366 399 .ExecuteCommandAsync();
367 400  
  401 + // 删除该合同的所有成本记录(逻辑删除)
  402 + await _db.Updateable<LqContractMonthlyCostEntity>()
  403 + .SetColumns(x => new LqContractMonthlyCostEntity
  404 + {
  405 + IsEffective = StatusEnum.无效.GetHashCode(),
  406 + UpdateTime = DateTime.Now
  407 + })
  408 + .Where(x => x.ContractId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  409 + .ExecuteCommandAsync();
  410 +
368 411 // 删除合同(逻辑删除)
369 412 contract.IsEffective = StatusEnum.无效.GetHashCode();
370 413 contract.UpdateUser = _userManager.UserId;
... ... @@ -792,6 +835,35 @@ namespace NCC.Extend
792 835  
793 836 #endregion
794 837  
  838 + #region 根据合同id获取合同成本列表
  839 + /// <summary>
  840 + /// 根据合同id获取合同成本列表
  841 + /// </summary>
  842 + /// <param name="contractId"></param>
  843 + /// <returns></returns>
  844 + [HttpGet("GetContractCostList")]
  845 + public async Task<dynamic> GetContractCostListAsync([FromQuery] string contractId)
  846 + {
  847 + try
  848 + {
  849 + if (string.IsNullOrWhiteSpace(contractId))
  850 + {
  851 + throw NCCException.Oh("合同ID不能为空");
  852 + }
  853 +
  854 + var costList = await _db.Queryable<LqContractMonthlyCostEntity>()
  855 + .Where(x => x.ContractId == contractId && x.IsEffective == StatusEnum.有效.GetHashCode())
  856 + .ToListAsync();
  857 + return costList;
  858 + }
  859 + catch (Exception ex)
  860 + {
  861 + _logger.LogError(ex, "获取合同成本列表失败");
  862 + throw NCCException.Oh($"获取合同成本列表失败:{ex.Message}");
  863 + }
  864 + }
  865 + #endregion
  866 +
795 867 #region 私有方法
796 868  
797 869 /// <summary>
... ... @@ -842,6 +914,57 @@ namespace NCC.Extend
842 914 }
843 915  
844 916 /// <summary>
  917 + /// 生成按月成本记录
  918 + /// </summary>
  919 + /// <param name="contractId">合同ID</param>
  920 + /// <param name="storeId">门店ID</param>
  921 + /// <param name="storeName">店名</param>
  922 + /// <param name="category">分类</param>
  923 + /// <param name="contractStartDate">合同起始日期</param>
  924 + /// <param name="contractEndDate">合同结束日期</param>
  925 + /// <param name="paymentCycle">交租周期(月)</param>
  926 + /// <param name="paymentAmount">缴租金额</param>
  927 + private async Task GenerateMonthlyCostAsync(string contractId, string storeId, string storeName, string category, DateTime contractStartDate, DateTime contractEndDate, int paymentCycle, decimal paymentAmount)
  928 + {
  929 + // 计算每个月成本 = 缴租金额 / 交租周期
  930 + var monthlyCost = paymentAmount / paymentCycle;
  931 +
  932 + var costRecords = new List<LqContractMonthlyCostEntity>();
  933 + var currentMonth = new DateTime(contractStartDate.Year, contractStartDate.Month, 1);
  934 +
  935 + // 从合同起始月份开始,逐月生成成本记录,直到合同结束月份
  936 + while (currentMonth <= contractEndDate)
  937 + {
  938 + var costRecord = new LqContractMonthlyCostEntity
  939 + {
  940 + Id = YitIdHelper.NextId().ToString(),
  941 + ContractId = contractId,
  942 + StoreId = storeId,
  943 + StoreName = storeName,
  944 + Category = category,
  945 + Month = currentMonth,
  946 + MonthlyCost = monthlyCost,
  947 + PaymentCycle = paymentCycle,
  948 + PaymentAmount = paymentAmount,
  949 + IsEffective = StatusEnum.有效.GetHashCode(),
  950 + CreateUser = _userManager.UserId,
  951 + CreateTime = DateTime.Now
  952 + };
  953 +
  954 + costRecords.Add(costRecord);
  955 +
  956 + // 计算下一个月
  957 + currentMonth = currentMonth.AddMonths(1);
  958 + }
  959 +
  960 + // 批量插入
  961 + if (costRecords.Any())
  962 + {
  963 + await _db.Insertable(costRecords).ExecuteCommandAsync();
  964 + }
  965 + }
  966 +
  967 + /// <summary>
845 968 /// 计算下次应交时间(匿名方法)
846 969 /// </summary>
847 970 /// <param name="contractId">合同ID</param>
... ... @@ -888,6 +1011,152 @@ namespace NCC.Extend
888 1011  
889 1012 #endregion
890 1013  
  1014 + #region 获取合同成本按月统计
  1015 +
  1016 + /// <summary>
  1017 + /// 获取合同成本按月统计
  1018 + /// </summary>
  1019 + /// <remarks>
  1020 + /// 根据合同ID、门店ID、月份等条件查询合同成本按月统计数据
  1021 + ///
  1022 + /// 示例请求:
  1023 + /// ```
  1024 + /// GET /api/Extend/LqContract/GetMonthlyCost?contractId=合同ID&storeId=门店ID&startMonth=2025-01&endMonth=2025-12
  1025 + /// ```
  1026 + ///
  1027 + /// 参数说明:
  1028 + /// - contractId: 合同ID(可选)
  1029 + /// - storeId: 门店ID(可选)
  1030 + /// - startMonth: 开始月份(格式:YYYY-MM,可选)
  1031 + /// - endMonth: 结束月份(格式:YYYY-MM,可选)
  1032 + /// </remarks>
  1033 + /// <param name="contractId">合同ID(可选)</param>
  1034 + /// <param name="storeId">门店ID(可选)</param>
  1035 + /// <param name="category">分类(可选)</param>
  1036 + /// <param name="startMonth">开始月份(格式:YYYY-MM,可选)</param>
  1037 + /// <param name="endMonth">结束月份(格式:YYYY-MM,可选)</param>
  1038 + /// <returns>合同成本按月统计列表</returns>
  1039 + /// <response code="200">查询成功</response>
  1040 + /// <response code="500">服务器错误</response>
  1041 + [HttpGet("GetMonthlyCost")]
  1042 + public async Task<dynamic> GetMonthlyCostAsync(
  1043 + [FromQuery] string contractId = null,
  1044 + [FromQuery] string storeId = null,
  1045 + [FromQuery] string category = null,
  1046 + [FromQuery] string startMonth = null,
  1047 + [FromQuery] string endMonth = null)
  1048 + {
  1049 + try
  1050 + {
  1051 + var query = _db.Queryable<LqContractMonthlyCostEntity>()
  1052 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
  1053 + .WhereIF(!string.IsNullOrWhiteSpace(contractId), x => x.ContractId == contractId)
  1054 + .WhereIF(!string.IsNullOrWhiteSpace(storeId), x => x.StoreId == storeId)
  1055 + .WhereIF(!string.IsNullOrWhiteSpace(category), x => x.Category == category);
  1056 +
  1057 + // 处理月份范围筛选
  1058 + if (!string.IsNullOrWhiteSpace(startMonth))
  1059 + {
  1060 + if (DateTime.TryParseExact(startMonth, "yyyy-MM", null, DateTimeStyles.None, out DateTime startDate))
  1061 + {
  1062 + var startMonthDate = new DateTime(startDate.Year, startDate.Month, 1);
  1063 + query = query.Where(x => x.Month >= startMonthDate);
  1064 + }
  1065 + }
  1066 +
  1067 + if (!string.IsNullOrWhiteSpace(endMonth))
  1068 + {
  1069 + if (DateTime.TryParseExact(endMonth, "yyyy-MM", null, DateTimeStyles.None, out DateTime endDate))
  1070 + {
  1071 + var endMonthDate = new DateTime(endDate.Year, endDate.Month, DateTime.DaysInMonth(endDate.Year, endDate.Month));
  1072 + query = query.Where(x => x.Month <= endMonthDate);
  1073 + }
  1074 + }
  1075 +
  1076 + var data = await query
  1077 + .OrderBy(x => x.Month)
  1078 + .Select(x => new
  1079 + {
  1080 + id = x.Id,
  1081 + contractId = x.ContractId,
  1082 + storeId = x.StoreId,
  1083 + storeName = x.StoreName,
  1084 + category = x.Category,
  1085 + month = x.Month.ToString("yyyy-MM"),
  1086 + monthlyCost = x.MonthlyCost,
  1087 + paymentCycle = x.PaymentCycle,
  1088 + paymentAmount = x.PaymentAmount,
  1089 + createTime = x.CreateTime
  1090 + })
  1091 + .ToListAsync();
  1092 +
  1093 + return new { code = 200, msg = "查询成功", data = data };
  1094 + }
  1095 + catch (Exception ex)
  1096 + {
  1097 + _logger.LogError(ex, "获取合同成本按月统计失败");
  1098 + throw NCCException.Oh($"查询失败:{ex.Message}");
  1099 + }
  1100 + }
  1101 +
  1102 + /// <summary>
  1103 + /// 按门店和月份统计合同成本
  1104 + /// </summary>
  1105 + /// <remarks>
  1106 + /// 根据门店ID和月份统计该门店的合同成本总和
  1107 + ///
  1108 + /// 示例请求:
  1109 + /// ```
  1110 + /// GET /api/Extend/LqContract/GetStoreMonthlyCost?storeId=门店ID&month=2025-11
  1111 + /// ```
  1112 + /// </remarks>
  1113 + /// <param name="storeId">门店ID</param>
  1114 + /// <param name="month">月份(格式:YYYY-MM)</param>
  1115 + /// <returns>该门店该月的合同成本总和</returns>
  1116 + /// <response code="200">查询成功</response>
  1117 + /// <response code="400">参数错误</response>
  1118 + /// <response code="500">服务器错误</response>
  1119 + [HttpGet("GetStoreMonthlyCost")]
  1120 + public async Task<dynamic> GetStoreMonthlyCostAsync(
  1121 + [FromQuery] string storeId,
  1122 + [FromQuery] string month)
  1123 + {
  1124 + try
  1125 + {
  1126 + if (string.IsNullOrWhiteSpace(storeId))
  1127 + {
  1128 + throw NCCException.Oh("门店ID不能为空");
  1129 + }
  1130 +
  1131 + if (string.IsNullOrWhiteSpace(month))
  1132 + {
  1133 + throw NCCException.Oh("月份不能为空");
  1134 + }
  1135 +
  1136 + if (!DateTime.TryParseExact(month, "yyyy-MM", null, DateTimeStyles.None, out DateTime monthDate))
  1137 + {
  1138 + throw NCCException.Oh("月份格式错误,应为 YYYY-MM");
  1139 + }
  1140 +
  1141 + var monthStart = new DateTime(monthDate.Year, monthDate.Month, 1);
  1142 +
  1143 + var totalCost = await _db.Queryable<LqContractMonthlyCostEntity>()
  1144 + .Where(x => x.StoreId == storeId
  1145 + && x.Month == monthStart
  1146 + && x.IsEffective == StatusEnum.有效.GetHashCode())
  1147 + .SumAsync(x => x.MonthlyCost);
  1148 +
  1149 + return new { code = 200, msg = "查询成功", data = new { storeId = storeId, month = month, totalCost = totalCost } };
  1150 + }
  1151 + catch (Exception ex)
  1152 + {
  1153 + _logger.LogError(ex, "按门店和月份统计合同成本失败");
  1154 + throw NCCException.Oh($"查询失败:{ex.Message}");
  1155 + }
  1156 + }
  1157 +
  1158 + #endregion
  1159 +
891 1160 #region 统计门店合同费用
892 1161  
893 1162 /// <summary>
... ... @@ -960,13 +1229,13 @@ namespace NCC.Extend
960 1229 var details = await _db.Queryable<LqContractRentDetailEntity, LqContractEntity>(
961 1230 (detail, contract) => new JoinQueryInfos(
962 1231 JoinType.Inner, detail.ContractId == contract.Id))
963   - .Where((detail, contract) =>
  1232 + .Where((detail, contract) =>
964 1233 contract.StoreId == input.StoreId &&
965 1234 contract.IsEffective == StatusEnum.有效.GetHashCode() &&
966 1235 detail.IsEffective == StatusEnum.有效.GetHashCode() &&
967 1236 detail.PaymentMonth >= monthStart &&
968 1237 detail.PaymentMonth <= monthEnd)
969   - .WhereIF(input.Categories != null && input.Categories.Length > 0,
  1238 + .WhereIF(input.Categories != null && input.Categories.Length > 0,
970 1239 (detail, contract) => contract.Category != null && input.Categories.Contains(contract.Category))
971 1240 .Select((detail, contract) => new
972 1241 {
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -706,14 +706,15 @@ namespace NCC.Extend
706 706 }
707 707  
708 708 // 2.3.1 查询开单业绩(按品项类型)
  709 + // 使用targetDict来关联,确保每个门店只关联一条目标记录,避免重复统计
  710 + // 先按门店统计业绩,再按部门汇总
709 711 var billingSql = $@"
710 712 SELECT
711   - target.{deptField} as TargetDeptId,
  713 + billing.djmd as StoreId,
712 714 COALESCE(SUM(pxmx.F_ActualPrice), 0) as StoreAmount
713 715 FROM lq_kd_pxmx pxmx
714 716 INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
715 717 INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
716   - INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}'
717 718 WHERE pxmx.F_IsEffective = 1
718 719 AND billing.F_IsEffective = 1
719 720 AND item.F_IsEffective = 1
... ... @@ -721,35 +722,48 @@ namespace NCC.Extend
721 722 AND billing.djmd IN ('{storeIdsStr}')
722 723 AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
723 724 AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
724   - GROUP BY target.{deptField}";
  725 + GROUP BY billing.djmd";
725 726  
726 727 var billingData = await _db.Ado.SqlQueryAsync<dynamic>(billingSql);
727 728  
728   - // 分配开单业绩到对应部门
  729 + // 分配开单业绩到对应部门(使用targetDict确保每个门店只关联一条目标记录)
729 730 foreach (var billing in billingData ?? Enumerable.Empty<dynamic>())
730 731 {
731 732 var storeAmount = billing?.StoreAmount != null ? Convert.ToDecimal(billing.StoreAmount) : 0m;
732   - var targetDeptId = billing?.TargetDeptId?.ToString();
  733 + var storeId = billing?.StoreId?.ToString();
733 734  
734   - if (storeAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  735 + if (storeAmount <= 0 || string.IsNullOrEmpty(storeId))
735 736 continue;
736 737  
737   - // 只分配给在查询列表中的部门,避免重复统计
738   - if (departmentDict.ContainsKey(targetDeptId))
  738 + // 从targetDict中获取门店对应的部门信息
  739 + if (targetDict.ContainsKey(storeId))
739 740 {
740   - departmentDict[targetDeptId].BillingPerformance += storeAmount;
  741 + var target = targetDict[storeId];
  742 + // 动态获取部门字段值
  743 + string targetDeptId = null;
  744 + if (deptField == "F_EducationDepartment")
  745 + targetDeptId = target?.F_EducationDepartment?.ToString();
  746 + else if (deptField == "F_TechDepartment")
  747 + targetDeptId = target?.F_TechDepartment?.ToString();
  748 + else if (deptField == "F_MajorProjectDepartment")
  749 + targetDeptId = target?.F_MajorProjectDepartment?.ToString();
  750 +
  751 + if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
  752 + {
  753 + departmentDict[targetDeptId].BillingPerformance += storeAmount;
  754 + }
741 755 }
742 756 }
743 757  
744 758 // 2.3.2 查询退卡业绩(按品项类型)
  759 + // 使用targetDict来关联,确保每个门店只关联一条目标记录,避免重复统计
745 760 var refundSql = $@"
746 761 SELECT
747   - target.{deptField} as TargetDeptId,
  762 + refund.md as StoreId,
748 763 COALESCE(SUM(refund_mx.tkje), 0) as StoreRefundAmount
749 764 FROM lq_hytk_mx refund_mx
750 765 INNER JOIN lq_hytk_hytk refund ON refund_mx.F_RefundInfoId = refund.F_Id
751 766 INNER JOIN lq_xmzl item ON refund_mx.px = item.F_Id
752   - INNER JOIN lq_md_target target ON refund.md = target.F_StoreId AND target.F_Month = '{month}'
753 767 WHERE refund_mx.F_IsEffective = 1
754 768 AND refund.F_IsEffective = 1
755 769 AND item.F_IsEffective = 1
... ... @@ -757,59 +771,86 @@ namespace NCC.Extend
757 771 AND refund.md IN ('{storeIdsStr}')
758 772 AND refund.tksj >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
759 773 AND refund.tksj < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
760   - GROUP BY target.{deptField}";
  774 + GROUP BY refund.md";
761 775  
762 776 var refundData = await _db.Ado.SqlQueryAsync<dynamic>(refundSql);
763 777  
764   - // 分配退卡业绩到对应部门
  778 + // 分配退卡业绩到对应部门(使用targetDict确保每个门店只关联一条目标记录)
765 779 foreach (var refund in refundData ?? Enumerable.Empty<dynamic>())
766 780 {
767 781 var storeRefundAmount = refund?.StoreRefundAmount != null ? Convert.ToDecimal(refund.StoreRefundAmount) : 0m;
768   - var targetDeptId = refund?.TargetDeptId?.ToString();
  782 + var storeId = refund?.StoreId?.ToString();
769 783  
770   - if (storeRefundAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  784 + if (storeRefundAmount <= 0 || string.IsNullOrEmpty(storeId))
771 785 continue;
772 786  
773   - // 只分配给在查询列表中的部门,避免重复统计
774   - if (departmentDict.ContainsKey(targetDeptId))
  787 + // 从targetDict中获取门店对应的部门信息
  788 + if (targetDict.ContainsKey(storeId))
775 789 {
776   - departmentDict[targetDeptId].RefundPerformance += storeRefundAmount;
  790 + var target = targetDict[storeId];
  791 + // 动态获取部门字段值
  792 + string targetDeptId = null;
  793 + if (deptField == "F_EducationDepartment")
  794 + targetDeptId = target?.F_EducationDepartment?.ToString();
  795 + else if (deptField == "F_TechDepartment")
  796 + targetDeptId = target?.F_TechDepartment?.ToString();
  797 + else if (deptField == "F_MajorProjectDepartment")
  798 + targetDeptId = target?.F_MajorProjectDepartment?.ToString();
  799 +
  800 + if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
  801 + {
  802 + departmentDict[targetDeptId].RefundPerformance += storeRefundAmount;
  803 + }
777 804 }
778 805 }
779 806  
780 807 // 2.3.3 查询储扣金额(按品项类型)
  808 + // 使用储扣记录表中的开单时间(F_BillingTime)进行时间过滤,如果为空则使用开单记录表的开单时间(kdrq)
  809 + // 使用targetDict来关联,确保每个门店只关联一条目标记录,避免重复统计
781 810 var deductSql = $@"
782 811 SELECT
783   - target.{deptField} as TargetDeptId,
  812 + billing.djmd as StoreId,
784 813 COALESCE(SUM(deduct.F_Amount), 0) as StoreDeductAmount
785 814 FROM lq_kd_deductinfo deduct
786 815 INNER JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id
787 816 INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
788   - INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId AND target.F_Month = '{month}'
789 817 WHERE deduct.F_IsEffective = 1
790 818 AND billing.F_IsEffective = 1
791 819 AND item.F_IsEffective = 1
792 820 AND item.qt2 = '{itemType}'
793 821 AND billing.djmd IN ('{storeIdsStr}')
794   - AND billing.kdrq >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
795   - AND billing.kdrq < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
796   - GROUP BY target.{deptField}";
  822 + AND COALESCE(deduct.F_BillingTime, billing.kdrq) >= '{startDate.ToString("yyyy-MM-dd")} 00:00:00'
  823 + AND COALESCE(deduct.F_BillingTime, billing.kdrq) < '{endDate.AddDays(1).ToString("yyyy-MM-dd")} 00:00:00'
  824 + GROUP BY billing.djmd";
797 825  
798 826 var deductData = await _db.Ado.SqlQueryAsync<dynamic>(deductSql);
799 827  
800   - // 分配储扣金额到对应部门
  828 + // 分配储扣金额到对应部门(使用targetDict确保每个门店只关联一条目标记录)
801 829 foreach (var deduct in deductData ?? Enumerable.Empty<dynamic>())
802 830 {
803 831 var storeDeductAmount = deduct?.StoreDeductAmount != null ? Convert.ToDecimal(deduct.StoreDeductAmount) : 0m;
804   - var targetDeptId = deduct?.TargetDeptId?.ToString();
  832 + var storeId = deduct?.StoreId?.ToString();
805 833  
806   - if (storeDeductAmount <= 0 || string.IsNullOrEmpty(targetDeptId))
  834 + if (storeDeductAmount <= 0 || string.IsNullOrEmpty(storeId))
807 835 continue;
808 836  
809   - // 只累加到在查询列表中的部门,避免重复统计
810   - if (departmentDict.ContainsKey(targetDeptId))
  837 + // 从targetDict中获取门店对应的部门信息
  838 + if (targetDict.ContainsKey(storeId))
811 839 {
812   - departmentDict[targetDeptId].DeductAmount += storeDeductAmount;
  840 + var target = targetDict[storeId];
  841 + // 动态获取部门字段值
  842 + string targetDeptId = null;
  843 + if (deptField == "F_EducationDepartment")
  844 + targetDeptId = target?.F_EducationDepartment?.ToString();
  845 + else if (deptField == "F_TechDepartment")
  846 + targetDeptId = target?.F_TechDepartment?.ToString();
  847 + else if (deptField == "F_MajorProjectDepartment")
  848 + targetDeptId = target?.F_MajorProjectDepartment?.ToString();
  849 +
  850 + if (!string.IsNullOrEmpty(targetDeptId) && departmentDict.ContainsKey(targetDeptId))
  851 + {
  852 + departmentDict[targetDeptId].DeductAmount += storeDeductAmount;
  853 + }
813 854 }
814 855 }
815 856 }
... ... @@ -1801,6 +1842,7 @@ namespace NCC.Extend
1801 1842 var (startDate, endDate) = GetTimeRange(input.StartTime, input.EndTime);
1802 1843  
1803 1844 // 统计储扣金额,按品项分类分组
  1845 + // 使用储扣记录表中的开单时间(F_BillingTime)进行时间过滤,如果为空则使用开单记录表的开单时间(kdrq)
1804 1846 var sql = $@"
1805 1847 SELECT
1806 1848 COALESCE(SUM(CASE WHEN item.qt2 = '医美' THEN deduct.F_Amount ELSE 0 END), 0) as YiMeiAmount,
... ... @@ -1813,8 +1855,8 @@ namespace NCC.Extend
1813 1855 LEFT JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id AND item.F_IsEffective = 1
1814 1856 WHERE deduct.F_IsEffective = 1
1815 1857 AND billing.F_IsEffective = 1
1816   - AND billing.kdrq >= '{startDate:yyyy-MM-dd} 00:00:00'
1817   - AND billing.kdrq < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'";
  1858 + AND COALESCE(deduct.F_BillingTime, billing.kdrq) >= '{startDate:yyyy-MM-dd} 00:00:00'
  1859 + AND COALESCE(deduct.F_BillingTime, billing.kdrq) < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'";
1818 1860  
1819 1861 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
1820 1862 var data = result.FirstOrDefault();
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
... ... @@ -296,6 +296,7 @@ namespace NCC.Extend
296 296 .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
297 297  
298 298 // 1.12 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
  299 + // 优先使用送出时间(F_SendTime),如果为空则使用创建时间(F_CreateTime)
299 300 var laundryCostSql = $@"
300 301 SELECT
301 302 F_StoreId as StoreId,
... ... @@ -303,7 +304,7 @@ namespace NCC.Extend
303 304 FROM lq_laundry_flow
304 305 WHERE F_IsEffective = 1
305 306 AND F_FlowType = 0
306   - AND DATE_FORMAT(F_CreateTime, '%Y%m') = @monthStr
  307 + AND DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @monthStr
307 308 GROUP BY F_StoreId";
308 309  
309 310 var laundryCostData = await _db.Ado.SqlQueryAsync<dynamic>(laundryCostSql, new { monthStr });
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
... ... @@ -186,8 +186,8 @@ namespace NCC.Extend
186 186 _db.Ado.BeginTran();
187 187 try
188 188 {
189   - var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
190   - if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  189 + var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
  190 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
191 191  
192 192 // 计算并更新产品的平均单价(加权平均成本法)
193 193 // 普通入库和采购入库都需要更新平均单价
... ... @@ -398,8 +398,8 @@ namespace NCC.Extend
398 398 _db.Ado.BeginTran();
399 399 try
400 400 {
401   - var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
402   - if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  401 + var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
  402 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
403 403  
404 404 // 如果数量或单价发生变化,需要重新计算平均单价
405 405 // 注意:更新库存时,如果数量或单价变化,需要重新计算整个产品的平均单价
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -503,6 +503,7 @@ namespace NCC.Extend
503 503 productName = product.ProductName,
504 504 productCategory = product.ProductCategory,
505 505 productPrice = product.Price,
  506 + productWarehouse = product.Warehouse, // 产品归属仓库
506 507 storeId = u.StoreId,
507 508 storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(store => store.Id == u.StoreId).Select(store => store.Dm),
508 509 usageTime = u.UsageTime,
... ... @@ -648,6 +649,7 @@ namespace NCC.Extend
648 649 productName = product.ProductName,
649 650 productCategory = product.ProductCategory,
650 651 productPrice = product.Price,
  652 + productWarehouse = product.Warehouse, // 产品归属仓库
651 653 storeId = u.StoreId,
652 654 storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(store => store.Id == u.StoreId).Select(store => store.Dm),
653 655 usageTime = u.UsageTime,
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -897,6 +897,7 @@ namespace NCC.Extend.LqKdKdjlb
897 897 {
898 898 Id = YitIdHelper.NextId().ToString(),
899 899 BillingId = newEntity.Id,
  900 + BillingTime = newEntity.Kdrq, // 设置开单时间
900 901 DeductId = item.DeductId,
901 902 DeductType = item.DeductType,
902 903 Amount = item.Amount,
... ... @@ -1243,6 +1244,7 @@ namespace NCC.Extend.LqKdKdjlb
1243 1244 {
1244 1245 Id = YitIdHelper.NextId().ToString(),
1245 1246 BillingId = id,
  1247 + BillingTime = entity.Kdrq, // 设置开单时间
1246 1248 DeductId = item.DeductId,
1247 1249 DeductType = item.DeductType,
1248 1250 Amount = item.Amount,
... ... @@ -1252,7 +1254,7 @@ namespace NCC.Extend.LqKdKdjlb
1252 1254 ItemId = item.ItemId,
1253 1255 IsEffective = StatusEnum.有效.GetHashCode(), // 设置为有效
1254 1256 CreateTime = DateTime.Now, // 设置创建时间
1255   - ItemCategory = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == item.DeductId).Select(x => x.Qt2).FirstAsync()
  1257 + ItemCategory = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == item.ItemId).Select(x => x.Qt2).FirstAsync() // 修复:使用 ItemId 而不是 DeductId
1256 1258 };
1257 1259 allDeductEntities.Add(lqKdDeductEntity);
1258 1260 }
... ... @@ -3791,11 +3793,19 @@ namespace NCC.Extend.LqKdKdjlb
3791 3793 }
3792 3794  
3793 3795 // 批量查询开单记录,获取门店ID
  3796 + // 批量查询开单记录,获取门店ID及扩展信息
3794 3797 var billingStoreDict = new Dictionary<string, string>();
  3798 + var billingExtraInfoDict = new Dictionary<string, dynamic>();
  3799 +
3795 3800 if (billingIds.Any())
3796 3801 {
3797   - var billings = await _db.Queryable<LqKdKdjlbEntity>().Where(x => billingIds.Contains(x.Id)).Select(x => new { x.Id, x.Djmd }).ToListAsync();
  3802 + var billings = await _db.Queryable<LqKdKdjlbEntity>()
  3803 + .Where(x => billingIds.Contains(x.Id))
  3804 + .Select(x => new { x.Id, x.Djmd, x.Hgjg, x.Fkyy })
  3805 + .ToListAsync();
  3806 +
3798 3807 billingStoreDict = billings.ToDictionary(x => x.Id, x => x.Djmd ?? "");
  3808 + billingExtraInfoDict = billings.ToDictionary(x => x.Id, x => (dynamic)new { Hgjg = x.Hgjg, Fkyy = x.Fkyy });
3799 3809 }
3800 3810  
3801 3811 // 批量查询门店信息
... ... @@ -3823,7 +3833,9 @@ namespace NCC.Extend.LqKdKdjlb
3823 3833 sourceType = pxmx.SourceType,
3824 3834 remark = pxmx.Remark,
3825 3835 storeId = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) ? billingStoreDict[pxmx.Glkdbh] : "",
3826   - storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : ""
  3836 + storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : "",
  3837 + hgjg = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Hgjg : "",
  3838 + fkyy = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Fkyy : ""
3827 3839 }).ToList();
3828 3840  
3829 3841 // 6. 返回分页结果
... ... @@ -3915,6 +3927,7 @@ namespace NCC.Extend.LqKdKdjlb
3915 3927  
3916 3928  
3917 3929 // 查询并分页,使用子查询获取开单类型
  3930 + // 优先使用储扣记录表中的开单时间,如果为空则使用开单记录表中的开单时间
3918 3931 var data = await baseQuery.Select((deduct, billing, member, store) => new LqKdDeductinfoListOutput
3919 3932 {
3920 3933 Id = deduct.Id ?? "",
... ... @@ -3929,13 +3942,13 @@ namespace NCC.Extend.LqKdKdjlb
3929 3942 CreateTime = deduct.CreateTime,
3930 3943 ProjectNumber = deduct.ProjectNumber,
3931 3944 ItemCategory = deduct.ItemCategory ?? "",
3932   - BillingDate = billing.Kdrq,
  3945 + BillingDate = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间
3933 3946 MemberId = billing.Kdhy ?? "",
3934 3947 MemberName = member.Khmc ?? "",
3935 3948 MemberPhone = member.Sjh ?? "",
3936 3949 StoreId = billing.Djmd ?? "",
3937 3950 StoreName = store.Dm ?? "",
3938   - TimePeriod = billing.Kdrq,
  3951 + TimePeriod = deduct.BillingTime ?? billing.Kdrq, // 优先使用储扣记录表中的开单时间
3939 3952 BillingType = SqlFunc.Subqueryable<LqKdPxmxEntity>()
3940 3953 .Where(pxmx => pxmx.Id == deduct.DeductId && pxmx.Px == deduct.ItemId)
3941 3954 .Select(pxmx => pxmx.SourceType),
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
... ... @@ -115,7 +115,8 @@ namespace NCC.Extend
115 115 Remark = input.Remark,
116 116 IsEffective = StatusEnum.有效.GetHashCode(),
117 117 CreateUser = _userManager.UserId,
118   - CreateTime = DateTime.Now
  118 + CreateTime = DateTime.Now,
  119 + SendTime = DateTime.Now // 设置送出时间
119 120 };
120 121  
121 122 var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
... ... @@ -212,7 +213,8 @@ namespace NCC.Extend
212 213 Remark = input.Remark,
213 214 IsEffective = StatusEnum.有效.GetHashCode(),
214 215 CreateUser = _userManager.UserId,
215   - CreateTime = DateTime.Now
  216 + CreateTime = DateTime.Now,
  217 + ReturnTime = DateTime.Now // 设置送回时间
216 218 };
217 219  
218 220 var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
... ... @@ -228,6 +230,109 @@ namespace NCC.Extend
228 230 }
229 231 #endregion
230 232  
  233 + #region 修改送出/送回记录
  234 + /// <summary>
  235 + /// 修改送出/送回记录
  236 + /// </summary>
  237 + /// <remarks>
  238 + /// 修改清洗流水记录的数量、送出时间、送回时间和备注
  239 + ///
  240 + /// 示例请求:
  241 + /// ```json
  242 + /// {
  243 + /// "id": "记录ID",
  244 + /// "quantity": 95,
  245 + /// "sendTime": "2025-11-01 10:00:00",
  246 + /// "returnTime": "2025-11-05 15:00:00",
  247 + /// "remark": "修改备注"
  248 + /// }
  249 + /// ```
  250 + ///
  251 + /// 参数说明:
  252 + /// - id: 记录ID(必填)
  253 + /// - quantity: 数量(可选,修改时需重新计算总费用)
  254 + /// - sendTime: 送出时间(可选,仅流水类型为0时有效)
  255 + /// - returnTime: 送回时间(可选,仅流水类型为1时有效)
  256 + /// - remark: 备注(可选)
  257 + /// </remarks>
  258 + /// <param name="input">修改输入</param>
  259 + /// <returns>修改结果</returns>
  260 + /// <response code="200">修改成功</response>
  261 + /// <response code="400">记录不存在或参数错误</response>
  262 + /// <response code="500">服务器错误</response>
  263 + [HttpPost("Update")]
  264 + public async Task<dynamic> UpdateAsync([FromBody] LqLaundryFlowUpdateInput input)
  265 + {
  266 + try
  267 + {
  268 + // 查询记录是否存在
  269 + var entity = await _db.Queryable<LqLaundryFlowEntity>()
  270 + .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  271 + .FirstAsync();
  272 +
  273 + if (entity == null)
  274 + {
  275 + throw NCCException.Oh("记录不存在或已失效");
  276 + }
  277 +
  278 + // 更新数量(如果提供)
  279 + if (input.Quantity.HasValue)
  280 + {
  281 + entity.Quantity = input.Quantity.Value;
  282 + // 重新计算总费用
  283 + entity.TotalPrice = entity.Quantity * entity.LaundryPrice;
  284 + }
  285 +
  286 + // 更新送出时间(仅流水类型为0时有效)
  287 + if (input.SendTime.HasValue)
  288 + {
  289 + if (entity.FlowType != 0)
  290 + {
  291 + throw NCCException.Oh("只有送出记录才能修改送出时间");
  292 + }
  293 + entity.SendTime = input.SendTime.Value;
  294 + }
  295 +
  296 + // 更新送回时间(仅流水类型为1时有效)
  297 + if (input.ReturnTime.HasValue)
  298 + {
  299 + if (entity.FlowType != 1)
  300 + {
  301 + throw NCCException.Oh("只有送回记录才能修改送回时间");
  302 + }
  303 + entity.ReturnTime = input.ReturnTime.Value;
  304 + }
  305 +
  306 + // 更新备注(如果提供)
  307 + if (input.Remark != null)
  308 + {
  309 + entity.Remark = input.Remark;
  310 + }
  311 +
  312 + // 执行更新
  313 + var isOk = await _db.Updateable(entity)
  314 + .UpdateColumns(it => new
  315 + {
  316 + it.Quantity,
  317 + it.TotalPrice,
  318 + it.SendTime,
  319 + it.ReturnTime,
  320 + it.Remark
  321 + })
  322 + .ExecuteCommandAsync();
  323 +
  324 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  325 +
  326 + return new { message = "修改成功", totalPrice = entity.TotalPrice };
  327 + }
  328 + catch (Exception ex)
  329 + {
  330 + _logger.LogError(ex, "修改清洗流水记录失败");
  331 + throw NCCException.Oh($"修改失败:{ex.Message}");
  332 + }
  333 + }
  334 + #endregion
  335 +
231 336 #region 获取清洗流水列表
232 337 /// <summary>
233 338 /// 获取清洗流水列表
... ... @@ -275,7 +380,9 @@ namespace NCC.Extend
275 380 isEffective = flow.IsEffective,
276 381 createUser = flow.CreateUser,
277 382 createUserName = "",
278   - createTime = flow.CreateTime
  383 + createTime = flow.CreateTime,
  384 + sendTime = flow.SendTime,
  385 + returnTime = flow.ReturnTime
279 386 })
280 387 .MergeTable()
281 388 .OrderBy(sidx + " " + sort)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
... ... @@ -1515,8 +1515,8 @@ namespace NCC.Extend.LqReimbursementApplication
1515 1515 // 查询本月已审核通过的报销申请
1516 1516 var applications = await _db.Queryable<LqReimbursementApplicationEntity>()
1517 1517 .Where(x => (x.ApprovalStatus ?? x.ApproveStatus) == "已通过")
1518   - .Where(x => x.ApplicationTime.HasValue &&
1519   - x.ApplicationTime.Value.Year == queryYear &&
  1518 + .Where(x => x.ApplicationTime.HasValue &&
  1519 + x.ApplicationTime.Value.Year == queryYear &&
1520 1520 x.ApplicationTime.Value.Month == int.Parse(queryMonth))
1521 1521 .ToListAsync();
1522 1522  
... ... @@ -1552,7 +1552,7 @@ namespace NCC.Extend.LqReimbursementApplication
1552 1552 foreach (var app in applications)
1553 1553 {
1554 1554 var appPurchaseRecords = purchaseRecords.Where(x => x.ApplicationId == app.Id).ToList();
1555   -
  1555 +
1556 1556 if (appPurchaseRecords.Any())
1557 1557 {
1558 1558 // 每个购买记录作为一行
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
... ... @@ -211,16 +211,31 @@ namespace NCC.Extend
211 211 throw new Exception($"第{i + 1}行:月份必须在1-12之间");
212 212 }
213 213  
  214 + // 辅助方法:清理数值字符串(去除千分位分隔符、货币符号等)
  215 + Func<string, string> CleanNumericString = (str) =>
  216 + {
  217 + if (string.IsNullOrWhiteSpace(str))
  218 + return "0";
  219 + // 去除常见的非数字字符(保留小数点和负号)
  220 + return str.Trim()
  221 + .Replace(",", "") // 去除千分位分隔符
  222 + .Replace(",", "") // 去除中文逗号
  223 + .Replace("¥", "") // 去除人民币符号
  224 + .Replace("$", "") // 去除美元符号
  225 + .Replace("元", "") // 去除"元"字
  226 + .Replace(" ", ""); // 去除空格
  227 + };
  228 +
214 229 // 解析数值字段(允许为空,默认为0)
215   - decimal.TryParse(baseRewardPerformanceText, out decimal baseRewardPerformance);
216   - decimal.TryParse(cooperationRewardPerformanceText, out decimal cooperationRewardPerformance);
217   - decimal.TryParse(newCustomerPerformanceText, out decimal newCustomerPerformance);
218   - decimal.TryParse(newCustomerConversionRateText, out decimal newCustomerConversionRate);
219   - decimal.TryParse(upgradePerformanceText, out decimal upgradePerformance);
220   - decimal.TryParse(upgradeConversionRateText, out decimal upgradeConversionRate);
221   - decimal.TryParse(upgradeCustomerCountText, out decimal upgradeCustomerCount);
222   - decimal.TryParse(otherPerformanceAddText, out decimal otherPerformanceAdd);
223   - decimal.TryParse(otherPerformanceSubtractText, out decimal otherPerformanceSubtract);
  230 + decimal.TryParse(CleanNumericString(baseRewardPerformanceText), out decimal baseRewardPerformance);
  231 + decimal.TryParse(CleanNumericString(cooperationRewardPerformanceText), out decimal cooperationRewardPerformance);
  232 + decimal.TryParse(CleanNumericString(newCustomerPerformanceText), out decimal newCustomerPerformance);
  233 + decimal.TryParse(CleanNumericString(newCustomerConversionRateText), out decimal newCustomerConversionRate);
  234 + decimal.TryParse(CleanNumericString(upgradePerformanceText), out decimal upgradePerformance);
  235 + decimal.TryParse(CleanNumericString(upgradeConversionRateText), out decimal upgradeConversionRate);
  236 + decimal.TryParse(CleanNumericString(upgradeCustomerCountText), out decimal upgradeCustomerCount);
  237 + decimal.TryParse(CleanNumericString(otherPerformanceAddText), out decimal otherPerformanceAdd);
  238 + decimal.TryParse(CleanNumericString(otherPerformanceSubtractText), out decimal otherPerformanceSubtract);
224 239  
225 240 var item = new SalaryExtraCalculationImportInput
226 241 {
... ... @@ -411,7 +426,22 @@ namespace NCC.Extend
411 426 // 批量更新现有记录
412 427 if (entitiesToUpdate.Any())
413 428 {
414   - await _db.Updateable(entitiesToUpdate).ExecuteCommandAsync();
  429 + // 明确指定要更新的字段,确保所有字段都被更新(包括升单业绩)
  430 + // 注意:Updateable接收实体列表时,会自动根据主键更新,不需要Where条件
  431 + await _db.Updateable(entitiesToUpdate)
  432 + .UpdateColumns(it => new
  433 + {
  434 + it.BaseRewardPerformance,
  435 + it.CooperationRewardPerformance,
  436 + it.NewCustomerPerformance,
  437 + it.NewCustomerConversionRate,
  438 + it.UpgradePerformance,
  439 + it.UpgradeConversionRate,
  440 + it.UpgradeCustomerCount,
  441 + it.OtherPerformanceAdd,
  442 + it.OtherPerformanceSubtract
  443 + })
  444 + .ExecuteCommandAsync();
415 445 }
416 446  
417 447 var result = new
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryService.cs
... ... @@ -623,8 +623,15 @@ namespace NCC.Extend
623 623 isNewStore);
624 624  
625 625 // 4.2 提成计算
626   - // 业绩门槛: 战队成员个人总业绩 <= 6000 无提成
627   - if (!string.IsNullOrEmpty(salary.GoldTriangleId) && salary.TotalPerformance <= 6000)
  626 + // 业绩门槛: 战队成员个人总业绩 <= 6000 无提成 (需按日均计算)
  627 + // 规则:战队成员日均业绩 <= 6000 / 当月天数 -> 无提成
  628 + decimal memberThreshold = 6000m;
  629 + if (daysInMonth > 0 && salary.WorkingDays > 0)
  630 + {
  631 + memberThreshold = (6000m / daysInMonth) * salary.WorkingDays;
  632 + }
  633 +
  634 + if (!string.IsNullOrEmpty(salary.GoldTriangleId) && salary.TotalPerformance < memberThreshold) // 修正为小于校验
628 635 {
629 636 salary.TotalCommission = 0;
630 637 salary.BasePerformanceCommission = 0;
... ... @@ -644,13 +651,15 @@ namespace NCC.Extend
644 651 // 获取战队人数 (注意:这里应该是有效战队人数)
645 652 var teamMemberCount = employeeStats.Values.Count(x => x.GoldTriangleId == salary.GoldTriangleId);
646 653 // 注意:提成点按原始基础业绩计算,不是实际基础业绩
647   - commissionPoint = GetTeamCommissionPoint(teamMemberCount, salary.TeamPerformance);
  654 + // 战队成员不按日均考核提成点,只考核个人门槛
  655 + commissionPoint = GetTeamCommissionPoint(teamMemberCount, salary.TeamPerformance, daysInMonth, salary.WorkingDays);
648 656 }
649 657 else
650 658 {
651 659 // 单人 (或被剔除出战队)
652 660 // 注意:提成点按原始总业绩计算
653   - commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance);
  661 + // 单人按日均考核提成点
  662 + commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance, daysInMonth, salary.WorkingDays);
654 663 }
655 664  
656 665 salary.CommissionPoint = commissionPoint;
... ... @@ -697,6 +706,17 @@ namespace NCC.Extend
697 706 if (!string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区"))
698 707 {
699 708 salary.StoreTZoneCommission = salary.StoreTotalPerformance * 0.05m * 0.05m;
  709 +
  710 + // T区人员仅核算提成,其他项(底薪、手工、社保等)归零
  711 + salary.HealthCoachBaseSalary = 0;
  712 + salary.HandworkFee = 0;
  713 + salary.BasePerformanceCommission = 0;
  714 + salary.CooperationPerformanceCommission = 0;
  715 + salary.ConsultantCommission = 0;
  716 + salary.NewCustomerPerformanceCommission = 0;
  717 + salary.UpgradePerformanceCommission = 0;
  718 + salary.TotalSubsidy = 0;
  719 + salary.TotalDeduction = 0;
700 720 }
701 721  
702 722 salary.TotalCommission = salary.BasePerformanceCommission
... ... @@ -801,7 +821,7 @@ namespace NCC.Extend
801 821 /// <summary>
802 822 /// 获取战队提成点
803 823 /// </summary>
804   - private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance)
  824 + private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance, int daysInMonth, decimal workingDays)
805 825 {
806 826 if (memberCount >= 3)
807 827 {
... ... @@ -820,10 +840,24 @@ namespace NCC.Extend
820 840 }
821 841 else // 1人
822 842 {
823   - if (teamPerformance >= 60000) return 0.06m;
824   - if (teamPerformance >= 40000) return 0.05m;
825   - if (teamPerformance >= 20000) return 0.04m;
826   - if (teamPerformance >= 10000) return 0.03m;
  843 + // 单人按照日均考核
  844 + decimal p1 = 60000m;
  845 + decimal p2 = 40000m;
  846 + decimal p3 = 20000m;
  847 + decimal p4 = 10000m;
  848 +
  849 + if (daysInMonth > 0 && workingDays > 0)
  850 + {
  851 + p1 = (p1 / daysInMonth) * workingDays;
  852 + p2 = (p2 / daysInMonth) * workingDays;
  853 + p3 = (p3 / daysInMonth) * workingDays;
  854 + p4 = (p4 / daysInMonth) * workingDays;
  855 + }
  856 +
  857 + if (teamPerformance >= p1) return 0.06m;
  858 + if (teamPerformance >= p2) return 0.05m;
  859 + if (teamPerformance >= p3) return 0.04m;
  860 + if (teamPerformance >= p4) return 0.03m;
827 861 }
828 862 return 0;
829 863 }
... ... @@ -839,21 +873,23 @@ namespace NCC.Extend
839 873  
840 874 // 注意:
841 875 // 1. "组员业绩"指除顾问外的其他成员业绩总和
842   - // 2. 只统计有效战队成员(考勤≥21天,未被剔除的成员)
  876 + // 2. 只统计有效战队成员(考勤≥20天,未被剔除的成员)
843 877 // 3. "达到X%以上"指:组员业绩总和 ≥ 团队总业绩 × X%
844 878 // 4. 新店顾问不考核消耗
  879 + // 5. 消耗达标:高级顾问整组消耗>=6万,普通顾问整组消耗>=4万
845 880  
846   - // 使用传入的 teamMembers 计算总消耗,或者直接使用 TeamTotalConsumption (如果已计算)
847   - // 但为了保险起见,这里重新计算或使用已有的逻辑
848   - // 注意:CalculateConsultantCommission 方法签名未变,但逻辑需确认 teamConsumption 来源
849   - // 在调用此方法前,teamMembers 已经有了 Consumption 数据
  881 + // 使用传入的 teamMembers 计算总消耗
850 882 var teamConsumption = teamMembers.Sum(x => x.Consumption);
851 883  
852 884 // 计算组员(非顾问)业绩总和
853   - // teamMembers 已经是过滤后的有效成员列表(GoldTriangleId 相同且未被剔除)
854 885 var memberPerformance = teamMembers.Where(x => x.Position != "顾问").Sum(x => x.TotalPerformance);
855 886  
856   - // 高级顾问:业绩≥6万 且 组员业绩≥40% 且 (新店 或 消耗≥6万)
  887 + // 高级顾问:业绩≥6万 且 组员业绩≥40% 且 (新店(第1,2阶段) 或 消耗≥6万)
  888 + // 注意:isNewStore 仅代表是否为新店,具体免考核阶段需确认。假设新店前两个阶段免考核,第三阶段需考核。
  889 + // 这里暂且沿用 isNewStore 逻辑,如果需要更细粒度控制,应传入 NewStoreProtectionStage
  890 + // 如果 isNewStore 为 true,则默认免考核消耗(根据原需求描述:新店第3个阶段时,有金三角,但是不考核消耗)
  891 + // 用户最新指示:新店第3个阶段时,有金三角,但是不考核消耗,默认达标 -> 意味着只要是新店,不管阶段,都不考核消耗
  892 +
857 893 if (teamPerformance >= 60000 && memberPerformance >= teamPerformance * 0.4m)
858 894 {
859 895 if (isNewStore || teamConsumption >= 60000)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreExpenseService.cs
... ... @@ -66,10 +66,36 @@ namespace NCC.Extend.LqStoreExpense
66 66 .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode())
67 67 .FirstAsync();
68 68 _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005);
69   - var output = entity.Adapt<LqStoreExpenseInfoOutput>();
  69 + var output = new LqStoreExpenseInfoOutput
  70 + {
  71 + id = entity.Id,
  72 + storeId = entity.StoreId,
  73 + storeName = entity.StoreName,
  74 + expenseCategoryId = entity.ExpenseCategoryId,
  75 + expenseCategoryName = entity.ExpenseCategoryName,
  76 + expenseDate = entity.ExpenseDate,
  77 + unitPrice = entity.UnitPrice,
  78 + quantity = entity.Quantity,
  79 + amount = entity.Amount,
  80 + memo = entity.Memo,
  81 + relatedReimbursementId = entity.RelatedReimbursementId,
  82 + relatedPurchaseRecordId = entity.RelatedPurchaseRecordId,
  83 + createUser = entity.CreateUser,
  84 + createTime = entity.CreateTime,
  85 + updateUser = entity.UpdateUser,
  86 + updateTime = entity.UpdateTime
  87 + };
  88 +
70 89 if (!string.IsNullOrEmpty(entity.Attachment))
71 90 {
72   - output.attachment = entity.Attachment.ToObject<List<NCC.Common.Model.FileControlsModel>>();
  91 + try
  92 + {
  93 + output.attachment = entity.Attachment.ToObject<List<NCC.Common.Model.FileControlsModel>>();
  94 + }
  95 + catch
  96 + {
  97 + output.attachment = new List<NCC.Common.Model.FileControlsModel>();
  98 + }
73 99 }
74 100 return output;
75 101 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs
... ... @@ -294,6 +294,7 @@ namespace NCC.Extend
294 294 .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
295 295  
296 296 // 1.12 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
  297 + // 优先使用送出时间(F_SendTime),如果为空则使用创建时间(F_CreateTime)
297 298 var laundryCostSql = $@"
298 299 SELECT
299 300 F_StoreId as StoreId,
... ... @@ -301,7 +302,7 @@ namespace NCC.Extend
301 302 FROM lq_laundry_flow
302 303 WHERE F_IsEffective = 1
303 304 AND F_FlowType = 0
304   - AND DATE_FORMAT(F_CreateTime, '%Y%m') = @monthStr
  305 + AND DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @monthStr
305 306 GROUP BY F_StoreId";
306 307  
307 308 var laundryCostData = await _db.Ado.SqlQueryAsync<dynamic>(laundryCostSql, new { monthStr });
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqYcsdJsjService.cs
... ... @@ -347,6 +347,18 @@ namespace NCC.Extend.LqYcsdJsj
347 347 }
348 348 }
349 349  
  350 + // 验证成员 UserID 不能为空
  351 + if (input.members != null && input.members.Count > 0)
  352 + {
  353 + foreach (var member in input.members)
  354 + {
  355 + if (string.IsNullOrEmpty(member.userId))
  356 + {
  357 + throw NCCException.Oh(ErrorCode.COM1000, $"成员【{member.userName}】的 UserID 不能为空");
  358 + }
  359 + }
  360 + }
  361 +
350 362 // 验证金三角名称是否已存在
351 363 var existingJsj = await _db.Queryable<LqYcsdJsjEntity>().Where(x => x.Yf == input.yf && x.Md == input.md && x.Jsj == input.jsj).FirstAsync();
352 364 if (existingJsj != null)
... ... @@ -434,7 +446,7 @@ namespace NCC.Extend.LqYcsdJsj
434 446 if (string.IsNullOrEmpty(input.jsjId))
435 447 throw NCCException.Oh("金三角ID不能为空");
436 448 if (string.IsNullOrEmpty(input.userId))
437   - throw NCCException.Oh("用户ID不能为空");
  449 + throw NCCException.Oh("用户ID不能为空(请确保选择了有效的系统用户)");
438 450 if (string.IsNullOrEmpty(input.userName))
439 451 throw NCCException.Oh("用户姓名不能为空");
440 452  
... ...
sql/主任工资表新增毛利相关字段.sql
... ... @@ -31,3 +31,4 @@ ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT &#39;毛利(销售业ç
31 31 ALTER TABLE lq_director_salary_statistics
32 32 MODIFY COLUMN F_StoreTotalPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT 'é—¨åº—æ€»ä¸šç»©ï¼ˆæ¯›åˆ©ï¼Œç”¨äºŽææˆè®¡ç®—ï¼‰';
33 33  
  34 +
... ...
sql/修复储扣表ItemCategory字段为空的数据.sql 0 → 100644
  1 +-- ============================================
  2 +-- 修复储扣表(lq_kd_deductinfo)中 F_ItemCategory 字段为空的数据
  3 +-- ============================================
  4 +-- 说明:此脚本用于修复储扣表中品项分类字段为空的历史数据
  5 +--
  6 +-- 问题原因:
  7 +-- 1. 更新开单时使用了错误的字段(item.DeductId 而不是 item.ItemId)来查询品项分类
  8 +-- 2. 如果 item.ItemId 为空或无效,查询会返回 null
  9 +--
  10 +-- 修复逻辑:
  11 +-- 通过 F_ItemId 关联 lq_xmzl 表,获取品项分类(qt2)并更新到 F_ItemCategory 字段
  12 +--
  13 +-- 注意事项:
  14 +-- - 只更新有效记录(F_IsEffective = 1)
  15 +-- - 只更新分类字段为空的记录
  16 +-- - 只更新品项分类存在且不为空的记录
  17 +
  18 +-- ============================================
  19 +-- 1. 查看需要修复的记录数
  20 +-- ============================================
  21 +SELECT
  22 + COUNT(*) as TotalNullCount,
  23 + COUNT(CASE WHEN item.qt2 IS NOT NULL THEN 1 END) as CanFixCount
  24 +FROM lq_kd_deductinfo deduct
  25 +LEFT JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
  26 +WHERE deduct.F_IsEffective = 1
  27 + AND deduct.F_ItemCategory IS NULL;
  28 +
  29 +-- ============================================
  30 +-- 2. 修复历史数据:从项目资料表中获取品项分类
  31 +-- ============================================
  32 +UPDATE lq_kd_deductinfo deduct
  33 +INNER JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
  34 +SET deduct.F_ItemCategory = item.qt2
  35 +WHERE deduct.F_IsEffective = 1
  36 + AND deduct.F_ItemCategory IS NULL
  37 + AND item.qt2 IS NOT NULL;
  38 +
  39 +-- ============================================
  40 +-- 3. 验证修复结果
  41 +-- ============================================
  42 +-- 查看修复后的统计信息
  43 +SELECT
  44 + COUNT(*) as TotalCount,
  45 + COUNT(F_ItemCategory) as HasCategoryCount,
  46 + COUNT(*) - COUNT(F_ItemCategory) as NullCategoryCount
  47 +FROM lq_kd_deductinfo
  48 +WHERE F_IsEffective = 1;
  49 +
  50 +-- 查看仍然为空的记录(需要人工处理)
  51 +SELECT
  52 + deduct.F_Id,
  53 + deduct.F_BillingId,
  54 + deduct.F_ItemId,
  55 + deduct.F_ItemName,
  56 + deduct.F_ItemCategory,
  57 + item.qt2 as ItemQt2,
  58 + CASE
  59 + WHEN item.F_Id IS NULL THEN '品项不存在'
  60 + WHEN item.qt2 IS NULL THEN '品项分类为空'
  61 + ELSE '其他原因'
  62 + END as Reason
  63 +FROM lq_kd_deductinfo deduct
  64 +LEFT JOIN lq_xmzl item ON deduct.F_ItemId = item.F_Id
  65 +WHERE deduct.F_IsEffective = 1
  66 + AND deduct.F_ItemCategory IS NULL
  67 +LIMIT 10;
  68 +
... ...
sql/创建合同成本按月统计表.sql 0 → 100644
  1 +-- ============================================
  2 +-- 创建合同成本按月统计表(lq_contract_monthly_cost)
  3 +-- ============================================
  4 +-- 说明:用于按月统计每个合同的成本,便于后续统计和查询
  5 +--
  6 +-- 业务逻辑:
  7 +-- 1. 根据合同起始日期、结束日期,按月生成成本记录
  8 +-- 2. 每个月成本 = 缴租金额 / 交租周期
  9 +-- 3. 例如:合同一年,交费周期3个月,缴租金额3000元
  10 +-- - 每个月成本 = 3000 / 3 = 1000元
  11 +-- - 生成12个月的记录,每个月都是1000元
  12 +--
  13 +-- 字段说明:
  14 +-- F_Month:统计月份(格式:YYYY-MM-01,表示该月的第一天)
  15 +-- F_MonthlyCost:该月的合同成本(缴租金额 / 交租周期)
  16 +--
  17 +-- 索引设计:
  18 +-- - 主键:F_Id
  19 +-- - 合同ID索引:F_ContractId(用于查询某个合同的所有月份成本)
  20 +-- - 月份索引:F_Month(用于按月份统计)
  21 +-- - 门店ID索引:F_StoreId(用于按门店统计)
  22 +-- - 联合索引:(F_StoreId, F_Month) - 用于查询某个门店某个月的成本
  23 +-- - 联合索引:(F_ContractId, F_Month) - 用于查询某个合同某个月的成本
  24 +
  25 +CREATE TABLE IF NOT EXISTS `lq_contract_monthly_cost` (
  26 + `F_Id` VARCHAR(50) NOT NULL COMMENT '主键ID',
  27 + `F_ContractId` VARCHAR(50) NOT NULL COMMENT '合同ID(关联lq_contract.F_Id)',
  28 + `F_StoreId` VARCHAR(50) NOT NULL COMMENT '门店ID(关联lq_mdxx.F_Id,冗余字段便于查询)',
  29 + `F_StoreName` VARCHAR(200) NOT NULL COMMENT '店名(冗余字段,便于查询)',
  30 + `F_Category` VARCHAR(100) DEFAULT NULL COMMENT '分类(冗余字段,便于按分类统计)',
  31 + `F_Month` DATETIME NOT NULL COMMENT '统计月份(格式:YYYY-MM-01,表示该月的第一天)',
  32 + `F_MonthlyCost` DECIMAL(18,2) NOT NULL COMMENT '该月的合同成本(缴租金额 / 交租周期)',
  33 + `F_PaymentCycle` INT NOT NULL COMMENT '交租周期(冗余字段,便于查询)',
  34 + `F_PaymentAmount` DECIMAL(18,2) NOT NULL COMMENT '缴租金额(冗余字段,便于查询)',
  35 + `F_IsEffective` INT DEFAULT 1 COMMENT '是否有效(1-有效,0-无效)',
  36 + `F_CreateUser` VARCHAR(50) NOT NULL COMMENT '创建人ID',
  37 + `F_CreateTime` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  38 + `F_UpdateTime` DATETIME DEFAULT NULL COMMENT '更新时间',
  39 + PRIMARY KEY (`F_Id`),
  40 + KEY `idx_contract_id` (`F_ContractId`) COMMENT '合同ID索引',
  41 + KEY `idx_month` (`F_Month`) COMMENT '月份索引',
  42 + KEY `idx_store_id` (`F_StoreId`) COMMENT '门店ID索引',
  43 + KEY `idx_category` (`F_Category`) COMMENT '分类索引',
  44 + KEY `idx_store_month` (`F_StoreId`, `F_Month`) COMMENT '门店+月份联合索引',
  45 + KEY `idx_contract_month` (`F_ContractId`, `F_Month`) COMMENT '合同+月份联合索引',
  46 + KEY `idx_category_month` (`F_Category`, `F_Month`) COMMENT '分类+月份联合索引',
  47 + KEY `idx_is_effective` (`F_IsEffective`) COMMENT '是否有效索引',
  48 + KEY `idx_create_time` (`F_CreateTime`) COMMENT '创建时间索引'
  49 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同成本按月统计表';
  50 +
  51 +-- ============================================
  52 +-- 数据示例说明
  53 +-- ============================================
  54 +-- 合同示例:
  55 +-- F_ContractId: '768041985045955845'
  56 +-- F_StoreId: '1649328471923847168'
  57 +-- F_StoreName: '绿纤总部'
  58 +-- F_ContractStartDate: '2025-01-01 00:00:00'
  59 +-- F_ContractEndDate: '2025-12-31 23:59:59'
  60 +-- F_PaymentAmount: 3000.00
  61 +-- F_PaymentCycle: 3
  62 +--
  63 +-- 每个月成本 = 3000 / 3 = 1000元
  64 +--
  65 +-- 对应的成本记录(自动生成):
  66 +-- 记录1: F_Month='2025-01-01', F_MonthlyCost=1000.00
  67 +-- 记录2: F_Month='2025-02-01', F_MonthlyCost=1000.00
  68 +-- 记录3: F_Month='2025-03-01', F_MonthlyCost=1000.00
  69 +-- 记录4: F_Month='2025-04-01', F_MonthlyCost=1000.00
  70 +-- ...(共12条记录)
  71 +
... ...
sql/合同成本表添加分类字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 为合同成本按月统计表(lq_contract_monthly_cost)添加分类字段
  3 +-- ============================================
  4 +-- 说明:此脚本为合同成本表添加分类字段,用于按分类统计合同成本
  5 +--
  6 +-- 字段说明:
  7 +-- F_Category:分类(冗余字段,便于按分类统计)
  8 +--
  9 +-- 业务含义:
  10 +-- - 分类字段用于区分不同类型的合同(如:租门店、员工宿舍、车辆、场所等)
  11 +-- - 便于后续按分类统计合同成本
  12 +--
  13 +-- 注意事项:
  14 +-- - 字段类型为VARCHAR(100),允许为NULL(历史数据可能没有分类)
  15 +-- - 字段位置:放在 F_StoreName 字段之后
  16 +-- - 创建后需要更新历史数据,从合同表中获取对应的分类
  17 +
  18 +-- ============================================
  19 +-- 1. 添加分类字段
  20 +-- ============================================
  21 +ALTER TABLE `lq_contract_monthly_cost`
  22 +ADD COLUMN `F_Category` VARCHAR(100) NULL COMMENT '分类(冗余字段,便于按分类统计)' AFTER `F_StoreName`;
  23 +
  24 +-- ============================================
  25 +-- 2. 添加分类索引
  26 +-- ============================================
  27 +ALTER TABLE `lq_contract_monthly_cost`
  28 +ADD INDEX `idx_category` (`F_Category`) COMMENT '分类索引';
  29 +
  30 +-- ============================================
  31 +-- 3. 添加分类+月份联合索引
  32 +-- ============================================
  33 +ALTER TABLE `lq_contract_monthly_cost`
  34 +ADD INDEX `idx_category_month` (`F_Category`, `F_Month`) COMMENT '分类+月份联合索引';
  35 +
  36 +-- ============================================
  37 +-- 4. 更新历史数据:从合同表中获取分类
  38 +-- ============================================
  39 +UPDATE `lq_contract_monthly_cost` cost
  40 +INNER JOIN `lq_contract` contract ON cost.F_ContractId = contract.F_Id
  41 +SET cost.F_Category = contract.F_Category
  42 +WHERE cost.F_Category IS NULL
  43 + AND contract.F_Category IS NOT NULL;
  44 +
  45 +-- ============================================
  46 +-- 5. 验证更新结果
  47 +-- ============================================
  48 +-- 查看更新后的统计信息
  49 +SELECT
  50 + COUNT(*) as TotalCount,
  51 + COUNT(F_Category) as HasCategoryCount,
  52 + COUNT(*) - COUNT(F_Category) as NullCategoryCount
  53 +FROM lq_contract_monthly_cost
  54 +WHERE F_IsEffective = 1;
  55 +
  56 +-- 查看各分类的统计信息
  57 +SELECT
  58 + F_Category,
  59 + COUNT(*) as RecordCount,
  60 + SUM(F_MonthlyCost) as TotalCost
  61 +FROM lq_contract_monthly_cost
  62 +WHERE F_IsEffective = 1
  63 +GROUP BY F_Category
  64 +ORDER BY TotalCost DESC;
  65 +
  66 +
... ...
sql/开单扣减信息表添加开单时间字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 为开单扣减信息表(lq_kd_deductinfo)添加开单时间字段
  3 +-- ============================================
  4 +-- 说明:此脚本为开单扣减信息表添加开单时间字段,用于存储对应的开单时间
  5 +--
  6 +-- 字段说明:
  7 +-- F_BillingTime:开单时间,用于存储对应的开单记录的开单时间(kdrq)
  8 +--
  9 +-- 业务含义:
  10 +-- - 开单时间用于记录储扣对应的开单时间,便于统计和查询
  11 +-- - 开单时间来源于开单记录表(lq_kd_kdjlb)的 kdrq 字段
  12 +--
  13 +-- 注意事项:
  14 +-- - 字段类型为DATETIME,允许为NULL(历史数据可能没有开单时间)
  15 +-- - 字段位置:放在 F_BillingId 字段之后
  16 +-- - 创建后需要更新历史数据,从开单记录表中获取对应的开单时间
  17 +
  18 +-- ============================================
  19 +-- 1. 添加开单时间字段
  20 +-- ============================================
  21 +ALTER TABLE `lq_kd_deductinfo`
  22 +ADD COLUMN `F_BillingTime` DATETIME NULL COMMENT '开单时间' AFTER `F_BillingId`;
  23 +
  24 +-- ============================================
  25 +-- 2. 更新历史数据:从开单记录表中获取开单时间
  26 +-- ============================================
  27 +UPDATE `lq_kd_deductinfo` deduct
  28 +INNER JOIN `lq_kd_kdjlb` billing ON deduct.F_BillingId = billing.F_Id
  29 +SET deduct.F_BillingTime = billing.kdrq
  30 +WHERE deduct.F_BillingTime IS NULL
  31 + AND billing.kdrq IS NOT NULL;
  32 +
  33 +-- ============================================
  34 +-- 3. 验证更新结果
  35 +-- ============================================
  36 +-- 查看更新后的统计信息
  37 +SELECT
  38 + COUNT(*) as TotalCount,
  39 + COUNT(F_BillingTime) as HasBillingTimeCount,
  40 + COUNT(*) - COUNT(F_BillingTime) as NullBillingTimeCount
  41 +FROM lq_kd_deductinfo;
  42 +
  43 +-- 查看有开单时间但开单记录不存在的记录(数据异常检查)
  44 +SELECT
  45 + deduct.F_Id,
  46 + deduct.F_BillingId,
  47 + deduct.F_BillingTime
  48 +FROM lq_kd_deductinfo deduct
  49 +LEFT JOIN lq_kd_kdjlb billing ON deduct.F_BillingId = billing.F_Id
  50 +WHERE deduct.F_BillingTime IS NOT NULL
  51 + AND billing.F_Id IS NULL
  52 +LIMIT 10;
  53 +
... ...
sql/排查生美业绩统计差异-简化版.sql 0 → 100644
  1 +-- ============================================
  2 +-- 排查生美业绩统计差异 - 简化版
  3 +-- ============================================
  4 +-- 问题:品项明细表统计生美数据是1869781.81,但日报天王团统计教育一部+教育二部合计是1876061.21,差异6279.40
  5 +--
  6 +-- 分析思路:
  7 +-- 1. 检查是否有门店在lq_md_target表中有多条记录(同一月份)
  8 +-- 2. 对比品项明细表统计和日报天王团统计的差异
  9 +-- 3. 检查是否有数据被重复统计
  10 +
  11 +-- ============================================
  12 +-- 1. 品项明细表统计生美业绩(所有门店,不限制部门归属)
  13 +-- ============================================
  14 +SELECT
  15 + '品项明细表统计' AS 统计来源,
  16 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  17 +FROM lq_kd_pxmx pxmx
  18 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  19 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  20 +WHERE pxmx.F_IsEffective = 1
  21 + AND billing.F_IsEffective = 1
  22 + AND item.F_IsEffective = 1
  23 + AND item.qt2 = '生美'
  24 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  25 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH);
  26 +
  27 +-- ============================================
  28 +-- 2. 日报天王团统计生美业绩(只统计有部门归属的门店,按部门分组)
  29 +-- ============================================
  30 +SELECT
  31 + '日报天王团统计' AS 统计来源,
  32 + target.F_EducationDepartment as 部门ID,
  33 + dept.F_FullName as 部门名称,
  34 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩
  35 +FROM lq_kd_pxmx pxmx
  36 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  37 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  38 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  39 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  40 +LEFT JOIN base_organize dept ON target.F_EducationDepartment = dept.F_Id
  41 +WHERE pxmx.F_IsEffective = 1
  42 + AND billing.F_IsEffective = 1
  43 + AND item.F_IsEffective = 1
  44 + AND item.qt2 = '生美'
  45 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  46 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  47 + AND target.F_EducationDepartment IS NOT NULL
  48 + AND target.F_EducationDepartment != ''
  49 +GROUP BY target.F_EducationDepartment, dept.F_FullName;
  50 +
  51 +-- ============================================
  52 +-- 3. 检查是否有门店在lq_md_target表中有多条记录(同一月份)
  53 +-- ============================================
  54 +SELECT
  55 + F_StoreId,
  56 + F_Month,
  57 + COUNT(*) as record_count
  58 +FROM lq_md_target
  59 +WHERE F_Month = DATE_FORMAT(NOW(), '%Y%m')
  60 + AND (F_EducationDepartment IS NOT NULL AND F_EducationDepartment != '')
  61 +GROUP BY F_StoreId, F_Month
  62 +HAVING COUNT(*) > 1;
  63 +
  64 +-- ============================================
  65 +-- 4. 检查是否有生美品项的开单记录,但门店在lq_md_target表中没有设置F_EducationDepartment
  66 +-- ============================================
  67 +SELECT
  68 + '未归属门店的生美业绩' AS 统计来源,
  69 + COUNT(DISTINCT billing.djmd) as 门店数量,
  70 + COUNT(*) as 开单记录数,
  71 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  72 +FROM lq_kd_pxmx pxmx
  73 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  74 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  75 +LEFT JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  76 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  77 +WHERE pxmx.F_IsEffective = 1
  78 + AND billing.F_IsEffective = 1
  79 + AND item.F_IsEffective = 1
  80 + AND item.qt2 = '生美'
  81 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  82 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  83 + AND (target.F_StoreId IS NULL
  84 + OR target.F_EducationDepartment IS NULL
  85 + OR target.F_EducationDepartment = '');
  86 +
  87 +-- ============================================
  88 +-- 5. 关键检查:查看每个门店的生美业绩,看看是否有重复统计
  89 +-- ============================================
  90 +SELECT
  91 + billing.djmd as 门店ID,
  92 + md.Dm as 门店名称,
  93 + target.F_EducationDepartment as 教育部门ID,
  94 + dept.F_FullName as 教育部门名称,
  95 + COUNT(*) as 开单记录数,
  96 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩,
  97 + COUNT(DISTINCT target.F_Id) as 目标表记录数
  98 +FROM lq_kd_pxmx pxmx
  99 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  100 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  101 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  102 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  103 +LEFT JOIN lq_mdxx md ON billing.djmd = md.F_Id
  104 +LEFT JOIN base_organize dept ON target.F_EducationDepartment = dept.F_Id
  105 +WHERE pxmx.F_IsEffective = 1
  106 + AND billing.F_IsEffective = 1
  107 + AND item.F_IsEffective = 1
  108 + AND item.qt2 = '生美'
  109 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  110 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  111 + AND target.F_EducationDepartment IS NOT NULL
  112 + AND target.F_EducationDepartment != ''
  113 +GROUP BY billing.djmd, md.Dm, target.F_EducationDepartment, dept.F_FullName
  114 +HAVING COUNT(DISTINCT target.F_Id) > 1
  115 +ORDER BY 生美业绩 DESC;
  116 +
  117 +
  118 +
... ...
sql/排查生美业绩统计差异详细.sql 0 → 100644
  1 +-- ============================================
  2 +-- 排查生美业绩统计差异详细分析
  3 +-- ============================================
  4 +-- 问题:品项明细表统计生美数据是1869781.81,但日报天王团统计教育一部+教育二部合计是1876061.21,差异6279.40
  5 +--
  6 +-- 分析思路:
  7 +-- 1. 检查是否有门店在lq_md_target表中有多条记录(同一月份)
  8 +-- 2. 对比品项明细表统计和日报天王团统计的差异
  9 +-- 3. 检查是否有数据被重复统计
  10 +
  11 +-- ============================================
  12 +-- 1. 检查lq_md_target表中是否有重复记录(同一门店同一月份多条记录)
  13 +-- ============================================
  14 +SELECT
  15 + F_StoreId,
  16 + F_Month,
  17 + COUNT(*) as record_count,
  18 + GROUP_CONCAT(DISTINCT F_EducationDepartment ORDER BY F_EducationDepartment) as education_depts
  19 +FROM lq_md_target
  20 +WHERE F_Month = DATE_FORMAT(NOW(), '%Y%m')
  21 + AND (F_EducationDepartment IS NOT NULL AND F_EducationDepartment != '')
  22 +GROUP BY F_StoreId, F_Month
  23 +HAVING COUNT(*) > 1;
  24 +
  25 +-- ============================================
  26 +-- 2. 品项明细表统计生美业绩(所有门店,不限制部门归属)
  27 +-- ============================================
  28 +SELECT
  29 + '品项明细表统计' AS 统计来源,
  30 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  31 +FROM lq_kd_pxmx pxmx
  32 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  33 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  34 +WHERE pxmx.F_IsEffective = 1
  35 + AND billing.F_IsEffective = 1
  36 + AND item.F_IsEffective = 1
  37 + AND item.qt2 = '生美'
  38 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  39 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH);
  40 +
  41 +-- ============================================
  42 +-- 3. 日报天王团统计生美业绩(只统计有部门归属的门店,按部门分组)
  43 +-- ============================================
  44 +SELECT
  45 + '日报天王团统计' AS 统计来源,
  46 + target.F_EducationDepartment as 部门ID,
  47 + dept.F_FullName as 部门名称,
  48 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩
  49 +FROM lq_kd_pxmx pxmx
  50 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  51 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  52 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  53 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  54 +LEFT JOIN base_organize dept ON target.F_EducationDepartment = dept.F_Id
  55 +WHERE pxmx.F_IsEffective = 1
  56 + AND billing.F_IsEffective = 1
  57 + AND item.F_IsEffective = 1
  58 + AND item.qt2 = '生美'
  59 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  60 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  61 + AND target.F_EducationDepartment IS NOT NULL
  62 + AND target.F_EducationDepartment != ''
  63 +GROUP BY target.F_EducationDepartment, dept.F_FullName;
  64 +
  65 +-- ============================================
  66 +-- 4. 检查是否有门店在lq_md_target表中有多条记录,导致重复统计
  67 +-- ============================================
  68 +SELECT
  69 + billing.djmd as 门店ID,
  70 + md.Dm as 门店名称,
  71 + COUNT(DISTINCT target.F_Id) as 目标表记录数,
  72 + COUNT(*) as 开单记录数,
  73 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额,
  74 + GROUP_CONCAT(DISTINCT target.F_EducationDepartment ORDER BY target.F_EducationDepartment) as 教育部门列表
  75 +FROM lq_kd_pxmx pxmx
  76 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  77 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  78 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  79 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  80 +LEFT JOIN lq_mdxx md ON billing.djmd = md.F_Id
  81 +WHERE pxmx.F_IsEffective = 1
  82 + AND billing.F_IsEffective = 1
  83 + AND item.F_IsEffective = 1
  84 + AND item.qt2 = '生美'
  85 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  86 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  87 + AND target.F_EducationDepartment IS NOT NULL
  88 + AND target.F_EducationDepartment != ''
  89 +GROUP BY billing.djmd, md.Dm
  90 +HAVING COUNT(DISTINCT target.F_Id) > 1;
  91 +
  92 +-- ============================================
  93 +-- 5. 检查是否有生美品项的开单记录,但门店在lq_md_target表中没有设置F_EducationDepartment
  94 +-- ============================================
  95 +SELECT
  96 + '未归属门店的生美业绩' AS 统计来源,
  97 + COUNT(DISTINCT billing.djmd) as 门店数量,
  98 + COUNT(*) as 开单记录数,
  99 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  100 +FROM lq_kd_pxmx pxmx
  101 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  102 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  103 +LEFT JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  104 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  105 +WHERE pxmx.F_IsEffective = 1
  106 + AND billing.F_IsEffective = 1
  107 + AND item.F_IsEffective = 1
  108 + AND item.qt2 = '生美'
  109 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  110 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  111 + AND (target.F_StoreId IS NULL
  112 + OR target.F_EducationDepartment IS NULL
  113 + OR target.F_EducationDepartment = '');
  114 +
  115 +-- ============================================
  116 +-- 6. 关键检查:查看每个门店的生美业绩,看看是否有重复统计
  117 +-- ============================================
  118 +SELECT
  119 + billing.djmd as 门店ID,
  120 + md.Dm as 门店名称,
  121 + target.F_EducationDepartment as 教育部门ID,
  122 + dept.F_FullName as 教育部门名称,
  123 + COUNT(*) as 开单记录数,
  124 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩,
  125 + COUNT(DISTINCT target.F_Id) as 目标表记录数
  126 +FROM lq_kd_pxmx pxmx
  127 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  128 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  129 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  130 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  131 +LEFT JOIN lq_mdxx md ON billing.djmd = md.F_Id
  132 +LEFT JOIN base_organize dept ON target.F_EducationDepartment = dept.F_Id
  133 +WHERE pxmx.F_IsEffective = 1
  134 + AND billing.F_IsEffective = 1
  135 + AND item.F_IsEffective = 1
  136 + AND item.qt2 = '生美'
  137 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  138 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  139 + AND target.F_EducationDepartment IS NOT NULL
  140 + AND target.F_EducationDepartment != ''
  141 +GROUP BY billing.djmd, md.Dm, target.F_EducationDepartment, dept.F_FullName
  142 +ORDER BY 生美业绩 DESC;
  143 +
  144 +-- ============================================
  145 +-- 7. 检查是否有门店在lq_md_target表中有多条记录(不同月份,但查询时可能有问题)
  146 +-- ============================================
  147 +SELECT
  148 + F_StoreId,
  149 + COUNT(DISTINCT F_Month) as 月份数,
  150 + GROUP_CONCAT(DISTINCT F_Month ORDER BY F_Month) as 月份列表,
  151 + COUNT(*) as 总记录数
  152 +FROM lq_md_target
  153 +WHERE F_StoreId IN (
  154 + SELECT DISTINCT billing.djmd
  155 + FROM lq_kd_pxmx pxmx
  156 + INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  157 + INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  158 + WHERE pxmx.F_IsEffective = 1
  159 + AND billing.F_IsEffective = 1
  160 + AND item.F_IsEffective = 1
  161 + AND item.qt2 = '生美'
  162 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  163 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  164 +)
  165 + AND (F_EducationDepartment IS NOT NULL AND F_EducationDepartment != '')
  166 +GROUP BY F_StoreId
  167 +HAVING COUNT(*) > 1;
  168 +
  169 +
  170 +
... ...
sql/检查生美业绩统计差异.sql 0 → 100644
  1 +-- ============================================
  2 +-- 检查生美业绩统计差异
  3 +-- ============================================
  4 +-- 问题:品项明细表统计生美数据是1869781.81,但日报天王团统计教育一部+教育二部合计是1876061.21,差异6279.40
  5 +--
  6 +-- 可能原因:
  7 +-- 1. 门店在lq_md_target表中有重复记录(同一月份多条记录)
  8 +-- 2. 统计范围不一致(时间范围或门店范围)
  9 +-- 3. 数据关联逻辑问题
  10 +
  11 +-- ============================================
  12 +-- 1. 检查lq_md_target表中是否有重复记录(同一门店同一月份多条记录)
  13 +-- ============================================
  14 +SELECT
  15 + F_StoreId,
  16 + F_Month,
  17 + COUNT(*) as record_count,
  18 + GROUP_CONCAT(DISTINCT F_EducationDepartment) as education_depts
  19 +FROM lq_md_target
  20 +WHERE F_Month = DATE_FORMAT(NOW(), '%Y%m')
  21 + AND (F_EducationDepartment IS NOT NULL AND F_EducationDepartment != '')
  22 +GROUP BY F_StoreId, F_Month
  23 +HAVING COUNT(*) > 1;
  24 +
  25 +-- ============================================
  26 +-- 2. 检查品项明细表统计生美业绩(所有门店,不限制部门归属)
  27 +-- ============================================
  28 +SELECT
  29 + '品项明细表统计' AS 统计来源,
  30 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  31 +FROM lq_kd_pxmx pxmx
  32 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  33 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  34 +WHERE pxmx.F_IsEffective = 1
  35 + AND billing.F_IsEffective = 1
  36 + AND item.F_IsEffective = 1
  37 + AND item.qt2 = '生美'
  38 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  39 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH);
  40 +
  41 +-- ============================================
  42 +-- 3. 检查日报天王团统计生美业绩(只统计有部门归属的门店,按部门分组)
  43 +-- ============================================
  44 +SELECT
  45 + '日报天王团统计' AS 统计来源,
  46 + target.F_EducationDepartment as 部门ID,
  47 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩
  48 +FROM lq_kd_pxmx pxmx
  49 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  50 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  51 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  52 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  53 +WHERE pxmx.F_IsEffective = 1
  54 + AND billing.F_IsEffective = 1
  55 + AND item.F_IsEffective = 1
  56 + AND item.qt2 = '生美'
  57 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  58 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  59 + AND target.F_EducationDepartment IS NOT NULL
  60 + AND target.F_EducationDepartment != ''
  61 +GROUP BY target.F_EducationDepartment;
  62 +
  63 +-- ============================================
  64 +-- 4. 检查是否有生美品项的开单记录,但门店在lq_md_target表中没有设置F_EducationDepartment
  65 +-- ============================================
  66 +SELECT
  67 + '未归属门店的生美业绩' AS 统计来源,
  68 + COUNT(DISTINCT billing.djmd) as 门店数量,
  69 + COUNT(*) as 开单记录数,
  70 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额
  71 +FROM lq_kd_pxmx pxmx
  72 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  73 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  74 +LEFT JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  75 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  76 +WHERE pxmx.F_IsEffective = 1
  77 + AND billing.F_IsEffective = 1
  78 + AND item.F_IsEffective = 1
  79 + AND item.qt2 = '生美'
  80 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  81 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  82 + AND (target.F_StoreId IS NULL
  83 + OR target.F_EducationDepartment IS NULL
  84 + OR target.F_EducationDepartment = '');
  85 +
  86 +-- ============================================
  87 +-- 5. 检查是否有门店在lq_md_target表中有多条记录(可能导致重复统计)
  88 +-- ============================================
  89 +SELECT
  90 + billing.djmd as 门店ID,
  91 + COUNT(DISTINCT target.F_Id) as 目标表记录数,
  92 + COUNT(*) as 开单记录数,
  93 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩总额,
  94 + GROUP_CONCAT(DISTINCT target.F_EducationDepartment) as 教育部门列表
  95 +FROM lq_kd_pxmx pxmx
  96 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  97 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  98 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  99 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  100 +WHERE pxmx.F_IsEffective = 1
  101 + AND billing.F_IsEffective = 1
  102 + AND item.F_IsEffective = 1
  103 + AND item.qt2 = '生美'
  104 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  105 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  106 + AND target.F_EducationDepartment IS NOT NULL
  107 + AND target.F_EducationDepartment != ''
  108 +GROUP BY billing.djmd
  109 +HAVING COUNT(DISTINCT target.F_Id) > 1;
  110 +
  111 +-- ============================================
  112 +-- 6. 详细检查:查看每个门店的生美业绩和部门归属情况
  113 +-- ============================================
  114 +SELECT
  115 + billing.djmd as 门店ID,
  116 + md.Dm as 门店名称,
  117 + target.F_EducationDepartment as 教育部门ID,
  118 + dept.F_FullName as 教育部门名称,
  119 + COUNT(*) as 开单记录数,
  120 + COALESCE(SUM(pxmx.F_ActualPrice), 0) as 生美业绩
  121 +FROM lq_kd_pxmx pxmx
  122 +INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  123 +INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  124 +INNER JOIN lq_md_target target ON billing.djmd = target.F_StoreId
  125 + AND target.F_Month = DATE_FORMAT(NOW(), '%Y%m')
  126 +LEFT JOIN lq_mdxx md ON billing.djmd = md.F_Id
  127 +LEFT JOIN base_organize dept ON target.F_EducationDepartment = dept.F_Id
  128 +WHERE pxmx.F_IsEffective = 1
  129 + AND billing.F_IsEffective = 1
  130 + AND item.F_IsEffective = 1
  131 + AND item.qt2 = '生美'
  132 + AND billing.kdrq >= DATE_FORMAT(NOW(), '%Y-%m-01')
  133 + AND billing.kdrq < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH)
  134 + AND target.F_EducationDepartment IS NOT NULL
  135 + AND target.F_EducationDepartment != ''
  136 +GROUP BY billing.djmd, md.Dm, target.F_EducationDepartment, dept.F_FullName
  137 +ORDER BY 生美业绩 DESC;
  138 +
  139 +
  140 +
... ...
sql/清洗流水表添加送出送回时间字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 为清洗流水表(lq_laundry_flow)添加送出/送回时间字段
  3 +-- ============================================
  4 +-- 说明:此脚本为清洗流水表添加送出时间和送回时间字段,用于记录实际的送出/送回时间
  5 +--
  6 +-- 字段说明:
  7 +-- F_SendTime:送出时间,用于记录实际的送出时间(流水类型为0时使用)
  8 +-- F_ReturnTime:送回时间,用于记录实际的送回时间(流水类型为1时使用)
  9 +--
  10 +-- 业务含义:
  11 +-- - 送出时间:记录实际送出清洗的时间,便于后续统计
  12 +-- - 送回时间:记录实际送回清洗的时间,便于后续统计
  13 +-- - F_CreateTime:保持为记录创建时间(系统时间),用于记录数据录入时间
  14 +--
  15 +-- 注意事项:
  16 +-- - 字段类型为DATETIME,允许为NULL(历史数据可能没有这些时间)
  17 +-- - 字段位置:放在 F_CreateTime 字段之后
  18 +-- - 创建后需要更新历史数据,将 F_CreateTime 的值复制到对应的时间字段
  19 +
  20 +-- ============================================
  21 +-- 1. 添加送出时间字段
  22 +-- ============================================
  23 +ALTER TABLE `lq_laundry_flow`
  24 +ADD COLUMN `F_SendTime` DATETIME NULL COMMENT '送出时间(流水类型为0时使用)' AFTER `F_CreateTime`;
  25 +
  26 +-- ============================================
  27 +-- 2. 添加送回时间字段
  28 +-- ============================================
  29 +ALTER TABLE `lq_laundry_flow`
  30 +ADD COLUMN `F_ReturnTime` DATETIME NULL COMMENT '送回时间(流水类型为1时使用)' AFTER `F_SendTime`;
  31 +
  32 +-- ============================================
  33 +-- 3. 更新历史数据:将创建时间复制到对应的时间字段
  34 +-- ============================================
  35 +-- 更新送出记录:将创建时间复制到送出时间
  36 +UPDATE `lq_laundry_flow`
  37 +SET `F_SendTime` = `F_CreateTime`
  38 +WHERE `F_FlowType` = 0
  39 + AND `F_SendTime` IS NULL
  40 + AND `F_CreateTime` IS NOT NULL;
  41 +
  42 +-- 更新送回记录:将创建时间复制到送回时间
  43 +UPDATE `lq_laundry_flow`
  44 +SET `F_ReturnTime` = `F_CreateTime`
  45 +WHERE `F_FlowType` = 1
  46 + AND `F_ReturnTime` IS NULL
  47 + AND `F_CreateTime` IS NOT NULL;
  48 +
  49 +-- ============================================
  50 +-- 4. 验证更新结果
  51 +-- ============================================
  52 +-- 查看更新后的统计信息
  53 +SELECT
  54 + F_FlowType,
  55 + CASE WHEN F_FlowType = 0 THEN '送出' ELSE '送回' END as FlowTypeName,
  56 + COUNT(*) as TotalCount,
  57 + COUNT(CASE WHEN F_FlowType = 0 THEN F_SendTime END) as HasSendTimeCount,
  58 + COUNT(CASE WHEN F_FlowType = 1 THEN F_ReturnTime END) as HasReturnTimeCount
  59 +FROM lq_laundry_flow
  60 +WHERE F_IsEffective = 1
  61 +GROUP BY F_FlowType;
  62 +
  63 +
... ...
test_tianwang_api.py 0 → 100644
  1 +#!/usr/bin/env python3
  2 +# -*- coding: utf-8 -*-
  3 +import json
  4 +import sys
  5 +from datetime import datetime
  6 +
  7 +# 测试接口
  8 +import subprocess
  9 +
  10 +# 测试2025年1月的数据
  11 +cmd1 = [
  12 + 'curl', '-s', '-X', 'POST',
  13 + 'http://localhost:2011/api/Extend/LqDailyReport/get-tianwang-group-performance-completion',
  14 + '-H', 'Content-Type: application/json',
  15 + '-d', '{"startTime": "2025-01-01", "endTime": "2025-01-31"}'
  16 +]
  17 +
  18 +result1 = subprocess.run(cmd1, capture_output=True, text=True)
  19 +data1 = json.loads(result1.stdout)
  20 +
  21 +print("=" * 60)
  22 +print("2025年1月数据")
  23 +print("=" * 60)
  24 +print(f"接口状态: {data1.get('code')}")
  25 +print(f"返回消息: {data1.get('msg')}")
  26 +
  27 +depts1 = [d for d in data1.get('data', []) if '教育' in d.get('DepartmentName', '')]
  28 +print("\n=== 教育部数据 ===")
  29 +for d in depts1:
  30 + print(f"{d.get('DepartmentName')}:")
  31 + print(f" BillingPerformance: {d.get('BillingPerformance')}")
  32 + print(f" RefundPerformance: {d.get('RefundPerformance')}")
  33 + print(f" DeductAmount: {d.get('DeductAmount')}")
  34 + print(f" CompletedPerformance: {d.get('CompletedPerformance')}")
  35 + print(f" StoreCount: {d.get('StoreCount')}")
  36 +
  37 +total1 = sum([d.get('BillingPerformance', 0) for d in depts1])
  38 +print(f"\n教育一部+教育二部合计 BillingPerformance: {total1}")
  39 +
  40 +# 测试当前月份的数据
  41 +current_month = datetime.now().strftime("%Y-%m")
  42 +start_date = f"{current_month}-01"
  43 +end_date = datetime.now().strftime("%Y-%m-%d")
  44 +
  45 +cmd2 = [
  46 + 'curl', '-s', '-X', 'POST',
  47 + 'http://localhost:2011/api/Extend/LqDailyReport/get-tianwang-group-performance-completion',
  48 + '-H', 'Content-Type: application/json',
  49 + '-d', f'{{"startTime": "{start_date}", "endTime": "{end_date}"}}'
  50 +]
  51 +
  52 +result2 = subprocess.run(cmd2, capture_output=True, text=True)
  53 +data2 = json.loads(result2.stdout)
  54 +
  55 +print("\n" + "=" * 60)
  56 +print(f"当前月份 ({current_month}) 数据")
  57 +print("=" * 60)
  58 +print(f"接口状态: {data2.get('code')}")
  59 +
  60 +depts2 = [d for d in data2.get('data', []) if '教育' in d.get('DepartmentName', '')]
  61 +print("\n=== 教育部数据 ===")
  62 +for d in depts2:
  63 + print(f"{d.get('DepartmentName')}:")
  64 + print(f" BillingPerformance: {d.get('BillingPerformance')}")
  65 + print(f" RefundPerformance: {d.get('RefundPerformance')}")
  66 + print(f" DeductAmount: {d.get('DeductAmount')}")
  67 + print(f" CompletedPerformance: {d.get('CompletedPerformance')}")
  68 + print(f" StoreCount: {d.get('StoreCount')}")
  69 +
  70 +total2 = sum([d.get('BillingPerformance', 0) for d in depts2])
  71 +print(f"\n教育一部+教育二部合计 BillingPerformance: {total2}")
  72 +
  73 +
... ...
主任工资毛利计算逻辑梳理.md
... ... @@ -300,3 +300,4 @@ ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT &#39;毛利(销售业ç
300 300 4. **业绩达标判断**:基于毛利是å¦â‰¥ç”Ÿå‘½çº¿
301 301 5. **ææˆè®¡ç®—**ï¼šåŸºäºŽæ¯›åˆ©ï¼Œä¸æ˜¯åŸºäºŽé”€å”®ä¸šç»©
302 302  
  303 +
... ...
储扣表ItemCategory字段为空问题梳理.md 0 → 100644
  1 +# 储扣表 F_ItemCategory 字段为空问题梳理
  2 +
  3 +## 问题描述
  4 +储扣表(lq_kd_deductinfo)中的 `F_ItemCategory` 字段有时候是空的。
  5 +
  6 +## 数据库统计
  7 +- 总有效记录数:1865
  8 +- 有分类字段的记录数:1863
  9 +- 分类字段为空的记录数:2
  10 +
  11 +## 问题分析
  12 +
  13 +### 1. 创建开单时的逻辑(第910行)
  14 +```csharp
  15 +ItemCategory = await _db.Queryable<LqXmzlEntity>()
  16 + .Where(x => x.Id == item.ItemId)
  17 + .Select(x => x.Qt2)
  18 + .FirstAsync(),
  19 +```
  20 +- 使用 `item.ItemId` 查询品项分类
  21 +- 如果 `item.ItemId` 为空或无效,查询会返回 null
  22 +
  23 +### 2. 更新开单时的逻辑(第1257行)
  24 +```csharp
  25 +ItemCategory = await _db.Queryable<LqXmzlEntity>()
  26 + .Where(x => x.Id == item.DeductId) // ❌ 错误:应该使用 item.ItemId
  27 + .Select(x => x.Qt2)
  28 + .FirstAsync()
  29 +```
  30 +- **问题**:使用了 `item.DeductId` 而不是 `item.ItemId`
  31 +- `DeductId` 是品项明细表(lq_kd_pxmx)的ID,不是项目资料表(lq_xmzl)的ID
  32 +- 这会导致查询不到结果,返回 null
  33 +
  34 +### 3. 字段说明
  35 +- **DeductId**:扣减品项关联ID,对应 `lq_kd_pxmx.F_Id`(品项明细表的主键)
  36 +- **ItemId**:品项id,对应 `lq_xmzl.F_Id`(项目资料表的主键)
  37 +- **ItemCategory**:品项分类,应该从 `lq_xmzl.qt2` 获取
  38 +
  39 +### 4. 导致空值的原因
  40 +1. **更新开单时使用了错误的字段**:使用 `item.DeductId` 而不是 `item.ItemId`
  41 +2. **查询不到结果**:如果 `item.ItemId` 为空或无效,`FirstAsync()` 会返回 null
  42 +3. **品项不存在或无效**:如果品项在 `lq_xmzl` 表中不存在或无效,查询也会返回 null
  43 +
  44 +## 解决方案
  45 +
  46 +### ✅ 方案1:修复更新开单时的逻辑(已修复)
  47 +将第1257行的 `item.DeductId` 改为 `item.ItemId`,与创建开单时的逻辑保持一致。
  48 +
  49 +**修复代码**:
  50 +```csharp
  51 +// 修复前(错误)
  52 +ItemCategory = await _db.Queryable<LqXmzlEntity>()
  53 + .Where(x => x.Id == item.DeductId) // ❌ 错误
  54 + .Select(x => x.Qt2)
  55 + .FirstAsync()
  56 +
  57 +// 修复后(正确)
  58 +ItemCategory = await _db.Queryable<LqXmzlEntity>()
  59 + .Where(x => x.Id == item.ItemId) // ✅ 正确
  60 + .Select(x => x.Qt2)
  61 + .FirstAsync()
  62 +```
  63 +
  64 +### ✅ 方案2:修复历史数据(已提供SQL脚本)
  65 +对于已经存在的空值记录,执行 `sql/修复储扣表ItemCategory字段为空的数据.sql` 脚本修复。
  66 +
  67 +### 方案3:增加容错处理(可选)
  68 +在查询时增加容错处理,如果查询不到结果,尝试从其他途径获取分类。
  69 +
  70 +## 验证
  71 +从数据库查询结果看:
  72 +- F_ItemId = '76',对应的 lq_xmzl 表中 qt2 = '科美'(存在且有效)
  73 +- F_ItemId = '100024',对应的 lq_xmzl 表中 qt2 = '科美'(存在且有效)
  74 +
  75 +说明这些记录的 `ItemId` 是有效的,问题应该是在更新开单时使用了错误的字段。
  76 +
... ...
测试储扣接口.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试储扣相关接口
  4 +
  5 +# 1. 获取token
  6 +echo "=== 1. 获取Token ==="
  7 +TOKEN_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  8 + -H "Content-Type: application/x-www-form-urlencoded" \
  9 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e")
  10 +
  11 +echo "$TOKEN_RESPONSE" | python3 -m json.tool
  12 +
  13 +TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['data']['token'])")
  14 +
  15 +if [ -z "$TOKEN" ]; then
  16 + echo "❌ Token获取失败"
  17 + exit 1
  18 +fi
  19 +
  20 +echo "✅ Token获取成功: ${TOKEN:0:50}..."
  21 +echo ""
  22 +
  23 +# 2. 测试储扣金额统计接口(按品项分类)
  24 +echo "=== 2. 测试储扣金额统计接口(get-deduct-amount-statistics)==="
  25 +echo "请求参数: {\"startTime\": \"2025-11-01\", \"endTime\": \"2025-11-30\"}"
  26 +echo ""
  27 +
  28 +DEDUCT_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqDailyReport/get-deduct-amount-statistics" \
  29 + -H "Authorization: $TOKEN" \
  30 + -H "Content-Type: application/json" \
  31 + -d '{"startTime": "2025-11-01", "endTime": "2025-11-30"}')
  32 +
  33 +echo "$DEDUCT_RESPONSE" | python3 -m json.tool
  34 +
  35 +# 检查返回结果
  36 +if echo "$DEDUCT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); exit(0 if data.get('code') == 200 else 1)" 2>/dev/null; then
  37 + echo "✅ 储扣金额统计接口调用成功"
  38 +
  39 + # 提取数据
  40 + YIMEI=$(echo "$DEDUCT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('yiMeiAmount', 0))" 2>/dev/null)
  41 + SHENGMEI=$(echo "$DEDUCT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('shengMeiAmount', 0))" 2>/dev/null)
  42 + KEMEI=$(echo "$DEDUCT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('keMeiAmount', 0))" 2>/dev/null)
  43 + TOTAL=$(echo "$DEDUCT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('data', {}).get('totalAmount', 0))" 2>/dev/null)
  44 +
  45 + echo "医美储扣金额: $YIMEI"
  46 + echo "生美储扣金额: $SHENGMEI"
  47 + echo "科美储扣金额: $KEMEI"
  48 + echo "总储扣金额: $TOTAL"
  49 +else
  50 + echo "❌ 储扣金额统计接口调用失败"
  51 +fi
  52 +
  53 +echo ""
  54 +echo ""
  55 +
  56 +# 3. 测试部门业绩完成情况接口(包含储扣统计)
  57 +echo "=== 3. 测试部门业绩完成情况接口(get-tianwang-group-performance-completion)==="
  58 +echo "请求参数: {\"startTime\": \"2025-11-01\", \"endTime\": \"2025-11-30\"}"
  59 +echo ""
  60 +
  61 +DEPT_RESPONSE=$(curl -s -X POST "http://localhost:2011/api/Extend/LqDailyReport/get-tianwang-group-performance-completion" \
  62 + -H "Authorization: $TOKEN" \
  63 + -H "Content-Type: application/json" \
  64 + -d '{"startTime": "2025-11-01", "endTime": "2025-11-30"}')
  65 +
  66 +echo "$DEPT_RESPONSE" | python3 -m json.tool
  67 +
  68 +# 检查返回结果
  69 +if echo "$DEPT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); exit(0 if data.get('code') == 200 else 1)" 2>/dev/null; then
  70 + echo "✅ 部门业绩完成情况接口调用成功"
  71 +
  72 + # 提取第一个部门的数据
  73 + FIRST_DEPT=$(echo "$DEPT_RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); depts=data.get('data', []); print(json.dumps(depts[0] if depts else {}, indent=2))" 2>/dev/null)
  74 +
  75 + if [ ! -z "$FIRST_DEPT" ] && [ "$FIRST_DEPT" != "{}" ]; then
  76 + echo ""
  77 + echo "第一个部门的数据:"
  78 + echo "$FIRST_DEPT"
  79 + fi
  80 +else
  81 + echo "❌ 部门业绩完成情况接口调用失败"
  82 +fi
  83 +
  84 +echo ""
  85 +echo "=== 测试完成 ==="
  86 +
... ...