IpqcInspectionService.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  3. /// <summary>
  4. /// S6 过程检验单(IPQC)只读 list 服务。
  5. ///
  6. /// 数据源:DOP 数据中台标准层 mdp_std_ipqc_inspection(头)。
  7. /// 由 IpqcInspectionMdpSyncService 从归口后的 aidopdev.qms_gcjyd(过程检验单头)同步标准化而来。
  8. ///
  9. /// 本服务仅 SELECT:无新增/编辑/删除/写入。
  10. /// 当前上游尚未抽数,归口源表 qms_gcjyd 为空 → 标准层为空 → list 返回空数组(前端空态)。
  11. /// 明细(qms_gcjydzb)与详情不在本批范围,留待 IPQC-INSPECTION-DETAIL-READONLY-1。
  12. /// </summary>
  13. [ApiDescriptionSettings(Order = 306, Description = "过程检验单")]
  14. [Route("api/IpqcInspection")]
  15. [NonUnify]
  16. public class IpqcInspectionService : 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 IpqcInspectionService(ISqlSugarClient db, UserManager userManager)
  23. {
  24. _db = db;
  25. _userManager = userManager;
  26. }
  27. /// <summary>
  28. /// IPQC 只读查询的可信租户解析(严格语义,无任何默认回退、不读前端 tenantId):
  29. /// - 普通租户用户:返回认证后的 JWT TenantId(&gt;0);
  30. /// - 无 Token(TenantId&lt;=0):拒绝(正常已被认证层 401 拦截,此处为纵深防御);
  31. /// - 超管未选择目标租户(TenantId==主租户):拒绝,必须先选择目标租户。
  32. /// 前端传入的 tenantId 一律忽略,绝不作为切换租户的权威来源。
  33. /// </summary>
  34. private long ResolveTenantOrThrow()
  35. {
  36. var tid = _userManager.TenantId;
  37. if (tid <= 0)
  38. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  39. if (_userManager.SuperAdmin && tid == MainTenantId)
  40. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  41. return tid;
  42. }
  43. /// <summary>
  44. /// 过程检验单列表(只读分页查询)。
  45. /// </summary>
  46. [DisplayName("过程检验单列表")]
  47. [HttpGet("list")]
  48. public async Task<object> GetList([FromQuery] IpqcInspectionListInput input)
  49. {
  50. var page = input.Page <= 0 ? 1 : input.Page;
  51. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  52. var offset = (page - 1) * pageSize;
  53. var where = new List<string> { "1=1" };
  54. var pars = new List<SugarParameter>();
  55. // 强制租户过滤:qms_gcjyd/qms_gcjydzb 全库共享无 tenant,标准层按源 tenant_id 落库。
  56. // 租户只来自认证后的可信上下文(严格 JWT),忽略前端传入的 input.TenantId,杜绝跨租户可见。
  57. var tid = ResolveTenantOrThrow();
  58. where.Add("m.tenant_id = @TenantId");
  59. pars.Add(new SugarParameter("@TenantId", tid));
  60. if (!string.IsNullOrWhiteSpace(input.BillNo))
  61. {
  62. where.Add("m.bill_no LIKE @BillNo");
  63. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  64. }
  65. if (!string.IsNullOrWhiteSpace(input.ProductionBatchNo))
  66. {
  67. where.Add("m.production_batch_no LIKE @ProductionBatchNo");
  68. pars.Add(new SugarParameter("@ProductionBatchNo", $"%{input.ProductionBatchNo.Trim()}%"));
  69. }
  70. var whereSql = string.Join(" AND ", where);
  71. var total = await _db.Ado.GetIntAsync(
  72. $"SELECT COUNT(1) FROM mdp_std_ipqc_inspection m WHERE {whereSql}", pars);
  73. var list = await _db.Ado.SqlQueryAsync<IpqcInspectionListRow>(
  74. $"""
  75. SELECT
  76. m.id AS Id,
  77. m.bill_no AS BillNo,
  78. m.product_model AS ProductModel,
  79. m.production_batch_no AS ProductionBatchNo,
  80. m.production_work_order AS ProductionWorkOrder,
  81. m.result_judgement AS ResultJudgement,
  82. m.attachment AS Attachment,
  83. m.remark AS Remark,
  84. m.inspector AS Inspector,
  85. m.process_code AS ProcessCode,
  86. m.process_name AS ProcessName,
  87. m.production_person AS ProductionPerson,
  88. m.sample_qty AS SampleQty,
  89. m.form_no AS FormNo,
  90. m.version_no AS VersionNo,
  91. m.effective_date AS EffectiveDate,
  92. m.material_code AS MaterialCode,
  93. m.material_name AS MaterialName,
  94. m.inspec_standard_version AS InspecStandardVersion,
  95. m.inspec_standard_code AS InspecStandardCode,
  96. CAST(m.source_row_id AS SIGNED) AS SourceRowId
  97. FROM mdp_std_ipqc_inspection m
  98. WHERE {whereSql}
  99. ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)}
  100. LIMIT {pageSize} OFFSET {offset}
  101. """,
  102. pars);
  103. return new { total, page, pageSize, list };
  104. }
  105. /// <summary>
  106. /// 过程检验单详情(只读:头 + 明细)。查不到返回 null。
  107. /// </summary>
  108. [DisplayName("过程检验单详情")]
  109. [HttpGet("detail")]
  110. public async Task<IpqcInspectionDetailDto?> GetDetail([FromQuery] long id)
  111. {
  112. if (id <= 0) return null;
  113. // 强制租户过滤(同 GetList):租户只来自认证后的可信上下文,忽略前端 tenantId,杜绝跨租户读取详情。
  114. var tid = ResolveTenantOrThrow();
  115. var headPars = new List<SugarParameter> { new("@Id", id), new("@TenantId", tid) };
  116. var headWhere = "m.id = @Id AND m.tenant_id = @TenantId";
  117. var head = await _db.Ado.SqlQuerySingleAsync<IpqcInspectionDetailDto>(
  118. $"""
  119. SELECT
  120. m.id AS Id,
  121. m.bill_no AS BillNo,
  122. m.product_model AS ProductModel,
  123. m.production_batch_no AS ProductionBatchNo,
  124. m.production_work_order AS ProductionWorkOrder,
  125. m.result_judgement AS ResultJudgement,
  126. m.attachment AS Attachment,
  127. m.remark AS Remark,
  128. m.inspector AS Inspector,
  129. m.process_code AS ProcessCode,
  130. m.process_name AS ProcessName,
  131. m.production_person AS ProductionPerson,
  132. m.sample_qty AS SampleQty,
  133. m.form_no AS FormNo,
  134. m.version_no AS VersionNo,
  135. m.effective_date AS EffectiveDate,
  136. m.material_code AS MaterialCode,
  137. m.material_name AS MaterialName,
  138. m.inspec_standard_version AS InspecStandardVersion,
  139. m.inspec_standard_code AS InspecStandardCode,
  140. m.inspection_status AS InspectionStatus,
  141. CAST(m.source_row_id AS SIGNED) AS SourceRowId
  142. FROM mdp_std_ipqc_inspection m
  143. WHERE {headWhere}
  144. LIMIT 1
  145. """,
  146. headPars);
  147. if (head == null || head.Id <= 0) return null;
  148. head.Lines = await _db.Ado.SqlQueryAsync<IpqcInspectionDetailLineDto>(
  149. """
  150. SELECT
  151. d.id AS Id,
  152. d.inspection_item AS InspectionItem,
  153. d.inspection_basis AS InspectionBasis,
  154. d.technical_standard AS TechnicalStandard,
  155. d.inspection_method AS InspectionMethod,
  156. d.upper_limit AS UpperLimit,
  157. d.lower_limit AS LowerLimit,
  158. d.process_code AS ProcessCode,
  159. d.process_name AS ProcessName,
  160. d.measurement_instrument AS MeasurementInstrument,
  161. d.inspector AS Inspector,
  162. d.inspection_time AS InspectionTime,
  163. d.sample_qty AS SampleQty,
  164. d.inspection_qty AS InspectionQty,
  165. d.unqualified_qty AS UnqualifiedQty,
  166. d.result_judgement AS ResultJudgement,
  167. d.non_numeric_ok AS NonNumericOk,
  168. d.non_numeric_ng AS NonNumericNg,
  169. d.remark AS Remark,
  170. d.attachment AS Attachment
  171. FROM mdp_std_ipqc_inspection_detail d
  172. WHERE d.std_head_id = @HeadId AND d.tenant_id = @TenantId
  173. ORDER BY d.id ASC
  174. """,
  175. new List<SugarParameter> { new("@HeadId", head.Id), new("@TenantId", tid) });
  176. return head;
  177. }
  178. /// <summary>
  179. /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。
  180. /// </summary>
  181. private static string BuildOrderBy(string? sortField, string? sortOrder)
  182. {
  183. var column = sortField switch
  184. {
  185. "billNo" => "m.bill_no",
  186. "effectiveDate" => "m.effective_date",
  187. _ => "m.effective_date",
  188. };
  189. var direction = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  190. return $"{column} {direction}, m.id DESC";
  191. }
  192. }