IqcTaskEntryService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. using Yitter.IdGenerator;
  3. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  4. /// <summary>
  5. /// S5 来料检验任务(报检分录)操作:认领 / 调配 / 优先级 / 检验完成 / 生成检验单。
  6. /// 对齐旧 DOP 任务列表(program 396052925665349)行为,数据落本库 qms_qcp_*。
  7. /// </summary>
  8. [ApiDescriptionSettings(Order = 311, Description = "来料检验任务操作")]
  9. [Route("api/S5IqcTaskEntry")]
  10. [NonUnify]
  11. public class IqcTaskEntryService : IDynamicApiController, ITransient
  12. {
  13. private const long MainTenantId = 1300000000001L;
  14. private readonly ISqlSugarClient _db;
  15. private readonly UserManager _userManager;
  16. /// <summary>B-2:生成检验单时解析并固化 S0 原材料检规元数据。只读,不阻断生成。</summary>
  17. private readonly S5IqcSpecResolver _specResolver;
  18. public IqcTaskEntryService(ISqlSugarClient db, UserManager userManager, S5IqcSpecResolver specResolver)
  19. {
  20. _db = db;
  21. _userManager = userManager;
  22. _specResolver = specResolver;
  23. }
  24. private long ResolveTenantOrThrow()
  25. {
  26. var tid = _userManager.TenantId;
  27. if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  28. if (_userManager.SuperAdmin && tid == MainTenantId)
  29. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  30. return tid;
  31. }
  32. [DisplayName("来料检验任务列表(报检分录)")]
  33. [HttpGet("list")]
  34. public async Task<object> GetList([FromQuery] IqcTaskEntryListInput input)
  35. {
  36. var page = input.Page <= 0 ? 1 : input.Page;
  37. var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
  38. var offset = (page - 1) * pageSize;
  39. var tenantId = ResolveTenantOrThrow();
  40. var where = new List<string> { "e.tenant_id = @TenantId" };
  41. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  42. if (!string.IsNullOrWhiteSpace(input.ApplyBillNo))
  43. {
  44. where.Add("a.FBILLNO LIKE @ApplyBillNo");
  45. pars.Add(new SugarParameter("@ApplyBillNo", $"%{input.ApplyBillNo.Trim()}%"));
  46. }
  47. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  48. {
  49. where.Add("e.FMATERIALCFG LIKE @MaterialCode");
  50. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  51. }
  52. if (!string.IsNullOrWhiteSpace(input.LotNo))
  53. {
  54. where.Add("e.FLOTNUMBER LIKE @LotNo");
  55. pars.Add(new SugarParameter("@LotNo", $"%{input.LotNo.Trim()}%"));
  56. }
  57. if (!string.IsNullOrWhiteSpace(input.InspectStatus))
  58. {
  59. where.Add("e.FINSPECTSTATUS = @InspectStatus");
  60. pars.Add(new SugarParameter("@InspectStatus", input.InspectStatus.Trim()));
  61. }
  62. if (!string.IsNullOrWhiteSpace(input.Receiver))
  63. {
  64. where.Add("e.FSRCORDERNUM LIKE @Receiver");
  65. pars.Add(new SugarParameter("@Receiver", $"%{input.Receiver.Trim()}%"));
  66. }
  67. var whereSql = string.Join(" AND ", where);
  68. var total = await _db.Ado.GetIntAsync(
  69. $"""
  70. SELECT COUNT(1)
  71. FROM qms_qcp_insappnentry e
  72. LEFT JOIN qms_qcp_inspecapplyn a ON a.id = e.glid AND a.tenant_id = e.tenant_id
  73. WHERE {whereSql}
  74. """,
  75. pars);
  76. var list = await _db.Ado.SqlQueryAsync<IqcTaskEntryRow>(
  77. $"""
  78. SELECT
  79. e.id AS Id,
  80. e.glid AS Glid,
  81. a.FBILLNO AS ApplyBillNo,
  82. a.FBIZTYPE AS BizType,
  83. e.FMATERIALCFG AS MaterialCode,
  84. e.wlmc AS MaterialName,
  85. e.ggxh AS Spec,
  86. e.FLOTNUMBER AS LotNo,
  87. e.FSRCORDERNUM AS Receiver,
  88. e.FAPPLYQTY AS ApplyQty,
  89. e.FINSPECTSTATUS AS InspectStatus,
  90. e.jyfzr AS OwnerId,
  91. CAST(NULL AS CHAR) AS OwnerName,
  92. e.yxj AS Priority,
  93. e.FSUPPLIER AS SupplierCode,
  94. e.gysmc AS SupplierName,
  95. e.shdh AS ShipmentNo,
  96. b.id AS InspBillId,
  97. b.FBILLNO AS InspBillNo
  98. FROM qms_qcp_insappnentry e
  99. LEFT JOIN qms_qcp_inspecapplyn a ON a.id = e.glid AND a.tenant_id = e.tenant_id
  100. LEFT JOIN qms_qcp_inspbill b ON b.hid = e.id AND b.tenant_id = e.tenant_id
  101. WHERE {whereSql}
  102. ORDER BY IFNULL(e.yxj, 999999) ASC, e.id DESC
  103. LIMIT {pageSize} OFFSET {offset}
  104. """,
  105. pars);
  106. await FillOwnerNamesAsync(list);
  107. return new { total, page, pageSize, list };
  108. }
  109. private async Task FillOwnerNamesAsync(List<IqcTaskEntryRow> list)
  110. {
  111. var ids = list
  112. .Select(x => x.OwnerId)
  113. .Where(x => !string.IsNullOrWhiteSpace(x) && long.TryParse(x, out _))
  114. .Select(x => long.Parse(x!))
  115. .Distinct()
  116. .ToList();
  117. if (ids.Count == 0) return;
  118. var users = await _db.Queryable<SysUser>()
  119. .ClearFilter()
  120. .Where(u => ids.Contains(u.Id))
  121. .Select(u => new { u.Id, u.RealName, u.Account })
  122. .ToListAsync();
  123. var map = users.ToDictionary(u => u.Id.ToString(), u => u.RealName ?? u.Account ?? u.Id.ToString());
  124. foreach (var row in list)
  125. {
  126. if (!string.IsNullOrWhiteSpace(row.OwnerId) && map.TryGetValue(row.OwnerId!, out var name))
  127. row.OwnerName = name;
  128. }
  129. }
  130. [DisplayName("认领检验任务")]
  131. [HttpPost("claim")]
  132. public async Task<object> Claim([FromBody] IqcTaskEntryIdsInput input)
  133. {
  134. var ids = NormalizeIds(input?.Ids);
  135. var tenantId = ResolveTenantOrThrow();
  136. var owner = _userManager.UserId.ToString();
  137. var n = await _db.Ado.ExecuteCommandAsync(
  138. $"""
  139. UPDATE qms_qcp_insappnentry
  140. SET jyfzr = @Owner
  141. WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})
  142. """,
  143. new SugarParameter("@Owner", owner),
  144. new SugarParameter("@TenantId", tenantId));
  145. return new { updated = n, ownerId = owner };
  146. }
  147. [DisplayName("检验员调配")]
  148. [HttpPost("assign")]
  149. public async Task<object> Assign([FromBody] IqcTaskEntryAssignInput input)
  150. {
  151. var ids = NormalizeIds(input?.Ids);
  152. if (input == null || input.InspectorUserId <= 0) throw Oops.Oh("请指定检验负责人");
  153. var tenantId = ResolveTenantOrThrow();
  154. var owner = input.InspectorUserId.ToString();
  155. var n = await _db.Ado.ExecuteCommandAsync(
  156. $"""
  157. UPDATE qms_qcp_insappnentry
  158. SET jyfzr = @Owner
  159. WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})
  160. """,
  161. new SugarParameter("@Owner", owner),
  162. new SugarParameter("@TenantId", tenantId));
  163. return new { updated = n, ownerId = owner };
  164. }
  165. [DisplayName("优先级调整")]
  166. [HttpPost("priority")]
  167. public async Task<object> AdjustPriority([FromBody] IqcTaskEntryPriorityInput input)
  168. {
  169. var ids = NormalizeIds(input?.Ids);
  170. var tenantId = ResolveTenantOrThrow();
  171. var n = await _db.Ado.ExecuteCommandAsync(
  172. $"""
  173. UPDATE qms_qcp_insappnentry
  174. SET yxj = @Priority
  175. WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})
  176. """,
  177. new SugarParameter("@Priority", input!.Priority),
  178. new SugarParameter("@TenantId", tenantId));
  179. return new { updated = n };
  180. }
  181. [DisplayName("检验完成(手工兜底)")]
  182. [HttpPost("complete")]
  183. public async Task<object> MarkComplete([FromBody] IqcTaskEntryIdsInput input)
  184. {
  185. var ids = NormalizeIds(input?.Ids);
  186. var tenantId = ResolveTenantOrThrow();
  187. var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  188. var n = await _db.Ado.ExecuteCommandAsync(
  189. $"""
  190. UPDATE qms_qcp_insappnentry
  191. SET FINSPECTSTATUS = '检验完成', jywcsj = @Now
  192. WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})
  193. """,
  194. new SugarParameter("@Now", now),
  195. new SugarParameter("@TenantId", tenantId));
  196. return new { updated = n };
  197. }
  198. /// <summary>
  199. /// 「来料检验」:校验认领人 + 幂等 → 置检验中 → 本库生成检验单(IQC+yyyyMMdd+4 位)。
  200. /// </summary>
  201. [DisplayName("生成来料检验单")]
  202. [HttpPost("generate-inspbill")]
  203. public async Task<IqcGenerateInspBillOutput> GenerateInspBill([FromBody] IqcGenerateInspBillInput input)
  204. {
  205. if (input == null || input.EntryId <= 0) throw Oops.Oh("报检分录 id 非法");
  206. var tenantId = ResolveTenantOrThrow();
  207. var userId = _userManager.UserId.ToString();
  208. var entry = await _db.Ado.SqlQuerySingleAsync<EntryGenRow>(
  209. """
  210. SELECT
  211. e.id AS Id, e.glid AS Glid, e.jyfzr AS OwnerId, e.FINSPECTSTATUS AS InspectStatus,
  212. e.FMATERIALCFG AS MaterialCode, e.wlmc AS MaterialName, e.ggxh AS Spec,
  213. e.FLOTNUMBER AS LotNo, e.FAPPLYQTY AS ApplyQty, e.gysmc AS SupplierName,
  214. a.FBILLNO AS ApplyBillNo, a.FBIZTYPE AS BizType
  215. FROM qms_qcp_insappnentry e
  216. LEFT JOIN qms_qcp_inspecapplyn a ON a.id = e.glid AND a.tenant_id = e.tenant_id
  217. WHERE e.id = @Id AND e.tenant_id = @TenantId
  218. LIMIT 1
  219. """,
  220. new SugarParameter("@Id", input.EntryId),
  221. new SugarParameter("@TenantId", tenantId));
  222. if (entry == null) throw Oops.Oh("报检分录不存在");
  223. if (string.IsNullOrWhiteSpace(entry.ApplyBillNo)) throw Oops.Oh("报检单号缺失,无法生成检验单");
  224. if (!string.Equals(entry.OwnerId?.Trim(), userId, StringComparison.Ordinal))
  225. throw Oops.Oh("尚未认领或非认领人操作!");
  226. var exists = await _db.Ado.GetIntAsync(
  227. """
  228. SELECT COUNT(1)
  229. FROM qms_qcp_inspbill
  230. WHERE tenant_id = @TenantId AND lydjbh = @ApplyBillNo AND hid = @Hid
  231. """,
  232. new SugarParameter("@TenantId", tenantId),
  233. new SugarParameter("@ApplyBillNo", entry.ApplyBillNo),
  234. new SugarParameter("@Hid", entry.Id));
  235. if (exists > 0) throw Oops.Oh("该检验任务已经有检验单,不能重新生成!");
  236. var billNo = await AllocateIqcBillNoAsync(tenantId);
  237. var billId = YitIdHelper.NextId();
  238. var now = DateTime.Now;
  239. var startText = now.ToString("yyyy-MM-dd HH:mm:ss");
  240. // B-2 检规解析(只读,事务外)。referenceTime 取检验单生成时刻 now——
  241. // 口径对齐 S6(S6ProcessInspectionService.cs:66「ReferenceDate 取检验时点(now)」):
  242. // 快照的是"生成时点当前有效"的检规版本。
  243. //
  244. // ⚠️ Resolver 绝不阻断生成:报检分录是 165 贴源既成事实,Ai-DOP 无权因检规缺失/歧义
  245. // 而拒绝为其建检验单。无检规 / 歧义 / 仅未来版本 → 四列留 NULL,行为与 B-2 之前完全一致。
  246. // 真正的系统错误(DB 不可用 / SQL 异常)仍按异常向上抛,不吞。
  247. var spec = await _specResolver.ResolveAsync(tenantId, entry.MaterialCode, now);
  248. var tran = await _db.Ado.UseTranAsync(async () =>
  249. {
  250. await _db.Ado.ExecuteCommandAsync(
  251. """
  252. UPDATE qms_qcp_insappnentry
  253. SET jykssj = @Start, FINSPECTSTATUS = '检验中'
  254. WHERE id = @Id AND tenant_id = @TenantId
  255. """,
  256. new SugarParameter("@Start", startText),
  257. new SugarParameter("@Id", entry.Id),
  258. new SugarParameter("@TenantId", tenantId));
  259. await _db.Ado.ExecuteCommandAsync(
  260. """
  261. INSERT INTO qms_qcp_inspbill (
  262. id, tenant_id, FBILLNO, lydjbh, hid, FMATERIALCFG, wlmc, ggxh, gysmc, pch,
  263. FRINSQTY, jysl, dhsl, bhgsl, FBILLSTATUS, jyr, FINSPECTORID,
  264. FINSPESTARTDATE, FCREATETIME,
  265. jgbh, jgbb, jgsxrq, jygfid
  266. ) VALUES (
  267. @Id, @TenantId, @FBILLNO, @Lydjbh, @Hid, @FMATERIALCFG, @Wlmc, @Ggxh, @Gysmc, @Pch,
  268. @FRINSQTY, @Jysl, 0, 0, '检验中', @Jyr, @Inspector,
  269. @StartDate, @CreateTime,
  270. @Jgbh, @Jgbb, @Jgsxrq, @Jygfid
  271. )
  272. """,
  273. new SugarParameter("@Id", billId),
  274. new SugarParameter("@TenantId", tenantId),
  275. new SugarParameter("@FBILLNO", billNo),
  276. new SugarParameter("@Lydjbh", entry.ApplyBillNo),
  277. new SugarParameter("@Hid", entry.Id),
  278. new SugarParameter("@FMATERIALCFG", entry.MaterialCode ?? (object)DBNull.Value),
  279. new SugarParameter("@Wlmc", entry.MaterialName ?? (object)DBNull.Value),
  280. new SugarParameter("@Ggxh", entry.Spec ?? (object)DBNull.Value),
  281. new SugarParameter("@Gysmc", entry.SupplierName ?? (object)DBNull.Value),
  282. new SugarParameter("@Pch", entry.LotNo ?? (object)DBNull.Value),
  283. new SugarParameter("@FRINSQTY", entry.ApplyQty ?? 0m),
  284. new SugarParameter("@Jysl", entry.ApplyQty ?? 0m),
  285. new SugarParameter("@Jyr", userId),
  286. new SugarParameter("@Inspector", _userManager.RealName ?? _userManager.Account ?? userId),
  287. new SugarParameter("@StartDate", now),
  288. new SugarParameter("@CreateTime", now),
  289. // 仅 MATCHED* 固化检规;NO_SPEC / NO_EFFECTIVE_SPEC / AMBIGUOUS 四列一律 NULL
  290. new SugarParameter("@Jgbh", spec.IsMatched ? (object?)spec.SpecNo ?? DBNull.Value : DBNull.Value),
  291. new SugarParameter("@Jgbb", spec.IsMatched ? (object?)spec.SpecVersion ?? DBNull.Value : DBNull.Value),
  292. new SugarParameter("@Jgsxrq", spec.IsMatched ? (object?)spec.EffectiveDateRaw ?? DBNull.Value : DBNull.Value),
  293. // jygfid 是 varchar 列,存 specId 的字符串形式(与 S7 qms_qcpp_inspbill.jygfid 同口径)
  294. new SugarParameter("@Jygfid", spec.IsMatched && spec.SpecId.HasValue
  295. ? spec.SpecId.Value.ToString()
  296. : (object)DBNull.Value));
  297. });
  298. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("生成检验单失败");
  299. return new IqcGenerateInspBillOutput
  300. {
  301. InspBillId = billId,
  302. InspBillNo = billNo,
  303. SpecStatus = spec.Status,
  304. SpecNo = spec.IsMatched ? spec.SpecNo : null,
  305. SpecWarning = spec.Warning,
  306. SpecCandidateCount = spec.CandidateCount,
  307. };
  308. }
  309. private async Task<string> AllocateIqcBillNoAsync(long tenantId)
  310. {
  311. var today = DateTime.Now.ToString("yyyyMMdd");
  312. var prefix = $"IQC{today}";
  313. var max = await _db.Ado.SqlQuerySingleAsync<string>(
  314. """
  315. SELECT FBILLNO
  316. FROM qms_qcp_inspbill
  317. WHERE tenant_id = @TenantId AND FBILLNO LIKE @Prefix
  318. ORDER BY FBILLNO DESC
  319. LIMIT 1
  320. """,
  321. new SugarParameter("@TenantId", tenantId),
  322. new SugarParameter("@Prefix", prefix + "%"));
  323. if (string.IsNullOrWhiteSpace(max) || max.Length < 4)
  324. return $"{prefix}0001";
  325. var last4 = max[^4..];
  326. if (!int.TryParse(last4, out var serial))
  327. return $"{prefix}0001";
  328. if (serial >= 9999)
  329. throw Oops.Oh($"当日检验单号已达上限 {prefix}9999");
  330. return $"{prefix}{(serial + 1).ToString().PadLeft(4, '0')}";
  331. }
  332. private static List<long> NormalizeIds(List<long>? ids)
  333. {
  334. var list = (ids ?? new List<long>()).Where(x => x > 0).Distinct().ToList();
  335. if (list.Count == 0) throw Oops.Oh("请至少选择一条任务");
  336. if (list.Count > 200) throw Oops.Oh("单次最多操作 200 条");
  337. return list;
  338. }
  339. private sealed class EntryGenRow
  340. {
  341. public long Id { get; set; }
  342. public long? Glid { get; set; }
  343. public string? OwnerId { get; set; }
  344. public string? InspectStatus { get; set; }
  345. public string? MaterialCode { get; set; }
  346. public string? MaterialName { get; set; }
  347. public string? Spec { get; set; }
  348. public string? LotNo { get; set; }
  349. public decimal? ApplyQty { get; set; }
  350. public string? SupplierName { get; set; }
  351. public string? ApplyBillNo { get; set; }
  352. public string? BizType { get; set; }
  353. }
  354. }