FqcInspBillService.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  3. /// <summary>
  4. /// S7 FQC 成品质量:检验任务列表 / 检验单(结果)列表 只读 list 服务。
  5. ///
  6. /// 数据源(均 aidopdev,只读):
  7. /// - task-list :qms_fqcbj(37 列,归口自旧系统 dopdemo,空表待上游抽数)。yxj/FINSPECTSTATUS 原样透出,
  8. /// rf_dictionary 优先级字典未接(CTO 拍板舍弃)。默认排序 FAPPLYTIME DESC, id DESC。
  9. /// - result-list:qms_qcpp_inspbill 主表;SAP 工单号经 (SELECT MIN(wo.WorkOrd) FROM WorkOrdMaster wo
  10. /// WHERE wo.Batch = a.sczld) 标量子查询取得(Batch 非唯一仅因 NULL;非空 Batch 现 1:1,
  11. /// MIN 保确定性、杜绝行倍增)。默认排序 FINSPEENDDATE DESC, id DESC。
  12. ///
  13. /// 归口说明(方案 A,复刻已闭环 S6 IPQC IpqcInspectionMdpSyncService.EnsureTablesAsync):
  14. /// qms_fqcbj 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
  15. /// EnsureFqcTaskTableAsync 在 aidopdev 建同构空表(CREATE TABLE IF NOT EXISTS,DDL 例外、仅一次、零 DML),
  16. /// 后续由上游一次性抽数填充,届时无需改代码即随查询流通。id 不设 AUTO_INCREMENT(抽数将携旧系统主键)。
  17. ///
  18. /// 只读边界:纯 SELECT + 一次性 DDL 建空表;绝不 INSERT/UPDATE/DELETE 任何业务表、不写库存/仓储/生产状态、
  19. /// 不接 ApprovalFlow、不做详情/子表(qms_qcpp_inspbillst 留下一批)/录入/审核/处置。两表无 tenant_id,不做租户过滤。
  20. /// </summary>
  21. [ApiDescriptionSettings(Order = 330, Description = "FQC检验单列表")]
  22. [Route("api/S7FqcInspBill")]
  23. [AllowAnonymous]
  24. [NonUnify]
  25. public class FqcInspBillService : IDynamicApiController, ITransient
  26. {
  27. private readonly ISqlSugarClient _db;
  28. /// <summary>归口空表建表守卫:每进程仅尝试一次 CREATE TABLE IF NOT EXISTS。</summary>
  29. private static int _taskTableEnsured;
  30. public FqcInspBillService(ISqlSugarClient db)
  31. {
  32. _db = db;
  33. }
  34. /// <summary>检验任务列表 排序白名单(前端 prop → 物理列,杜绝 orderBy 注入)。</summary>
  35. private static readonly Dictionary<string, string> TaskOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  36. {
  37. ["applyTime"] = "t.FAPPLYTIME",
  38. ["productionBatchNo"] = "t.scph",
  39. ["productionOrderNo"] = "t.sczld",
  40. ["materialCode"] = "t.wlbm",
  41. ["priority"] = "t.yxj",
  42. ["inspectStartTime"] = "t.jykssj",
  43. ["inspectFinishTime"] = "t.jywcsj",
  44. ["inspectProgress"] = "t.FINSPECTSTATUS",
  45. };
  46. /// <summary>检验单列表 排序白名单(前端 prop → 物理列)。</summary>
  47. private static readonly Dictionary<string, string> ResultOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
  48. {
  49. ["inspectTime"] = "a.FINSPEENDDATE",
  50. ["billNo"] = "a.FBILLNO",
  51. ["productionBatchNo"] = "a.scph",
  52. ["materialCode"] = "a.FMATERIALCFG",
  53. ["inspector"] = "a.jyr",
  54. ["inspectQty"] = "a.jysj",
  55. ["productionOrderNo"] = "a.sczld",
  56. };
  57. /// <summary>
  58. /// FQC 检验任务列表(只读分页查询,源 qms_fqcbj)。
  59. /// </summary>
  60. [DisplayName("FQC检验任务列表")]
  61. [HttpGet("task-list")]
  62. public async Task<object> GetTaskList([FromQuery] FqcTaskListInput input)
  63. {
  64. await EnsureFqcTaskTableAsync();
  65. var page = input.Page <= 0 ? 1 : input.Page;
  66. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  67. var offset = (page - 1) * pageSize;
  68. // qms_fqcbj 无 tenant_id:不做租户过滤(TenantId 接收即忽略)。
  69. var where = new List<string> { "1=1" };
  70. var pars = new List<SugarParameter>();
  71. void AddLike(string? v, string col, string p)
  72. {
  73. if (string.IsNullOrWhiteSpace(v)) return;
  74. where.Add($"{col} LIKE @{p}");
  75. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  76. }
  77. AddLike(input.ProductionOrderNo, "t.sczld", "ProductionOrderNo");
  78. AddLike(input.ProductionBatchNo, "t.scph", "ProductionBatchNo");
  79. AddLike(input.MaterialCode, "t.wlbm", "MaterialCode");
  80. AddLike(input.Priority, "t.yxj", "Priority");
  81. AddLike(input.Inspector, "t.jyfzr", "Inspector");
  82. if (!string.IsNullOrWhiteSpace(input.InspectProgress))
  83. {
  84. where.Add("t.FINSPECTSTATUS = @InspectProgress");
  85. pars.Add(new SugarParameter("@InspectProgress", input.InspectProgress.Trim()));
  86. }
  87. if (!string.IsNullOrWhiteSpace(input.ApplyTimeStart))
  88. {
  89. where.Add("t.FAPPLYTIME >= @ApplyTimeStart");
  90. pars.Add(new SugarParameter("@ApplyTimeStart", $"{input.ApplyTimeStart.Trim()} 00:00:00"));
  91. }
  92. if (!string.IsNullOrWhiteSpace(input.ApplyTimeEnd))
  93. {
  94. where.Add("t.FAPPLYTIME <= @ApplyTimeEnd");
  95. pars.Add(new SugarParameter("@ApplyTimeEnd", $"{input.ApplyTimeEnd.Trim()} 23:59:59"));
  96. }
  97. var whereSql = string.Join(" AND ", where);
  98. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, TaskOrderWhitelist, "t.FAPPLYTIME DESC, t.id DESC", "t.id");
  99. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_fqcbj t WHERE {whereSql}", pars);
  100. var list = await _db.Ado.SqlQueryAsync<FqcTaskListRow>(
  101. $"""
  102. SELECT
  103. t.id AS Id,
  104. t.FBILLNO AS BillNo,
  105. t.sczld AS ProductionOrderNo,
  106. t.FORGID AS ApplyOrgId,
  107. t.FAPPLYTIME AS ApplyTime,
  108. t.wlbm AS MaterialCode,
  109. t.cpmc AS ProductName,
  110. t.cpxh AS ProductModel,
  111. t.scph AS ProductionBatchNo,
  112. t.sczldsl AS OrderQty,
  113. t.sl AS Qty,
  114. t.FAPPLYUSER AS Applicant,
  115. t.FCOMMENT AS Remark,
  116. t.yxj AS Priority,
  117. t.jykssj AS InspectStartTime,
  118. t.jywcsj AS InspectFinishTime,
  119. t.jyfzr AS Inspector,
  120. t.FINSPECTSTATUS AS InspectProgress
  121. FROM qms_fqcbj t
  122. WHERE {whereSql}
  123. ORDER BY {orderSql}
  124. LIMIT {pageSize} OFFSET {offset}
  125. """,
  126. pars);
  127. return new { total, page, pageSize, list };
  128. }
  129. /// <summary>
  130. /// FQC 检验单(结果)列表(只读分页查询,源 qms_qcpp_inspbill + WorkOrdMaster 取 SAP 工单号)。
  131. /// </summary>
  132. [DisplayName("FQC检验单结果列表")]
  133. [HttpGet("result-list")]
  134. public async Task<object> GetResultList([FromQuery] FqcResultListInput input)
  135. {
  136. var page = input.Page <= 0 ? 1 : input.Page;
  137. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  138. var offset = (page - 1) * pageSize;
  139. // qms_qcpp_inspbill 无 tenant_id:不做租户过滤(TenantId 接收即忽略)。
  140. var where = new List<string> { "1=1" };
  141. var pars = new List<SugarParameter>();
  142. void AddLike(string? v, string col, string p)
  143. {
  144. if (string.IsNullOrWhiteSpace(v)) return;
  145. where.Add($"{col} LIKE @{p}");
  146. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  147. }
  148. AddLike(input.BillNo, "a.FBILLNO", "BillNo");
  149. AddLike(input.ProductionBatchNo, "a.scph", "ProductionBatchNo");
  150. AddLike(input.MaterialCode, "a.FMATERIALCFG", "MaterialCode");
  151. AddLike(input.MaterialName, "a.wlmc", "MaterialName");
  152. AddLike(input.Inspector, "a.jyr", "Inspector");
  153. AddLike(input.SourceBillNo, "a.lydjbh", "SourceBillNo");
  154. AddLike(input.ProductionOrderNo, "a.sczld", "ProductionOrderNo");
  155. if (!string.IsNullOrWhiteSpace(input.Judgment))
  156. {
  157. where.Add("a.pd = @Judgment");
  158. pars.Add(new SugarParameter("@Judgment", input.Judgment.Trim()));
  159. }
  160. if (!string.IsNullOrWhiteSpace(input.InspectDateStart))
  161. {
  162. where.Add("a.FINSPEENDDATE >= @InspectDateStart");
  163. pars.Add(new SugarParameter("@InspectDateStart", $"{input.InspectDateStart.Trim()} 00:00:00"));
  164. }
  165. if (!string.IsNullOrWhiteSpace(input.InspectDateEnd))
  166. {
  167. where.Add("a.FINSPEENDDATE <= @InspectDateEnd");
  168. pars.Add(new SugarParameter("@InspectDateEnd", $"{input.InspectDateEnd.Trim()} 23:59:59"));
  169. }
  170. var whereSql = string.Join(" AND ", where);
  171. var orderSql = ResolveOrder(input.OrderBy, input.OrderDir, ResultOrderWhitelist, "a.FINSPEENDDATE DESC, a.id DESC", "a.id");
  172. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_qcpp_inspbill a WHERE {whereSql}", pars);
  173. var list = await _db.Ado.SqlQueryAsync<FqcResultListRow>(
  174. $"""
  175. SELECT
  176. a.id AS Id,
  177. a.FBILLNO AS BillNo,
  178. a.FINSPEENDDATE AS InspectTime,
  179. a.scph AS ProductionBatchNo,
  180. a.FMATERIALCFG AS MaterialCode,
  181. a.wlmc AS MaterialName,
  182. a.ggxh AS Spec,
  183. a.jyr AS Inspector,
  184. a.jysj AS InspectQty,
  185. a.lydjbh AS SourceBillNo,
  186. a.sczld AS ProductionOrderNo,
  187. a.sczldsl AS OrderQty,
  188. (SELECT MIN(wo.WorkOrd) FROM WorkOrdMaster wo WHERE wo.Batch = a.sczld) AS SapWorkOrderNo,
  189. a.pd AS Judgment,
  190. a.hgsl AS QualifiedQty,
  191. a.bhgsl AS UnqualifiedQty
  192. FROM qms_qcpp_inspbill a
  193. WHERE {whereSql}
  194. ORDER BY {orderSql}
  195. LIMIT {pageSize} OFFSET {offset}
  196. """,
  197. pars);
  198. return new { total, page, pageSize, list };
  199. }
  200. /// <summary>
  201. /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
  202. /// </summary>
  203. private static string ResolveOrder(string? orderBy, string? orderDir, Dictionary<string, string> whitelist, string fallback, string tie)
  204. {
  205. if (!string.IsNullOrWhiteSpace(orderBy) && whitelist.TryGetValue(orderBy.Trim(), out var col))
  206. {
  207. var dir = string.Equals(orderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  208. return $"{col} {dir}, {tie} DESC";
  209. }
  210. return fallback;
  211. }
  212. /// <summary>
  213. /// 归口空表建表(方案 A,DDL 例外、每进程一次、零 DML):
  214. /// aidopdev 无 qms_fqcbj 时 CREATE TABLE IF NOT EXISTS 建同构 dopdemo 的 37 列空表,待上游抽数填充。
  215. /// 仅建表,不写任何数据行。
  216. /// </summary>
  217. private async Task EnsureFqcTaskTableAsync()
  218. {
  219. if (Interlocked.Exchange(ref _taskTableEnsured, 1) == 1) return;
  220. try
  221. {
  222. await _db.Ado.ExecuteCommandAsync(
  223. """
  224. CREATE TABLE IF NOT EXISTS qms_fqcbj (
  225. id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
  226. FBILLNO VARCHAR(80) NULL COMMENT '单据编号',
  227. FBILLSTATUS VARCHAR(50) NULL COMMENT '单据状态',
  228. FCREATORID BIGINT NULL,
  229. FMODIFIERID BIGINT NULL,
  230. FAUDITORID BIGINT NULL,
  231. FAUDITDATE DATETIME NULL,
  232. FMODIFYTIME DATETIME NULL,
  233. FCREATETIME DATETIME NULL,
  234. FORGID BIGINT NULL COMMENT '申请组织',
  235. FQUALITYORG BIGINT NULL,
  236. FAPPLYUSER VARCHAR(50) NULL COMMENT '申请人',
  237. FAPPLYTIME DATETIME NULL COMMENT '申请时间',
  238. FBILLTYPE VARCHAR(50) NULL COMMENT '单据类型',
  239. FBIZTYPE BIGINT NULL COMMENT '业务类型',
  240. FINSPECORGID BIGINT NULL,
  241. FCOMMENT TEXT NULL COMMENT '备注',
  242. FAUTHORIZEOBJID BIGINT NULL,
  243. FINTERFACEID VARCHAR(50) NULL,
  244. cpmc VARCHAR(255) NULL COMMENT '产品名称',
  245. cpxh VARCHAR(255) NULL COMMENT '产品型号',
  246. scph VARCHAR(255) NULL COMMENT '生产批号',
  247. sl DECIMAL(10,0) NULL COMMENT '数量',
  248. jfbh TEXT NULL COMMENT '击发编号',
  249. wlbm VARCHAR(255) NULL COMMENT '物料编码',
  250. jyfzr VARCHAR(255) NULL COMMENT '检验负责人',
  251. yxj VARCHAR(255) NULL COMMENT '检验优先级',
  252. jykssj DATETIME NULL COMMENT '检验开始时间',
  253. jywcsj DATETIME NULL COMMENT '检验完成时间',
  254. mjpc VARCHAR(255) NULL,
  255. tsyq VARCHAR(255) NULL,
  256. FINSPECTSTATUS VARCHAR(255) NULL COMMENT '检验进度',
  257. sczld VARCHAR(255) NULL COMMENT '生产指令单',
  258. sczldsl DECIMAL(10,0) NULL COMMENT '指令单数量',
  259. lydjbh VARCHAR(100) NULL COMMENT '来源单据编号',
  260. ISscrap INT NULL,
  261. CirculationCard TEXT NULL
  262. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='FQC检验任务(报检) 归口自旧系统dopdemo,空表待上游抽数'
  263. """);
  264. }
  265. catch
  266. {
  267. // 建表失败(如权限/并发)不吞成静默通过:复位守卫,让后续调用可再尝试,并向上抛出真实错误。
  268. Interlocked.Exchange(ref _taskTableEnsured, 0);
  269. throw;
  270. }
  271. }
  272. }