S6ProductionInstructionService.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. pars.Add(new SugarParameter("@TenantId", tid));
  61. if (!string.IsNullOrWhiteSpace(input.WorkOrderNo))
  62. {
  63. where.Add("m.work_order LIKE @WorkOrderNo");
  64. pars.Add(new SugarParameter("@WorkOrderNo", $"%{input.WorkOrderNo.Trim()}%"));
  65. }
  66. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  67. {
  68. where.Add("m.lot_serial LIKE @LotSerial");
  69. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  70. }
  71. if (!string.IsNullOrWhiteSpace(input.ItemCode))
  72. {
  73. where.Add("m.item_code LIKE @ItemCode");
  74. pars.Add(new SugarParameter("@ItemCode", $"%{input.ItemCode.Trim()}%"));
  75. }
  76. if (!string.IsNullOrWhiteSpace(input.Specification))
  77. {
  78. where.Add("m.specification LIKE @Specification");
  79. pars.Add(new SugarParameter("@Specification", $"%{input.Specification.Trim()}%"));
  80. }
  81. if (!string.IsNullOrWhiteSpace(input.Status))
  82. {
  83. where.Add("m.status = @Status");
  84. pars.Add(new SugarParameter("@Status", input.Status.Trim()));
  85. }
  86. if (!string.IsNullOrWhiteSpace(input.OrderDateStart))
  87. {
  88. where.Add("m.order_date >= @OrderDateStart");
  89. pars.Add(new SugarParameter("@OrderDateStart", input.OrderDateStart.Trim()));
  90. }
  91. if (!string.IsNullOrWhiteSpace(input.OrderDateEnd))
  92. {
  93. // 含止日当天:order_date < 止日 + 1 天
  94. where.Add("m.order_date < DATE_ADD(@OrderDateEnd, INTERVAL 1 DAY)");
  95. pars.Add(new SugarParameter("@OrderDateEnd", input.OrderDateEnd.Trim()));
  96. }
  97. var whereSql = string.Join(" AND ", where);
  98. var total = await _db.Ado.GetIntAsync(
  99. $"SELECT COUNT(1) FROM mdp_std_work_order_schedule m WHERE {whereSql}", pars);
  100. var list = total == 0
  101. ? new List<S6ProductionInstructionRow>()
  102. : await _db.Ado.SqlQueryAsync<S6ProductionInstructionRow>(
  103. $"""
  104. SELECT
  105. {SelectColumns}
  106. FROM mdp_std_work_order_schedule m
  107. WHERE {whereSql}
  108. ORDER BY m.order_date DESC, m.work_order DESC
  109. LIMIT {pageSize} OFFSET {offset}
  110. """,
  111. pars);
  112. return new { total, page, pageSize, list };
  113. }
  114. /// <summary>
  115. /// 生产指令单详情(只读,按工单编号)。查不到 / 跨租户 → 返回 null(不泄漏“无权限”信息)。
  116. /// </summary>
  117. [DisplayName("生产指令单详情")]
  118. [HttpGet("detail")]
  119. public async Task<S6ProductionInstructionRow?> GetDetail([FromQuery] string workOrderNo)
  120. {
  121. if (string.IsNullOrWhiteSpace(workOrderNo)) return null;
  122. var tid = ResolveTenantOrThrow();
  123. var pars = new List<SugarParameter>
  124. {
  125. new("@TenantId", tid),
  126. new("@WorkOrderNo", workOrderNo.Trim())
  127. };
  128. return await _db.Ado.SqlQuerySingleAsync<S6ProductionInstructionRow>(
  129. $"""
  130. SELECT
  131. {SelectColumns}
  132. FROM mdp_std_work_order_schedule m
  133. WHERE m.tenant_id = @TenantId AND m.work_order = @WorkOrderNo
  134. LIMIT 1
  135. """,
  136. pars);
  137. }
  138. }