using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse; /// /// S7 FQC 检规结构化判定维护(Phase 1B,语义纠偏版)。 /// /// 检规模板 qms_ccjygfzb 维护每个检验项目的"录入类型 + 数值判定条件": /// lrlx=NUMERIC 数值型:维护 UpperToleranceLimit(上限)/LowerToleranceLimit(下限) /// → 生成检验单时快照进 inspbillst.sx/xx,检验员按样本录 j1..j50,未来 xx≤jN≤sx 自动判定; /// lrlx=NON_NUMERIC 非数值型:不维护上下限,检验时只录"本次实际合格数/不合格数" /// → 落 inspbillst.NonNumericalTypeOK/NG(检验结果数量,非检规模板字段,本服务绝不写)。 /// jsyq(技术要求) 为人读原文,不解析、不覆盖。 /// /// 【语义纠偏】NonNumericalTypeOK/NG = 检验单本次实际合格/不合格数量,属 inspbillst,绝不放检规模板; /// 本服务不再维护任何"非数值规则编码"。检规模板的非数值项目仅由 lrlx 标识类型。 /// 硬边界:不新建规则表、不从 jsyq 猜类型、不批量填限值、不改历史数据;tenant 走 ResolveTenantOrThrow; /// 上下限长度 ≤255(保证快照进 inspbillst.sx/xx varchar(255) 无截断)。 /// [ApiDescriptionSettings(Order = 341, Description = "FQC检规判定维护")] [Route("api/S7FqcSpecRule")] [NonUnify] public class FqcSpecRuleService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public FqcSpecRuleService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager); /// 快照承载长度上界(inspbillst.sx/xx varchar(255))。 private const int LimitMaxLen = 255; private const string TypeNumeric = "NUMERIC"; private const string TypeNonNumeric = "NON_NUMERIC"; /// 检规主表列表(维护入口第一层:选检规)。 [DisplayName("检规主表列表")] [HttpGet("spec-page")] public async Task SpecPage([FromQuery] FqcSpecRulePageInput input) { var tenantId = ResolveTenantOrThrow(); var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize); var where = new List { "tenant_id=@t" }; var pars = new List { new("@t", tenantId) }; if (!string.IsNullOrWhiteSpace(input.Wjbh)) { where.Add("wjbh LIKE @w"); pars.Add(new("@w", $"%{input.Wjbh.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Cpmc)) { where.Add("cpmc LIKE @c"); pars.Add(new("@c", $"%{input.Cpmc.Trim()}%")); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_ccjygf WHERE {whereSql}", pars); var list = await _db.Ado.SqlQueryAsync( $@"SELECT id AS Id, wjbh AS Wjbh, bb AS Bb, cpmc AS Cpmc, cpxh AS Cpxh, sxrq AS Sxrq FROM qms_ccjygf WHERE {whereSql} ORDER BY wjbh, bb DESC LIMIT {pageSize} OFFSET {(page - 1) * pageSize}", pars); return new { total, page, pageSize, list }; } /// 某检规的检验项目明细 + 录入类型 + 数值上下限(维护第二层)。 [DisplayName("检规明细判定条件")] [HttpGet("detail-list")] public async Task DetailList([FromQuery] long jygfid) { var tenantId = ResolveTenantOrThrow(); if (jygfid <= 0) throw Oops.Oh("检规 id 非法"); var rows = await _db.Ado.SqlQueryAsync( @"SELECT z.id AS Id, z.xh AS Xh, z.jyxm AS Jyxm, z.jsyq AS Jsyq, z.jsbz AS Jsbz, z.cyfa AS Cyfa, z.lrlx AS Lrlx, z.UpperToleranceLimit AS UpperLimit, z.LowerToleranceLimit AS LowerLimit FROM qms_ccjygfzb z JOIN qms_ccjygf g ON g.id = z.glid AND g.tenant_id = z.tenant_id WHERE z.glid = @j AND z.tenant_id = @t ORDER BY z.xh, z.id", new SugarParameter("@j", jygfid), new SugarParameter("@t", tenantId)); foreach (var r in rows) r.JudgmentType = DeriveJudgmentType(r.Lrlx, r.UpperLimit, r.LowerLimit); return new { jygfid, list = rows }; } /// 保存单条检验项目的录入类型 + 数值上下限(后端全量兜底校验)。 [DisplayName("保存检规判定条件")] [HttpPost("save-rule")] public async Task SaveRule([FromBody] FqcSpecRuleSaveInput input) { var tenantId = ResolveTenantOrThrow(); if (input == null || input.Id <= 0) throw Oops.Oh("明细 id 非法"); var lrlx = NormalizeType(input.Lrlx); var upper = Trim(input.UpperLimit); var lower = Trim(input.LowerLimit); // 1) 明细存在且属当前租户 var exist = await _db.Ado.SqlQuerySingleAsync( "SELECT COUNT(1) FROM qms_ccjygfzb WHERE id=@id AND tenant_id=@t", new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId)); if (exist == 0) throw Oops.Oh("检规明细不存在或不属于当前租户"); // 2) 非数值型不维护上下限:切换到 NON_NUMERIC 时清空上下限(避免残留脏数据) if (lrlx == TypeNonNumeric) { upper = string.Empty; lower = string.Empty; } // 3) 数值上下限校验(仅当有值时);NUMERIC 但暂无限值(未维护)允许 var hasLimit = !string.IsNullOrEmpty(upper) || !string.IsNullOrEmpty(lower); if (hasLimit) { if (lrlx == TypeNonNumeric) throw Oops.Oh("非数值型项目不能配置上下限"); if (upper.Length > LimitMaxLen || lower.Length > LimitMaxLen) throw Oops.Oh($"上/下限长度不能超过 {LimitMaxLen}(快照承载限制)"); decimal? u = ParseDec(upper), l = ParseDec(lower); if (!string.IsNullOrEmpty(upper) && u == null) throw Oops.Oh("上限必须是数值"); if (!string.IsNullOrEmpty(lower) && l == null) throw Oops.Oh("下限必须是数值"); if (u.HasValue && l.HasValue && l.Value > u.Value) throw Oops.Oh("下限不能大于上限"); } // 4) 写回 var n = await _db.Ado.ExecuteCommandAsync( @"UPDATE qms_ccjygfzb SET lrlx=@lrlx, UpperToleranceLimit=@upper, LowerToleranceLimit=@lower WHERE id=@id AND tenant_id=@t", new SugarParameter("@lrlx", (object?)lrlx ?? DBNull.Value), new SugarParameter("@upper", (object?)NullIfEmpty(upper) ?? DBNull.Value), new SugarParameter("@lower", (object?)NullIfEmpty(lower) ?? DBNull.Value), new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId)); return new { ok = n > 0, judgmentType = DeriveJudgmentType(lrlx, upper, lower) }; } /// 清空某明细的录入类型与上下限(→ 未配置)。 [DisplayName("清空检规判定条件")] [HttpPost("clear-rule")] public async Task ClearRule([FromBody] FqcSpecRuleClearInput input) { var tenantId = ResolveTenantOrThrow(); if (input == null || input.Id <= 0) throw Oops.Oh("明细 id 非法"); var n = await _db.Ado.ExecuteCommandAsync( @"UPDATE qms_ccjygfzb SET lrlx=NULL, UpperToleranceLimit=NULL, LowerToleranceLimit=NULL WHERE id=@id AND tenant_id=@t", new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId)); return new { ok = n > 0, judgmentType = "NONE" }; } // ===== 辅助 ===== /// 判定方式推导:显式 lrlx 优先;未设置时有上下限视为 NUMERIC,否则 NONE。 /// (不从"上下限为空"反推 NON_NUMERIC,非数值型必须显式 lrlx=NON_NUMERIC 标注。) private static string DeriveJudgmentType(string? lrlx, string? upper, string? lower) { if (lrlx == TypeNumeric) return TypeNumeric; if (lrlx == TypeNonNumeric) return TypeNonNumeric; var hasLimit = !string.IsNullOrWhiteSpace(upper) || !string.IsNullOrWhiteSpace(lower); return hasLimit ? TypeNumeric : "NONE"; } private static string? NormalizeType(string? t) { if (string.IsNullOrWhiteSpace(t)) return null; var v = t.Trim().ToUpperInvariant(); if (v == TypeNumeric || v == TypeNonNumeric) return v; throw Oops.Oh("录入类型仅支持 NUMERIC / NON_NUMERIC"); } private static string Trim(string? s) => (s ?? string.Empty).Trim(); private static string? NullIfEmpty(string s) => string.IsNullOrEmpty(s) ? null : s; private static decimal? ParseDec(string s) => decimal.TryParse(s, out var d) ? d : null; } public class FqcSpecRulePageInput { public string? Wjbh { get; set; } public string? Cpmc { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } public class FqcSpecHeadRow { public long Id { get; set; } public string? Wjbh { get; set; } public string? Bb { get; set; } public string? Cpmc { get; set; } public string? Cpxh { get; set; } public string? Sxrq { get; set; } } public class FqcSpecRuleRow { public long Id { get; set; } public string? Xh { get; set; } public string? Jyxm { get; set; } public string? Jsyq { get; set; } public string? Jsbz { get; set; } public string? Cyfa { get; set; } /// 录入类型 NUMERIC / NON_NUMERIC / null public string? Lrlx { get; set; } public string? UpperLimit { get; set; } public string? LowerLimit { get; set; } /// 推导判定方式 NUMERIC / NON_NUMERIC / NONE public string? JudgmentType { get; set; } } public class FqcSpecRuleSaveInput { public long Id { get; set; } /// 录入类型 NUMERIC / NON_NUMERIC(可空=未设置) public string? Lrlx { get; set; } public string? UpperLimit { get; set; } public string? LowerLimit { get; set; } } public class FqcSpecRuleClearInput { public long Id { get; set; } }