using Admin.NET.Plugin.AiDOP.Infrastructure; using Microsoft.Extensions.Logging; using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse; /// /// S7 FQC 成品报检(Phase 1C)。复刻旧系统"组织实验通知单":选一张(完工)工单 → 回填 → 保存成 qms_fqcbj 报检。 /// /// 复用 S5 IqcTaskEntryService 范式:ResolveTenantOrThrow(登录租户,非透传源)、雪花 id、本库取号(FQCBJ+日期+序号,配 1A UNIQUE)、 /// UseTranAsync、写成功后触发 FQC refresh(解耦,失败不回滚业务)。 /// /// 回填可信边界(§十二):wlbm/cpmc/cpxh/scph/sczld/sczldsl 一律由后端按用户选的 WorkOrd + ItemMaster 真实数据重读, /// 不信前端提交值;人工字段 sl/yxj/jfbh/FCOMMENT 取前端。 /// 完工口径(§八):不做无证据硬拦截,候选正常展示 + 完工标记由前端提示;仅缺 ItemNum/关键键 硬拒。 /// 只读 WorkOrdMaster/ItemMaster,只写 qms_fqcbj。本轮不实现"生成检验单"(Phase 1D)。 /// [ApiDescriptionSettings(Order = 342, Description = "FQC成品报检")] [Route("api/S7FqcApply")] [NonUnify] public class FqcApplyService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; private readonly FqcMdpSyncService _fqcMdpSyncService; private readonly FqcSpecMaterialMapService _specMap; private readonly ILogger _logger; public FqcApplyService(ISqlSugarClient db, UserManager userManager, FqcMdpSyncService fqcMdpSyncService, FqcSpecMaterialMapService specMap, ILoggerFactory loggerFactory) { _db = db; _userManager = userManager; _fqcMdpSyncService = fqcMdpSyncService; _specMap = specMap; _logger = loggerFactory.CreateLogger(nameof(FqcApplyService)); } private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager); /// 成品完工报检默认业务类型/单据类型(占位,实际字典未接,原样落库)。 private const string FqcBillType = "FQC"; // ============================ 完工工单候选 ============================ /// /// 引用生产指令单:完工工单候选(WorkOrdMaster + ItemMaster 富化,只读,租户隔离)。 /// 不做完工硬拦截;返回 qtyOrded/qtyCompleted/status 供前端行内提示。 /// [DisplayName("完工工单候选")] [HttpGet("work-orders")] public async Task WorkOrders([FromQuery] FqcWorkOrderQueryInput input) { var tenantId = ResolveTenantOrThrow(); var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize); var where = new List { "w.tenant_id=@t", "w.ItemNum IS NOT NULL", "w.ItemNum<>''" }; var pars = new List { new("@t", tenantId) }; void Like(string? v, string col, string p) { if (!string.IsNullOrWhiteSpace(v)) { where.Add($"{col} LIKE @{p}"); pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%")); } } Like(input.Batch, "w.Batch", "Batch"); Like(input.WorkOrd, "w.WorkOrd", "WorkOrd"); Like(input.MaterialCode, "w.ItemNum", "Mat"); Like(input.LotSerial, "w.LotSerial", "Lot"); if (!string.IsNullOrWhiteSpace(input.Status)) { where.Add("LOWER(w.Status)=@st"); pars.Add(new SugarParameter("@st", input.Status.Trim().ToLowerInvariant())); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM WorkOrdMaster w WHERE {whereSql}", pars); var list = await _db.Ado.SqlQueryAsync( $@"SELECT w.RecID AS RecId, w.Batch AS Batch, w.WorkOrd AS WorkOrd, w.ItemNum AS MaterialCode, i.Descr AS ProductName, i.Descr1 AS ProductModel, w.LotSerial AS LotSerial, w.QtyOrded AS QtyOrded, w.QtyCompleted AS QtyCompleted, LOWER(w.Status) AS Status FROM WorkOrdMaster w LEFT JOIN ItemMaster i ON i.ItemNum=w.ItemNum AND i.tenant_id=w.tenant_id WHERE {whereSql} ORDER BY w.RecID DESC LIMIT {pageSize} OFFSET {(page - 1) * pageSize}", pars); return new { total, page, pageSize, list }; } // ============================ 报检列表/详情 ============================ /// 成品报检列表(读 qms_fqcbj 本源,租户隔离)。 [DisplayName("成品报检列表")] [HttpGet("page")] public async Task Page([FromQuery] FqcApplyQueryInput input) { var tenantId = ResolveTenantOrThrow(); var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize); var where = new List { "tenant_id=@t" }; var pars = new List { new("@t", tenantId) }; void Like(string? v, string col, string p) { if (!string.IsNullOrWhiteSpace(v)) { where.Add($"{col} LIKE @{p}"); pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%")); } } Like(input.BillNo, "FBILLNO", "Bill"); Like(input.MaterialCode, "wlbm", "Mat"); Like(input.ProductionBatchNo, "scph", "Scph"); Like(input.ProductionOrderNo, "sczld", "Sczld"); var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_fqcbj WHERE {whereSql}", pars); var list = await _db.Ado.SqlQueryAsync( $@"SELECT q.id AS Id, q.FBILLNO AS BillNo, q.wlbm AS MaterialCode, q.cpmc AS ProductName, q.cpxh AS ProductModel, q.scph AS ProductionBatchNo, q.sczld AS ProductionOrderNo, q.sczldsl AS OrderQty, q.sl AS Qty, q.yxj AS Priority, q.FAPPLYUSER AS Applicant, q.FAPPLYTIME AS ApplyTime, q.FINSPECTSTATUS AS InspectStatus, q.FCOMMENT AS Remark, ib.id AS InspBillId, ib.FBILLNO AS InspBillNo FROM qms_fqcbj q LEFT JOIN qms_qcpp_inspbill ib ON ib.tenant_id=q.tenant_id AND ib.lydjbh=q.FBILLNO AND ib.hid=q.id WHERE {whereSql.Replace("tenant_id", "q.tenant_id").Replace("FBILLNO", "q.FBILLNO").Replace("wlbm", "q.wlbm").Replace("scph", "q.scph").Replace("sczld", "q.sczld")} ORDER BY q.FAPPLYTIME DESC, q.id DESC LIMIT {pageSize} OFFSET {(page - 1) * pageSize}", pars); return new { total, page, pageSize, list }; } /// 成品报检详情(读 qms_fqcbj 单行)。 [DisplayName("成品报检详情")] [HttpGet("detail")] public async Task Detail([FromQuery] long id) { if (id <= 0) return null; var tenantId = ResolveTenantOrThrow(); return (await _db.Ado.SqlQueryAsync( @"SELECT id AS Id, FBILLNO AS BillNo, wlbm AS MaterialCode, cpmc AS ProductName, cpxh AS ProductModel, scph AS ProductionBatchNo, sczld AS ProductionOrderNo, sczldsl AS OrderQty, sl AS Qty, yxj AS Priority, jfbh AS FireNo, FAPPLYUSER AS Applicant, FAPPLYTIME AS ApplyTime, FINSPECTSTATUS AS InspectStatus, FCOMMENT AS Remark FROM qms_fqcbj WHERE id=@id AND tenant_id=@t LIMIT 1", new SugarParameter("@id", id), new SugarParameter("@t", tenantId))).FirstOrDefault(); } // ============================ 创建报检 ============================ /// /// 从完工工单创建成品报检。回填字段后端按 WorkOrd+ItemMaster 真实数据重读(防前端篡改),人工字段取前端。 /// [DisplayName("创建成品报检")] [HttpPost("create")] public async Task Create([FromBody] FqcApplyCreateInput input) { var tenantId = ResolveTenantOrThrow(); if (input == null || input.WorkOrdRecId <= 0) throw Oops.Oh("请选择生产指令单(工单)"); // 后端按用户选的 WorkOrd 重读真实数据(不信前端回填值) var wo = await _db.Ado.SqlQuerySingleAsync( @"SELECT w.RecID AS RecId, w.Batch AS Batch, w.ItemNum AS ItemNum, w.LotSerial AS LotSerial, w.QtyOrded AS QtyOrded, i.Descr AS ProductName, i.Descr1 AS ProductModel FROM WorkOrdMaster w LEFT JOIN ItemMaster i ON i.ItemNum=w.ItemNum AND i.tenant_id=w.tenant_id WHERE w.RecID=@rid AND w.tenant_id=@t LIMIT 1", new SugarParameter("@rid", input.WorkOrdRecId), new SugarParameter("@t", tenantId)); if (wo == null) throw Oops.Oh("工单不存在或不属于当前租户"); if (string.IsNullOrWhiteSpace(wo.ItemNum)) throw Oops.Oh("工单缺物料编码,无法报检"); var now = DateTime.Now; var applyUser = _userManager.RealName ?? _userManager.Account ?? _userManager.UserId.ToString(); // 取号 + 并发唯一冲突有限重试(配 1A uk_qms_fqcbj_tenant_billno) Exception? lastEx = null; for (var attempt = 0; attempt < 5; attempt++) { var billNo = await AllocateApplyNoAsync(tenantId); var billId = YitIdHelper.NextId(); try { await _db.Ado.ExecuteCommandAsync( @"INSERT INTO qms_fqcbj (id, tenant_id, FBILLNO, FBILLTYPE, FINSPECTSTATUS, FAPPLYUSER, FAPPLYTIME, FORGID, FCREATETIME, wlbm, cpmc, cpxh, scph, sczld, sczldsl, sl, yxj, jfbh, FCOMMENT) VALUES (@id, @t, @no, @btype, '未检验', @user, @now, @org, @now, @wlbm, @cpmc, @cpxh, @scph, @sczld, @sczldsl, @sl, @yxj, @jfbh, @comment)", new SugarParameter("@id", billId), new SugarParameter("@t", tenantId), new SugarParameter("@no", billNo), new SugarParameter("@btype", FqcBillType), new SugarParameter("@user", applyUser), new SugarParameter("@now", now), new SugarParameter("@org", (object?)_userManager.OrgId ?? DBNull.Value), new SugarParameter("@wlbm", wo.ItemNum), new SugarParameter("@cpmc", (object?)wo.ProductName ?? DBNull.Value), new SugarParameter("@cpxh", (object?)wo.ProductModel ?? DBNull.Value), new SugarParameter("@scph", (object?)wo.LotSerial ?? DBNull.Value), new SugarParameter("@sczld", (object?)wo.Batch ?? DBNull.Value), new SugarParameter("@sczldsl", (object?)wo.QtyOrded ?? DBNull.Value), new SugarParameter("@sl", (object?)input.Qty ?? DBNull.Value), new SugarParameter("@yxj", (object?)NullIfEmpty(input.Priority) ?? DBNull.Value), new SugarParameter("@jfbh", (object?)NullIfEmpty(input.FireNo) ?? DBNull.Value), new SugarParameter("@comment", (object?)NullIfEmpty(input.Remark) ?? DBNull.Value)); await TriggerRefreshSafeAsync(); return new { ok = true, id = billId, billNo }; } catch (Exception ex) when (IsDupKey(ex)) { lastEx = ex; // 号撞了,重取重试 } } throw Oops.Oh("报检单号并发冲突,请重试").WithData(lastEx?.Message); } // ============================ 生成检验单(Phase 1D) ============================ /// /// 报检 → 生成检验单头 + 检规明细快照(同事务)。应用级幂等(lydjbh+hid) + DB UNIQUE(uk_qms_qcpp_inspbill_source) 双保险。 /// 检规精确走 Phase 1A 桥表(禁 LIKE);lrlx/上下限固化进 inspbillst;NonNumericalTypeOK/NG 是检验结果数量不从检规带值。 /// 本轮不做检验录入/判定/提交(Phase 1E)。 /// [DisplayName("生成检验单")] [HttpPost("generate-inspection")] public async Task GenerateInspection([FromBody] FqcGenerateInput input) { var tenantId = ResolveTenantOrThrow(); if (input == null || input.ApplyId <= 0) throw Oops.Oh("报检 id 非法"); // 1) 报检存在 + 归属 + 关键字段 var apply = await _db.Ado.SqlQuerySingleAsync( @"SELECT id AS Id, FBILLNO AS BillNo, wlbm AS Wlbm, cpmc AS Cpmc, cpxh AS Cpxh, scph AS Scph, sczld AS Sczld, sczldsl AS Sczldsl FROM qms_fqcbj WHERE id=@id AND tenant_id=@t LIMIT 1", new SugarParameter("@id", input.ApplyId), new SugarParameter("@t", tenantId)); if (apply == null) throw Oops.Oh("报检不存在或不属于当前租户"); if (string.IsNullOrWhiteSpace(apply.BillNo)) throw Oops.Oh("报检单号缺失"); if (string.IsNullOrWhiteSpace(apply.Wlbm)) throw Oops.Oh("报检缺物料编码,无法生成检验单"); // 2) 应用级幂等:一个报检(lydjbh=报检号, hid=报检id)只一张检验单 var existing = await QueryExistingInspAsync(tenantId, apply.BillNo!, apply.Id); if (existing != null) return new { created = false, id = existing.Id, billNo = existing.BillNo }; // 3) 精确解析检规(走桥表,禁 LIKE) var candidates = await _specMap.ResolveInternalAsync(tenantId, apply.Wlbm!); if (candidates.Count == 0) throw Oops.Oh("当前物料未配置成品检规映射,请先维护检规物料映射"); var chosen = ChooseSpec(candidates, input.SpecId); // 4) 检规明细 var details = await _db.Ado.SqlQueryAsync( @"SELECT id AS Id, xh AS Xh, jyxm AS Jyxm, UpperToleranceLimit AS UpperLimit, LowerToleranceLimit AS LowerLimit, lrlx AS Lrlx, jsyq AS Jsyq, jyyqsbgj AS Jyyqsbgj, jyff AS Jyff, jytk AS Jytk, jsbz AS Jsbz, cyfa AS Cyfa, jl AS Jl, SizeSpecifications AS SizeSpecifications FROM qms_ccjygfzb WHERE glid=@g AND tenant_id=@t ORDER BY xh, id", new SugarParameter("@g", chosen.Jygfid), new SugarParameter("@t", tenantId)); if (details.Count == 0) throw Oops.Oh("当前检规无有效检验项目,不能生成检验单"); foreach (var d in details) { if (string.IsNullOrWhiteSpace(d.Jyxm)) throw Oops.Oh("检规存在空检验项目,无法生成检验单"); if (d.Lrlx != null && d.Lrlx != "NUMERIC" && d.Lrlx != "NON_NUMERIC") throw Oops.Oh($"检规项目录入类型非法:{d.Jyxm}"); } var now = DateTime.Now; for (var attempt = 0; attempt < 5; attempt++) { var billNo = await AllocateInspBillNoAsync(tenantId); var headId = YitIdHelper.NextId(); var stRows = details.Select(d => new Dictionary { ["id"] = YitIdHelper.NextId(), ["tenant_id"] = tenantId, ["glid"] = headId, ["xh"] = (object?)ParseXh(d.Xh) ?? DBNull.Value, ["jyxm"] = (object?)d.Jyxm ?? DBNull.Value, ["sx"] = (object?)d.UpperLimit ?? DBNull.Value, // 数值上限快照 ["xx"] = (object?)d.LowerLimit ?? DBNull.Value, // 数值下限快照 ["lrlx"] = (object?)d.Lrlx ?? DBNull.Value, // 录入类型快照(NUMERIC/NON_NUMERIC) ["jsyq"] = (object?)d.Jsyq ?? DBNull.Value, ["jyyqsbgj"] = (object?)d.Jyyqsbgj ?? DBNull.Value, ["jyff"] = (object?)d.Jyff ?? DBNull.Value, ["jytk"] = (object?)d.Jytk ?? DBNull.Value, ["jsbz"] = (object?)d.Jsbz ?? DBNull.Value, ["cyfa"] = (object?)d.Cyfa ?? DBNull.Value, ["jl"] = (object?)d.Jl ?? DBNull.Value, ["SizeSpecifications"] = (object?)d.SizeSpecifications ?? DBNull.Value, // NonNumericalTypeOK/NG(检验结果数量) 与 j1..j50/pd 留空,等 Phase 1E 检验员录入;绝不从检规带值 }).ToList(); try { var tran = await _db.Ado.UseTranAsync(async () => { await _db.Ado.ExecuteCommandAsync( @"INSERT INTO qms_qcpp_inspbill (id, tenant_id, FBILLNO, lydjbh, hid, scph, sczld, sczldsl, wlmc, ggxh, jgbh, jgbb, jygfid, sxrq, FBILLSTATUS, FCREATETIME) VALUES (@id, @t, @no, @ly, @hid, @scph, @sczld, @sczldsl, @wlmc, @ggxh, @jgbh, @jgbb, @jygfid, @sxrq, '未检验', @now)", new SugarParameter("@id", headId), new SugarParameter("@t", tenantId), new SugarParameter("@no", billNo), new SugarParameter("@ly", apply.BillNo), new SugarParameter("@hid", apply.Id), new SugarParameter("@scph", (object?)apply.Scph ?? DBNull.Value), new SugarParameter("@sczld", (object?)apply.Sczld ?? DBNull.Value), new SugarParameter("@sczldsl", (object?)apply.Sczldsl ?? DBNull.Value), new SugarParameter("@wlmc", (object?)apply.Cpmc ?? DBNull.Value), new SugarParameter("@ggxh", (object?)apply.Cpxh ?? DBNull.Value), new SugarParameter("@jgbh", (object?)chosen.Wjbh ?? DBNull.Value), new SugarParameter("@jgbb", (object?)chosen.Bb ?? DBNull.Value), new SugarParameter("@jygfid", chosen.Jygfid.ToString()), new SugarParameter("@sxrq", (object?)chosen.NormalizedEffectiveDate ?? DBNull.Value), new SugarParameter("@now", now)); await _db.Insertable(stRows).AS("qms_qcpp_inspbillst").ExecuteCommandAsync(); }); if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("生成检验单失败"); await TriggerRefreshSafeAsync(); return new { created = true, id = headId, billNo }; } catch (Exception ex) when (IsDup(ex, "uk_qms_qcpp_inspbill_source")) { // 并发:另一请求已生成 → 回查已有,返回 created=false var e2 = await QueryExistingInspAsync(tenantId, apply.BillNo!, apply.Id); if (e2 != null) return new { created = false, id = e2.Id, billNo = e2.BillNo }; throw; } catch (Exception ex) when (IsDup(ex, "uk_qms_qcpp_inspbill_tenant_billno")) { // 号撞,重取重试 _ = ex; } } throw Oops.Oh("检验单号并发冲突,请重试"); } private async Task QueryExistingInspAsync(long tenantId, string applyBillNo, long applyId) { return await _db.Ado.SqlQuerySingleAsync( "SELECT id AS Id, FBILLNO AS BillNo FROM qms_qcpp_inspbill WHERE tenant_id=@t AND lydjbh=@ly AND hid=@hid LIMIT 1", new SugarParameter("@t", tenantId), new SugarParameter("@ly", applyBillNo), new SugarParameter("@hid", applyId)); } /// 选检规:人工指定优先(须属该物料候选);否则唯一 jygfid 直取;多版本取生效日期严格最新;同优先级歧义则拒。 private static FqcSpecCandidate ChooseSpec(List candidates, long? specId) { if (specId.HasValue && specId.Value > 0) return candidates.FirstOrDefault(c => c.Jygfid == specId.Value) ?? throw Oops.Oh("所选检规不属于该物料的有效映射"); var distinct = candidates.Select(c => c.Jygfid).Distinct().ToList(); if (distinct.Count == 1) return candidates[0]; // candidates 已按 normalized_effective_date DESC 排序:取严格最新,否则歧义拒绝(禁 First 偷取) var top = candidates[0]; var second = candidates.FirstOrDefault(c => c.Jygfid != top.Jygfid); if (top.NormalizedEffectiveDate.HasValue && (second?.NormalizedEffectiveDate == null || top.NormalizedEffectiveDate.Value > second.NormalizedEffectiveDate.Value)) return top; throw Oops.Oh("当前物料存在多个有效成品检规,请先完成检规映射确认(人工绑定唯一检规)"); } private static int? ParseXh(string? xh) => int.TryParse((xh ?? "").Trim(), out var n) ? n : null; /// 检验单号 FQC+yyyyMMdd+4 位(配 1A uk_qms_qcpp_inspbill_tenant_billno + 上层重试)。 private async Task AllocateInspBillNoAsync(long tenantId) { var prefix = $"FQC{DateTime.Now:yyyyMMdd}"; var max = await _db.Ado.SqlQuerySingleAsync( "SELECT FBILLNO FROM qms_qcpp_inspbill WHERE tenant_id=@t AND FBILLNO LIKE @p ORDER BY FBILLNO DESC LIMIT 1", new SugarParameter("@t", tenantId), new SugarParameter("@p", prefix + "%")); if (string.IsNullOrWhiteSpace(max) || max.Length < 4) return $"{prefix}0001"; return int.TryParse(max[^4..], out var n) && n < 9999 ? $"{prefix}{(n + 1):0000}" : $"{prefix}0001"; } private static bool IsDup(Exception ex, string indexName) { var m = ex.Message + " " + (ex.InnerException?.Message ?? ""); return (m.Contains("Duplicate", StringComparison.OrdinalIgnoreCase) || m.Contains("1062")) && m.Contains(indexName, StringComparison.OrdinalIgnoreCase); } // ============================ 辅助 ============================ /// 报检号 FQCBJ+yyyyMMdd+4 位(本库租户内,配 1A UNIQUE + 上层重试)。 private async Task AllocateApplyNoAsync(long tenantId) { var prefix = $"FQCBJ{DateTime.Now:yyyyMMdd}"; var max = await _db.Ado.SqlQuerySingleAsync( "SELECT FBILLNO FROM qms_fqcbj WHERE tenant_id=@t AND FBILLNO LIKE @p ORDER BY FBILLNO DESC LIMIT 1", new SugarParameter("@t", tenantId), new SugarParameter("@p", prefix + "%")); if (string.IsNullOrWhiteSpace(max) || max.Length < 4) return $"{prefix}0001"; return int.TryParse(max[^4..], out var n) && n < 9999 ? $"{prefix}{(n + 1):0000}" : $"{prefix}0001"; } /// 写成功后触发 FQC refresh(解耦,失败只记录不影响报检成立)。 private async Task TriggerRefreshSafeAsync() { try { await _fqcMdpSyncService.RunFullAsync(CancellationToken.None, "APPLY_CREATE"); } catch (Exception ex) { _logger.LogWarning(ex, "[FqcApply] 报检已保存,但 FQC refresh 失败,可稍后手动刷新"); } } private static bool IsDupKey(Exception ex) { var m = ex.Message + " " + (ex.InnerException?.Message ?? ""); return m.Contains("Duplicate", StringComparison.OrdinalIgnoreCase) || m.Contains("1062") || m.Contains("uk_qms_fqcbj"); } private static string? NullIfEmpty(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim(); } public class FqcWorkOrderQueryInput { public string? Batch { get; set; } public string? WorkOrd { get; set; } public string? MaterialCode { get; set; } public string? LotSerial { get; set; } public string? Status { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; } public class FqcWorkOrderRow { public long RecId { get; set; } public string? Batch { get; set; } public string? WorkOrd { get; set; } public string? MaterialCode { get; set; } public string? ProductName { get; set; } public string? ProductModel { get; set; } public string? LotSerial { get; set; } public decimal? QtyOrded { get; set; } public decimal? QtyCompleted { get; set; } public string? Status { get; set; } } public class FqcApplyQueryInput { public string? BillNo { get; set; } public string? MaterialCode { get; set; } public string? ProductionBatchNo { get; set; } public string? ProductionOrderNo { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; } public class FqcApplyRow { public long Id { get; set; } public string? BillNo { get; set; } public string? MaterialCode { get; set; } public string? ProductName { get; set; } public string? ProductModel { get; set; } public string? ProductionBatchNo { get; set; } public string? ProductionOrderNo { get; set; } public decimal? OrderQty { get; set; } public decimal? Qty { get; set; } public string? Priority { get; set; } public string? FireNo { get; set; } public string? Applicant { get; set; } public DateTime? ApplyTime { get; set; } public string? InspectStatus { get; set; } public string? Remark { get; set; } /// 已生成的检验单 id/号(未生成为 null) public long? InspBillId { get; set; } public string? InspBillNo { get; set; } } public class FqcApplyCreateInput { public long WorkOrdRecId { get; set; } public decimal? Qty { get; set; } public string? Priority { get; set; } public string? FireNo { get; set; } public string? Remark { get; set; } } internal sealed class WorkOrdGenRow { public long RecId { get; set; } public string? Batch { get; set; } public string? ItemNum { get; set; } public string? LotSerial { get; set; } public decimal? QtyOrded { get; set; } public string? ProductName { get; set; } public string? ProductModel { get; set; } public long? ForgId { get; set; } } public class FqcGenerateInput { /// 报检 id(qms_fqcbj.id) public long ApplyId { get; set; } /// 可选:人工指定检规 id(须属该物料桥表候选,后端二次校验) public long? SpecId { get; set; } } internal sealed class ApplyGenRow { public long Id { get; set; } public string? BillNo { get; set; } public string? Wlbm { get; set; } public string? Cpmc { get; set; } public string? Cpxh { get; set; } public string? Scph { get; set; } public string? Sczld { get; set; } public decimal? Sczldsl { get; set; } } internal sealed class ExistingInspRow { public long Id { get; set; } public string? BillNo { get; set; } } internal sealed class SpecDetailRow { public long Id { get; set; } public string? Xh { get; set; } public string? Jyxm { get; set; } public string? UpperLimit { get; set; } public string? LowerLimit { get; set; } public string? Lrlx { get; set; } public string? Jsyq { get; set; } public string? Jyyqsbgj { get; set; } public string? Jyff { get; set; } public string? Jytk { get; set; } public string? Jsbz { get; set; } public string? Cyfa { get; set; } public string? Jl { get; set; } public string? SizeSpecifications { get; set; } }