Commit 273e501a67e35ce6595cb3443ae60f8b374df2e6

Authored by “wangming”
1 parent 404cbc89

修复产品上下架功能;修复门店每日统计中消耗业绩和项目数统计逻辑,避免笛卡尔积问题

Showing 23 changed files with 2568 additions and 371 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventory/LqInventoryCrInput.cs
... ... @@ -9,50 +9,45 @@ namespace NCC.Extend.Entitys.Dto.LqInventory
9 9 public class LqInventoryCrInput
10 10 {
11 11 /// <summary>
12   - /// 产品名称
  12 + /// 产品ID
13 13 /// </summary>
14   - [Required(ErrorMessage = "产品名称不能为空")]
15   - [StringLength(100, ErrorMessage = "产品名称长度不能超过100个字符")]
16   - [Display(Name = "产品名称")]
17   - public string ProductName { get; set; }
  14 + [Required(ErrorMessage = "产品ID不能为空")]
  15 + [StringLength(50, ErrorMessage = "产品ID长度不能超过50个字符")]
  16 + [Display(Name = "产品ID")]
  17 + public string ProductId { get; set; }
18 18  
19 19 /// <summary>
20   - /// 价格
  20 + /// 库存数量
21 21 /// </summary>
22   - [Required(ErrorMessage = "价格不能为空")]
23   - [Range(0, double.MaxValue, ErrorMessage = "价格不能小于0")]
24   - [Display(Name = "价格")]
25   - public decimal Price { get; set; }
  22 + [Required(ErrorMessage = "库存数量不能为空")]
  23 + [Range(1, int.MaxValue, ErrorMessage = "库存数量必须大于0")]
  24 + [Display(Name = "库存数量")]
  25 + public int Quantity { get; set; }
26 26  
27 27 /// <summary>
28   - /// 数量
  28 + /// 入库时间
29 29 /// </summary>
30   - [Range(0, int.MaxValue, ErrorMessage = "数量不能小于0")]
31   - [Display(Name = "数量")]
32   - public int Quantity { get; set; } = 0;
  30 + [Display(Name = "入库时间")]
  31 + public DateTime? StockInTime { get; set; }
33 32  
34 33 /// <summary>
35   - /// 产品类别
  34 + /// 生产日期
36 35 /// </summary>
37   - [Required(ErrorMessage = "产品类别不能为空")]
38   - [StringLength(50, ErrorMessage = "产品类别长度不能超过50个字符")]
39   - [Display(Name = "产品类别")]
40   - public string ProductCategory { get; set; }
  36 + [Display(Name = "生产日期")]
  37 + public DateTime? ProductionDate { get; set; }
41 38  
42 39 /// <summary>
43   - /// 归属部门ID
  40 + /// 保质期(天数)
44 41 /// </summary>
45   - [Required(ErrorMessage = "归属部门ID不能为空")]
46   - [StringLength(50, ErrorMessage = "归属部门ID长度不能超过50个字符")]
47   - [Display(Name = "归属部门ID")]
48   - public string DepartmentId { get; set; }
  42 + [Range(0, int.MaxValue, ErrorMessage = "保质期不能小于0")]
  43 + [Display(Name = "保质期")]
  44 + public int? ShelfLife { get; set; }
49 45  
50 46 /// <summary>
51   - /// 标准单位
  47 + /// 批次号
52 48 /// </summary>
53   - [Required(ErrorMessage = "标准单位不能为空")]
54   - [StringLength(20, ErrorMessage = "标准单位长度不能超过20个字符")]
55   - [Display(Name = "标准单位")]
56   - public string StandardUnit { get; set; }
  49 + [StringLength(100, ErrorMessage = "批次号长度不能超过100个字符")]
  50 + [Display(Name = "批次号")]
  51 + public string BatchNumber { get; set; }
57 52 }
58 53 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventory/LqInventoryListOutput.cs
... ... @@ -15,52 +15,64 @@ namespace NCC.Extend.Entitys.Dto.LqInventory
15 15 public string id { get; set; }
16 16  
17 17 /// <summary>
  18 + /// 产品ID
  19 + /// </summary>
  20 + [Display(Name = "产品ID")]
  21 + public string productId { get; set; }
  22 +
  23 + /// <summary>
18 24 /// 产品名称
19 25 /// </summary>
20 26 [Display(Name = "产品名称")]
21 27 public string productName { get; set; }
22 28  
23 29 /// <summary>
24   - /// 价格
  30 + /// 产品价格
25 31 /// </summary>
26   - [Display(Name = "价格")]
27   - public decimal price { get; set; }
  32 + [Display(Name = "产品价格")]
  33 + public decimal productPrice { get; set; }
28 34  
29 35 /// <summary>
30   - /// 数量
  36 + /// 库存数量
31 37 /// </summary>
32   - [Display(Name = "数量")]
  38 + [Display(Name = "库存数量")]
33 39 public int quantity { get; set; }
34 40  
35 41 /// <summary>
36   - /// 产品类别
  42 + /// 已使用数量
  43 + /// </summary>
  44 + [Display(Name = "已使用数量")]
  45 + public int usedQuantity { get; set; }
  46 +
  47 + /// <summary>
  48 + /// 可用数量(库存数量 - 已使用数量)
37 49 /// </summary>
38   - [Display(Name = "产品类别")]
39   - public string productCategory { get; set; }
  50 + [Display(Name = "可用数量")]
  51 + public int availableQuantity { get; set; }
40 52  
41 53 /// <summary>
42   - /// 归属部门ID
  54 + /// 入库时间
43 55 /// </summary>
44   - [Display(Name = "归属部门ID")]
45   - public string departmentId { get; set; }
  56 + [Display(Name = "入库时间")]
  57 + public DateTime? stockInTime { get; set; }
46 58  
47 59 /// <summary>
48   - /// 部门名称
  60 + /// 生产日期
49 61 /// </summary>
50   - [Display(Name = "部门名称")]
51   - public string departmentName { get; set; }
  62 + [Display(Name = "生产日期")]
  63 + public DateTime? productionDate { get; set; }
52 64  
53 65 /// <summary>
54   - /// 标准单位
  66 + /// 保质期(天数)
55 67 /// </summary>
56   - [Display(Name = "标准单位")]
57   - public string standardUnit { get; set; }
  68 + [Display(Name = "保质期")]
  69 + public int? shelfLife { get; set; }
58 70  
59 71 /// <summary>
60   - /// 总价值
  72 + /// 批次号
61 73 /// </summary>
62   - [Display(Name = "总价值")]
63   - public decimal totalValue { get; set; }
  74 + [Display(Name = "批次号")]
  75 + public string batchNumber { get; set; }
64 76  
65 77 /// <summary>
66 78 /// 创建人ID
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventory/LqInventoryListQueryInput.cs
... ... @@ -10,34 +10,22 @@ namespace NCC.Extend.Entitys.Dto.LqInventory
10 10 public class LqInventoryListQueryInput : PageInputBase
11 11 {
12 12 /// <summary>
13   - /// 产品名称
  13 + /// 产品ID
14 14 /// </summary>
15   - [Display(Name = "产品名称", Description = "根据产品名称筛选")]
16   - public string ProductName { get; set; }
17   -
18   - /// <summary>
19   - /// 产品类别
20   - /// </summary>
21   - [Display(Name = "产品类别", Description = "根据产品类别筛选")]
22   - public string ProductCategory { get; set; }
  15 + [Display(Name = "产品ID", Description = "根据产品ID筛选")]
  16 + public string ProductId { get; set; }
23 17  
24 18 /// <summary>
25   - /// 归属部门ID
  19 + /// 产品名称(模糊查询)
26 20 /// </summary>
27   - [Display(Name = "归属部门ID", Description = "根据归属部门ID筛选")]
28   - public string DepartmentId { get; set; }
29   -
30   - /// <summary>
31   - /// 价格最小值
32   - /// </summary>
33   - [Display(Name = "价格最小值", Description = "价格范围的最小值")]
34   - public decimal? PriceMin { get; set; }
  21 + [Display(Name = "产品名称", Description = "根据产品名称筛选")]
  22 + public string ProductName { get; set; }
35 23  
36 24 /// <summary>
37   - /// 价格最大值
  25 + /// 批次号
38 26 /// </summary>
39   - [Display(Name = "价格最大值", Description = "价格范围的最大值")]
40   - public decimal? PriceMax { get; set; }
  27 + [Display(Name = "批次号", Description = "根据批次号筛选")]
  28 + public string BatchNumber { get; set; }
41 29  
42 30 /// <summary>
43 31 /// 数量最小值
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductCrInput.cs 0 → 100644
  1 +using System;
  2 +using System.ComponentModel.DataAnnotations;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqProduct
  5 +{
  6 + /// <summary>
  7 + /// 产品创建输入
  8 + /// </summary>
  9 + public class LqProductCrInput
  10 + {
  11 + /// <summary>
  12 + /// 产品名称
  13 + /// </summary>
  14 + [Required(ErrorMessage = "产品名称不能为空")]
  15 + [StringLength(200, ErrorMessage = "产品名称长度不能超过200个字符")]
  16 + [Display(Name = "产品名称")]
  17 + public string ProductName { get; set; }
  18 +
  19 + /// <summary>
  20 + /// 价格
  21 + /// </summary>
  22 + [Required(ErrorMessage = "价格不能为空")]
  23 + [Range(0, double.MaxValue, ErrorMessage = "价格不能小于0")]
  24 + [Display(Name = "价格")]
  25 + public decimal Price { get; set; }
  26 +
  27 + /// <summary>
  28 + /// 产品类别
  29 + /// </summary>
  30 + [StringLength(50, ErrorMessage = "产品类别长度不能超过50个字符")]
  31 + [Display(Name = "产品类别")]
  32 + public string ProductCategory { get; set; }
  33 +
  34 + /// <summary>
  35 + /// 归属部门ID
  36 + /// </summary>
  37 + [StringLength(50, ErrorMessage = "归属部门ID长度不能超过50个字符")]
  38 + [Display(Name = "归属部门ID")]
  39 + public string DepartmentId { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 标准单位
  43 + /// </summary>
  44 + [StringLength(20, ErrorMessage = "标准单位长度不能超过20个字符")]
  45 + [Display(Name = "标准单位")]
  46 + public string StandardUnit { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 上架状态(1:上架 0:下架)
  50 + /// </summary>
  51 + [Display(Name = "上架状态")]
  52 + public int OnShelfStatus { get; set; } = 1;
  53 +
  54 + /// <summary>
  55 + /// 统计分类
  56 + /// </summary>
  57 + [StringLength(50, ErrorMessage = "统计分类长度不能超过50个字符")]
  58 + [Display(Name = "统计分类")]
  59 + public string StatisticsCategory { get; set; }
  60 +
  61 + /// <summary>
  62 + /// 归属仓库
  63 + /// </summary>
  64 + [StringLength(100, ErrorMessage = "归属仓库长度不能超过100个字符")]
  65 + [Display(Name = "归属仓库")]
  66 + public string Warehouse { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 单位换算
  70 + /// </summary>
  71 + [StringLength(200, ErrorMessage = "单位换算长度不能超过200个字符")]
  72 + [Display(Name = "单位换算")]
  73 + public string UnitConversion { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 供应商名称
  77 + /// </summary>
  78 + [StringLength(200, ErrorMessage = "供应商名称长度不能超过200个字符")]
  79 + [Display(Name = "供应商名称")]
  80 + public string SupplierName { get; set; }
  81 +
  82 + /// <summary>
  83 + /// 合同签订日期
  84 + /// </summary>
  85 + [Display(Name = "合同签订日期")]
  86 + public DateTime? ContractSignDate { get; set; }
  87 +
  88 + /// <summary>
  89 + /// 合同结束日期
  90 + /// </summary>
  91 + [Display(Name = "合同结束日期")]
  92 + public DateTime? ContractEndDate { get; set; }
  93 +
  94 + /// <summary>
  95 + /// 备注
  96 + /// </summary>
  97 + [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
  98 + [Display(Name = "备注")]
  99 + public string Remark { get; set; }
  100 + }
  101 +}
  102 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqProduct
  4 +{
  5 + /// <summary>
  6 + /// 产品详情输出
  7 + /// </summary>
  8 + public class LqProductInfoOutput
  9 + {
  10 + /// <summary>
  11 + /// 产品ID
  12 + /// </summary>
  13 + public string id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 产品名称
  17 + /// </summary>
  18 + public string productName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 价格
  22 + /// </summary>
  23 + public decimal price { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 产品类别
  27 + /// </summary>
  28 + public string productCategory { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 归属部门ID
  32 + /// </summary>
  33 + public string departmentId { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 部门名称
  37 + /// </summary>
  38 + public string departmentName { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 标准单位
  42 + /// </summary>
  43 + public string standardUnit { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 上架状态(1:上架 0:下架)
  47 + /// </summary>
  48 + public int onShelfStatus { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 统计分类
  52 + /// </summary>
  53 + public string statisticsCategory { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 归属仓库
  57 + /// </summary>
  58 + public string warehouse { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 单位换算
  62 + /// </summary>
  63 + public string unitConversion { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 供应商名称
  67 + /// </summary>
  68 + public string supplierName { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 合同签订日期
  72 + /// </summary>
  73 + public DateTime? contractSignDate { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 合同结束日期
  77 + /// </summary>
  78 + public DateTime? contractEndDate { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 备注
  82 + /// </summary>
  83 + public string remark { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 创建人ID
  87 + /// </summary>
  88 + public string createUser { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 创建人姓名
  92 + /// </summary>
  93 + public string createUserName { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 创建时间
  97 + /// </summary>
  98 + public DateTime createTime { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 更新人ID
  102 + /// </summary>
  103 + public string updateUser { get; set; }
  104 +
  105 + /// <summary>
  106 + /// 更新人姓名
  107 + /// </summary>
  108 + public string updateUserName { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 更新时间
  112 + /// </summary>
  113 + public DateTime? updateTime { get; set; }
  114 + }
  115 +}
  116 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInventoryDetailOutput.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqProduct
  5 +{
  6 + /// <summary>
  7 + /// 产品库存详情输出
  8 + /// </summary>
  9 + public class LqProductInventoryDetailOutput
  10 + {
  11 + /// <summary>
  12 + /// 产品信息
  13 + /// </summary>
  14 + public LqProductInfoOutput productInfo { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 库存列表
  18 + /// </summary>
  19 + public List<ProductInventoryItem> inventoryList { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 使用记录列表
  23 + /// </summary>
  24 + public List<ProductUsageItem> usageList { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 总库存数量
  28 + /// </summary>
  29 + public int totalInventory { get; set; }
  30 +
  31 + /// <summary>
  32 + /// 已使用数量
  33 + /// </summary>
  34 + public int totalUsage { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 可用库存
  38 + /// </summary>
  39 + public int availableInventory { get; set; }
  40 + }
  41 +
  42 + /// <summary>
  43 + /// 产品库存项
  44 + /// </summary>
  45 + public class ProductInventoryItem
  46 + {
  47 + /// <summary>
  48 + /// 库存ID
  49 + /// </summary>
  50 + public string id { get; set; }
  51 +
  52 + /// <summary>
  53 + /// 库存数量
  54 + /// </summary>
  55 + public int quantity { get; set; }
  56 +
  57 + /// <summary>
  58 + /// 入库时间
  59 + /// </summary>
  60 + public DateTime? stockInTime { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 生产日期
  64 + /// </summary>
  65 + public DateTime? productionDate { get; set; }
  66 +
  67 + /// <summary>
  68 + /// 保质期(天数)
  69 + /// </summary>
  70 + public int? shelfLife { get; set; }
  71 +
  72 + /// <summary>
  73 + /// 批次号
  74 + /// </summary>
  75 + public string batchNumber { get; set; }
  76 +
  77 + /// <summary>
  78 + /// 是否有效
  79 + /// </summary>
  80 + public int isEffective { get; set; }
  81 +
  82 + /// <summary>
  83 + /// 创建时间
  84 + /// </summary>
  85 + public DateTime createTime { get; set; }
  86 + }
  87 +
  88 + /// <summary>
  89 + /// 产品使用记录项
  90 + /// </summary>
  91 + public class ProductUsageItem
  92 + {
  93 + /// <summary>
  94 + /// 使用记录ID
  95 + /// </summary>
  96 + public string id { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 门店ID
  100 + /// </summary>
  101 + public string storeId { get; set; }
  102 +
  103 + /// <summary>
  104 + /// 门店名称
  105 + /// </summary>
  106 + public string storeName { get; set; }
  107 +
  108 + /// <summary>
  109 + /// 使用时间
  110 + /// </summary>
  111 + public DateTime usageTime { get; set; }
  112 +
  113 + /// <summary>
  114 + /// 使用数量
  115 + /// </summary>
  116 + public int usageQuantity { get; set; }
  117 +
  118 + /// <summary>
  119 + /// 关联消耗ID
  120 + /// </summary>
  121 + public string relatedConsumeId { get; set; }
  122 +
  123 + /// <summary>
  124 + /// 创建时间
  125 + /// </summary>
  126 + public DateTime createTime { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 是否有效
  130 + /// </summary>
  131 + public int isEffective { get; set; }
  132 + }
  133 +}
  134 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqProduct
  4 +{
  5 + /// <summary>
  6 + /// 产品列表输出
  7 + /// </summary>
  8 + public class LqProductListOutput
  9 + {
  10 + /// <summary>
  11 + /// 产品ID
  12 + /// </summary>
  13 + public string id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 产品名称
  17 + /// </summary>
  18 + public string productName { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 价格
  22 + /// </summary>
  23 + public decimal price { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 产品类别
  27 + /// </summary>
  28 + public string productCategory { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 归属部门ID
  32 + /// </summary>
  33 + public string departmentId { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 部门名称
  37 + /// </summary>
  38 + public string departmentName { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 标准单位
  42 + /// </summary>
  43 + public string standardUnit { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 上架状态(1:上架 0:下架)
  47 + /// </summary>
  48 + public int onShelfStatus { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 统计分类
  52 + /// </summary>
  53 + public string statisticsCategory { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 归属仓库
  57 + /// </summary>
  58 + public string warehouse { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 单位换算
  62 + /// </summary>
  63 + public string unitConversion { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 供应商名称
  67 + /// </summary>
  68 + public string supplierName { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 合同签订日期
  72 + /// </summary>
  73 + public DateTime? contractSignDate { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 合同结束日期
  77 + /// </summary>
  78 + public DateTime? contractEndDate { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 备注
  82 + /// </summary>
  83 + public string remark { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 现有库存数量(所有有效库存的总和)
  87 + /// </summary>
  88 + public int currentInventory { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 创建人ID
  92 + /// </summary>
  93 + public string createUser { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 创建人姓名
  97 + /// </summary>
  98 + public string createUserName { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 创建时间
  102 + /// </summary>
  103 + public DateTime createTime { get; set; }
  104 +
  105 + /// <summary>
  106 + /// 更新人ID
  107 + /// </summary>
  108 + public string updateUser { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 更新人姓名
  112 + /// </summary>
  113 + public string updateUserName { get; set; }
  114 +
  115 + /// <summary>
  116 + /// 更新时间
  117 + /// </summary>
  118 + public DateTime? updateTime { get; set; }
  119 + }
  120 +}
  121 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListQueryInput.cs 0 → 100644
  1 +using NCC.Common.Filter;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqProduct
  4 +{
  5 + /// <summary>
  6 + /// 产品列表查询输入
  7 + /// </summary>
  8 + public class LqProductListQueryInput : PageInputBase
  9 + {
  10 + /// <summary>
  11 + /// 产品名称(模糊查询)
  12 + /// </summary>
  13 + public string ProductName { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 产品类别
  17 + /// </summary>
  18 + public string ProductCategory { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 归属部门ID
  22 + /// </summary>
  23 + public string DepartmentId { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 上架状态(1:上架 0:下架)
  27 + /// </summary>
  28 + public int? OnShelfStatus { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 统计分类
  32 + /// </summary>
  33 + public string StatisticsCategory { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 归属仓库
  37 + /// </summary>
  38 + public string Warehouse { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 供应商名称(模糊查询)
  42 + /// </summary>
  43 + public string SupplierName { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 价格最小值
  47 + /// </summary>
  48 + public decimal? PriceMin { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 价格最大值
  52 + /// </summary>
  53 + public decimal? PriceMax { get; set; }
  54 + }
  55 +}
  56 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductSelectOutput.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.LqProduct
  2 +{
  3 + /// <summary>
  4 + /// 产品下拉选择输出
  5 + /// </summary>
  6 + public class LqProductSelectOutput
  7 + {
  8 + /// <summary>
  9 + /// 产品ID
  10 + /// </summary>
  11 + public string id { get; set; }
  12 +
  13 + /// <summary>
  14 + /// 产品名称
  15 + /// </summary>
  16 + public string productName { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 价格
  20 + /// </summary>
  21 + public decimal price { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 标准单位
  25 + /// </summary>
  26 + public string standardUnit { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 现有库存数量
  30 + /// </summary>
  31 + public int currentInventory { get; set; }
  32 + }
  33 +}
  34 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductToggleShelfInput.cs 0 → 100644
  1 +using System.ComponentModel.DataAnnotations;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqProduct
  4 +{
  5 + /// <summary>
  6 + /// 产品上下架输入
  7 + /// </summary>
  8 + public class LqProductToggleShelfInput
  9 + {
  10 + /// <summary>
  11 + /// 产品ID
  12 + /// </summary>
  13 + [Required(ErrorMessage = "产品ID不能为空")]
  14 + [Display(Name = "产品ID")]
  15 + public string ProductId { get; set; }
  16 +
  17 + /// <summary>
  18 + /// 上架状态(1:上架 0:下架)
  19 + /// </summary>
  20 + [Required(ErrorMessage = "上架状态不能为空")]
  21 + [Range(0, 1, ErrorMessage = "上架状态只能是0(下架)或1(上架)")]
  22 + [Display(Name = "上架状态")]
  23 + public int OnShelfStatus { get; set; }
  24 + }
  25 +}
  26 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductUpInput.cs 0 → 100644
  1 +using System;
  2 +using System.ComponentModel.DataAnnotations;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqProduct
  5 +{
  6 + /// <summary>
  7 + /// 产品更新输入
  8 + /// </summary>
  9 + public class LqProductUpInput
  10 + {
  11 + /// <summary>
  12 + /// 产品ID
  13 + /// </summary>
  14 + [Required(ErrorMessage = "产品ID不能为空")]
  15 + [Display(Name = "产品ID")]
  16 + public string Id { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 产品名称
  20 + /// </summary>
  21 + [Required(ErrorMessage = "产品名称不能为空")]
  22 + [StringLength(200, ErrorMessage = "产品名称长度不能超过200个字符")]
  23 + [Display(Name = "产品名称")]
  24 + public string ProductName { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 价格
  28 + /// </summary>
  29 + [Required(ErrorMessage = "价格不能为空")]
  30 + [Range(0, double.MaxValue, ErrorMessage = "价格不能小于0")]
  31 + [Display(Name = "价格")]
  32 + public decimal Price { get; set; }
  33 +
  34 + /// <summary>
  35 + /// 产品类别
  36 + /// </summary>
  37 + [StringLength(50, ErrorMessage = "产品类别长度不能超过50个字符")]
  38 + [Display(Name = "产品类别")]
  39 + public string ProductCategory { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 归属部门ID
  43 + /// </summary>
  44 + [StringLength(50, ErrorMessage = "归属部门ID长度不能超过50个字符")]
  45 + [Display(Name = "归属部门ID")]
  46 + public string DepartmentId { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 标准单位
  50 + /// </summary>
  51 + [StringLength(20, ErrorMessage = "标准单位长度不能超过20个字符")]
  52 + [Display(Name = "标准单位")]
  53 + public string StandardUnit { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 上架状态(1:上架 0:下架)
  57 + /// </summary>
  58 + [Display(Name = "上架状态")]
  59 + public int OnShelfStatus { get; set; } = 1;
  60 +
  61 + /// <summary>
  62 + /// 统计分类
  63 + /// </summary>
  64 + [StringLength(50, ErrorMessage = "统计分类长度不能超过50个字符")]
  65 + [Display(Name = "统计分类")]
  66 + public string StatisticsCategory { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 归属仓库
  70 + /// </summary>
  71 + [StringLength(100, ErrorMessage = "归属仓库长度不能超过100个字符")]
  72 + [Display(Name = "归属仓库")]
  73 + public string Warehouse { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 单位换算
  77 + /// </summary>
  78 + [StringLength(200, ErrorMessage = "单位换算长度不能超过200个字符")]
  79 + [Display(Name = "单位换算")]
  80 + public string UnitConversion { get; set; }
  81 +
  82 + /// <summary>
  83 + /// 供应商名称
  84 + /// </summary>
  85 + [StringLength(200, ErrorMessage = "供应商名称长度不能超过200个字符")]
  86 + [Display(Name = "供应商名称")]
  87 + public string SupplierName { get; set; }
  88 +
  89 + /// <summary>
  90 + /// 合同签订日期
  91 + /// </summary>
  92 + [Display(Name = "合同签订日期")]
  93 + public DateTime? ContractSignDate { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 合同结束日期
  97 + /// </summary>
  98 + [Display(Name = "合同结束日期")]
  99 + public DateTime? ContractEndDate { get; set; }
  100 +
  101 + /// <summary>
  102 + /// 备注
  103 + /// </summary>
  104 + [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
  105 + [Display(Name = "备注")]
  106 + public string Remark { get; set; }
  107 + }
  108 +}
  109 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory/LqInventoryEntity.cs
... ... @@ -18,40 +18,46 @@ namespace NCC.Extend.Entitys.lq_inventory
18 18 public string Id { get; set; }
19 19  
20 20 /// <summary>
21   - /// 产品名称
  21 + /// 产品ID(关联产品表)
22 22 /// </summary>
23   - [SugarColumn(ColumnName = "F_ProductName")]
24   - public string ProductName { get; set; }
  23 + [SugarColumn(ColumnName = "F_ProductId")]
  24 + public string ProductId { get; set; }
25 25  
26 26 /// <summary>
27   - /// 价格
  27 + /// 库存数量
28 28 /// </summary>
29   - [SugarColumn(ColumnName = "F_Price")]
30   - public decimal Price { get; set; }
  29 + [SugarColumn(ColumnName = "F_Quantity")]
  30 + public int Quantity { get; set; } = 0;
31 31  
32 32 /// <summary>
33   - /// 数量
  33 + /// 入库时间
34 34 /// </summary>
35   - [SugarColumn(ColumnName = "F_Quantity")]
36   - public int Quantity { get; set; } = 0;
  35 + [SugarColumn(ColumnName = "F_StockInTime")]
  36 + public DateTime? StockInTime { get; set; }
37 37  
38 38 /// <summary>
39   - /// 产品类别
  39 + /// 生产日期
40 40 /// </summary>
41   - [SugarColumn(ColumnName = "F_ProductCategory")]
42   - public string ProductCategory { get; set; }
  41 + [SugarColumn(ColumnName = "F_ProductionDate")]
  42 + public DateTime? ProductionDate { get; set; }
43 43  
44 44 /// <summary>
45   - /// 归属部门ID
  45 + /// 保质期(天数)
46 46 /// </summary>
47   - [SugarColumn(ColumnName = "F_DepartmentId")]
48   - public string DepartmentId { get; set; }
  47 + [SugarColumn(ColumnName = "F_ShelfLife")]
  48 + public int? ShelfLife { get; set; }
49 49  
50 50 /// <summary>
51   - /// 标准单位
  51 + /// 批次号
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_BatchNumber")]
  54 + public string BatchNumber { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 是否有效(1:有效 0:无效)
52 58 /// </summary>
53   - [SugarColumn(ColumnName = "F_StandardUnit")]
54   - public string StandardUnit { get; set; }
  59 + [SugarColumn(ColumnName = "F_IsEffective")]
  60 + public int IsEffective { get; set; } = 1;
55 61  
56 62 /// <summary>
57 63 /// 创建人ID
... ... @@ -76,11 +82,5 @@ namespace NCC.Extend.Entitys.lq_inventory
76 82 /// </summary>
77 83 [SugarColumn(ColumnName = "F_UpdateTime")]
78 84 public DateTime? UpdateTime { get; set; }
79   -
80   - /// <summary>
81   - /// 是否有效(1:有效 0:无效)
82   - /// </summary>
83   - [SugarColumn(ColumnName = "F_IsEffective")]
84   - public int IsEffective { get; set; } = 1;
85 85 }
86 86 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs 0 → 100644
  1 +using System;
  2 +using NCC.Common.Const;
  3 +using SqlSugar;
  4 +
  5 +namespace NCC.Extend.Entitys.lq_product
  6 +{
  7 + /// <summary>
  8 + /// 产品表
  9 + /// </summary>
  10 + [SugarTable("lq_product")]
  11 + [Tenant(ClaimConst.TENANT_ID)]
  12 + public class LqProductEntity
  13 + {
  14 + /// <summary>
  15 + /// 产品ID
  16 + /// </summary>
  17 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  18 + public string Id { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 产品名称
  22 + /// </summary>
  23 + [SugarColumn(ColumnName = "F_ProductName")]
  24 + public string ProductName { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 价格
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_Price")]
  30 + public decimal Price { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 产品类别
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_ProductCategory")]
  36 + public string ProductCategory { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 归属部门ID
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_DepartmentId")]
  42 + public string DepartmentId { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 标准单位
  46 + /// </summary>
  47 + [SugarColumn(ColumnName = "F_StandardUnit")]
  48 + public string StandardUnit { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 上架状态(1:上架 0:下架)
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_OnShelfStatus")]
  54 + public int OnShelfStatus { get; set; } = 1;
  55 +
  56 + /// <summary>
  57 + /// 统计分类
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_StatisticsCategory")]
  60 + public string StatisticsCategory { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 归属仓库
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "F_Warehouse")]
  66 + public string Warehouse { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 单位换算
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "F_UnitConversion")]
  72 + public string UnitConversion { get; set; }
  73 +
  74 + /// <summary>
  75 + /// 供应商名称
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_SupplierName")]
  78 + public string SupplierName { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 合同签订日期
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_ContractSignDate")]
  84 + public DateTime? ContractSignDate { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 合同结束日期
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_ContractEndDate")]
  90 + public DateTime? ContractEndDate { get; set; }
  91 +
  92 + /// <summary>
  93 + /// 备注
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_Remark")]
  96 + public string Remark { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 创建人ID
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_CreateUser")]
  102 + public string CreateUser { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 创建时间
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_CreateTime")]
  108 + public DateTime CreateTime { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 更新人ID
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_UpdateUser")]
  114 + public string UpdateUser { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 更新时间
  118 + /// </summary>
  119 + [SugarColumn(ColumnName = "F_UpdateTime")]
  120 + public DateTime? UpdateTime { get; set; }
  121 + }
  122 +}
  123 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Interfaces/LqInventoryUsage/ILqInventoryUsageService.cs
... ... @@ -9,54 +9,6 @@ namespace NCC.Extend.Interfaces.LqInventoryUsage
9 9 /// </summary>
10 10 public interface ILqInventoryUsageService
11 11 {
12   - /// <summary>
13   - /// 添加库存使用记录
14   - /// </summary>
15   - /// <param name="input">创建输入</param>
16   - /// <returns></returns>
17   - Task CreateAsync(LqInventoryUsageCrInput input);
18 12  
19   - /// <summary>
20   - /// 作废库存使用记录
21   - /// </summary>
22   - /// <param name="id">使用记录ID</param>
23   - /// <param name="remarks">作废备注</param>
24   - /// <returns></returns>
25   - Task CancelAsync(string id, string remarks = null);
26   -
27   - /// <summary>
28   - /// 获取使用记录列表
29   - /// </summary>
30   - /// <param name="input">查询输入</param>
31   - /// <returns></returns>
32   - Task<dynamic> GetListAsync(LqInventoryUsageListQueryInput input);
33   -
34   - /// <summary>
35   - /// 统计时间周期内每个产品的使用数量
36   - /// </summary>
37   - /// <param name="input">统计查询输入</param>
38   - /// <returns></returns>
39   - Task<dynamic> GetProductUsageStatisticsAsync(LqInventoryUsageStatisticsInput input);
40   -
41   - /// <summary>
42   - /// 统计时间周期内每个门店的使用情况
43   - /// </summary>
44   - /// <param name="input">统计查询输入</param>
45   - /// <returns></returns>
46   - Task<dynamic> GetStoreUsageStatisticsAsync(LqInventoryUsageStatisticsInput input);
47   -
48   - /// <summary>
49   - /// 统计时间周期内使用趋势
50   - /// </summary>
51   - /// <param name="input">统计查询输入</param>
52   - /// <returns></returns>
53   - Task<dynamic> GetUsageTrendStatisticsAsync(LqInventoryUsageStatisticsInput input);
54   -
55   - /// <summary>
56   - /// 统计产品使用排行榜
57   - /// </summary>
58   - /// <param name="input">统计查询输入</param>
59   - /// <returns></returns>
60   - Task<dynamic> GetProductUsageRankingAsync(LqInventoryUsageStatisticsInput input);
61 13 }
62 14 }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Interfaces/LqProduct/ILqProductService.cs 0 → 100644
  1 +using System.Threading.Tasks;
  2 +using NCC.Extend.Entitys.Dto.LqProduct;
  3 +using NCC.Common.Filter;
  4 +
  5 +namespace NCC.Extend.Interfaces.LqProduct
  6 +{
  7 + /// <summary>
  8 + /// 产品服务接口
  9 + /// </summary>
  10 + public interface ILqProductService
  11 + {
  12 + /// <summary>
  13 + /// 添加产品信息
  14 + /// </summary>
  15 + /// <param name="input">创建输入</param>
  16 + /// <returns></returns>
  17 + Task CreateAsync(LqProductCrInput input);
  18 +
  19 + /// <summary>
  20 + /// 更新产品信息
  21 + /// </summary>
  22 + /// <param name="input">更新输入</param>
  23 + /// <returns></returns>
  24 + Task UpdateAsync(LqProductUpInput input);
  25 +
  26 + /// <summary>
  27 + /// 获取产品列表(包含现有库存)
  28 + /// </summary>
  29 + /// <param name="input">查询输入</param>
  30 + /// <returns></returns>
  31 + Task<dynamic> GetListAsync(LqProductListQueryInput input);
  32 +
  33 + /// <summary>
  34 + /// 获取产品详情
  35 + /// </summary>
  36 + /// <param name="id">产品ID</param>
  37 + /// <returns></returns>
  38 + Task<dynamic> GetInfoAsync(string id);
  39 +
  40 + /// <summary>
  41 + /// 获取产品下拉列表
  42 + /// </summary>
  43 + /// <param name="productName">产品名称(可选,模糊查询)</param>
  44 + /// <param name="onShelfStatus">上架状态(可选,1:上架 0:下架)</param>
  45 + /// <returns></returns>
  46 + Task<dynamic> GetSelectListAsync(string productName = null, int? onShelfStatus = 1);
  47 +
  48 + /// <summary>
  49 + /// 作废产品
  50 + /// </summary>
  51 + /// <param name="id">产品ID</param>
  52 + /// <param name="remarks">作废备注</param>
  53 + /// <returns></returns>
  54 + Task CancelAsync(string id, string remarks = null);
  55 +
  56 + /// <summary>
  57 + /// 产品上下架
  58 + /// </summary>
  59 + /// <param name="input">上下架输入</param>
  60 + /// <returns></returns>
  61 + Task ToggleShelfAsync(LqProductToggleShelfInput input);
  62 + }
  63 +}
  64 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -129,7 +129,7 @@ namespace NCC.Extend
129 129 }
130 130  
131 131 // SQL查询:获取门店每日运营数据(只统计zxzt为"开店"的门店)
132   - // 优化:使用 LEFT JOIN 一次性统计所有项目数,避免多个子查询
  132 + // 注意:避免笛卡尔积问题,消耗业绩和项目数分别统计,避免重复计算
133 133 var sql = $@"
134 134 SELECT
135 135 consume.Md as StoreId,
... ... @@ -138,23 +138,40 @@ namespace NCC.Extend
138 138 COALESCE(COUNT(DISTINCT consume.Hy), 0) as HeadCount,
139 139 -- 人次(日度去重客户数)
140 140 COALESCE(COUNT(DISTINCT CONCAT(consume.Hy, '-', DATE_FORMAT(consume.Hksj, '%Y-%m-%d'))), 0) as PersonCount,
141   - -- 项目数(消耗的项目总数,从品项明细表统计)
142   - COALESCE(SUM(project.F_ProjectNumber), 0) as ProjectCount,
143   - -- 消耗总项目数(健康师业绩表统计,包含原始+加班+陪同)
144   - COALESCE(SUM(COALESCE(jksyj.F_kdpxNumber, 0)), 0) as TotalProjectCount,
145   - -- 消耗原始项目数(健康师业绩表统计)
146   - COALESCE(SUM(COALESCE(jksyj.F_OriginalKdpxNumber, jksyj.F_kdpxNumber, 0)), 0) as OriginalProjectCount,
147   - -- 消耗加班项目数(健康师业绩表统计)
148   - COALESCE(SUM(COALESCE(jksyj.F_OvertimeKdpxNumber, 0)), 0) as OvertimeProjectCount,
149   - -- 消耗陪同项目数(健康师业绩表统计)
150   - COALESCE(SUM(COALESCE(jksyj.F_AccompaniedProjectNumber, 0)), 0) as AccompaniedProjectCount,
151   - -- 消耗业绩(总金额)
152   - COALESCE(SUM(project.F_TotalPrice), 0) as ConsumePerformance
  141 + -- 项目数(消耗的项目总数,从品项明细表统计原始项目数,与健康师业绩表的原始项目数保持一致)
  142 + COALESCE(SUM((
  143 + SELECT COALESCE(SUM(COALESCE(px.F_OriginalProjectNumber, px.F_ProjectNumber, 0)), 0)
  144 + FROM lq_xh_pxmx px
  145 + WHERE px.F_ConsumeInfoId = consume.F_Id AND px.F_IsEffective = 1
  146 + )), 0) as ProjectCount,
  147 + -- 消耗总项目数(健康师业绩表统计,包含原始+加班+陪同,使用SUM聚合子查询结果)
  148 + COALESCE(SUM((
  149 + SELECT COALESCE(SUM(COALESCE(j.F_kdpxNumber, 0)), 0)
  150 + FROM lq_xh_jksyj j
  151 + WHERE j.glkdbh = consume.F_Id AND j.F_IsEffective = 1
  152 + )), 0) as TotalProjectCount,
  153 + -- 消耗原始项目数(健康师业绩表统计,使用SUM聚合子查询结果)
  154 + COALESCE(SUM((
  155 + SELECT COALESCE(SUM(COALESCE(j.F_OriginalKdpxNumber, j.F_kdpxNumber, 0)), 0)
  156 + FROM lq_xh_jksyj j
  157 + WHERE j.glkdbh = consume.F_Id AND j.F_IsEffective = 1
  158 + )), 0) as OriginalProjectCount,
  159 + -- 消耗加班项目数(健康师业绩表统计,使用SUM聚合子查询结果)
  160 + COALESCE(SUM((
  161 + SELECT COALESCE(SUM(COALESCE(j.F_OvertimeKdpxNumber, 0)), 0)
  162 + FROM lq_xh_jksyj j
  163 + WHERE j.glkdbh = consume.F_Id AND j.F_IsEffective = 1
  164 + )), 0) as OvertimeProjectCount,
  165 + -- 消耗陪同项目数(健康师业绩表统计,使用SUM聚合子查询结果)
  166 + COALESCE(SUM((
  167 + SELECT COALESCE(SUM(COALESCE(j.F_AccompaniedProjectNumber, 0)), 0)
  168 + FROM lq_xh_jksyj j
  169 + WHERE j.glkdbh = consume.F_Id AND j.F_IsEffective = 1
  170 + )), 0) as AccompaniedProjectCount,
  171 + -- 消耗业绩(总金额,直接从主表xfje字段统计,避免子查询和JOIN导致的性能问题)
  172 + COALESCE(SUM(consume.xfje), 0) as ConsumePerformance
153 173 FROM lq_xh_hyhk consume
154 174 INNER JOIN lq_mdxx store ON consume.Md = store.F_Id AND store.zxzt = '开店'
155   - LEFT JOIN lq_xh_pxmx project ON consume.F_Id = project.F_ConsumeInfoId AND project.F_IsEffective = 1
156   - LEFT JOIN lq_xh_jksyj jksyj ON jksyj.glkdbh = consume.F_Id
157   - AND jksyj.F_IsEffective = 1
158 175 WHERE consume.F_IsEffective = 1
159 176 AND consume.Hksj >= '{startDate:yyyy-MM-dd} 00:00:00'
160 177 AND consume.Hksj < '{endDate.AddDays(1):yyyy-MM-dd} 00:00:00'
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
1 1 using System;
  2 +using System.Linq;
2 3 using System.Threading.Tasks;
3 4 using Microsoft.AspNetCore.Mvc;
4 5 using Microsoft.Extensions.Logging;
... ... @@ -10,6 +11,8 @@ using NCC.DynamicApiController;
10 11 using NCC.Extend.Entitys.Dto.LqInventory;
11 12 using NCC.Extend.Entitys.Enum;
12 13 using NCC.Extend.Entitys.lq_inventory;
  14 +using NCC.Extend.Entitys.lq_inventory_usage;
  15 +using NCC.Extend.Entitys.lq_product;
13 16 using NCC.Extend.Interfaces.LqInventory;
14 17 using NCC.FriendlyException;
15 18 using NCC.System.Entitys.Permission;
... ... @@ -42,39 +45,58 @@ namespace NCC.Extend
42 45 _db = db;
43 46 }
44 47  
45   - #region 创建库存
  48 + #region 添加库存信息
46 49 /// <summary>
47   - /// 创建库存
  50 + /// 添加库存信息
48 51 /// </summary>
  52 + /// <remarks>
  53 + /// 针对某个产品添加库存记录
  54 + ///
  55 + /// 示例请求:
  56 + /// ```json
  57 + /// {
  58 + /// "productId": "产品ID",
  59 + /// "quantity": 100,
  60 + /// "stockInTime": "2024-01-01",
  61 + /// "productionDate": "2023-12-01",
  62 + /// "shelfLife": 365,
  63 + /// "batchNumber": "批次号001"
  64 + /// }
  65 + /// ```
  66 + /// </remarks>
49 67 /// <param name="input">创建输入</param>
50 68 /// <returns>创建结果</returns>
  69 + /// <response code="200">创建成功</response>
  70 + /// <response code="400">产品不存在或参数错误</response>
  71 + /// <response code="500">服务器错误</response>
51 72 [HttpPost("Create")]
52 73 public async Task CreateAsync([FromBody] LqInventoryCrInput input)
53 74 {
54 75 try
55 76 {
56   - // 检查产品名称是否已存在
57   - var existingProduct = await _db.Queryable<LqInventoryEntity>()
58   - .Where(x => x.ProductName == input.ProductName && x.IsEffective == StatusEnum.有效.GetHashCode())
  77 + // 验证产品是否存在
  78 + var product = await _db.Queryable<LqProductEntity>()
  79 + .Where(x => x.Id == input.ProductId)
59 80 .FirstAsync();
60   - if (existingProduct != null)
  81 +
  82 + if (product == null)
61 83 {
62   - throw NCCException.Oh($"产品名称'{input.ProductName}'已存在");
  84 + throw NCCException.Oh("产品不存在");
63 85 }
64 86  
65 87 // 创建库存记录
66 88 var inventoryEntity = new LqInventoryEntity
67 89 {
68 90 Id = YitIdHelper.NextId().ToString(),
69   - ProductName = input.ProductName,
70   - Price = input.Price,
  91 + ProductId = input.ProductId,
71 92 Quantity = input.Quantity,
72   - ProductCategory = input.ProductCategory,
73   - DepartmentId = input.DepartmentId,
74   - StandardUnit = input.StandardUnit,
  93 + StockInTime = input.StockInTime ?? DateTime.Now,
  94 + ProductionDate = input.ProductionDate,
  95 + ShelfLife = input.ShelfLife,
  96 + BatchNumber = input.BatchNumber,
  97 + IsEffective = StatusEnum.有效.GetHashCode(),
75 98 CreateUser = _userManager.UserId,
76   - CreateTime = DateTime.Now,
77   - IsEffective = StatusEnum.有效.GetHashCode()
  99 + CreateTime = DateTime.Now
78 100 };
79 101  
80 102 var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
... ... @@ -88,9 +110,9 @@ namespace NCC.Extend
88 110 }
89 111 #endregion
90 112  
91   - #region 更新库存
  113 + #region 更新库存信息
92 114 /// <summary>
93   - /// 更新库存
  115 + /// 更新库存信息
94 116 /// </summary>
95 117 /// <param name="input">更新输入</param>
96 118 /// <returns>更新结果</returns>
... ... @@ -99,7 +121,6 @@ namespace NCC.Extend
99 121 {
100 122 try
101 123 {
102   - // 检查库存记录是否存在
103 124 var existingInventory = await _db.Queryable<LqInventoryEntity>()
104 125 .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
105 126 .FirstAsync();
... ... @@ -109,23 +130,23 @@ namespace NCC.Extend
109 130 throw NCCException.Oh("库存记录不存在或已失效");
110 131 }
111 132  
112   - // 检查产品名称是否与其他记录重复
113   - var duplicateProduct = await _db.Queryable<LqInventoryEntity>()
114   - .Where(x => x.ProductName == input.ProductName && x.Id != input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  133 + // 验证产品是否存在
  134 + var product = await _db.Queryable<LqProductEntity>()
  135 + .Where(x => x.Id == input.ProductId)
115 136 .FirstAsync();
116 137  
117   - if (duplicateProduct != null)
  138 + if (product == null)
118 139 {
119   - throw NCCException.Oh($"产品名称'{input.ProductName}'已存在");
  140 + throw NCCException.Oh("产品不存在");
120 141 }
121 142  
122 143 // 更新库存记录
123   - existingInventory.ProductName = input.ProductName;
124   - existingInventory.Price = input.Price;
  144 + existingInventory.ProductId = input.ProductId;
125 145 existingInventory.Quantity = input.Quantity;
126   - existingInventory.ProductCategory = input.ProductCategory;
127   - existingInventory.DepartmentId = input.DepartmentId;
128   - existingInventory.StandardUnit = input.StandardUnit;
  146 + existingInventory.StockInTime = input.StockInTime;
  147 + existingInventory.ProductionDate = input.ProductionDate;
  148 + existingInventory.ShelfLife = input.ShelfLife;
  149 + existingInventory.BatchNumber = input.BatchNumber;
129 150 existingInventory.UpdateUser = _userManager.UserId;
130 151 existingInventory.UpdateTime = DateTime.Now;
131 152  
... ... @@ -144,6 +165,9 @@ namespace NCC.Extend
144 165 /// <summary>
145 166 /// 获取库存列表
146 167 /// </summary>
  168 + /// <remarks>
  169 + /// 返回库存列表,包含产品信息、已使用数量、可用数量等
  170 + /// </remarks>
147 171 /// <param name="input">查询输入</param>
148 172 /// <returns>库存列表</returns>
149 173 [HttpGet("GetList")]
... ... @@ -151,60 +175,98 @@ namespace NCC.Extend
151 175 {
152 176 try
153 177 {
154   - var sidx = input.sidx == null ? "id" : input.sidx;
155   -
156   - // 查询库存信息
157   - var data = await _db.Queryable<LqInventoryEntity>()
158   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductName), x => x.ProductName.Contains(input.ProductName))
159   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), x => x.ProductCategory.Contains(input.ProductCategory))
160   - .WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentId), x => x.DepartmentId == input.DepartmentId)
161   - .WhereIF(input.PriceMin.HasValue, x => x.Price >= input.PriceMin.Value)
162   - .WhereIF(input.PriceMax.HasValue, x => x.Price <= input.PriceMax.Value)
163   - .WhereIF(input.QuantityMin.HasValue, x => x.Quantity >= input.QuantityMin.Value)
164   - .WhereIF(input.QuantityMax.HasValue, x => x.Quantity <= input.QuantityMax.Value)
165   - .WhereIF(input.IsEffective.HasValue, x => x.IsEffective == input.IsEffective.Value)
166   - .Select(x => new LqInventoryListOutput
  178 + var sidx = string.IsNullOrEmpty(input.sidx) ? "createTime" : input.sidx;
  179 + var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
  180 +
  181 + // 查询库存信息,关联产品表
  182 + var data = await _db.Queryable<LqInventoryEntity, LqProductEntity>(
  183 + (inv, prod) => inv.ProductId == prod.Id)
  184 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (inv, prod) => inv.ProductId == input.ProductId)
  185 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductName), (inv, prod) => prod.ProductName.Contains(input.ProductName))
  186 + .WhereIF(!string.IsNullOrWhiteSpace(input.BatchNumber), (inv, prod) => inv.BatchNumber != null && inv.BatchNumber.Contains(input.BatchNumber))
  187 + .WhereIF(input.QuantityMin.HasValue, (inv, prod) => inv.Quantity >= input.QuantityMin.Value)
  188 + .WhereIF(input.QuantityMax.HasValue, (inv, prod) => inv.Quantity <= input.QuantityMax.Value)
  189 + .WhereIF(input.IsEffective.HasValue, (inv, prod) => inv.IsEffective == input.IsEffective.Value)
  190 + .Select((inv, prod) => new LqInventoryListOutput
167 191 {
168   - id = x.Id,
169   - productName = x.ProductName,
170   - price = x.Price,
171   - quantity = x.Quantity,
172   - productCategory = x.ProductCategory,
173   - departmentId = x.DepartmentId,
174   - departmentName = "",
175   - standardUnit = x.StandardUnit,
176   - totalValue = x.Price * x.Quantity,
177   - createUser = x.CreateUser,
  192 + id = inv.Id,
  193 + productId = inv.ProductId,
  194 + productName = prod.ProductName,
  195 + productPrice = prod.Price,
  196 + quantity = inv.Quantity,
  197 + usedQuantity = 0, // 稍后计算
  198 + availableQuantity = inv.Quantity, // 稍后计算
  199 + stockInTime = inv.StockInTime,
  200 + productionDate = inv.ProductionDate,
  201 + shelfLife = inv.ShelfLife,
  202 + batchNumber = inv.BatchNumber,
  203 + createUser = inv.CreateUser,
178 204 createUserName = "",
179   - createTime = x.CreateTime,
180   - updateUser = x.UpdateUser,
  205 + createTime = inv.CreateTime,
  206 + updateUser = inv.UpdateUser,
181 207 updateUserName = "",
182   - updateTime = x.UpdateTime,
183   - isEffective = x.IsEffective
  208 + updateTime = inv.UpdateTime,
  209 + isEffective = inv.IsEffective
184 210 })
185 211 .MergeTable()
186   - .OrderBy(sidx + " " + input.sort)
  212 + .OrderBy(sidx + " " + sort)
187 213 .ToPagedListAsync(input.currentPage, input.pageSize);
188 214  
189   - // 补充用户名称信息
190   - foreach (var item in data.list)
  215 + // 批量查询每个库存的已使用数量
  216 + var inventoryIds = data.list.Select(x => x.id).ToList();
  217 + if (inventoryIds.Any())
191 218 {
192   - if (!string.IsNullOrEmpty(item.departmentId))
193   - {
194   - var deptUser = await _db.Queryable<UserEntity>().Where(u => u.Id == item.departmentId).FirstAsync();
195   - item.departmentName = deptUser?.RealName ?? "";
196   - }
197   - if (!string.IsNullOrEmpty(item.createUser))
  219 + // 注意:库存使用记录表中存储的是产品ID,不是库存ID
  220 + // 这里需要根据业务逻辑调整,如果使用记录需要关联到具体库存批次,需要修改使用记录表结构
  221 + // 暂时按产品ID统计已使用数量
  222 + var productIds = data.list.Select(x => x.productId).Distinct().ToList();
  223 + var usageDict = await _db.Queryable<LqInventoryUsageEntity>()
  224 + .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
  225 + .GroupBy(x => x.ProductId)
  226 + .Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum((decimal?)x.UsageQuantity) })
  227 + .ToListAsync();
  228 +
  229 + var usageDictMap = usageDict.ToDictionary(k => k.ProductId, v => v.TotalUsage.HasValue ? (int)v.TotalUsage.Value : 0);
  230 +
  231 + // 计算每个库存的已使用数量和可用数量
  232 + // 注意:这里假设同一产品的所有库存共享使用记录,如果需要按批次区分,需要修改使用记录表
  233 + foreach (var item in data.list)
198 234 {
199   - var createUser = await _db.Queryable<UserEntity>().Where(u => u.Id == item.createUser).FirstAsync();
200   - item.createUserName = createUser?.RealName ?? "";
  235 + var totalUsage = usageDictMap.ContainsKey(item.productId) ? usageDictMap[item.productId] : 0;
  236 + // 按比例分配已使用数量(简化处理,实际可能需要更复杂的逻辑)
  237 + var productInventoryList = data.list.Where(x => x.productId == item.productId).ToList();
  238 + var totalProductQuantity = productInventoryList.Sum(x => x.quantity);
  239 + if (totalProductQuantity > 0)
  240 + {
  241 + item.usedQuantity = (int)(item.quantity * 1.0m / totalProductQuantity * totalUsage);
  242 + }
  243 + item.availableQuantity = item.quantity - item.usedQuantity;
201 244 }
202   - if (!string.IsNullOrEmpty(item.updateUser))
  245 + }
  246 +
  247 + // 补充用户名称信息
  248 + var userIds = data.list.SelectMany(x => new[] { x.createUser, x.updateUser })
  249 + .Where(x => !string.IsNullOrEmpty(x))
  250 + .Distinct()
  251 + .ToList();
  252 +
  253 + if (userIds.Any())
  254 + {
  255 + var userList = await _db.Queryable<UserEntity>()
  256 + .Where(x => userIds.Contains(x.Id))
  257 + .Select(x => new { x.Id, x.RealName })
  258 + .ToListAsync();
  259 + var userDict = userList.ToDictionary(k => k.Id, v => v.RealName);
  260 +
  261 + foreach (var item in data.list)
203 262 {
204   - var updateUser = await _db.Queryable<UserEntity>().Where(u => u.Id == item.updateUser).FirstAsync();
205   - item.updateUserName = updateUser?.RealName ?? "";
  263 + if (!string.IsNullOrEmpty(item.createUser) && userDict.ContainsKey(item.createUser))
  264 + item.createUserName = userDict[item.createUser];
  265 + if (!string.IsNullOrEmpty(item.updateUser) && userDict.ContainsKey(item.updateUser))
  266 + item.updateUserName = userDict[item.updateUser];
206 267 }
207 268 }
  269 +
208 270 return PageResult<LqInventoryListOutput>.SqlSugarPageResult(data);
209 271 }
210 272 catch (Exception ex)
... ... @@ -230,52 +292,75 @@ namespace NCC.Extend
230 292 {
231 293 throw NCCException.Oh("库存ID不能为空");
232 294 }
233   - // 查询库存信息
234   - var inventory = await _db.Queryable<LqInventoryEntity>().Where(x => x.Id == id).FirstAsync();
  295 +
  296 + var inventory = await _db.Queryable<LqInventoryEntity, LqProductEntity>(
  297 + (inv, prod) => inv.ProductId == prod.Id)
  298 + .Where((inv, prod) => inv.Id == id)
  299 + .Select((inv, prod) => new LqInventoryInfoOutput
  300 + {
  301 + id = inv.Id,
  302 + productId = inv.ProductId,
  303 + productName = prod.ProductName,
  304 + productPrice = prod.Price,
  305 + quantity = inv.Quantity,
  306 + usedQuantity = 0, // 稍后计算
  307 + availableQuantity = inv.Quantity, // 稍后计算
  308 + stockInTime = inv.StockInTime,
  309 + productionDate = inv.ProductionDate,
  310 + shelfLife = inv.ShelfLife,
  311 + batchNumber = inv.BatchNumber,
  312 + createUser = inv.CreateUser,
  313 + createUserName = "",
  314 + createTime = inv.CreateTime,
  315 + updateUser = inv.UpdateUser,
  316 + updateUserName = "",
  317 + updateTime = inv.UpdateTime,
  318 + isEffective = inv.IsEffective
  319 + })
  320 + .FirstAsync();
235 321  
236 322 if (inventory == null)
237 323 {
238 324 throw NCCException.Oh("库存记录不存在");
239 325 }
240 326  
241   - var result = new LqInventoryInfoOutput
242   - {
243   - id = inventory.Id,
244   - productName = inventory.ProductName,
245   - price = inventory.Price,
246   - quantity = inventory.Quantity,
247   - productCategory = inventory.ProductCategory,
248   - departmentId = inventory.DepartmentId,
249   - departmentName = "",
250   - standardUnit = inventory.StandardUnit,
251   - totalValue = inventory.Price * inventory.Quantity,
252   - createUser = inventory.CreateUser,
253   - createUserName = "",
254   - createTime = inventory.CreateTime,
255   - updateUser = inventory.UpdateUser,
256   - updateUserName = "",
257   - updateTime = inventory.UpdateTime,
258   - isEffective = inventory.IsEffective
259   - };
  327 + // 计算已使用数量
  328 + var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
  329 + .Where(x => x.ProductId == inventory.productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  330 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
260 331  
261   - // 补充用户名称信息
262   - if (!string.IsNullOrEmpty(result.departmentId))
263   - {
264   - var deptUser = await _db.Queryable<UserEntity>().Where(u => u.Id == result.departmentId).FirstAsync();
265   - result.departmentName = deptUser?.RealName ?? "";
266   - }
267   - if (!string.IsNullOrEmpty(result.createUser))
  332 + // 计算该产品的总库存
  333 + var totalInventory = await _db.Queryable<LqInventoryEntity>()
  334 + .Where(x => x.ProductId == inventory.productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  335 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  336 +
  337 + if (totalInventory > 0)
268 338 {
269   - var createUser = await _db.Queryable<UserEntity>().Where(u => u.Id == result.createUser).FirstAsync();
270   - result.createUserName = createUser?.RealName ?? "";
  339 + inventory.usedQuantity = (int)(inventory.quantity * 1.0m / totalInventory * totalUsage);
271 340 }
272   - if (!string.IsNullOrEmpty(result.updateUser))
  341 + inventory.availableQuantity = inventory.quantity - inventory.usedQuantity;
  342 +
  343 + // 补充用户名称信息
  344 + var userIds = new[] { inventory.createUser, inventory.updateUser }
  345 + .Where(x => !string.IsNullOrEmpty(x))
  346 + .Distinct()
  347 + .ToList();
  348 +
  349 + if (userIds.Any())
273 350 {
274   - var updateUser = await _db.Queryable<UserEntity>().Where(u => u.Id == result.updateUser).FirstAsync();
275   - result.updateUserName = updateUser?.RealName ?? "";
  351 + var userList = await _db.Queryable<UserEntity>()
  352 + .Where(x => userIds.Contains(x.Id))
  353 + .Select(x => new { x.Id, x.RealName })
  354 + .ToListAsync();
  355 + var userDict = userList.ToDictionary(k => k.Id, v => v.RealName);
  356 +
  357 + if (!string.IsNullOrEmpty(inventory.createUser) && userDict.ContainsKey(inventory.createUser))
  358 + inventory.createUserName = userDict[inventory.createUser];
  359 + if (!string.IsNullOrEmpty(inventory.updateUser) && userDict.ContainsKey(inventory.updateUser))
  360 + inventory.updateUserName = userDict[inventory.updateUser];
276 361 }
277 362  
278   - return result;
  363 + return inventory;
279 364 }
280 365 catch (Exception ex)
281 366 {
... ... @@ -285,6 +370,77 @@ namespace NCC.Extend
285 370 }
286 371 #endregion
287 372  
  373 + #region 作废库存
  374 + /// <summary>
  375 + /// 作废库存
  376 + /// </summary>
  377 + /// <remarks>
  378 + /// 作废库存记录,需要判断已使用数量是否超过作废后的总数量
  379 + /// 如果已使用数量 > 作废后的总数量,则不允许作废
  380 + ///
  381 + /// 示例请求:
  382 + /// ```
  383 + /// PUT /api/Extend/LqInventory/Cancel/{id}?remarks=作废备注
  384 + /// ```
  385 + /// </remarks>
  386 + /// <param name="id">库存ID</param>
  387 + /// <param name="remarks">作废备注</param>
  388 + /// <returns>作废结果</returns>
  389 + /// <response code="200">作废成功</response>
  390 + /// <response code="400">库存不存在或已使用数量超过作废后的总数量</response>
  391 + /// <response code="500">服务器错误</response>
  392 + [HttpPut("Cancel/{id}")]
  393 + public async Task CancelAsync([FromRoute] string id, [FromQuery] string remarks = null)
  394 + {
  395 + try
  396 + {
  397 + if (string.IsNullOrWhiteSpace(id))
  398 + {
  399 + throw NCCException.Oh("库存ID不能为空");
  400 + }
  401 +
  402 + var inventory = await _db.Queryable<LqInventoryEntity>()
  403 + .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  404 + .FirstAsync();
  405 +
  406 + if (inventory == null)
  407 + {
  408 + throw NCCException.Oh("库存记录不存在或已失效");
  409 + }
  410 +
  411 + // 计算该产品的总库存(包括当前库存)
  412 + var totalInventory = await _db.Queryable<LqInventoryEntity>()
  413 + .Where(x => x.ProductId == inventory.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
  414 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  415 +
  416 + // 计算该产品的已使用数量
  417 + var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
  418 + .Where(x => x.ProductId == inventory.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
  419 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  420 +
  421 + // 作废后的总数量 = 当前总库存 - 当前库存数量
  422 + var inventoryAfterCancel = totalInventory - inventory.Quantity;
  423 +
  424 + // 判断:如果已使用数量 > 作废后的总数量,则不允许作废
  425 + if (totalUsage > inventoryAfterCancel)
  426 + {
  427 + throw NCCException.Oh($"不允许作废:已使用数量({totalUsage})超过作废后的总数量({inventoryAfterCancel})");
  428 + }
  429 +
  430 + // 作废库存
  431 + inventory.IsEffective = StatusEnum.无效.GetHashCode();
  432 + inventory.UpdateUser = _userManager.UserId;
  433 + inventory.UpdateTime = DateTime.Now;
288 434  
  435 + var isOk = await _db.Updateable(inventory).ExecuteCommandAsync();
  436 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  437 + }
  438 + catch (Exception ex)
  439 + {
  440 + _logger.LogError(ex, "作废库存失败");
  441 + throw NCCException.Oh($"作废失败:{ex.Message}");
  442 + }
  443 + }
  444 + #endregion
289 445 }
290 446 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -11,6 +11,7 @@ using NCC.DynamicApiController;
11 11 using NCC.Extend.Entitys.Dto.LqInventoryUsage;
12 12 using NCC.Extend.Entitys.Enum;
13 13 using NCC.Extend.Entitys.lq_inventory;
  14 +using NCC.Extend.Entitys.lq_product;
14 15 using NCC.Extend.Entitys.lq_inventory_usage;
15 16 using NCC.Extend.Entitys.lq_mdxx;
16 17 using NCC.Extend.Interfaces.LqInventoryUsage;
... ... @@ -57,17 +58,32 @@ namespace NCC.Extend
57 58 try
58 59 {
59 60 // 验证产品是否存在
60   - var product = await _db.Queryable<LqInventoryEntity>().Where(x => x.Id == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync();
  61 + var product = await _db.Queryable<LqProductEntity>()
  62 + .Where(x => x.Id == input.ProductId)
  63 + .FirstAsync();
61 64  
62 65 if (product == null)
63 66 {
64   - throw NCCException.Oh("产品不存在或已失效");
  67 + throw NCCException.Oh("产品不存在");
65 68 }
66 69  
  70 + // 计算该产品的总库存数量(所有有效库存的总和)
  71 + var totalInventory = await _db.Queryable<LqInventoryEntity>()
  72 + .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
  73 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  74 +
  75 + // 计算该产品的已使用数量
  76 + var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
  77 + .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
  78 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  79 +
  80 + // 计算可用库存
  81 + var availableInventory = totalInventory - totalUsage;
  82 +
67 83 // 检查库存数量是否足够
68   - if (product.Quantity < input.UsageQuantity)
  84 + if (availableInventory < input.UsageQuantity)
69 85 {
70   - throw NCCException.Oh($"库存不足,当前库存:{product.Quantity},需要数量:{input.UsageQuantity}");
  86 + throw NCCException.Oh($"库存不足,当前可用库存:{availableInventory},需要数量:{input.UsageQuantity}");
71 87 }
72 88  
73 89 _db.Ado.BeginTran();
... ... @@ -89,12 +105,7 @@ namespace NCC.Extend
89 105 var isOk = await _db.Insertable(usageEntity).ExecuteCommandAsync();
90 106 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
91 107  
92   - // 更新库存数量
93   - product.Quantity -= input.UsageQuantity;
94   - product.UpdateUser = _userManager.UserId;
95   - product.UpdateTime = DateTime.Now;
96   - var updateOk = await _db.Updateable(product).ExecuteCommandAsync();
97   - if (!(updateOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
  108 + // 注意:新结构下不需要更新库存表的数量,因为库存数量是固定的,使用记录只是记录使用情况
98 109 _db.Ado.CommitTran();
99 110 }
100 111 catch (Exception ex)
... ... @@ -140,17 +151,8 @@ namespace NCC.Extend
140 151  
141 152 var isOk = await _db.Updateable(usageRecord).ExecuteCommandAsync();
142 153 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1003);
143   - // 恢复库存数量
144   - var product = await _db.Queryable<LqInventoryEntity>().Where(x => x.Id == usageRecord.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode()).FirstAsync();
145   - if (product != null)
146   - {
147   - product.Quantity += usageRecord.UsageQuantity;
148   - product.UpdateUser = _userManager.UserId;
149   - product.UpdateTime = DateTime.Now;
150   - var updateOk = await _db.Updateable(product).ExecuteCommandAsync();
151   - if (!(updateOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
152   - }
153 154  
  155 + // 注意:新结构下不需要恢复库存表的数量,因为库存数量是固定的,使用记录只是记录使用情况
154 156 _db.Ado.CommitTran();
155 157 }
156 158 catch (Exception ex)
... ... @@ -175,82 +177,77 @@ namespace NCC.Extend
175 177 {
176 178 var sidx = input.sidx == null ? "id" : input.sidx;
177 179  
178   - // 查询使用记录信息
179   - var data = await _db.Queryable<LqInventoryUsageEntity>()
180   - .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), x => x.ProductId == input.ProductId)
181   - .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), x => x.StoreId == input.StoreId)
182   - .WhereIF(input.UsageStartTime.HasValue, x => x.UsageTime >= input.UsageStartTime.Value)
183   - .WhereIF(input.UsageEndTime.HasValue, x => x.UsageTime <= input.UsageEndTime.Value)
184   - .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), x => x.RelatedConsumeId == input.RelatedConsumeId)
185   - .WhereIF(input.IsEffective.HasValue, x => x.IsEffective == input.IsEffective.Value)
186   - .Select(x => new LqInventoryUsageListOutput
  180 + // 查询使用记录信息,关联产品表
  181 + var data = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>(
  182 + (usage, product) => usage.ProductId == product.Id)
  183 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
  184 + .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product) => usage.StoreId == input.StoreId)
  185 + .WhereIF(input.UsageStartTime.HasValue, (usage, product) => usage.UsageTime >= input.UsageStartTime.Value)
  186 + .WhereIF(input.UsageEndTime.HasValue, (usage, product) => usage.UsageTime <= input.UsageEndTime.Value)
  187 + .WhereIF(!string.IsNullOrWhiteSpace(input.RelatedConsumeId), (usage, product) => usage.RelatedConsumeId == input.RelatedConsumeId)
  188 + .WhereIF(input.IsEffective.HasValue, (usage, product) => usage.IsEffective == input.IsEffective.Value)
  189 + .Select((usage, product) => new LqInventoryUsageListOutput
187 190 {
188   - id = x.Id,
189   - productId = x.ProductId,
190   - productName = "",
191   - productCategory = "",
192   - productPrice = 0,
193   - storeId = x.StoreId,
194   - storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(u => u.Id == x.StoreId).Select(u => u.Dm),
195   - usageTime = x.UsageTime,
196   - usageQuantity = x.UsageQuantity,
197   - relatedConsumeId = x.RelatedConsumeId,
198   - createUser = x.CreateUser,
  191 + id = usage.Id,
  192 + productId = usage.ProductId,
  193 + productName = product.ProductName,
  194 + productCategory = product.ProductCategory,
  195 + productPrice = product.Price,
  196 + storeId = usage.StoreId,
  197 + storeName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(u => u.Id == usage.StoreId).Select(u => u.Dm),
  198 + usageTime = usage.UsageTime,
  199 + usageQuantity = usage.UsageQuantity,
  200 + relatedConsumeId = usage.RelatedConsumeId,
  201 + createUser = usage.CreateUser,
199 202 createUserName = "",
200   - createTime = x.CreateTime,
201   - updateUser = x.UpdateUser,
  203 + createTime = usage.CreateTime,
  204 + updateUser = usage.UpdateUser,
202 205 updateUserName = "",
203   - updateTime = x.UpdateTime,
204   - isEffective = x.IsEffective
  206 + updateTime = usage.UpdateTime,
  207 + isEffective = usage.IsEffective
205 208 })
206 209 .MergeTable()
207 210 .OrderBy(sidx + " " + input.sort)
208 211 .ToPagedListAsync(input.currentPage, input.pageSize);
209 212  
210   - // 补充产品信息和用户信息
211   - foreach (var item in data.list)
  213 + // 补充用户信息
  214 + var userIds = data.list.SelectMany(x => new[] { x.createUser, x.updateUser })
  215 + .Where(x => !string.IsNullOrEmpty(x))
  216 + .Distinct()
  217 + .ToList();
  218 +
  219 + if (userIds.Any())
212 220 {
213   - if (!string.IsNullOrEmpty(item.productId))
214   - {
215   - var product = await _db.Queryable<LqInventoryEntity>().Where(p => p.Id == item.productId).FirstAsync();
216   - if (product != null)
217   - {
218   - item.productName = product.ProductName;
219   - item.productCategory = product.ProductCategory;
220   - item.productPrice = product.Price;
221   - }
222   - }
223   - if (!string.IsNullOrEmpty(item.storeId))
224   - {
225   - var store = await _db.Queryable<UserEntity>().Where(u => u.Id == item.storeId).FirstAsync();
226   - item.storeName = store?.RealName ?? "";
227   - }
228   - if (!string.IsNullOrEmpty(item.createUser))
229   - {
230   - var createUser = await _db.Queryable<UserEntity>().Where(u => u.Id == item.createUser).FirstAsync();
231   - item.createUserName = createUser?.RealName ?? "";
232   - }
233   - if (!string.IsNullOrEmpty(item.updateUser))
  221 + var userList = await _db.Queryable<UserEntity>()
  222 + .Where(x => userIds.Contains(x.Id))
  223 + .Select(x => new { x.Id, x.RealName })
  224 + .ToListAsync();
  225 + var userDict = userList.ToDictionary(k => k.Id, v => v.RealName);
  226 +
  227 + foreach (var item in data.list)
234 228 {
235   - var updateUser = await _db.Queryable<UserEntity>().Where(u => u.Id == item.updateUser).FirstAsync();
236   - item.updateUserName = updateUser?.RealName ?? "";
  229 + if (!string.IsNullOrEmpty(item.createUser) && userDict.ContainsKey(item.createUser))
  230 + item.createUserName = userDict[item.createUser];
  231 + if (!string.IsNullOrEmpty(item.updateUser) && userDict.ContainsKey(item.updateUser))
  232 + item.updateUserName = userDict[item.updateUser];
237 233 }
238 234 }
239 235  
240   - // 应用产品名称和分类的过滤条件
  236 + // 应用产品名称和分类的过滤条件(在内存中过滤,因为已经在Select中获取了)
241 237 if (!string.IsNullOrWhiteSpace(input.ProductName) || !string.IsNullOrWhiteSpace(input.ProductCategory))
242 238 {
243 239 data.list = data.list.Where(x =>
244   - (string.IsNullOrWhiteSpace(input.ProductName) || x.productName.Contains(input.ProductName)) &&
245   - (string.IsNullOrWhiteSpace(input.ProductCategory) || x.productCategory.Contains(input.ProductCategory))
  240 + (string.IsNullOrWhiteSpace(input.ProductName) || (x.productName != null && x.productName.Contains(input.ProductName))) &&
  241 + (string.IsNullOrWhiteSpace(input.ProductCategory) || (x.productCategory != null && x.productCategory.Contains(input.ProductCategory)))
246 242 ).ToList();
247 243 }
248 244  
249   - // 应用门店名称的过滤条件
  245 + // 应用门店名称的过滤条件(在内存中过滤,因为已经在Select中获取了)
250 246 if (!string.IsNullOrWhiteSpace(input.StoreName))
251 247 {
252   - data.list = data.list.Where(x => x.storeName.Contains(input.StoreName)).ToList();
  248 + data.list = data.list.Where(x => x.storeName != null && x.storeName.Contains(input.StoreName)).ToList();
253 249 }
  250 +
254 251 return PageResult<LqInventoryUsageListOutput>.SqlSugarPageResult(data);
255 252 }
256 253 catch (Exception ex)
... ... @@ -273,7 +270,7 @@ namespace NCC.Extend
273 270 try
274 271 {
275 272 var data = await _db.Queryable<LqInventoryUsageEntity>()
276   - .LeftJoin<LqInventoryEntity>((usage, product) => usage.ProductId == product.Id)
  273 + .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
277 274 .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
278 275 .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
279 276 .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
... ... @@ -321,18 +318,18 @@ namespace NCC.Extend
321 318 try
322 319 {
323 320 var data = await _db.Queryable<LqInventoryUsageEntity>()
324   - .LeftJoin<LqInventoryEntity>((usage, product) => usage.ProductId == product.Id)
325   - .LeftJoin<UserEntity>((usage, product, store) => usage.StoreId == store.Id)
  321 + .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
  322 + .LeftJoin<LqMdxxEntity>((usage, product, store) => usage.StoreId == store.Id)
326 323 .Where((usage, product, store) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
327 324 .Where((usage, product, store) => usage.IsEffective == StatusEnum.有效.GetHashCode())
328 325 .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product, store) => usage.ProductId == input.ProductId)
329 326 .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), (usage, product, store) => usage.StoreId == input.StoreId)
330 327 .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), (usage, product, store) => product.ProductCategory == input.ProductCategory)
331   - .GroupBy((usage, product, store) => new { usage.StoreId, store.RealName })
  328 + .GroupBy((usage, product, store) => new { usage.StoreId, store.Dm })
332 329 .Select((usage, product, store) => new StoreUsageStatisticsOutput
333 330 {
334 331 StoreId = usage.StoreId,
335   - StoreName = store.RealName,
  332 + StoreName = store.Dm,
336 333 TotalUsageQuantity = SqlFunc.AggregateSum(usage.UsageQuantity),
337 334 TotalUsageAmount = SqlFunc.AggregateSum(usage.UsageQuantity * product.Price),
338 335 UsageCount = SqlFunc.AggregateCount(usage.Id),
... ... @@ -369,7 +366,7 @@ namespace NCC.Extend
369 366 {
370 367 // 先获取基础数据
371 368 var baseData = await _db.Queryable<LqInventoryUsageEntity>()
372   - .LeftJoin<LqInventoryEntity>((usage, product) => usage.ProductId == product.Id)
  369 + .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
373 370 .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
374 371 .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
375 372 .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
... ... @@ -450,7 +447,7 @@ namespace NCC.Extend
450 447 try
451 448 {
452 449 var data = await _db.Queryable<LqInventoryUsageEntity>()
453   - .LeftJoin<LqInventoryEntity>((usage, product) => usage.ProductId == product.Id)
  450 + .LeftJoin<LqProductEntity>((usage, product) => usage.ProductId == product.Id)
454 451 .Where((usage, product) => usage.UsageTime >= input.StartTime && usage.UsageTime <= input.EndTime)
455 452 .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode())
456 453 .WhereIF(!string.IsNullOrWhiteSpace(input.ProductId), (usage, product) => usage.ProductId == input.ProductId)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
... ... @@ -3678,5 +3678,188 @@ namespace NCC.Extend.LqKdKdjlb
3678 3678 }
3679 3679 #endregion
3680 3680  
  3681 + #region 批量处理历史开单数据的升单类型
  3682 + /// <summary>
  3683 + /// 批量处理历史开单数据的升单类型
  3684 + /// </summary>
  3685 + /// <remarks>
  3686 + /// 批量更新历史开单记录的升单类型字段(升生美、升科美、升医美)
  3687 + ///
  3688 + /// 处理逻辑:
  3689 + /// 1. 查询所有有效的开单记录
  3690 + /// 2. 对于每条开单记录,应用升单判断逻辑:
  3691 + /// - 判断当前开单是否包含医美/科美/生美品项
  3692 + /// - 判断该会员在当前开单日期之前是否有对应类型的开单记录(重要:只看该开单之前的记录)
  3693 + /// - 更新升单类型字段
  3694 + /// 3. 批量更新开单记录
  3695 + ///
  3696 + /// 升单判断规则:
  3697 + /// - 升医美:当前开单包含医美品项 + 该会员在当前开单日期之前有医美类型的开单记录 + 当前开单医美品项金额>=1000
  3698 + /// - 升科美:当前开单包含科美品项 + 该会员在当前开单日期之前有科美类型的开单记录
  3699 + /// - 升生美:当前开单包含生美品项 + 该会员在当前开单日期之前有生美类型的开单记录
  3700 + /// </remarks>
  3701 + /// <returns>处理结果,包含处理的记录数和成功数</returns>
  3702 + /// <response code="200">处理成功</response>
  3703 + /// <response code="500">服务器错误</response>
  3704 + [HttpPost("batch-update-upgrade-type")]
  3705 + public async Task<dynamic> BatchUpdateUpgradeType()
  3706 + {
  3707 + try
  3708 + {
  3709 + // 1. 查询所有有效的开单记录
  3710 + var billingList = await _db.Queryable<LqKdKdjlbEntity>().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync();
  3711 + if (!billingList.Any())
  3712 + {
  3713 + return new { success = true, message = "没有需要处理的开单记录", totalCount = 0, successCount = 0 };
  3714 + }
  3715 + _logger.LogInformation($"找到 {billingList.Count} 条开单记录需要处理");
  3716 + // 2. 批量查询关联数据
  3717 + var billingIds = billingList.Select(x => x.Id).ToList();
  3718 + var memberIds = billingList.Select(x => x.Kdhy).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
  3719 + // 查询所有品项明细
  3720 + var pxmxList = await _db.Queryable<LqKdPxmxEntity>().Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync();
  3721 + // 查询所有项目资料(用于获取品项分类)
  3722 + var itemIds = pxmxList.Where(x => !string.IsNullOrEmpty(x.Px)).Select(x => x.Px).Distinct().ToList();
  3723 + var itemCategoryDict = new Dictionary<string, string>();
  3724 + if (itemIds.Any())
  3725 + {
  3726 + var itemCategories = await _db.Queryable<LqXmzlEntity>().Where(x => itemIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode()).Select(x => new { x.Id, x.Qt2 }).ToListAsync();
  3727 + itemCategoryDict = itemCategories.ToDictionary(k => k.Id, v => v.Qt2 ?? "");
  3728 + }
  3729 +
  3730 + // 按开单ID分组品项明细
  3731 + var pxmxGrouped = pxmxList.GroupBy(x => x.Glkdbh).ToDictionary(g => g.Key, g => g.ToList());
  3732 +
  3733 + // 3. 批量处理,每批500条
  3734 + const int batchSize = 500;
  3735 + var totalBatches = (int)Math.Ceiling((double)billingList.Count / batchSize);
  3736 + var successCount = 0;
  3737 + var updateList = new List<LqKdKdjlbEntity>();
  3738 +
  3739 + for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++)
  3740 + {
  3741 + var batchBillingList = billingList.Skip(batchIndex * batchSize).Take(batchSize).ToList();
  3742 +
  3743 + foreach (var billing in batchBillingList)
  3744 + {
  3745 + try
  3746 + {
  3747 + // 获取该开单的品项明细
  3748 + var currentPxmxList = pxmxGrouped.ContainsKey(billing.Id) ? pxmxGrouped[billing.Id] : new List<LqKdPxmxEntity>();
  3749 +
  3750 + if (!currentPxmxList.Any())
  3751 + {
  3752 + // 如果没有品项明细,设置为"否"
  3753 + billing.UpgradeMedicalBeauty = "否";
  3754 + billing.UpgradeTechBeauty = "否";
  3755 + billing.UpgradeLifeBeauty = "否";
  3756 + updateList.Add(billing);
  3757 + continue;
  3758 + }
  3759 +
  3760 + // 判断当前开单是否包含医美/科美/生美品项
  3761 + var hasMedicalItem = currentPxmxList.Any(x =>
  3762 + !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "医美");
  3763 + var medicalItemAmount = currentPxmxList
  3764 + .Where(x => !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "医美")
  3765 + .Sum(x => x.ActualPrice);
  3766 +
  3767 + var hasTechItem = currentPxmxList.Any(x =>
  3768 + !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "科美");
  3769 +
  3770 + var hasLifeItem = currentPxmxList.Any(x =>
  3771 + !string.IsNullOrEmpty(x.Px) && itemCategoryDict.ContainsKey(x.Px) && itemCategoryDict[x.Px] == "生美");
  3772 +
  3773 + // 判断该会员在当前开单日期之前是否有对应类型的开单记录
  3774 + if (!string.IsNullOrEmpty(billing.Kdhy) && billing.Kdrq != null)
  3775 + {
  3776 + var currentBillingDate = billing.Kdrq.Value;
  3777 +
  3778 + // 查询该会员在当前开单日期之前的开单记录
  3779 + var previousBillingIds = await _db.Queryable<LqKdKdjlbEntity>()
  3780 + .Where(x => x.Kdhy == billing.Kdhy
  3781 + && x.IsEffective == StatusEnum.有效.GetHashCode()
  3782 + && x.Kdrq < currentBillingDate)
  3783 + .Select(x => x.Id)
  3784 + .ToListAsync();
  3785 +
  3786 + if (previousBillingIds.Any())
  3787 + {
  3788 + // 查询这些开单的品项明细
  3789 + var previousPxmxList = await _db.Queryable<LqKdPxmxEntity>()
  3790 + .Where(x => previousBillingIds.Contains(x.Glkdbh)
  3791 + && x.IsEffective == StatusEnum.有效.GetHashCode()
  3792 + && x.ActualPrice > 0
  3793 + && x.Px != "61")
  3794 + .Select(x => x.ItemCategory)
  3795 + .Distinct()
  3796 + .ToListAsync();
  3797 +
  3798 + var hasPreviousMedical = previousPxmxList.Contains("医美");
  3799 + var hasPreviousTech = previousPxmxList.Contains("科美");
  3800 + var hasPreviousLife = previousPxmxList.Contains("生美");
  3801 +
  3802 + // 判断升医美
  3803 + billing.UpgradeMedicalBeauty = (hasMedicalItem && hasPreviousMedical && medicalItemAmount >= 1000) ? "是" : "否";
  3804 +
  3805 + // 判断升科美
  3806 + billing.UpgradeTechBeauty = (hasTechItem && hasPreviousTech) ? "是" : "否";
  3807 +
  3808 + // 判断升生美
  3809 + billing.UpgradeLifeBeauty = (hasLifeItem && hasPreviousLife) ? "是" : "否";
  3810 + }
  3811 + else
  3812 + {
  3813 + billing.UpgradeMedicalBeauty = "否";
  3814 + billing.UpgradeTechBeauty = "否";
  3815 + billing.UpgradeLifeBeauty = "否";
  3816 + }
  3817 + }
  3818 + else
  3819 + {
  3820 + billing.UpgradeMedicalBeauty = "否";
  3821 + billing.UpgradeTechBeauty = "否";
  3822 + billing.UpgradeLifeBeauty = "否";
  3823 + }
  3824 +
  3825 + updateList.Add(billing);
  3826 + }
  3827 + catch (Exception ex)
  3828 + {
  3829 + _logger.LogError(ex, $"处理开单记录 {billing.Id} 失败: {ex.Message}");
  3830 + }
  3831 + }
  3832 +
  3833 + // 批量更新
  3834 + if (updateList.Any())
  3835 + {
  3836 + await _db.Updateable(updateList)
  3837 + .UpdateColumns(it => new { it.UpgradeMedicalBeauty, it.UpgradeTechBeauty, it.UpgradeLifeBeauty })
  3838 + .ExecuteCommandAsync();
  3839 + successCount += updateList.Count;
  3840 + updateList.Clear();
  3841 + }
  3842 +
  3843 + _logger.LogInformation($"已处理 {Math.Min((batchIndex + 1) * batchSize, billingList.Count)} / {billingList.Count} 条记录");
  3844 + }
  3845 +
  3846 + _logger.LogInformation($"批量处理完成,共处理 {billingList.Count} 条记录,成功 {successCount} 条");
  3847 +
  3848 + return new
  3849 + {
  3850 + success = true,
  3851 + message = "批量处理完成",
  3852 + totalCount = billingList.Count,
  3853 + successCount = successCount
  3854 + };
  3855 + }
  3856 + catch (Exception ex)
  3857 + {
  3858 + _logger.LogError(ex, $"批量处理历史开单数据的升单类型失败: {ex.ToString()}");
  3859 + throw NCCException.Oh(ErrorCode.COM1005, $"批量处理历史开单数据的升单类型失败: {ex.Message}");
  3860 + }
  3861 + }
  3862 + #endregion
  3863 +
3681 3864 }
3682 3865 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs 0 → 100644
  1 +using System;
  2 +using System.Linq;
  3 +using System.Threading.Tasks;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Microsoft.Extensions.Logging;
  6 +using NCC.Common.Core.Manager;
  7 +using NCC.Common.Enum;
  8 +using NCC.Common.Filter;
  9 +using NCC.Dependency;
  10 +using NCC.DynamicApiController;
  11 +using NCC.Extend.Entitys.Dto.LqProduct;
  12 +using NCC.Extend.Entitys.Enum;
  13 +using NCC.Extend.Entitys.lq_inventory;
  14 +using NCC.Extend.Entitys.lq_inventory_usage;
  15 +using NCC.Extend.Entitys.lq_mdxx;
  16 +using NCC.Extend.Entitys.lq_product;
  17 +using NCC.Extend.Interfaces.LqProduct;
  18 +using NCC.FriendlyException;
  19 +using NCC.System.Entitys.Permission;
  20 +using SqlSugar;
  21 +using Yitter.IdGenerator;
  22 +
  23 +namespace NCC.Extend
  24 +{
  25 + /// <summary>
  26 + /// 产品服务
  27 + /// </summary>
  28 + [ApiDescriptionSettings(Tag = "绿纤产品管理", Name = "LqProduct", Order = 200)]
  29 + [Route("api/Extend/LqProduct")]
  30 + public class LqProductService : IDynamicApiController, ITransient, ILqProductService
  31 + {
  32 + private readonly IUserManager _userManager;
  33 + private readonly ILogger<LqProductService> _logger;
  34 + private readonly ISqlSugarClient _db;
  35 +
  36 + /// <summary>
  37 + /// 构造函数
  38 + /// </summary>
  39 + /// <param name="userManager">用户管理器</param>
  40 + /// <param name="logger">日志记录器</param>
  41 + /// <param name="db">数据库客户端</param>
  42 + public LqProductService(IUserManager userManager, ILogger<LqProductService> logger, ISqlSugarClient db)
  43 + {
  44 + _userManager = userManager;
  45 + _logger = logger;
  46 + _db = db;
  47 + }
  48 +
  49 + #region 添加产品信息
  50 + /// <summary>
  51 + /// 添加产品信息
  52 + /// </summary>
  53 + /// <remarks>
  54 + /// 创建新产品,包含产品的基本信息、价格、类别等
  55 + ///
  56 + /// 示例请求:
  57 + /// ```json
  58 + /// {
  59 + /// "productName": "产品名称",
  60 + /// "price": 100.00,
  61 + /// "productCategory": "产品类别",
  62 + /// "departmentId": "部门ID",
  63 + /// "standardUnit": "标准单位",
  64 + /// "onShelfStatus": 1,
  65 + /// "statisticsCategory": "统计分类",
  66 + /// "warehouse": "归属仓库",
  67 + /// "supplierName": "供应商名称"
  68 + /// }
  69 + /// ```
  70 + /// </remarks>
  71 + /// <param name="input">创建输入</param>
  72 + /// <returns>创建结果</returns>
  73 + /// <response code="200">创建成功</response>
  74 + /// <response code="400">请求参数错误</response>
  75 + /// <response code="500">服务器错误</response>
  76 + [HttpPost("Create")]
  77 + public async Task CreateAsync([FromBody] LqProductCrInput input)
  78 + {
  79 + try
  80 + {
  81 + // 检查产品名称是否已存在
  82 + var existingProduct = await _db.Queryable<LqProductEntity>()
  83 + .Where(x => x.ProductName == input.ProductName)
  84 + .FirstAsync();
  85 +
  86 + if (existingProduct != null)
  87 + {
  88 + throw NCCException.Oh($"产品名称'{input.ProductName}'已存在");
  89 + }
  90 +
  91 + // 创建产品
  92 + var productEntity = new LqProductEntity
  93 + {
  94 + Id = YitIdHelper.NextId().ToString(),
  95 + ProductName = input.ProductName,
  96 + Price = input.Price,
  97 + ProductCategory = input.ProductCategory,
  98 + DepartmentId = input.DepartmentId,
  99 + StandardUnit = input.StandardUnit,
  100 + OnShelfStatus = input.OnShelfStatus,
  101 + StatisticsCategory = input.StatisticsCategory,
  102 + Warehouse = input.Warehouse,
  103 + UnitConversion = input.UnitConversion,
  104 + SupplierName = input.SupplierName,
  105 + ContractSignDate = input.ContractSignDate,
  106 + ContractEndDate = input.ContractEndDate,
  107 + Remark = input.Remark,
  108 + CreateUser = _userManager.UserId,
  109 + CreateTime = DateTime.Now
  110 + };
  111 +
  112 + var isOk = await _db.Insertable(productEntity).ExecuteCommandAsync();
  113 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  114 + }
  115 + catch (Exception ex)
  116 + {
  117 + _logger.LogError(ex, "创建产品失败");
  118 + throw NCCException.Oh($"创建失败:{ex.Message}");
  119 + }
  120 + }
  121 + #endregion
  122 +
  123 + #region 更新产品信息
  124 + /// <summary>
  125 + /// 更新产品信息
  126 + /// </summary>
  127 + /// <param name="input">更新输入</param>
  128 + /// <returns>更新结果</returns>
  129 + [HttpPut("Update")]
  130 + public async Task UpdateAsync([FromBody] LqProductUpInput input)
  131 + {
  132 + try
  133 + {
  134 + var existingProduct = await _db.Queryable<LqProductEntity>()
  135 + .Where(x => x.Id == input.Id)
  136 + .FirstAsync();
  137 +
  138 + if (existingProduct == null)
  139 + {
  140 + throw NCCException.Oh("产品不存在");
  141 + }
  142 +
  143 + // 检查产品名称是否与其他产品重复
  144 + var duplicateProduct = await _db.Queryable<LqProductEntity>()
  145 + .Where(x => x.ProductName == input.ProductName && x.Id != input.Id)
  146 + .FirstAsync();
  147 +
  148 + if (duplicateProduct != null)
  149 + {
  150 + throw NCCException.Oh($"产品名称'{input.ProductName}'已存在");
  151 + }
  152 +
  153 + // 更新产品信息
  154 + existingProduct.ProductName = input.ProductName;
  155 + existingProduct.Price = input.Price;
  156 + existingProduct.ProductCategory = input.ProductCategory;
  157 + existingProduct.DepartmentId = input.DepartmentId;
  158 + existingProduct.StandardUnit = input.StandardUnit;
  159 + existingProduct.OnShelfStatus = input.OnShelfStatus;
  160 + existingProduct.StatisticsCategory = input.StatisticsCategory;
  161 + existingProduct.Warehouse = input.Warehouse;
  162 + existingProduct.UnitConversion = input.UnitConversion;
  163 + existingProduct.SupplierName = input.SupplierName;
  164 + existingProduct.ContractSignDate = input.ContractSignDate;
  165 + existingProduct.ContractEndDate = input.ContractEndDate;
  166 + existingProduct.Remark = input.Remark;
  167 + existingProduct.UpdateUser = _userManager.UserId;
  168 + existingProduct.UpdateTime = DateTime.Now;
  169 +
  170 + var isOk = await _db.Updateable(existingProduct).ExecuteCommandAsync();
  171 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  172 + }
  173 + catch (Exception ex)
  174 + {
  175 + _logger.LogError(ex, "更新产品失败");
  176 + throw NCCException.Oh($"更新失败:{ex.Message}");
  177 + }
  178 + }
  179 + #endregion
  180 +
  181 + #region 获取产品列表(包含现有库存)
  182 + /// <summary>
  183 + /// 获取产品列表(包含现有库存)
  184 + /// </summary>
  185 + /// <remarks>
  186 + /// 返回产品列表,每个产品包含当前库存数量(所有有效库存的总和)
  187 + ///
  188 + /// 示例请求:
  189 + /// ```json
  190 + /// {
  191 + /// "currentPage": 1,
  192 + /// "pageSize": 10,
  193 + /// "sidx": "createTime",
  194 + /// "sort": "desc",
  195 + /// "productName": "产品名称",
  196 + /// "productCategory": "产品类别",
  197 + /// "onShelfStatus": 1
  198 + /// }
  199 + /// ```
  200 + /// </remarks>
  201 + /// <param name="input">查询输入</param>
  202 + /// <returns>产品列表(包含现有库存)</returns>
  203 + /// <response code="200">查询成功</response>
  204 + /// <response code="500">服务器错误</response>
  205 + [HttpGet("GetList")]
  206 + public async Task<dynamic> GetListAsync([FromQuery] LqProductListQueryInput input)
  207 + {
  208 + try
  209 + {
  210 + var sidx = string.IsNullOrEmpty(input.sidx) ? "createTime" : input.sidx;
  211 + var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort;
  212 +
  213 + // 查询产品信息
  214 + var data = await _db.Queryable<LqProductEntity>()
  215 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductName), x => x.ProductName.Contains(input.ProductName))
  216 + .WhereIF(!string.IsNullOrWhiteSpace(input.ProductCategory), x => x.ProductCategory == input.ProductCategory)
  217 + .WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentId), x => x.DepartmentId == input.DepartmentId)
  218 + .WhereIF(input.OnShelfStatus.HasValue, x => x.OnShelfStatus == input.OnShelfStatus.Value)
  219 + .WhereIF(!string.IsNullOrWhiteSpace(input.StatisticsCategory), x => x.StatisticsCategory == input.StatisticsCategory)
  220 + .WhereIF(!string.IsNullOrWhiteSpace(input.Warehouse), x => x.Warehouse == input.Warehouse)
  221 + .WhereIF(!string.IsNullOrWhiteSpace(input.SupplierName), x => x.SupplierName != null && x.SupplierName.Contains(input.SupplierName))
  222 + .WhereIF(input.PriceMin.HasValue, x => x.Price >= input.PriceMin.Value)
  223 + .WhereIF(input.PriceMax.HasValue, x => x.Price <= input.PriceMax.Value)
  224 + .Select(x => new LqProductListOutput
  225 + {
  226 + id = x.Id,
  227 + productName = x.ProductName,
  228 + price = x.Price,
  229 + productCategory = x.ProductCategory,
  230 + departmentId = x.DepartmentId,
  231 + departmentName = "",
  232 + standardUnit = x.StandardUnit,
  233 + onShelfStatus = x.OnShelfStatus,
  234 + statisticsCategory = x.StatisticsCategory,
  235 + warehouse = x.Warehouse,
  236 + unitConversion = x.UnitConversion,
  237 + supplierName = x.SupplierName,
  238 + contractSignDate = x.ContractSignDate,
  239 + contractEndDate = x.ContractEndDate,
  240 + remark = x.Remark,
  241 + currentInventory = 0, // 稍后计算
  242 + createUser = x.CreateUser,
  243 + createUserName = "",
  244 + createTime = x.CreateTime,
  245 + updateUser = x.UpdateUser,
  246 + updateUserName = "",
  247 + updateTime = x.UpdateTime
  248 + })
  249 + .MergeTable()
  250 + .OrderBy(sidx + " " + sort)
  251 + .ToPagedListAsync(input.currentPage, input.pageSize);
  252 +
  253 + // 批量查询每个产品的库存数量
  254 + var productIds = data.list.Select(x => x.id).ToList();
  255 + if (productIds.Any())
  256 + {
  257 + var inventoryDict = await _db.Queryable<LqInventoryEntity>()
  258 + .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
  259 + .GroupBy(x => x.ProductId)
  260 + .Select(x => new { ProductId = x.ProductId, TotalQuantity = SqlFunc.AggregateSum((decimal?)x.Quantity) })
  261 + .ToListAsync();
  262 +
  263 + var inventoryDictMap = inventoryDict.ToDictionary(k => k.ProductId, v => v.TotalQuantity.HasValue ? (int)v.TotalQuantity.Value : 0);
  264 +
  265 + // 填充库存数量
  266 + foreach (var item in data.list)
  267 + {
  268 + item.currentInventory = inventoryDictMap.ContainsKey(item.id) ? inventoryDictMap[item.id] : 0;
  269 + }
  270 + }
  271 +
  272 + // 补充用户名称信息
  273 + var userIds = data.list.SelectMany(x => new[] { x.createUser, x.updateUser, x.departmentId })
  274 + .Where(x => !string.IsNullOrEmpty(x))
  275 + .Distinct()
  276 + .ToList();
  277 +
  278 + if (userIds.Any())
  279 + {
  280 + var userList = await _db.Queryable<UserEntity>()
  281 + .Where(x => userIds.Contains(x.Id))
  282 + .Select(x => new { x.Id, x.RealName })
  283 + .ToListAsync();
  284 + var userDict = userList.ToDictionary(k => k.Id, v => v.RealName);
  285 +
  286 + foreach (var item in data.list)
  287 + {
  288 + if (!string.IsNullOrEmpty(item.createUser) && userDict.ContainsKey(item.createUser))
  289 + item.createUserName = userDict[item.createUser];
  290 + if (!string.IsNullOrEmpty(item.updateUser) && userDict.ContainsKey(item.updateUser))
  291 + item.updateUserName = userDict[item.updateUser];
  292 + if (!string.IsNullOrEmpty(item.departmentId) && userDict.ContainsKey(item.departmentId))
  293 + item.departmentName = userDict[item.departmentId];
  294 + }
  295 + }
  296 +
  297 + return PageResult<LqProductListOutput>.SqlSugarPageResult(data);
  298 + }
  299 + catch (Exception ex)
  300 + {
  301 + _logger.LogError(ex, "获取产品列表失败");
  302 + throw NCCException.Oh($"获取产品列表失败:{ex.Message}");
  303 + }
  304 + }
  305 + #endregion
  306 +
  307 + #region 获取产品详情
  308 + /// <summary>
  309 + /// 获取产品详情
  310 + /// </summary>
  311 + /// <param name="id">产品ID</param>
  312 + /// <returns>产品详情</returns>
  313 + [HttpGet("GetInfo")]
  314 + public async Task<dynamic> GetInfoAsync([FromQuery] string id)
  315 + {
  316 + try
  317 + {
  318 + if (string.IsNullOrWhiteSpace(id))
  319 + {
  320 + throw NCCException.Oh("产品ID不能为空");
  321 + }
  322 +
  323 + var product = await _db.Queryable<LqProductEntity>()
  324 + .Where(x => x.Id == id)
  325 + .FirstAsync();
  326 +
  327 + if (product == null)
  328 + {
  329 + throw NCCException.Oh("产品不存在");
  330 + }
  331 +
  332 + var result = new LqProductInfoOutput
  333 + {
  334 + id = product.Id,
  335 + productName = product.ProductName,
  336 + price = product.Price,
  337 + productCategory = product.ProductCategory,
  338 + departmentId = product.DepartmentId,
  339 + departmentName = "",
  340 + standardUnit = product.StandardUnit,
  341 + onShelfStatus = product.OnShelfStatus,
  342 + statisticsCategory = product.StatisticsCategory,
  343 + warehouse = product.Warehouse,
  344 + unitConversion = product.UnitConversion,
  345 + supplierName = product.SupplierName,
  346 + contractSignDate = product.ContractSignDate,
  347 + contractEndDate = product.ContractEndDate,
  348 + remark = product.Remark,
  349 + createUser = product.CreateUser,
  350 + createUserName = "",
  351 + createTime = product.CreateTime,
  352 + updateUser = product.UpdateUser,
  353 + updateUserName = "",
  354 + updateTime = product.UpdateTime
  355 + };
  356 +
  357 + // 补充用户名称信息
  358 + var userIds = new[] { result.createUser, result.updateUser, result.departmentId }
  359 + .Where(x => !string.IsNullOrEmpty(x))
  360 + .Distinct()
  361 + .ToList();
  362 +
  363 + if (userIds.Any())
  364 + {
  365 + var userList = await _db.Queryable<UserEntity>()
  366 + .Where(x => userIds.Contains(x.Id))
  367 + .Select(x => new { x.Id, x.RealName })
  368 + .ToListAsync();
  369 + var userDict = userList.ToDictionary(k => k.Id, v => v.RealName);
  370 +
  371 + if (!string.IsNullOrEmpty(result.createUser) && userDict.ContainsKey(result.createUser))
  372 + result.createUserName = userDict[result.createUser];
  373 + if (!string.IsNullOrEmpty(result.updateUser) && userDict.ContainsKey(result.updateUser))
  374 + result.updateUserName = userDict[result.updateUser];
  375 + if (!string.IsNullOrEmpty(result.departmentId) && userDict.ContainsKey(result.departmentId))
  376 + result.departmentName = userDict[result.departmentId];
  377 + }
  378 +
  379 + return result;
  380 + }
  381 + catch (Exception ex)
  382 + {
  383 + _logger.LogError(ex, "获取产品详情失败");
  384 + throw NCCException.Oh($"获取产品详情失败:{ex.Message}");
  385 + }
  386 + }
  387 + #endregion
  388 +
  389 + #region 获取产品下拉列表
  390 + /// <summary>
  391 + /// 获取产品下拉列表
  392 + /// </summary>
  393 + /// <remarks>
  394 + /// 用于下拉选择的产品列表,只返回上架的产品
  395 + ///
  396 + /// 示例请求:
  397 + /// ```
  398 + /// GET /api/Extend/LqProduct/GetSelectList?productName=产品
  399 + /// ```
  400 + /// </remarks>
  401 + /// <param name="productName">产品名称(可选,模糊查询)</param>
  402 + /// <param name="onShelfStatus">上架状态(可选,1:上架 0:下架,默认1)</param>
  403 + /// <returns>产品下拉列表</returns>
  404 + /// <response code="200">查询成功</response>
  405 + /// <response code="500">服务器错误</response>
  406 + [HttpGet("GetSelectList")]
  407 + public async Task<dynamic> GetSelectListAsync([FromQuery] string productName = null, [FromQuery] int? onShelfStatus = 1)
  408 + {
  409 + try
  410 + {
  411 + var query = _db.Queryable<LqProductEntity>()
  412 + .WhereIF(!string.IsNullOrWhiteSpace(productName), x => x.ProductName.Contains(productName))
  413 + .WhereIF(onShelfStatus.HasValue, x => x.OnShelfStatus == onShelfStatus.Value);
  414 +
  415 + var products = await query
  416 + .Select(x => new LqProductSelectOutput
  417 + {
  418 + id = x.Id,
  419 + productName = x.ProductName,
  420 + price = x.Price,
  421 + standardUnit = x.StandardUnit,
  422 + currentInventory = 0 // 稍后计算
  423 + })
  424 + .ToListAsync();
  425 +
  426 + // 批量查询每个产品的库存数量
  427 + var productIds = products.Select(x => x.id).ToList();
  428 + if (productIds.Any())
  429 + {
  430 + var inventoryDict = await _db.Queryable<LqInventoryEntity>()
  431 + .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
  432 + .GroupBy(x => x.ProductId)
  433 + .Select(x => new { ProductId = x.ProductId, TotalQuantity = SqlFunc.AggregateSum((decimal?)x.Quantity) })
  434 + .ToListAsync();
  435 +
  436 + var inventoryDictMap = inventoryDict.ToDictionary(k => k.ProductId, v => v.TotalQuantity.HasValue ? (int)v.TotalQuantity.Value : 0);
  437 +
  438 + // 填充库存数量
  439 + foreach (var item in products)
  440 + {
  441 + item.currentInventory = inventoryDictMap.ContainsKey(item.id) ? inventoryDictMap[item.id] : 0;
  442 + }
  443 + }
  444 +
  445 + return products;
  446 + }
  447 + catch (Exception ex)
  448 + {
  449 + _logger.LogError(ex, "获取产品下拉列表失败");
  450 + throw NCCException.Oh($"获取产品下拉列表失败:{ex.Message}");
  451 + }
  452 + }
  453 + #endregion
  454 +
  455 + #region 作废产品
  456 + /// <summary>
  457 + /// 作废产品
  458 + /// </summary>
  459 + /// <remarks>
  460 + /// 作废产品,将产品的上架状态改为下架(OnShelfStatus = 0)
  461 + /// 注意:如果产品有已使用的库存,需要先检查是否允许作废
  462 + /// </remarks>
  463 + /// <param name="id">产品ID</param>
  464 + /// <param name="remarks">作废备注</param>
  465 + /// <returns>作废结果</returns>
  466 + /// <response code="200">作废成功</response>
  467 + /// <response code="400">产品不存在或已被作废</response>
  468 + /// <response code="500">服务器错误</response>
  469 + [HttpPut("Cancel/{id}")]
  470 + public async Task CancelAsync([FromRoute] string id, [FromQuery] string remarks = null)
  471 + {
  472 + try
  473 + {
  474 + if (string.IsNullOrWhiteSpace(id))
  475 + {
  476 + throw NCCException.Oh("产品ID不能为空");
  477 + }
  478 +
  479 + var product = await _db.Queryable<LqProductEntity>()
  480 + .Where(x => x.Id == id)
  481 + .FirstAsync();
  482 +
  483 + if (product == null)
  484 + {
  485 + throw NCCException.Oh("产品不存在");
  486 + }
  487 +
  488 + // 计算该产品的总库存数量(所有有效库存的总和)
  489 + var totalInventory = await _db.Queryable<LqInventoryEntity>()
  490 + .Where(x => x.ProductId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  491 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  492 +
  493 + // 计算该产品的已使用数量
  494 + var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
  495 + .Where(x => x.ProductId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  496 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  497 +
  498 + // 作废后的总库存(作废后库存为0)
  499 + var inventoryAfterCancel = 0;
  500 +
  501 + // 判断:如果已使用的数量超过了作废后的总数量,不允许作废
  502 + if (totalUsage > inventoryAfterCancel)
  503 + {
  504 + throw NCCException.Oh($"不允许作废该产品,已使用数量({totalUsage})超过了作废后的总数量({inventoryAfterCancel})");
  505 + }
  506 +
  507 + // 作废产品(将上架状态改为下架)
  508 + product.OnShelfStatus = 0;
  509 + if (!string.IsNullOrEmpty(remarks))
  510 + {
  511 + product.Remark = string.IsNullOrEmpty(product.Remark)
  512 + ? $"作废备注:{remarks}"
  513 + : $"{product.Remark}\n作废备注:{remarks}";
  514 + }
  515 + product.UpdateUser = _userManager.UserId;
  516 + product.UpdateTime = DateTime.Now;
  517 +
  518 + var isOk = await _db.Updateable(product).ExecuteCommandAsync();
  519 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  520 + }
  521 + catch (Exception ex)
  522 + {
  523 + _logger.LogError(ex, "作废产品失败");
  524 + throw NCCException.Oh($"作废失败:{ex.Message}");
  525 + }
  526 + }
  527 + #endregion
  528 +
  529 + #region 查看产品的所有库存和使用记录
  530 + /// <summary>
  531 + /// 查看产品的所有库存和使用记录
  532 + /// </summary>
  533 + /// <remarks>
  534 + /// 返回产品的详细信息、所有库存记录和使用记录
  535 + ///
  536 + /// 示例请求:
  537 + /// ```
  538 + /// GET /api/Extend/LqProduct/GetInventoryDetail?productId=产品ID
  539 + /// ```
  540 + /// </remarks>
  541 + /// <param name="productId">产品ID</param>
  542 + /// <returns>产品库存详情(包含库存列表和使用记录列表)</returns>
  543 + /// <response code="200">查询成功</response>
  544 + /// <response code="400">产品不存在</response>
  545 + /// <response code="500">服务器错误</response>
  546 + [HttpGet("GetInventoryDetail")]
  547 + public async Task<dynamic> GetInventoryDetailAsync([FromQuery] string productId)
  548 + {
  549 + try
  550 + {
  551 + if (string.IsNullOrWhiteSpace(productId))
  552 + {
  553 + throw NCCException.Oh("产品ID不能为空");
  554 + }
  555 +
  556 + // 获取产品信息
  557 + var productInfo = await GetInfoAsync(productId);
  558 + if (productInfo == null)
  559 + {
  560 + throw NCCException.Oh("产品不存在");
  561 + }
  562 +
  563 + // 获取该产品的所有库存记录
  564 + var inventoryList = await _db.Queryable<LqInventoryEntity>()
  565 + .Where(x => x.ProductId == productId)
  566 + .OrderBy(x => x.CreateTime, OrderByType.Desc)
  567 + .Select(x => new ProductInventoryItem
  568 + {
  569 + id = x.Id,
  570 + quantity = x.Quantity,
  571 + stockInTime = x.StockInTime,
  572 + productionDate = x.ProductionDate,
  573 + shelfLife = x.ShelfLife,
  574 + batchNumber = x.BatchNumber,
  575 + isEffective = x.IsEffective,
  576 + createTime = x.CreateTime
  577 + })
  578 + .ToListAsync();
  579 +
  580 + // 获取该产品的所有使用记录
  581 + var usageList = await _db.Queryable<LqInventoryUsageEntity, LqMdxxEntity>(
  582 + (usage, store) => usage.StoreId == store.Id)
  583 + .Where((usage, store) => usage.ProductId == productId)
  584 + .OrderBy((usage, store) => usage.UsageTime, OrderByType.Desc)
  585 + .Select((usage, store) => new ProductUsageItem
  586 + {
  587 + id = usage.Id,
  588 + storeId = usage.StoreId,
  589 + storeName = store.Dm,
  590 + usageTime = usage.UsageTime,
  591 + usageQuantity = usage.UsageQuantity,
  592 + relatedConsumeId = usage.RelatedConsumeId,
  593 + createTime = usage.CreateTime,
  594 + isEffective = usage.IsEffective
  595 + })
  596 + .ToListAsync();
  597 +
  598 + // 计算总库存、已使用数量、可用库存
  599 + var totalInventory = inventoryList.Where(x => x.isEffective == StatusEnum.有效.GetHashCode()).Sum(x => x.quantity);
  600 + var totalUsage = usageList.Where(x => x.isEffective == StatusEnum.有效.GetHashCode()).Sum(x => x.usageQuantity);
  601 + var availableInventory = totalInventory - totalUsage;
  602 +
  603 + var result = new LqProductInventoryDetailOutput
  604 + {
  605 + productInfo = productInfo as LqProductInfoOutput,
  606 + inventoryList = inventoryList,
  607 + usageList = usageList,
  608 + totalInventory = totalInventory,
  609 + totalUsage = totalUsage,
  610 + availableInventory = availableInventory
  611 + };
  612 +
  613 + return result;
  614 + }
  615 + catch (Exception ex)
  616 + {
  617 + _logger.LogError(ex, "获取产品库存详情失败");
  618 + throw NCCException.Oh($"获取产品库存详情失败:{ex.Message}");
  619 + }
  620 + }
  621 + #endregion
  622 +
  623 + #region 产品上下架
  624 + /// <summary>
  625 + /// 产品上下架
  626 + /// </summary>
  627 + /// <remarks>
  628 + /// 设置产品的上架状态,1表示上架,0表示下架
  629 + ///
  630 + /// 示例请求:
  631 + /// ```json
  632 + /// {
  633 + /// "productId": "产品ID",
  634 + /// "onShelfStatus": 1
  635 + /// }
  636 + /// ```
  637 + ///
  638 + /// 参数说明:
  639 + /// - productId: 产品ID(必填)
  640 + /// - onShelfStatus: 上架状态,1表示上架,0表示下架(必填)
  641 + /// </remarks>
  642 + /// <param name="input">上下架输入</param>
  643 + /// <returns>操作结果</returns>
  644 + /// <response code="200">操作成功</response>
  645 + /// <response code="400">产品不存在或参数错误</response>
  646 + /// <response code="500">服务器错误</response>
  647 + [HttpPut("ToggleShelf")]
  648 + public async Task ToggleShelfAsync([FromBody] LqProductToggleShelfInput input)
  649 + {
  650 + try
  651 + {
  652 + if (string.IsNullOrWhiteSpace(input.ProductId))
  653 + {
  654 + throw NCCException.Oh("产品ID不能为空");
  655 + }
  656 +
  657 + if (input.OnShelfStatus != 0 && input.OnShelfStatus != 1)
  658 + {
  659 + throw NCCException.Oh("上架状态只能是0(下架)或1(上架)");
  660 + }
  661 +
  662 + var product = await _db.Queryable<LqProductEntity>()
  663 + .Where(x => x.Id == input.ProductId)
  664 + .FirstAsync();
  665 +
  666 + if (product == null)
  667 + {
  668 + throw NCCException.Oh("产品不存在");
  669 + }
  670 +
  671 + // 更新上架状态
  672 + product.OnShelfStatus = input.OnShelfStatus;
  673 + product.UpdateUser = _userManager.UserId;
  674 + product.UpdateTime = DateTime.Now;
  675 +
  676 + var isOk = await _db.Updateable(product)
  677 + .UpdateColumns(x => new { x.OnShelfStatus, x.UpdateUser, x.UpdateTime })
  678 + .ExecuteCommandAsync();
  679 +
  680 + if (!(isOk > 0))
  681 + {
  682 + throw NCCException.Oh(ErrorCode.COM1000);
  683 + }
  684 + }
  685 + catch (Exception ex)
  686 + {
  687 + _logger.LogError(ex, "产品上下架操作失败");
  688 + throw NCCException.Oh($"产品上下架操作失败:{ex.Message}");
  689 + }
  690 + }
  691 + #endregion
  692 + }
  693 +}
  694 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -3512,39 +3512,59 @@ namespace NCC.Extend.LqStatistics
3512 3512 }
3513 3513  
3514 3514 /// <summary>
3515   - /// 统计人头(月度去重客户数
  3515 + /// 统计人头(从人次记录表统计,按健康师+月份+客户+数量去重后累加
3516 3516 /// </summary>
3517 3517 private async Task<int> GetHeadCount(string userId, string month)
3518 3518 {
  3519 + // 按健康师+月份+客户+数量去重,然后累加数量
  3520 + // 注意:userId 和 month 都是内部参数,相对安全
3519 3521 var sql = $@"
3520   - SELECT COUNT(DISTINCT hyhk.hy) as Count
3521   - FROM lq_xh_jksyj jksyj
3522   - INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
3523   - WHERE jksyj.jkszh = '{userId}'
3524   - AND jksyj.F_IsEffective = 1
3525   - AND hyhk.F_IsEffective = 1
3526   - AND DATE_FORMAT(hyhk.hksj, '%Y%m') = '{month}'";
  3522 + SELECT COALESCE(SUM(F_Quantity), 0) as Count
  3523 + FROM (
  3524 + SELECT
  3525 + F_PersonId,
  3526 + F_WorkMonth,
  3527 + F_MemberId,
  3528 + F_Quantity
  3529 + FROM lq_person_times_record
  3530 + WHERE F_PersonId = '{userId}'
  3531 + AND F_PersonType = '健康师'
  3532 + AND F_WorkMonth = '{month}'
  3533 + AND F_IsEffective = 1
  3534 + GROUP BY F_PersonId, F_WorkMonth, F_MemberId, F_Quantity
  3535 + ) as distinct_records";
3527 3536  
3528 3537 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
3529   - return Convert.ToInt32(result.FirstOrDefault()?.Count ?? 0);
  3538 + var count = result.FirstOrDefault()?.Count;
  3539 + return count != null ? Convert.ToInt32(count) : 0;
3530 3540 }
3531 3541  
3532 3542 /// <summary>
3533   - /// 统计人次(日度去重客户数
  3543 + /// 统计人次(从人次记录表统计,按健康师+日期+客户+数量去重后累加
3534 3544 /// </summary>
3535 3545 private async Task<int> GetPersonCount(string userId, string month)
3536 3546 {
  3547 + // 按健康师+日期+客户+数量去重,然后累加数量
  3548 + // 注意:userId 和 month 都是内部参数,相对安全
3537 3549 var sql = $@"
3538   - SELECT COUNT(DISTINCT CONCAT(hyhk.hy, '-', DATE(hyhk.hksj))) as Count
3539   - FROM lq_xh_jksyj jksyj
3540   - INNER JOIN lq_xh_hyhk hyhk ON jksyj.glkdbh = hyhk.F_Id
3541   - WHERE jksyj.jkszh = '{userId}'
3542   - AND jksyj.F_IsEffective = 1
3543   - AND hyhk.F_IsEffective = 1
3544   - AND DATE_FORMAT(hyhk.hksj, '%Y%m') = '{month}'";
  3550 + SELECT COALESCE(SUM(F_Quantity), 0) as Count
  3551 + FROM (
  3552 + SELECT
  3553 + F_PersonId,
  3554 + F_WorkDate,
  3555 + F_MemberId,
  3556 + F_Quantity
  3557 + FROM lq_person_times_record
  3558 + WHERE F_PersonId = '{userId}'
  3559 + AND F_PersonType = '健康师'
  3560 + AND F_WorkMonth = '{month}'
  3561 + AND F_IsEffective = 1
  3562 + GROUP BY F_PersonId, F_WorkDate, F_MemberId, F_Quantity
  3563 + ) as distinct_records";
3545 3564  
3546 3565 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
3547   - return Convert.ToInt32(result.FirstOrDefault()?.Count ?? 0);
  3566 + var count = result.FirstOrDefault()?.Count;
  3567 + return count != null ? Convert.ToInt32(count) : 0;
3548 3568 }
3549 3569  
3550 3570 /// <summary>
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqXhHyhkService.cs
... ... @@ -1003,10 +1003,12 @@ namespace NCC.Extend.LqXhHyhk
1003 1003 }
1004 1004 }
1005 1005 }
  1006 + // 剔除所有T区健康师(姓名中包含"T区"的健康师)
  1007 + var NoTallJksyjEntities = allJksyjEntities.Where(x => x.Jks != null && x.Jks != "" && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
1006 1008 //获取这次耗卡有多少个健康师
1007   - var jksCount = allJksyjEntities.GroupBy(x => x.Jks).Count();
  1009 + var jksCount = NoTallJksyjEntities.GroupBy(x => x.Jks).Count();
1008 1010 //添加到人次表里面去
1009   - foreach (var item in allJksyjEntities.Select(x => x.Jks).Distinct())
  1011 + foreach (var item in NoTallJksyjEntities.Select(x => x.Jks).Distinct())
1010 1012 {
1011 1013 LqPersonTimesRecordEntity personTimesRecordEntity = new LqPersonTimesRecordEntity
1012 1014 {
... ... @@ -1015,7 +1017,7 @@ namespace NCC.Extend.LqXhHyhk
1015 1017 BusinessType = "耗卡",
1016 1018 PersonType = "健康师",
1017 1019 PersonId = item,
1018   - PersonName = allJksyjEntities.Where(x => x.Jks == item).Select(x => x.Jksxm).First(),
  1020 + PersonName = NoTallJksyjEntities.Where(x => x.Jks == item).Select(x => x.Jksxm).First(),
1019 1021 MemberId = entity.Hy,
1020 1022 MemberName = memberInfo.Khmc,
1021 1023 WorkDate = input.hksj,
... ... @@ -1026,10 +1028,12 @@ namespace NCC.Extend.LqXhHyhk
1026 1028 };
1027 1029 allPersonTimesRecordEntities.Add(personTimesRecordEntity);
1028 1030 }
  1031 + //剔除所有T区科技部老师(姓名中包含"T区"的科技部老师)
  1032 + var NoTallKjbsyjEntities = allKjbsyjEntities.Where(x => x.Kjbls != null && x.Kjbls != "" && (x.Kjblsxm == null || !x.Kjblsxm.Contains("T区"))).ToList();
1029 1033 //获取这次耗卡有多少个科技部老师
1030   - var kjbCount = allKjbsyjEntities.GroupBy(x => x.Kjbls).Count();
  1034 + var kjbCount = NoTallKjbsyjEntities.GroupBy(x => x.Kjbls).Count();
1031 1035 //添加到人次表里面去
1032   - foreach (var item in allKjbsyjEntities.Select(x => x.Kjbls).Distinct())
  1036 + foreach (var item in NoTallKjbsyjEntities.Select(x => x.Kjbls).Distinct())
1033 1037 {
1034 1038 LqPersonTimesRecordEntity personTimesRecordEntity = new LqPersonTimesRecordEntity
1035 1039 {
... ... @@ -1038,7 +1042,7 @@ namespace NCC.Extend.LqXhHyhk
1038 1042 BusinessType = "耗卡",
1039 1043 PersonType = "科技部老师",
1040 1044 PersonId = item,
1041   - PersonName = allKjbsyjEntities.Where(x => x.Kjbls == item).Select(x => x.Kjblsxm).First(),
  1045 + PersonName = NoTallKjbsyjEntities.Where(x => x.Kjbls == item).Select(x => x.Kjblsxm).First(),
1042 1046 MemberId = entity.Hy,
1043 1047 MemberName = memberInfo.Khmc,
1044 1048 WorkDate = input.hksj,
... ... @@ -1360,10 +1364,12 @@ namespace NCC.Extend.LqXhHyhk
1360 1364 }
1361 1365 }
1362 1366 }
  1367 + //剔除所有T区健康师(姓名中包含"T区"的健康师)
  1368 + var NoTallJksyjEntities = allJksyjEntities.Where(x => x.Jks != null && x.Jks != "" && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
1363 1369 //获取这次耗卡有多少个健康师
1364   - var jksCount = allJksyjEntities.GroupBy(x => x.Jks).Count();
  1370 + var jksCount = NoTallJksyjEntities.GroupBy(x => x.Jks).Count();
1365 1371 //添加到人次表里面去
1366   - foreach (var item in allJksyjEntities.Select(x => x.Jks).Distinct())
  1372 + foreach (var item in NoTallJksyjEntities.Select(x => x.Jks).Distinct())
1367 1373 {
1368 1374 LqPersonTimesRecordEntity personTimesRecordEntity = new LqPersonTimesRecordEntity
1369 1375 {
... ... @@ -1372,7 +1378,7 @@ namespace NCC.Extend.LqXhHyhk
1372 1378 BusinessType = "耗卡",
1373 1379 PersonType = "健康师",
1374 1380 PersonId = item,
1375   - PersonName = allJksyjEntities.Where(x => x.Jks == item).Select(x => x.Jksxm).First(),
  1381 + PersonName = NoTallJksyjEntities.Where(x => x.Jks == item).Select(x => x.Jksxm).First(),
1376 1382 MemberId = entity.Hy,
1377 1383 MemberName = memberInfo.Khmc,
1378 1384 WorkDate = input.hksj,
... ... @@ -1383,10 +1389,12 @@ namespace NCC.Extend.LqXhHyhk
1383 1389 };
1384 1390 allPersonTimesRecordEntities.Add(personTimesRecordEntity);
1385 1391 }
  1392 + //剔除所有T区科技部老师(姓名中包含"T区"的科技部老师)
  1393 + var NoTallKjbsyjEntities = allKjbsyjEntities.Where(x => x.Kjbls != null && x.Kjbls != "" && (x.Kjblsxm == null || !x.Kjblsxm.Contains("T区"))).ToList();
1386 1394 //获取这次耗卡有多少个科技部老师
1387   - var kjbCount = allKjbsyjEntities.GroupBy(x => x.Kjbls).Count();
  1395 + var kjbCount = NoTallKjbsyjEntities.GroupBy(x => x.Kjbls).Count();
1388 1396 //添加到人次表里面去
1389   - foreach (var item in allKjbsyjEntities.Select(x => x.Kjbls).Distinct())
  1397 + foreach (var item in NoTallKjbsyjEntities.Select(x => x.Kjbls).Distinct())
1390 1398 {
1391 1399 LqPersonTimesRecordEntity personTimesRecordEntity = new LqPersonTimesRecordEntity
1392 1400 {
... ... @@ -1395,7 +1403,7 @@ namespace NCC.Extend.LqXhHyhk
1395 1403 BusinessType = "耗卡",
1396 1404 PersonType = "科技部老师",
1397 1405 PersonId = item,
1398   - PersonName = allKjbsyjEntities.Where(x => x.Kjbls == item).Select(x => x.Kjblsxm).First(),
  1406 + PersonName = NoTallKjbsyjEntities.Where(x => x.Kjbls == item).Select(x => x.Kjblsxm).First(),
1399 1407 MemberId = entity.Hy,
1400 1408 MemberName = memberInfo.Khmc,
1401 1409 WorkDate = input.hksj,
... ... @@ -1677,5 +1685,214 @@ namespace NCC.Extend.LqXhHyhk
1677 1685 }
1678 1686 #endregion
1679 1687  
  1688 + #region 同步耗卡数据到人次记录表(同步的时候才用)
  1689 + /// <summary>
  1690 + /// 同步耗卡数据到人次记录表(同步的时候才用)
  1691 + /// </summary>
  1692 + /// <remarks>
  1693 + /// 将耗卡记录中的健康师和科技老师业绩数据同步到人次记录表(lq_person_times_record)
  1694 + ///
  1695 + /// 同步逻辑:
  1696 + /// 1. 查询所有有效的耗卡记录
  1697 + /// 2. 对于每条耗卡记录,查询关联的健康师业绩和科技老师业绩
  1698 + /// 3. 对健康师和科技老师进行去重(按人员ID)
  1699 + /// 4. 为每个去重后的健康师和科技老师创建人次记录
  1700 + /// 5. 计算人次数量:1 / 健康师(或科技老师)数量
  1701 + ///
  1702 + /// 字段映射:
  1703 + /// - F_BusinessId: 耗卡ID(lq_xh_hyhk.F_Id)
  1704 + /// - F_BusinessType: "耗卡"
  1705 + /// - F_PersonType: "健康师" 或 "科技老师"
  1706 + /// - F_PersonId: 健康师ID(lq_xh_jksyj.jks)或科技老师ID(lq_xh_kjbsyj.kjbls)
  1707 + /// - F_PersonName: 健康师姓名(lq_xh_jksyj.jksxm)或科技老师姓名(lq_xh_kjbsyj.kjblsxm)
  1708 + /// - F_MemberId: 客户ID(lq_xh_hyhk.hy)
  1709 + /// - F_MemberName: 客户姓名(lq_xh_hyhk.hymc)
  1710 + /// - F_WorkDate: 耗卡时间的日期部分(lq_xh_hyhk.hksj)
  1711 + /// - F_WorkMonth: 耗卡时间的月份部分,格式:YYYYMM(lq_xh_hyhk.hksj)
  1712 + /// - F_Quantity: 人次数量 = 1 / 健康师(或科技老师)数量
  1713 + ///
  1714 + /// 注意事项:
  1715 + /// - 只同步有效的耗卡记录(F_IsEffective = 1)
  1716 + /// - 只同步有效的健康师业绩和科技老师业绩(F_IsEffective = 1)
  1717 + /// - 如果已存在相同业务ID的记录,会先删除再重新插入
  1718 + /// </remarks>
  1719 + /// <param name="consumeId">可选,指定耗卡ID,如果为空则同步所有耗卡数据</param>
  1720 + /// <returns>同步结果,包含同步的记录数</returns>
  1721 + /// <response code="200">同步成功</response>
  1722 + /// <response code="500">服务器内部错误</response>
  1723 + [HttpPost("sync-person-times-record")]
  1724 + public async Task<dynamic> SyncPersonTimesRecord([FromQuery] string consumeId = null)
  1725 + {
  1726 + try
  1727 + {
  1728 + _logger.LogInformation($"开始同步耗卡数据到人次记录表,耗卡ID: {consumeId ?? "全部"}");
  1729 + // 1. 查询耗卡记录
  1730 + var consumeQuery = _db.Queryable<LqXhHyhkEntity>().Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
  1731 + if (!string.IsNullOrEmpty(consumeId))
  1732 + {
  1733 + consumeQuery = consumeQuery.Where(x => x.Id == consumeId);
  1734 + }
  1735 + var consumeList = await consumeQuery.ToListAsync();
  1736 + if (!consumeList.Any())
  1737 + {
  1738 + return new { success = true, message = "没有需要同步的耗卡记录", count = 0 };
  1739 + }
  1740 + _logger.LogInformation($"找到 {consumeList.Count} 条耗卡记录需要同步");
  1741 + // 2. 批量查询关联数据
  1742 + var consumeIds = consumeList.Select(x => x.Id).ToList();
  1743 + // 查询健康师业绩
  1744 + var jksyjList = await _db.Queryable<LqXhJksyjEntity>().Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync();
  1745 +
  1746 + // 查询科技老师业绩
  1747 + var kjbsyjList = await _db.Queryable<LqXhKjbsyjEntity>().Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode()).ToListAsync();
  1748 +
  1749 + // 查询已存在的人次记录(按BusinessId去重,避免重复添加)
  1750 + var existingBusinessIds = await _db.Queryable<LqPersonTimesRecordEntity>()
  1751 + .Where(x => consumeIds.Contains(x.BusinessId) && x.BusinessType == "耗卡" && x.IsEffective == StatusEnum.有效.GetHashCode())
  1752 + .Select(x => x.BusinessId)
  1753 + .Distinct()
  1754 + .ToListAsync();
  1755 +
  1756 + // 3. 构建人次记录列表
  1757 + var personTimesRecords = new List<LqPersonTimesRecordEntity>();
  1758 +
  1759 + foreach (var consume in consumeList)
  1760 + {
  1761 + // 如果该耗卡记录已存在,跳过
  1762 + if (existingBusinessIds.Contains(consume.Id))
  1763 + {
  1764 + _logger.LogInformation($"耗卡记录 {consume.Id} 已存在人次记录,跳过");
  1765 + continue;
  1766 + }
  1767 +
  1768 + if (consume.Hksj == null)
  1769 + {
  1770 + _logger.LogWarning($"耗卡记录 {consume.Id} 的耗卡时间为空,跳过");
  1771 + continue;
  1772 + }
  1773 + var workDate = consume.Hksj.Value.Date; // 工作日期(用于人次统计)
  1774 + var workMonth = consume.Hksj.Value.ToString("yyyyMM"); // 工作月份(用于人头统计)
  1775 +
  1776 + // 处理健康师业绩:去重后计算人次数量(剔除T区健康师)
  1777 + var consumeJksyjList = jksyjList.Where(x => x.Glkdbh == consume.Id
  1778 + && !string.IsNullOrEmpty(x.Jks)
  1779 + && !string.IsNullOrEmpty(x.Jksxm)
  1780 + && (x.Jksxm == null || !x.Jksxm.Contains("T区"))).ToList();
  1781 + if (consumeJksyjList.Any())
  1782 + {
  1783 + // 按健康师ID去重
  1784 + var distinctJksyjList = consumeJksyjList
  1785 + .GroupBy(x => x.Jks)
  1786 + .Select(g => g.First())
  1787 + .ToList();
  1788 +
  1789 + // 计算人次数量:1 / 健康师数量
  1790 + var jksQuantity = distinctJksyjList.Count > 0 ? 1.0m / distinctJksyjList.Count : 0;
  1791 +
  1792 + foreach (var jksyj in distinctJksyjList)
  1793 + {
  1794 + personTimesRecords.Add(new LqPersonTimesRecordEntity
  1795 + {
  1796 + Id = YitIdHelper.NextId().ToString(),
  1797 + BusinessId = consume.Id,
  1798 + BusinessType = "耗卡",
  1799 + PersonType = "健康师",
  1800 + PersonId = jksyj.Jks,
  1801 + PersonName = jksyj.Jksxm,
  1802 + MemberId = consume.Hy,
  1803 + MemberName = consume.Hymc,
  1804 + WorkDate = workDate,
  1805 + WorkMonth = workMonth,
  1806 + Quantity = jksQuantity,
  1807 + CreateTime = DateTime.Now,
  1808 + IsEffective = StatusEnum.有效.GetHashCode()
  1809 + });
  1810 + }
  1811 + }
  1812 +
  1813 + // 处理科技老师业绩:去重后计算人次数量(剔除T区科技老师)
  1814 + var consumeKjbsyjList = kjbsyjList.Where(x => x.Glkdbh == consume.Id
  1815 + && !string.IsNullOrEmpty(x.Kjbls)
  1816 + && !string.IsNullOrEmpty(x.Kjblsxm)
  1817 + && (x.Kjblsxm == null || !x.Kjblsxm.Contains("T区"))).ToList();
  1818 + if (consumeKjbsyjList.Any())
  1819 + {
  1820 + // 按科技老师ID去重
  1821 + var distinctKjbsyjList = consumeKjbsyjList
  1822 + .GroupBy(x => x.Kjbls)
  1823 + .Select(g => g.First())
  1824 + .ToList();
  1825 +
  1826 + // 计算人次数量:1 / 科技老师数量
  1827 + var kjbsQuantity = distinctKjbsyjList.Count > 0 ? 1.0m / distinctKjbsyjList.Count : 0;
  1828 +
  1829 + foreach (var kjbsyj in distinctKjbsyjList)
  1830 + {
  1831 + personTimesRecords.Add(new LqPersonTimesRecordEntity
  1832 + {
  1833 + Id = YitIdHelper.NextId().ToString(),
  1834 + BusinessId = consume.Id,
  1835 + BusinessType = "耗卡",
  1836 + PersonType = "科技老师",
  1837 + PersonId = kjbsyj.Kjbls,
  1838 + PersonName = kjbsyj.Kjblsxm,
  1839 + MemberId = consume.Hy,
  1840 + MemberName = consume.Hymc,
  1841 + WorkDate = workDate,
  1842 + WorkMonth = workMonth,
  1843 + Quantity = kjbsQuantity,
  1844 + CreateTime = DateTime.Now,
  1845 + IsEffective = StatusEnum.有效.GetHashCode()
  1846 + });
  1847 + }
  1848 + }
  1849 + }
  1850 +
  1851 + // 4. 使用事务保存数据
  1852 + var result = await _db.Ado.UseTranAsync(async () =>
  1853 + {
  1854 + // 如果指定了耗卡ID,删除该耗卡的旧记录(用于重新同步)
  1855 + // 如果没有指定耗卡ID,不删除任何记录(因为已经在构建记录列表时跳过了已存在的记录)
  1856 + if (!string.IsNullOrEmpty(consumeId))
  1857 + {
  1858 + await _db.Deleteable<LqPersonTimesRecordEntity>()
  1859 + .Where(x => x.BusinessId == consumeId && x.BusinessType == "耗卡")
  1860 + .ExecuteCommandAsync();
  1861 + }
  1862 +
  1863 + // 批量插入新记录
  1864 + if (personTimesRecords.Any())
  1865 + {
  1866 + // 分批插入,每批1000条
  1867 + var batchSize = 1000;
  1868 + for (int i = 0; i < personTimesRecords.Count; i += batchSize)
  1869 + {
  1870 + var batch = personTimesRecords.Skip(i).Take(batchSize).ToList();
  1871 + await _db.Insertable(batch).ExecuteCommandAsync();
  1872 + }
  1873 + }
  1874 +
  1875 + return personTimesRecords.Count;
  1876 + });
  1877 +
  1878 + if (result.IsSuccess)
  1879 + {
  1880 + _logger.LogInformation($"同步完成,共同步 {result.Data} 条人次记录");
  1881 + return new { success = true, message = "同步成功", count = result.Data };
  1882 + }
  1883 + else
  1884 + {
  1885 + _logger.LogError($"同步失败: {result.ErrorMessage}");
  1886 + throw NCCException.Oh(ErrorCode.COM1000, $"同步失败: {result.ErrorMessage}");
  1887 + }
  1888 + }
  1889 + catch (Exception ex)
  1890 + {
  1891 + _logger.LogError(ex, $"同步耗卡数据到人次记录表失败: {ex.ToString()}");
  1892 + throw NCCException.Oh(ErrorCode.COM1005, $"同步耗卡数据到人次记录表失败: {ex.Message}");
  1893 + }
  1894 + }
  1895 + #endregion
  1896 +
1680 1897 }
1681 1898 }
... ...
sql/拆分库存表为产品表和库存表.sql 0 → 100644
  1 +-- ============================================
  2 +-- 拆分库存表为产品表和库存表
  3 +-- ============================================
  4 +-- 说明:将 lq_inventory 表拆分为 lq_product(产品表)和 lq_inventory(库存表)
  5 +--
  6 +-- 注意:此脚本只创建新表,不迁移数据
  7 +-- ============================================
  8 +
  9 +-- ============================================
  10 +-- 1. 创建产品表(lq_product)
  11 +-- ============================================
  12 +CREATE TABLE IF NOT EXISTS `lq_product` (
  13 + `F_Id` VARCHAR(50) NOT NULL COMMENT '产品ID',
  14 + `F_ProductName` VARCHAR(200) NULL COMMENT '产品名称',
  15 + `F_Price` DECIMAL(18,2) NULL DEFAULT 0 COMMENT '价格',
  16 + `F_ProductCategory` VARCHAR(50) NULL COMMENT '产品类别',
  17 + `F_DepartmentId` VARCHAR(50) NULL COMMENT '归属部门ID',
  18 + `F_StandardUnit` VARCHAR(20) NULL COMMENT '标准单位',
  19 + `F_OnShelfStatus` INT NULL DEFAULT 1 COMMENT '上架状态(1:上架 0:下架)',
  20 + `F_StatisticsCategory` VARCHAR(50) NULL COMMENT '统计分类',
  21 + `F_Warehouse` VARCHAR(100) NULL COMMENT '归属仓库',
  22 + `F_UnitConversion` VARCHAR(200) NULL COMMENT '单位换算',
  23 + `F_SupplierName` VARCHAR(200) NULL COMMENT '供应商名称',
  24 + `F_ContractSignDate` DATETIME NULL COMMENT '合同签订日期',
  25 + `F_ContractEndDate` DATETIME NULL COMMENT '合同结束日期',
  26 + `F_Remark` VARCHAR(1000) NULL COMMENT '备注',
  27 + `F_CreateUser` VARCHAR(50) NULL COMMENT '创建人ID',
  28 + `F_CreateTime` DATETIME NULL COMMENT '创建时间',
  29 + `F_UpdateUser` VARCHAR(50) NULL COMMENT '更新人ID',
  30 + `F_UpdateTime` DATETIME NULL COMMENT '更新时间',
  31 + PRIMARY KEY (`F_Id`),
  32 + INDEX `idx_product_category` (`F_ProductCategory`) COMMENT '产品类别索引',
  33 + INDEX `idx_department` (`F_DepartmentId`) COMMENT '部门索引',
  34 + INDEX `idx_onshelf_status` (`F_OnShelfStatus`) COMMENT '上架状态索引'
  35 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';
  36 +
  37 +-- ============================================
  38 +-- 2. 修改库存表结构(lq_inventory)
  39 +-- ============================================
  40 +-- 注意:MySQL不支持 IF NOT EXISTS 语法,需要先检查字段是否存在
  41 +-- 如果字段已存在,执行对应的ALTER语句会报错,可以忽略
  42 +
  43 +-- 2.1 添加产品ID字段
  44 +-- 如果字段已存在,会报错,可以忽略
  45 +ALTER TABLE `lq_inventory`
  46 +ADD COLUMN `F_ProductId` VARCHAR(50) NULL COMMENT '产品ID(关联产品表)' AFTER `F_Id`;
  47 +
  48 +-- 2.2 添加新字段
  49 +-- 如果字段已存在,会报错,可以忽略
  50 +ALTER TABLE `lq_inventory`
  51 +ADD COLUMN `F_StockInTime` DATETIME NULL COMMENT '入库时间' AFTER `F_Quantity`;
  52 +
  53 +ALTER TABLE `lq_inventory`
  54 +ADD COLUMN `F_ProductionDate` DATETIME NULL COMMENT '生产日期' AFTER `F_StockInTime`;
  55 +
  56 +ALTER TABLE `lq_inventory`
  57 +ADD COLUMN `F_ShelfLife` INT NULL COMMENT '保质期(天数)' AFTER `F_ProductionDate`;
  58 +
  59 +ALTER TABLE `lq_inventory`
  60 +ADD COLUMN `F_BatchNumber` VARCHAR(100) NULL COMMENT '批次号' AFTER `F_ShelfLife`;
  61 +
  62 +-- 2.3 删除产品相关字段(可选,执行前请先备份数据)
  63 +-- 注意:如果字段不存在,会报错,可以忽略
  64 +-- ALTER TABLE `lq_inventory` DROP COLUMN `F_ProductName`;
  65 +-- ALTER TABLE `lq_inventory` DROP COLUMN `F_Price`;
  66 +-- ALTER TABLE `lq_inventory` DROP COLUMN `F_ProductCategory`;
  67 +-- ALTER TABLE `lq_inventory` DROP COLUMN `F_DepartmentId`;
  68 +-- ALTER TABLE `lq_inventory` DROP COLUMN `F_StandardUnit`;
  69 +
  70 +-- 2.4 添加产品ID索引
  71 +-- 如果索引已存在,会报错,可以忽略
  72 +ALTER TABLE `lq_inventory`
  73 +ADD INDEX `idx_product_id` (`F_ProductId`) COMMENT '产品ID索引';
  74 +
  75 +-- ============================================
  76 +-- 注意事项
  77 +-- ============================================
  78 +-- 1. 此脚本只创建新表,不迁移数据
  79 +-- 2. 如果需要删除旧字段,请先备份数据
  80 +-- 3. 建议在测试环境先验证
  81 +-- 4. 执行ALTER语句前,请确认表结构
... ...