S6ProductionInstructionService.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  3. /// <summary>
  4. /// S6 生产指令单 只读消费服务(Phase 1A)。
  5. ///
  6. /// 数据源:数据中台标准层 mdp_std_work_order_schedule(S2/Production 从工单主表贴源
  7. /// → mdp_stg_schedule → 标准化而来;specification 由 S2 转换层从物料主数据 enrich)。
  8. ///
  9. /// 只读铁律:本服务只 SELECT mdp_std_*,绝不直连源库业务表(工单主表 / 物料主表 / MES 工单表),绝不回退源库、绝不写库。
  10. ///
  11. /// 租户隔离(P0):租户只来自认证后 JWT(AidopTenantScope.ResolveOrThrow),忽略前端传入的任何 tenantId;
  12. /// COUNT / PAGE / DETAIL 三处 SQL 都显式带 tenant_id = @TenantId(mdp_std 实体非 ITenantIdFilter,不依赖框架自动过滤)。
  13. /// 工厂隔离:当前 S6 体系只可靠解析租户、无稳定工厂作用域 → FACTORY_SCOPE = PARTIAL(tenant-only,不硬编码 factory_id)。
  14. /// </summary>
  15. [ApiDescriptionSettings(Order = 305, Description = "生产指令单")]
  16. [Route("api/S6ProductionInstruction")]
  17. [NonUnify]
  18. public class S6ProductionInstructionService : IDynamicApiController, ITransient
  19. {
  20. private readonly ISqlSugarClient _db;
  21. private readonly UserManager _userManager;
  22. public S6ProductionInstructionService(ISqlSugarClient db, UserManager userManager)
  23. {
  24. _db = db;
  25. _userManager = userManager;
  26. }
  27. /// <summary>严格可信租户解析,见 <see cref="Infrastructure.AidopTenantScope.ResolveOrThrow"/>。</summary>
  28. private long ResolveTenantOrThrow() => Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager);
  29. private const string SelectColumns = """
  30. m.id AS Id,
  31. m.work_order AS WorkOrderNo,
  32. m.lot_serial AS LotSerial,
  33. m.item_code AS ItemCode,
  34. m.item_name AS ItemName,
  35. m.specification AS Specification,
  36. m.drawing_no AS DrawingNo,
  37. m.project AS Project,
  38. m.qty_ordered AS OrderQty,
  39. m.qty_completed AS CompletedQty,
  40. m.order_date AS OrderDate,
  41. m.due_date AS DueDate,
  42. m.status AS Status,
  43. m.work_order_type AS WorkOrderType
  44. """;
  45. /// <summary>
  46. /// 生产指令单列表(只读真分页)。
  47. /// </summary>
  48. [DisplayName("生产指令单列表")]
  49. [HttpGet("page")]
  50. public async Task<object> GetPage([FromQuery] S6ProductionInstructionInput input)
  51. {
  52. var page = input.Page <= 0 ? 1 : input.Page;
  53. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  54. var offset = (page - 1) * pageSize;
  55. var where = new List<string> { "1=1" };
  56. var pars = new List<SugarParameter>();
  57. // 强制租户过滤:mdp_std_work_order_schedule 非 ITenantIdFilter,隔离全靠显式 SQL 谓词。
  58. var tid = ResolveTenantOrThrow();
  59. where.Add("m.tenant_id = @TenantId");
  60. where.Add("m.doc_type = 'PROD_TASK'");
  61. where.Add("IFNULL(m.source_system,'') <> 'T8'");
  62. pars.Add(new SugarParameter("@TenantId", tid));
  63. if (!string.IsNullOrWhiteSpace(input.WorkOrderNo))
  64. {
  65. where.Add("m.work_order LIKE @WorkOrderNo");
  66. pars.Add(new SugarParameter("@WorkOrderNo", $"%{input.WorkOrderNo.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.ItemCode))
  74. {
  75. where.Add("m.item_code LIKE @ItemCode");
  76. pars.Add(new SugarParameter("@ItemCode", $"%{input.ItemCode.Trim()}%"));
  77. }
  78. if (!string.IsNullOrWhiteSpace(input.Specification))
  79. {
  80. where.Add("m.specification LIKE @Specification");
  81. pars.Add(new SugarParameter("@Specification", $"%{input.Specification.Trim()}%"));
  82. }
  83. if (!string.IsNullOrWhiteSpace(input.Status))
  84. {
  85. where.Add("m.status = @Status");
  86. pars.Add(new SugarParameter("@Status", input.Status.Trim()));
  87. }
  88. if (!string.IsNullOrWhiteSpace(input.OrderDateStart))
  89. {
  90. where.Add("m.order_date >= @OrderDateStart");
  91. pars.Add(new SugarParameter("@OrderDateStart", input.OrderDateStart.Trim()));
  92. }
  93. if (!string.IsNullOrWhiteSpace(input.OrderDateEnd))
  94. {
  95. // 含止日当天:order_date < 止日 + 1 天
  96. where.Add("m.order_date < DATE_ADD(@OrderDateEnd, INTERVAL 1 DAY)");
  97. pars.Add(new SugarParameter("@OrderDateEnd", input.OrderDateEnd.Trim()));
  98. }
  99. var whereSql = string.Join(" AND ", where);
  100. var total = await _db.Ado.GetIntAsync(
  101. $"SELECT COUNT(1) FROM mdp_std_work_order_schedule m WHERE {whereSql}", pars);
  102. var list = total == 0
  103. ? new List<S6ProductionInstructionRow>()
  104. : await _db.Ado.SqlQueryAsync<S6ProductionInstructionRow>(
  105. $"""
  106. SELECT
  107. {SelectColumns},
  108. pb.bill_id AS BillId,
  109. r.status AS InspectionResultStatus,
  110. r.overall_result AS OverallResult,
  111. (SELECT i.Status FROM ApprovalFlowInstance i WHERE i.BizType='S6_PROCESS_INSPECTION' AND i.BizId=pb.bill_id ORDER BY i.Id DESC LIMIT 1) AS FlowStatusRaw
  112. FROM mdp_std_work_order_schedule m
  113. LEFT JOIN (SELECT work_order_no, MAX(id) AS bill_id FROM ado_s6_process_inspection_bill WHERE tenant_id=@TenantId GROUP BY work_order_no) pb ON pb.work_order_no = m.work_order
  114. LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id = pb.bill_id AND r.tenant_id = @TenantId
  115. WHERE {whereSql}
  116. ORDER BY m.order_date DESC, m.work_order DESC
  117. LIMIT {pageSize} OFFSET {offset}
  118. """,
  119. pars);
  120. return new { total, page, pageSize, list };
  121. }
  122. /// <summary>
  123. /// 生产指令单详情(只读,按工单编号)。查不到 / 跨租户 → 返回 null(不泄漏“无权限”信息)。
  124. /// </summary>
  125. [DisplayName("生产指令单详情")]
  126. [HttpGet("detail")]
  127. public async Task<S6ProductionInstructionRow?> GetDetail([FromQuery] string workOrderNo)
  128. {
  129. if (string.IsNullOrWhiteSpace(workOrderNo)) return null;
  130. var tid = ResolveTenantOrThrow();
  131. var pars = new List<SugarParameter>
  132. {
  133. new("@TenantId", tid),
  134. new("@WorkOrderNo", workOrderNo.Trim())
  135. };
  136. return await _db.Ado.SqlQuerySingleAsync<S6ProductionInstructionRow>(
  137. $"""
  138. SELECT
  139. {SelectColumns}
  140. FROM mdp_std_work_order_schedule m
  141. WHERE m.tenant_id = @TenantId AND m.work_order = @WorkOrderNo
  142. AND m.doc_type = 'PROD_TASK' AND IFNULL(m.source_system,'') <> 'T8'
  143. LIMIT 1
  144. """,
  145. pars);
  146. }
  147. }