| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Microsoft.Extensions.Logging;
- using Yitter.IdGenerator;
- namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
- /// <summary>
- /// 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)。
- /// </summary>
- [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);
- /// <summary>成品完工报检默认业务类型/单据类型(占位,实际字典未接,原样落库)。</summary>
- private const string FqcBillType = "FQC";
- // ============================ 完工工单候选 ============================
- /// <summary>
- /// 引用生产指令单:完工工单候选(WorkOrdMaster + ItemMaster 富化,只读,租户隔离)。
- /// 不做完工硬拦截;返回 qtyOrded/qtyCompleted/status 供前端行内提示。
- /// </summary>
- [DisplayName("完工工单候选")]
- [HttpGet("work-orders")]
- public async Task<object> 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<string> { "w.tenant_id=@t", "w.ItemNum IS NOT NULL", "w.ItemNum<>''" };
- var pars = new List<SugarParameter> { 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<FqcWorkOrderRow>(
- $@"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 };
- }
- // ============================ 报检列表/详情 ============================
- /// <summary>成品报检列表(读 qms_fqcbj 本源,租户隔离)。</summary>
- [DisplayName("成品报检列表")]
- [HttpGet("page")]
- public async Task<object> 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<string> { "tenant_id=@t" };
- var pars = new List<SugarParameter> { 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<FqcApplyRow>(
- $@"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 };
- }
- /// <summary>成品报检详情(读 qms_fqcbj 单行)。</summary>
- [DisplayName("成品报检详情")]
- [HttpGet("detail")]
- public async Task<FqcApplyRow?> Detail([FromQuery] long id)
- {
- if (id <= 0) return null;
- var tenantId = ResolveTenantOrThrow();
- return (await _db.Ado.SqlQueryAsync<FqcApplyRow>(
- @"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();
- }
- // ============================ 创建报检 ============================
- /// <summary>
- /// 从完工工单创建成品报检。回填字段后端按 WorkOrd+ItemMaster 真实数据重读(防前端篡改),人工字段取前端。
- /// </summary>
- [DisplayName("创建成品报检")]
- [HttpPost("create")]
- public async Task<object> Create([FromBody] FqcApplyCreateInput input)
- {
- var tenantId = ResolveTenantOrThrow();
- if (input == null || input.WorkOrdRecId <= 0) throw Oops.Oh("请选择生产指令单(工单)");
- // 后端按用户选的 WorkOrd 重读真实数据(不信前端回填值)
- var wo = await _db.Ado.SqlQuerySingleAsync<WorkOrdGenRow>(
- @"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) ============================
- /// <summary>
- /// 报检 → 生成检验单头 + 检规明细快照(同事务)。应用级幂等(lydjbh+hid) + DB UNIQUE(uk_qms_qcpp_inspbill_source) 双保险。
- /// 检规精确走 Phase 1A 桥表(禁 LIKE);lrlx/上下限固化进 inspbillst;NonNumericalTypeOK/NG 是检验结果数量不从检规带值。
- /// 本轮不做检验录入/判定/提交(Phase 1E)。
- /// </summary>
- [DisplayName("生成检验单")]
- [HttpPost("generate-inspection")]
- public async Task<object> GenerateInspection([FromBody] FqcGenerateInput input)
- {
- var tenantId = ResolveTenantOrThrow();
- if (input == null || input.ApplyId <= 0) throw Oops.Oh("报检 id 非法");
- // 1) 报检存在 + 归属 + 关键字段
- var apply = await _db.Ado.SqlQuerySingleAsync<ApplyGenRow>(
- @"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<SpecDetailRow>(
- @"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<string, object>
- {
- ["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<ExistingInspRow?> QueryExistingInspAsync(long tenantId, string applyBillNo, long applyId)
- {
- return await _db.Ado.SqlQuerySingleAsync<ExistingInspRow>(
- "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));
- }
- /// <summary>选检规:人工指定优先(须属该物料候选);否则唯一 jygfid 直取;多版本取生效日期严格最新;同优先级歧义则拒。</summary>
- private static FqcSpecCandidate ChooseSpec(List<FqcSpecCandidate> 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;
- /// <summary>检验单号 FQC+yyyyMMdd+4 位(配 1A uk_qms_qcpp_inspbill_tenant_billno + 上层重试)。</summary>
- private async Task<string> AllocateInspBillNoAsync(long tenantId)
- {
- var prefix = $"FQC{DateTime.Now:yyyyMMdd}";
- var max = await _db.Ado.SqlQuerySingleAsync<string>(
- "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);
- }
- // ============================ 辅助 ============================
- /// <summary>报检号 FQCBJ+yyyyMMdd+4 位(本库租户内,配 1A UNIQUE + 上层重试)。</summary>
- private async Task<string> AllocateApplyNoAsync(long tenantId)
- {
- var prefix = $"FQCBJ{DateTime.Now:yyyyMMdd}";
- var max = await _db.Ado.SqlQuerySingleAsync<string>(
- "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";
- }
- /// <summary>写成功后触发 FQC refresh(解耦,失败只记录不影响报检成立)。</summary>
- 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; }
- /// <summary>已生成的检验单 id/号(未生成为 null)</summary>
- 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
- {
- /// <summary>报检 id(qms_fqcbj.id)</summary>
- public long ApplyId { get; set; }
- /// <summary>可选:人工指定检规 id(须属该物料桥表候选,后端二次校验)</summary>
- 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; }
- }
|