ProductionReceiptService.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 生产入库单 只读 list/detail 服务。
  5. ///
  6. /// 数据源:DOP 数据中台标准层 mdp_std_production_receipt(扁平,一行=一条 NbrDetail 明细)。
  7. /// 由 ProductionReceiptMdpSyncService 从 aidopdev.NbrMaster/NbrDetail Type='WOI' 同步标准化而来。
  8. ///
  9. /// Route 用 api/S5ProductionReceipt(S5 前缀),规避 S7 成品仓储未来同名 ProductionReceipt 服务撞名。
  10. /// 本服务仅 SELECT:无新增/编辑/删除/入库确认/库存事务/状态流转。
  11. /// 当前上游未产 WOI 单据,标准层为空,list 返回空数组、detail 返回空数组(前端空态)。
  12. /// </summary>
  13. [ApiDescriptionSettings(Order = 307, Description = "生产入库单")]
  14. [Route("api/S5ProductionReceipt")]
  15. [NonUnify]
  16. public class ProductionReceiptService : IDynamicApiController, ITransient
  17. {
  18. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  19. private const long MainTenantId = 1300000000001L;
  20. private readonly ISqlSugarClient _db;
  21. private readonly UserManager _userManager;
  22. public ProductionReceiptService(ISqlSugarClient db, UserManager userManager)
  23. {
  24. _db = db;
  25. _userManager = userManager;
  26. }
  27. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  28. private long ResolveTenantOrThrow()
  29. {
  30. var tid = _userManager.TenantId;
  31. if (tid <= 0)
  32. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  33. if (_userManager.SuperAdmin && tid == MainTenantId)
  34. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  35. return tid;
  36. }
  37. /// <summary>
  38. /// 生产入库单列表(只读分页查询,扁平:一行=一条明细)。
  39. /// </summary>
  40. [DisplayName("生产入库单列表")]
  41. [HttpGet("list")]
  42. public async Task<object> GetList([FromQuery] ProductionReceiptListInput input)
  43. {
  44. var page = input.Page <= 0 ? 1 : input.Page;
  45. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  46. var offset = (page - 1) * pageSize;
  47. var where = new List<string> { "1=1" };
  48. var pars = new List<SugarParameter>();
  49. // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
  50. var tid = ResolveTenantOrThrow();
  51. where.Add("m.tenant_id = @TenantId");
  52. pars.Add(new SugarParameter("@TenantId", tid));
  53. if (!string.IsNullOrWhiteSpace(input.Nbr))
  54. {
  55. where.Add("m.nbr LIKE @Nbr");
  56. pars.Add(new SugarParameter("@Nbr", $"%{input.Nbr.Trim()}%"));
  57. }
  58. if (!string.IsNullOrWhiteSpace(input.WorkOrd))
  59. {
  60. where.Add("m.work_ord LIKE @WorkOrd");
  61. pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%"));
  62. }
  63. if (!string.IsNullOrWhiteSpace(input.ErpWorkOrd))
  64. {
  65. where.Add("m.erp_work_ord LIKE @ErpWorkOrd");
  66. pars.Add(new SugarParameter("@ErpWorkOrd", $"%{input.ErpWorkOrd.Trim()}%"));
  67. }
  68. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  69. {
  70. where.Add("m.lot_serial LIKE @LotSerial");
  71. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  72. }
  73. if (!string.IsNullOrWhiteSpace(input.DateFrom))
  74. {
  75. where.Add("m.receipt_date >= @DateFrom");
  76. pars.Add(new SugarParameter("@DateFrom", $"{input.DateFrom.Trim()} 00:00:00"));
  77. }
  78. if (!string.IsNullOrWhiteSpace(input.DateTo))
  79. {
  80. where.Add("m.receipt_date <= @DateTo");
  81. pars.Add(new SugarParameter("@DateTo", $"{input.DateTo.Trim()} 23:59:59"));
  82. }
  83. var whereSql = string.Join(" AND ", where);
  84. var total = await _db.Ado.GetIntAsync(
  85. $"SELECT COUNT(1) FROM mdp_std_production_receipt m WHERE {whereSql}", pars);
  86. var list = await _db.Ado.SqlQueryAsync<ProductionReceiptListRow>(
  87. $"""
  88. {SelectColumns()}
  89. FROM mdp_std_production_receipt m
  90. WHERE {whereSql}
  91. ORDER BY {BuildOrderBy(input.OrderBy, input.OrderDir)}
  92. LIMIT {pageSize} OFFSET {offset}
  93. """,
  94. pars);
  95. return new { total, page, pageSize, list };
  96. }
  97. /// <summary>
  98. /// 生产入库单详情(只读:同一入库单号的扁平明细行,按 line 排序)。查不到返回空数组。
  99. /// </summary>
  100. [DisplayName("生产入库单详情")]
  101. [HttpGet("detail")]
  102. public async Task<List<ProductionReceiptListRow>> GetDetail([FromQuery] string nbr)
  103. {
  104. if (string.IsNullOrWhiteSpace(nbr)) return new List<ProductionReceiptListRow>();
  105. // 强制租户过滤:明细行必须归属可信 JWT 租户,忽略前端 tenantId。
  106. var tid = ResolveTenantOrThrow();
  107. var pars = new List<SugarParameter> { new("@Nbr", nbr.Trim()), new("@TenantId", tid) };
  108. var where = "m.nbr = @Nbr AND m.tenant_id = @TenantId";
  109. return await _db.Ado.SqlQueryAsync<ProductionReceiptListRow>(
  110. $"""
  111. {SelectColumns()}
  112. FROM mdp_std_production_receipt m
  113. WHERE {where}
  114. ORDER BY m.line ASC, m.id ASC
  115. """,
  116. pars);
  117. }
  118. /// <summary>统一 SELECT 列(list/detail 共用;扁平行)。</summary>
  119. private static string SelectColumns()
  120. => """
  121. SELECT
  122. m.id AS Id,
  123. m.nbr AS Nbr,
  124. m.line AS Line,
  125. m.receipt_date AS ReceiptDate,
  126. m.status AS Status,
  127. m.status_desc AS StatusDesc,
  128. m.remark AS Remark,
  129. m.prod_line AS ProdLine,
  130. m.work_ord AS WorkOrd,
  131. m.department_desc AS DepartmentDesc,
  132. m.erp_work_ord AS ErpWorkOrd,
  133. m.item_num AS ItemNum,
  134. m.item_name AS ItemName,
  135. m.item_spec AS ItemSpec,
  136. m.location_to AS LocationTo,
  137. m.location_to_desc AS LocationToDesc,
  138. m.lot_serial AS LotSerial,
  139. m.qty_rec AS QtyRec,
  140. m.um AS Um,
  141. m.ord_nbr AS OrdNbr,
  142. m.applicant_name AS ApplicantName
  143. """;
  144. /// <summary>
  145. /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入;非法字段回落默认。
  146. /// </summary>
  147. private static string BuildOrderBy(string? orderBy, string? orderDir)
  148. {
  149. var column = orderBy switch
  150. {
  151. "nbr" => "m.nbr",
  152. "receiptDate" => "m.receipt_date",
  153. "status" => "m.status",
  154. "workOrd" => "m.work_ord",
  155. "erpWorkOrd" => "m.erp_work_ord",
  156. "lotSerial" => "m.lot_serial",
  157. "itemNum" => "m.item_num",
  158. "createTime" => "m.create_time",
  159. _ => "m.receipt_date",
  160. };
  161. var direction = string.Equals(orderDir, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  162. return $"{column} {direction}, m.id DESC";
  163. }
  164. }