FqcInspBillService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. using System.Linq;
  2. using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  5. /// <summary>
  6. /// S7 FQC 成品质量:检验任务列表 / 检验单(结果)列表 / 检验单详情 只读 list 服务。
  7. ///
  8. /// 数据源(2026-07-30 起):DOP 数据中台标准层(CTO 拍板:可接受批次快照延迟、全迁 std):
  9. /// - task-list :mdp_std_fqc_task(源 qms_fqcbj)
  10. /// - result-list:mdp_std_fqc_result(源 qms_qcpp_inspbill + WorkOrdMaster 富化 SAP 工单号)
  11. /// - detail :mdp_std_fqc_result 头 + mdp_std_fqc_result_detail 明细(glid=源头 id;j1..j50 样本 → Samples[])
  12. /// 三 std 由 FqcMdpSyncService 从源同步标准化而来,且已双源(dopdemorq status=0 就位不启用)。
  13. ///
  14. /// 只读边界:纯 SELECT + POST mdp/refresh 触发同步(RunFullAsync,只读源/仅写 mdp_*);
  15. /// 绝不 INSERT/UPDATE/DELETE 业务表、不接 ApprovalFlow、不做录入/审核/处置。WP0-D4:按 tenant_id 过滤(std 保留源 tenant)。
  16. /// id 语义:list 返回源主键(source_row_id),detail 按 source_row_id/glid 关联,前端契约不变。
  17. /// </summary>
  18. [ApiDescriptionSettings(Order = 330, Description = "FQC检验单列表")]
  19. [Route("api/S7FqcInspBill")]
  20. [NonUnify]
  21. public class FqcInspBillService : IDynamicApiController, ITransient
  22. {
  23. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  24. private const long MainTenantId = 1300000000001L;
  25. private readonly ISqlSugarClient _db;
  26. private readonly UserManager _userManager;
  27. private readonly FqcMdpSyncService _fqcMdpSyncService;
  28. public FqcInspBillService(ISqlSugarClient db, UserManager userManager, FqcMdpSyncService fqcMdpSyncService)
  29. {
  30. _db = db;
  31. _userManager = userManager;
  32. _fqcMdpSyncService = fqcMdpSyncService;
  33. }
  34. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  35. private long ResolveTenantOrThrow()
  36. {
  37. var tid = _userManager.TenantId;
  38. if (tid <= 0)
  39. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  40. if (_userManager.SuperAdmin && tid == MainTenantId)
  41. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  42. return tid;
  43. }
  44. /// <summary>检验任务列表 排序白名单(前端 prop → std 物理列,杜绝 orderBy 注入)。</summary>
  45. private static readonly Dictionary<string, string> TaskOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  46. {
  47. ["applyTime"] = "t.apply_time",
  48. ["productionBatchNo"] = "t.production_batch_no",
  49. ["productionOrderNo"] = "t.production_order_no",
  50. ["materialCode"] = "t.material_code",
  51. ["priority"] = "t.priority",
  52. ["inspectStartTime"] = "t.inspect_start_time",
  53. ["inspectFinishTime"] = "t.inspect_finish_time",
  54. ["inspectProgress"] = "t.inspect_progress",
  55. };
  56. /// <summary>检验单列表 排序白名单(前端 prop → std 物理列)。</summary>
  57. private static readonly Dictionary<string, string> ResultOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  58. {
  59. ["inspectTime"] = "a.inspect_time",
  60. ["billNo"] = "a.bill_no",
  61. ["productionBatchNo"] = "a.production_batch_no",
  62. ["materialCode"] = "a.material_code",
  63. ["inspector"] = "a.inspector",
  64. ["inspectQty"] = "a.inspect_qty",
  65. ["productionOrderNo"] = "a.production_order_no",
  66. };
  67. /// <summary>
  68. /// FQC 数据中台同步刷新(RunFullAsync:只读源 qms_fqcbj/qms_qcpp_inspbill/qms_qcpp_inspbillst,仅写 mdp_*)。
  69. /// </summary>
  70. [DisplayName("FQC数据中台刷新")]
  71. [HttpPost("mdp/refresh")]
  72. public async Task<object> RefreshMdp(CancellationToken cancellationToken)
  73. {
  74. var r = await _fqcMdpSyncService.RunFullAsync(cancellationToken, "MANUAL");
  75. return new { ok = true, batchId = r.BatchId, stageRows = r.StageRows, taskRows = r.TaskRows, resultRows = r.ResultRows, detailRows = r.DetailRows };
  76. }
  77. /// <summary>
  78. /// FQC 检验任务列表(只读分页查询,读标准层 mdp_std_fqc_task)。
  79. /// </summary>
  80. [DisplayName("FQC检验任务列表")]
  81. [HttpGet("task-list")]
  82. public async Task<object> GetTaskList([FromQuery] FqcTaskListInput input)
  83. {
  84. var page = input.Page <= 0 ? 1 : input.Page;
  85. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  86. var offset = (page - 1) * pageSize;
  87. var tid = ResolveTenantOrThrow();
  88. var where = new List<string> { "t.tenant_id = @TenantFilter" };
  89. var pars = new List<SugarParameter> { new("@TenantFilter", tid) };
  90. void AddLike(string? v, string col, string p)
  91. {
  92. if (string.IsNullOrWhiteSpace(v)) return;
  93. where.Add($"{col} LIKE @{p}");
  94. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  95. }
  96. AddLike(input.ProductionOrderNo, "t.production_order_no", "ProductionOrderNo");
  97. AddLike(input.ProductionBatchNo, "t.production_batch_no", "ProductionBatchNo");
  98. AddLike(input.MaterialCode, "t.material_code", "MaterialCode");
  99. AddLike(input.Priority, "t.priority", "Priority");
  100. AddLike(input.Inspector, "t.inspector", "Inspector");
  101. if (!string.IsNullOrWhiteSpace(input.InspectProgress))
  102. {
  103. where.Add("t.inspect_progress = @InspectProgress");
  104. pars.Add(new SugarParameter("@InspectProgress", input.InspectProgress.Trim()));
  105. }
  106. if (!string.IsNullOrWhiteSpace(input.ApplyTimeStart))
  107. {
  108. where.Add("t.apply_time >= @ApplyTimeStart");
  109. pars.Add(new SugarParameter("@ApplyTimeStart", $"{input.ApplyTimeStart.Trim()} 00:00:00"));
  110. }
  111. if (!string.IsNullOrWhiteSpace(input.ApplyTimeEnd))
  112. {
  113. where.Add("t.apply_time <= @ApplyTimeEnd");
  114. pars.Add(new SugarParameter("@ApplyTimeEnd", $"{input.ApplyTimeEnd.Trim()} 23:59:59"));
  115. }
  116. var whereSql = string.Join(" AND ", where);
  117. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, TaskOrderWhitelist, "t.apply_time DESC, t.id DESC", "t.id");
  118. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_std_fqc_task t WHERE {whereSql}", pars);
  119. var list = await _db.Ado.SqlQueryAsync<FqcTaskListRow>(
  120. $"""
  121. SELECT
  122. CAST(t.source_row_id AS SIGNED) AS Id,
  123. t.bill_no AS BillNo,
  124. t.production_order_no AS ProductionOrderNo,
  125. t.apply_org_id AS ApplyOrgId,
  126. t.apply_time AS ApplyTime,
  127. t.material_code AS MaterialCode,
  128. t.product_name AS ProductName,
  129. t.product_model AS ProductModel,
  130. t.production_batch_no AS ProductionBatchNo,
  131. t.order_qty AS OrderQty,
  132. t.qty AS Qty,
  133. t.applicant AS Applicant,
  134. t.remark AS Remark,
  135. t.priority AS Priority,
  136. t.inspect_start_time AS InspectStartTime,
  137. t.inspect_finish_time AS InspectFinishTime,
  138. t.inspector AS Inspector,
  139. t.inspect_progress AS InspectProgress
  140. FROM mdp_std_fqc_task t
  141. WHERE {whereSql}
  142. ORDER BY {orderSql}
  143. LIMIT {pageSize} OFFSET {offset}
  144. """,
  145. pars);
  146. return new { total, page, pageSize, list };
  147. }
  148. /// <summary>
  149. /// FQC 检验单(结果)列表(只读分页查询,读标准层 mdp_std_fqc_result)。
  150. /// </summary>
  151. [DisplayName("FQC检验单结果列表")]
  152. [HttpGet("result-list")]
  153. public async Task<object> GetResultList([FromQuery] FqcResultListInput input)
  154. {
  155. var page = input.Page <= 0 ? 1 : input.Page;
  156. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  157. var offset = (page - 1) * pageSize;
  158. var tid = ResolveTenantOrThrow();
  159. var where = new List<string> { "a.tenant_id = @TenantFilter" };
  160. var pars = new List<SugarParameter> { new("@TenantFilter", tid) };
  161. void AddLike(string? v, string col, string p)
  162. {
  163. if (string.IsNullOrWhiteSpace(v)) return;
  164. where.Add($"{col} LIKE @{p}");
  165. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  166. }
  167. AddLike(input.BillNo, "a.bill_no", "BillNo");
  168. AddLike(input.ProductionBatchNo, "a.production_batch_no", "ProductionBatchNo");
  169. AddLike(input.MaterialCode, "a.material_code", "MaterialCode");
  170. AddLike(input.MaterialName, "a.material_name", "MaterialName");
  171. AddLike(input.Inspector, "a.inspector", "Inspector");
  172. AddLike(input.SourceBillNo, "a.source_bill_no", "SourceBillNo");
  173. AddLike(input.ProductionOrderNo, "a.production_order_no", "ProductionOrderNo");
  174. if (!string.IsNullOrWhiteSpace(input.Judgment))
  175. {
  176. where.Add("a.judgment = @Judgment");
  177. pars.Add(new SugarParameter("@Judgment", input.Judgment.Trim()));
  178. }
  179. if (!string.IsNullOrWhiteSpace(input.InspectDateStart))
  180. {
  181. where.Add("a.inspect_time >= @InspectDateStart");
  182. pars.Add(new SugarParameter("@InspectDateStart", $"{input.InspectDateStart.Trim()} 00:00:00"));
  183. }
  184. if (!string.IsNullOrWhiteSpace(input.InspectDateEnd))
  185. {
  186. where.Add("a.inspect_time <= @InspectDateEnd");
  187. pars.Add(new SugarParameter("@InspectDateEnd", $"{input.InspectDateEnd.Trim()} 23:59:59"));
  188. }
  189. var whereSql = string.Join(" AND ", where);
  190. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, ResultOrderWhitelist, "a.inspect_time DESC, a.id DESC", "a.id");
  191. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_std_fqc_result a WHERE {whereSql}", pars);
  192. var list = await _db.Ado.SqlQueryAsync<FqcResultListRow>(
  193. $"""
  194. SELECT
  195. CAST(a.source_row_id AS SIGNED) AS Id,
  196. a.bill_no AS BillNo,
  197. a.inspect_time AS InspectTime,
  198. a.production_batch_no AS ProductionBatchNo,
  199. a.material_code AS MaterialCode,
  200. a.material_name AS MaterialName,
  201. a.spec AS Spec,
  202. a.inspector AS Inspector,
  203. a.inspect_qty AS InspectQty,
  204. a.source_bill_no AS SourceBillNo,
  205. a.production_order_no AS ProductionOrderNo,
  206. a.order_qty AS OrderQty,
  207. a.sap_work_order_no AS SapWorkOrderNo,
  208. a.judgment AS Judgment,
  209. a.qualified_qty AS QualifiedQty,
  210. a.unqualified_qty AS UnqualifiedQty
  211. FROM mdp_std_fqc_result a
  212. WHERE {whereSql}
  213. ORDER BY {orderSql}
  214. LIMIT {pageSize} OFFSET {offset}
  215. """,
  216. pars);
  217. return new { total, page, pageSize, list };
  218. }
  219. /// <summary>
  220. /// FQC 检验单只读详情(读标准层 mdp_std_fqc_result 头 + mdp_std_fqc_result_detail 明细,glid=源头 id)。
  221. /// 契约对齐 S6 IPQC:id&lt;=0 或未命中返回 null(HTTP 200/204);扁平头 + Lines[];样本 j1..j50 收敛 Samples[]。仅 SELECT。
  222. /// </summary>
  223. [DisplayName("FQC检验单详情")]
  224. [HttpGet("detail")]
  225. public async Task<FqcInspBillDetailDto?> GetDetail([FromQuery] long id)
  226. {
  227. if (id <= 0) return null;
  228. var tid = ResolveTenantOrThrow();
  229. var head = (await _db.Ado.SqlQueryAsync<FqcInspBillDetailDto>(
  230. """
  231. SELECT
  232. CAST(a.source_row_id AS SIGNED) AS Id,
  233. a.bill_no AS BillNo,
  234. a.inspect_time AS InspectTime,
  235. a.source_bill_no AS SourceBillNo,
  236. a.inspector AS Inspector,
  237. a.judgment AS Judgment,
  238. a.material_code AS MaterialCode,
  239. a.material_name AS MaterialName,
  240. a.spec AS Spec,
  241. a.production_batch_no AS ProductionBatchNo,
  242. a.production_order_no AS ProductionOrderNo,
  243. a.order_qty AS OrderQty,
  244. a.sap_work_order_no AS SapWorkOrderNo,
  245. a.inspect_qty AS InspectQty,
  246. a.qualified_qty AS QualifiedQty,
  247. a.unqualified_qty AS UnqualifiedQty,
  248. a.fire_no AS FireNo,
  249. a.inspect_spec_no AS InspectSpecNo,
  250. a.inspect_spec_version AS InspectSpecVersion
  251. FROM mdp_std_fqc_result a
  252. WHERE a.source_row_id = @id AND a.tenant_id = @TenantFilter
  253. LIMIT 1
  254. """,
  255. new List<SugarParameter> { new("@id", id), new("@TenantFilter", tid) })).FirstOrDefault();
  256. if (head == null || head.Id <= 0) return null;
  257. var jSelect = string.Join(", ", Enumerable.Range(1, 50).Select(i => $"st.j{i}"));
  258. var lineRows = await _db.Ado.SqlQueryAsync<FqcInspBillDetailLineRow>(
  259. $"""
  260. SELECT
  261. st.id AS Id,
  262. st.seq_no AS SeqNo,
  263. st.inspection_item AS InspectionItem,
  264. st.accept_standard AS AcceptStandard,
  265. st.tech_requirement AS TechRequirement,
  266. st.sampling_plan AS SamplingPlan,
  267. st.sample_qty AS SampleQty,
  268. st.upper_limit AS UpperLimit,
  269. st.lower_limit AS LowerLimit,
  270. st.size_specification AS SizeSpecification,
  271. st.judgment AS Judgment,
  272. st.non_numeric_ok AS NonNumericOk,
  273. st.non_numeric_ng AS NonNumericNg,
  274. st.remark AS Remark,
  275. {jSelect}
  276. FROM mdp_std_fqc_result_detail st
  277. WHERE st.glid = @id AND st.tenant_id = @TenantFilter
  278. ORDER BY st.seq_no, st.id
  279. """,
  280. new List<SugarParameter> { new("@id", id), new("@TenantFilter", tid) });
  281. head.Lines = lineRows.Select(r => r.ToLine()).ToList();
  282. return head;
  283. }
  284. /// <summary>
  285. /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
  286. /// </summary>
  287. private static string ResolveOrder(string? orderBy, string? orderDir, Dictionary<string, string> whitelist, string fallback, string tie)
  288. {
  289. if (!string.IsNullOrWhiteSpace(orderBy) && whitelist.TryGetValue(orderBy.Trim(), out var col))
  290. {
  291. var dir = string.Equals(orderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  292. return $"{col} {dir}, {tie} DESC";
  293. }
  294. return fallback;
  295. }
  296. }