FqcInspBillService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. [AllowAnonymous]
  21. [NonUnify]
  22. public class FqcInspBillService : IDynamicApiController, ITransient
  23. {
  24. private readonly ISqlSugarClient _db;
  25. private readonly UserManager _userManager;
  26. private readonly FqcMdpSyncService _fqcMdpSyncService;
  27. public FqcInspBillService(ISqlSugarClient db, UserManager userManager, FqcMdpSyncService fqcMdpSyncService)
  28. {
  29. _db = db;
  30. _userManager = userManager;
  31. _fqcMdpSyncService = fqcMdpSyncService;
  32. }
  33. /// <summary>检验任务列表 排序白名单(前端 prop → std 物理列,杜绝 orderBy 注入)。</summary>
  34. private static readonly Dictionary<string, string> TaskOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  35. {
  36. ["applyTime"] = "t.apply_time",
  37. ["productionBatchNo"] = "t.production_batch_no",
  38. ["productionOrderNo"] = "t.production_order_no",
  39. ["materialCode"] = "t.material_code",
  40. ["priority"] = "t.priority",
  41. ["inspectStartTime"] = "t.inspect_start_time",
  42. ["inspectFinishTime"] = "t.inspect_finish_time",
  43. ["inspectProgress"] = "t.inspect_progress",
  44. };
  45. /// <summary>检验单列表 排序白名单(前端 prop → std 物理列)。</summary>
  46. private static readonly Dictionary<string, string> ResultOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  47. {
  48. ["inspectTime"] = "a.inspect_time",
  49. ["billNo"] = "a.bill_no",
  50. ["productionBatchNo"] = "a.production_batch_no",
  51. ["materialCode"] = "a.material_code",
  52. ["inspector"] = "a.inspector",
  53. ["inspectQty"] = "a.inspect_qty",
  54. ["productionOrderNo"] = "a.production_order_no",
  55. };
  56. /// <summary>
  57. /// FQC 数据中台同步刷新(RunFullAsync:只读源 qms_fqcbj/qms_qcpp_inspbill/qms_qcpp_inspbillst,仅写 mdp_*)。
  58. /// </summary>
  59. [DisplayName("FQC数据中台刷新")]
  60. [HttpPost("mdp/refresh")]
  61. public async Task<object> RefreshMdp(CancellationToken cancellationToken)
  62. {
  63. var r = await _fqcMdpSyncService.RunFullAsync(cancellationToken, "MANUAL");
  64. return new { ok = true, batchId = r.BatchId, stageRows = r.StageRows, taskRows = r.TaskRows, resultRows = r.ResultRows, detailRows = r.DetailRows };
  65. }
  66. /// <summary>
  67. /// FQC 检验任务列表(只读分页查询,读标准层 mdp_std_fqc_task)。
  68. /// </summary>
  69. [DisplayName("FQC检验任务列表")]
  70. [HttpGet("task-list")]
  71. public async Task<object> GetTaskList([FromQuery] FqcTaskListInput input)
  72. {
  73. var page = input.Page <= 0 ? 1 : input.Page;
  74. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  75. var offset = (page - 1) * pageSize;
  76. var tenantId = QmsTenantScope.Resolve(input.TenantId, _userManager.TenantId);
  77. var where = new List<string> { "t.tenant_id = @TenantFilter" };
  78. var pars = new List<SugarParameter> { new("@TenantFilter", tenantId) };
  79. void AddLike(string? v, string col, string p)
  80. {
  81. if (string.IsNullOrWhiteSpace(v)) return;
  82. where.Add($"{col} LIKE @{p}");
  83. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  84. }
  85. AddLike(input.ProductionOrderNo, "t.production_order_no", "ProductionOrderNo");
  86. AddLike(input.ProductionBatchNo, "t.production_batch_no", "ProductionBatchNo");
  87. AddLike(input.MaterialCode, "t.material_code", "MaterialCode");
  88. AddLike(input.Priority, "t.priority", "Priority");
  89. AddLike(input.Inspector, "t.inspector", "Inspector");
  90. if (!string.IsNullOrWhiteSpace(input.InspectProgress))
  91. {
  92. where.Add("t.inspect_progress = @InspectProgress");
  93. pars.Add(new SugarParameter("@InspectProgress", input.InspectProgress.Trim()));
  94. }
  95. if (!string.IsNullOrWhiteSpace(input.ApplyTimeStart))
  96. {
  97. where.Add("t.apply_time >= @ApplyTimeStart");
  98. pars.Add(new SugarParameter("@ApplyTimeStart", $"{input.ApplyTimeStart.Trim()} 00:00:00"));
  99. }
  100. if (!string.IsNullOrWhiteSpace(input.ApplyTimeEnd))
  101. {
  102. where.Add("t.apply_time <= @ApplyTimeEnd");
  103. pars.Add(new SugarParameter("@ApplyTimeEnd", $"{input.ApplyTimeEnd.Trim()} 23:59:59"));
  104. }
  105. var whereSql = string.Join(" AND ", where);
  106. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, TaskOrderWhitelist, "t.apply_time DESC, t.id DESC", "t.id");
  107. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_std_fqc_task t WHERE {whereSql}", pars);
  108. var list = await _db.Ado.SqlQueryAsync<FqcTaskListRow>(
  109. $"""
  110. SELECT
  111. CAST(t.source_row_id AS SIGNED) AS Id,
  112. t.bill_no AS BillNo,
  113. t.production_order_no AS ProductionOrderNo,
  114. t.apply_org_id AS ApplyOrgId,
  115. t.apply_time AS ApplyTime,
  116. t.material_code AS MaterialCode,
  117. t.product_name AS ProductName,
  118. t.product_model AS ProductModel,
  119. t.production_batch_no AS ProductionBatchNo,
  120. t.order_qty AS OrderQty,
  121. t.qty AS Qty,
  122. t.applicant AS Applicant,
  123. t.remark AS Remark,
  124. t.priority AS Priority,
  125. t.inspect_start_time AS InspectStartTime,
  126. t.inspect_finish_time AS InspectFinishTime,
  127. t.inspector AS Inspector,
  128. t.inspect_progress AS InspectProgress
  129. FROM mdp_std_fqc_task t
  130. WHERE {whereSql}
  131. ORDER BY {orderSql}
  132. LIMIT {pageSize} OFFSET {offset}
  133. """,
  134. pars);
  135. return new { total, page, pageSize, list };
  136. }
  137. /// <summary>
  138. /// FQC 检验单(结果)列表(只读分页查询,读标准层 mdp_std_fqc_result)。
  139. /// </summary>
  140. [DisplayName("FQC检验单结果列表")]
  141. [HttpGet("result-list")]
  142. public async Task<object> GetResultList([FromQuery] FqcResultListInput input)
  143. {
  144. var page = input.Page <= 0 ? 1 : input.Page;
  145. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  146. var offset = (page - 1) * pageSize;
  147. var tenantId = QmsTenantScope.Resolve(input.TenantId, _userManager.TenantId);
  148. var where = new List<string> { "a.tenant_id = @TenantFilter" };
  149. var pars = new List<SugarParameter> { new("@TenantFilter", tenantId) };
  150. void AddLike(string? v, string col, string p)
  151. {
  152. if (string.IsNullOrWhiteSpace(v)) return;
  153. where.Add($"{col} LIKE @{p}");
  154. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  155. }
  156. AddLike(input.BillNo, "a.bill_no", "BillNo");
  157. AddLike(input.ProductionBatchNo, "a.production_batch_no", "ProductionBatchNo");
  158. AddLike(input.MaterialCode, "a.material_code", "MaterialCode");
  159. AddLike(input.MaterialName, "a.material_name", "MaterialName");
  160. AddLike(input.Inspector, "a.inspector", "Inspector");
  161. AddLike(input.SourceBillNo, "a.source_bill_no", "SourceBillNo");
  162. AddLike(input.ProductionOrderNo, "a.production_order_no", "ProductionOrderNo");
  163. if (!string.IsNullOrWhiteSpace(input.Judgment))
  164. {
  165. where.Add("a.judgment = @Judgment");
  166. pars.Add(new SugarParameter("@Judgment", input.Judgment.Trim()));
  167. }
  168. if (!string.IsNullOrWhiteSpace(input.InspectDateStart))
  169. {
  170. where.Add("a.inspect_time >= @InspectDateStart");
  171. pars.Add(new SugarParameter("@InspectDateStart", $"{input.InspectDateStart.Trim()} 00:00:00"));
  172. }
  173. if (!string.IsNullOrWhiteSpace(input.InspectDateEnd))
  174. {
  175. where.Add("a.inspect_time <= @InspectDateEnd");
  176. pars.Add(new SugarParameter("@InspectDateEnd", $"{input.InspectDateEnd.Trim()} 23:59:59"));
  177. }
  178. var whereSql = string.Join(" AND ", where);
  179. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, ResultOrderWhitelist, "a.inspect_time DESC, a.id DESC", "a.id");
  180. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_std_fqc_result a WHERE {whereSql}", pars);
  181. var list = await _db.Ado.SqlQueryAsync<FqcResultListRow>(
  182. $"""
  183. SELECT
  184. CAST(a.source_row_id AS SIGNED) AS Id,
  185. a.bill_no AS BillNo,
  186. a.inspect_time AS InspectTime,
  187. a.production_batch_no AS ProductionBatchNo,
  188. a.material_code AS MaterialCode,
  189. a.material_name AS MaterialName,
  190. a.spec AS Spec,
  191. a.inspector AS Inspector,
  192. a.inspect_qty AS InspectQty,
  193. a.source_bill_no AS SourceBillNo,
  194. a.production_order_no AS ProductionOrderNo,
  195. a.order_qty AS OrderQty,
  196. a.sap_work_order_no AS SapWorkOrderNo,
  197. a.judgment AS Judgment,
  198. a.qualified_qty AS QualifiedQty,
  199. a.unqualified_qty AS UnqualifiedQty
  200. FROM mdp_std_fqc_result a
  201. WHERE {whereSql}
  202. ORDER BY {orderSql}
  203. LIMIT {pageSize} OFFSET {offset}
  204. """,
  205. pars);
  206. return new { total, page, pageSize, list };
  207. }
  208. /// <summary>
  209. /// FQC 检验单只读详情(读标准层 mdp_std_fqc_result 头 + mdp_std_fqc_result_detail 明细,glid=源头 id)。
  210. /// 契约对齐 S6 IPQC:id&lt;=0 或未命中返回 null(HTTP 200/204);扁平头 + Lines[];样本 j1..j50 收敛 Samples[]。仅 SELECT。
  211. /// </summary>
  212. [DisplayName("FQC检验单详情")]
  213. [HttpGet("detail")]
  214. public async Task<FqcInspBillDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
  215. {
  216. if (id <= 0) return null;
  217. var tid = QmsTenantScope.Resolve(tenantId, _userManager.TenantId);
  218. var head = (await _db.Ado.SqlQueryAsync<FqcInspBillDetailDto>(
  219. """
  220. SELECT
  221. CAST(a.source_row_id AS SIGNED) AS Id,
  222. a.bill_no AS BillNo,
  223. a.inspect_time AS InspectTime,
  224. a.source_bill_no AS SourceBillNo,
  225. a.inspector AS Inspector,
  226. a.judgment AS Judgment,
  227. a.material_code AS MaterialCode,
  228. a.material_name AS MaterialName,
  229. a.spec AS Spec,
  230. a.production_batch_no AS ProductionBatchNo,
  231. a.production_order_no AS ProductionOrderNo,
  232. a.order_qty AS OrderQty,
  233. a.sap_work_order_no AS SapWorkOrderNo,
  234. a.inspect_qty AS InspectQty,
  235. a.qualified_qty AS QualifiedQty,
  236. a.unqualified_qty AS UnqualifiedQty,
  237. a.fire_no AS FireNo,
  238. a.inspect_spec_no AS InspectSpecNo,
  239. a.inspect_spec_version AS InspectSpecVersion
  240. FROM mdp_std_fqc_result a
  241. WHERE a.source_row_id = @id AND a.tenant_id = @TenantFilter
  242. LIMIT 1
  243. """,
  244. new List<SugarParameter> { new("@id", id), new("@TenantFilter", tid) })).FirstOrDefault();
  245. if (head == null || head.Id <= 0) return null;
  246. var jSelect = string.Join(", ", Enumerable.Range(1, 50).Select(i => $"st.j{i}"));
  247. var lineRows = await _db.Ado.SqlQueryAsync<FqcInspBillDetailLineRow>(
  248. $"""
  249. SELECT
  250. st.id AS Id,
  251. st.seq_no AS SeqNo,
  252. st.inspection_item AS InspectionItem,
  253. st.accept_standard AS AcceptStandard,
  254. st.tech_requirement AS TechRequirement,
  255. st.sampling_plan AS SamplingPlan,
  256. st.sample_qty AS SampleQty,
  257. st.upper_limit AS UpperLimit,
  258. st.lower_limit AS LowerLimit,
  259. st.size_specification AS SizeSpecification,
  260. st.judgment AS Judgment,
  261. st.non_numeric_ok AS NonNumericOk,
  262. st.non_numeric_ng AS NonNumericNg,
  263. st.remark AS Remark,
  264. {jSelect}
  265. FROM mdp_std_fqc_result_detail st
  266. WHERE st.glid = @id AND st.tenant_id = @TenantFilter
  267. ORDER BY st.seq_no, st.id
  268. """,
  269. new List<SugarParameter> { new("@id", id), new("@TenantFilter", tid) });
  270. head.Lines = lineRows.Select(r => r.ToLine()).ToList();
  271. return head;
  272. }
  273. /// <summary>
  274. /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
  275. /// </summary>
  276. private static string ResolveOrder(string? orderBy, string? orderDir, Dictionary<string, string> whitelist, string fallback, string tie)
  277. {
  278. if (!string.IsNullOrWhiteSpace(orderBy) && whitelist.TryGetValue(orderBy.Trim(), out var col))
  279. {
  280. var dir = string.Equals(orderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  281. return $"{col} {dir}, {tie} DESC";
  282. }
  283. return fallback;
  284. }
  285. }