FqcInspBillService.cs 15 KB

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