FqcSpecRuleService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  3. /// <summary>
  4. /// S7 FQC 检规结构化判定维护(Phase 1B,语义纠偏版)。
  5. ///
  6. /// 检规模板 qms_ccjygfzb 维护每个检验项目的"录入类型 + 数值判定条件":
  7. /// lrlx=NUMERIC 数值型:维护 UpperToleranceLimit(上限)/LowerToleranceLimit(下限)
  8. /// → 生成检验单时快照进 inspbillst.sx/xx,检验员按样本录 j1..j50,未来 xx≤jN≤sx 自动判定;
  9. /// lrlx=NON_NUMERIC 非数值型:不维护上下限,检验时只录"本次实际合格数/不合格数"
  10. /// → 落 inspbillst.NonNumericalTypeOK/NG(检验结果数量,非检规模板字段,本服务绝不写)。
  11. /// jsyq(技术要求) 为人读原文,不解析、不覆盖。
  12. ///
  13. /// 【语义纠偏】NonNumericalTypeOK/NG = 检验单本次实际合格/不合格数量,属 inspbillst,绝不放检规模板;
  14. /// 本服务不再维护任何"非数值规则编码"。检规模板的非数值项目仅由 lrlx 标识类型。
  15. /// 硬边界:不新建规则表、不从 jsyq 猜类型、不批量填限值、不改历史数据;tenant 走 ResolveTenantOrThrow;
  16. /// 上下限长度 ≤255(保证快照进 inspbillst.sx/xx varchar(255) 无截断)。
  17. /// </summary>
  18. [ApiDescriptionSettings(Order = 341, Description = "FQC检规判定维护")]
  19. [Route("api/S7FqcSpecRule")]
  20. [NonUnify]
  21. public class FqcSpecRuleService : IDynamicApiController, ITransient
  22. {
  23. private readonly ISqlSugarClient _db;
  24. private readonly UserManager _userManager;
  25. public FqcSpecRuleService(ISqlSugarClient db, UserManager userManager)
  26. {
  27. _db = db;
  28. _userManager = userManager;
  29. }
  30. private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
  31. /// <summary>快照承载长度上界(inspbillst.sx/xx varchar(255))。</summary>
  32. private const int LimitMaxLen = 255;
  33. private const string TypeNumeric = "NUMERIC";
  34. private const string TypeNonNumeric = "NON_NUMERIC";
  35. /// <summary>检规主表列表(维护入口第一层:选检规)。</summary>
  36. [DisplayName("检规主表列表")]
  37. [HttpGet("spec-page")]
  38. public async Task<object> SpecPage([FromQuery] FqcSpecRulePageInput input)
  39. {
  40. var tenantId = ResolveTenantOrThrow();
  41. var page = input.Page <= 0 ? 1 : input.Page;
  42. var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
  43. var where = new List<string> { "tenant_id=@t" };
  44. var pars = new List<SugarParameter> { new("@t", tenantId) };
  45. if (!string.IsNullOrWhiteSpace(input.Wjbh)) { where.Add("wjbh LIKE @w"); pars.Add(new("@w", $"%{input.Wjbh.Trim()}%")); }
  46. if (!string.IsNullOrWhiteSpace(input.Cpmc)) { where.Add("cpmc LIKE @c"); pars.Add(new("@c", $"%{input.Cpmc.Trim()}%")); }
  47. var whereSql = string.Join(" AND ", where);
  48. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM qms_ccjygf WHERE {whereSql}", pars);
  49. var list = await _db.Ado.SqlQueryAsync<FqcSpecHeadRow>(
  50. $@"SELECT id AS Id, wjbh AS Wjbh, bb AS Bb, cpmc AS Cpmc, cpxh AS Cpxh, sxrq AS Sxrq
  51. FROM qms_ccjygf WHERE {whereSql} ORDER BY wjbh, bb DESC LIMIT {pageSize} OFFSET {(page - 1) * pageSize}", pars);
  52. return new { total, page, pageSize, list };
  53. }
  54. /// <summary>某检规的检验项目明细 + 录入类型 + 数值上下限(维护第二层)。</summary>
  55. [DisplayName("检规明细判定条件")]
  56. [HttpGet("detail-list")]
  57. public async Task<object> DetailList([FromQuery] long jygfid)
  58. {
  59. var tenantId = ResolveTenantOrThrow();
  60. if (jygfid <= 0) throw Oops.Oh("检规 id 非法");
  61. var rows = await _db.Ado.SqlQueryAsync<FqcSpecRuleRow>(
  62. @"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,
  63. z.lrlx AS Lrlx, z.UpperToleranceLimit AS UpperLimit, z.LowerToleranceLimit AS LowerLimit
  64. FROM qms_ccjygfzb z
  65. JOIN qms_ccjygf g ON g.id = z.glid AND g.tenant_id = z.tenant_id
  66. WHERE z.glid = @j AND z.tenant_id = @t
  67. ORDER BY z.xh, z.id",
  68. new SugarParameter("@j", jygfid), new SugarParameter("@t", tenantId));
  69. foreach (var r in rows) r.JudgmentType = DeriveJudgmentType(r.Lrlx, r.UpperLimit, r.LowerLimit);
  70. return new { jygfid, list = rows };
  71. }
  72. /// <summary>保存单条检验项目的录入类型 + 数值上下限(后端全量兜底校验)。</summary>
  73. [DisplayName("保存检规判定条件")]
  74. [HttpPost("save-rule")]
  75. public async Task<object> SaveRule([FromBody] FqcSpecRuleSaveInput input)
  76. {
  77. var tenantId = ResolveTenantOrThrow();
  78. if (input == null || input.Id <= 0) throw Oops.Oh("明细 id 非法");
  79. var lrlx = NormalizeType(input.Lrlx);
  80. var upper = Trim(input.UpperLimit);
  81. var lower = Trim(input.LowerLimit);
  82. // 1) 明细存在且属当前租户
  83. var exist = await _db.Ado.SqlQuerySingleAsync<long>(
  84. "SELECT COUNT(1) FROM qms_ccjygfzb WHERE id=@id AND tenant_id=@t",
  85. new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId));
  86. if (exist == 0) throw Oops.Oh("检规明细不存在或不属于当前租户");
  87. // 2) 非数值型不维护上下限:切换到 NON_NUMERIC 时清空上下限(避免残留脏数据)
  88. if (lrlx == TypeNonNumeric)
  89. {
  90. upper = string.Empty;
  91. lower = string.Empty;
  92. }
  93. // 3) 数值上下限校验(仅当有值时);NUMERIC 但暂无限值(未维护)允许
  94. var hasLimit = !string.IsNullOrEmpty(upper) || !string.IsNullOrEmpty(lower);
  95. if (hasLimit)
  96. {
  97. if (lrlx == TypeNonNumeric) throw Oops.Oh("非数值型项目不能配置上下限");
  98. if (upper.Length > LimitMaxLen || lower.Length > LimitMaxLen)
  99. throw Oops.Oh($"上/下限长度不能超过 {LimitMaxLen}(快照承载限制)");
  100. decimal? u = ParseDec(upper), l = ParseDec(lower);
  101. if (!string.IsNullOrEmpty(upper) && u == null) throw Oops.Oh("上限必须是数值");
  102. if (!string.IsNullOrEmpty(lower) && l == null) throw Oops.Oh("下限必须是数值");
  103. if (u.HasValue && l.HasValue && l.Value > u.Value) throw Oops.Oh("下限不能大于上限");
  104. }
  105. // 4) 写回
  106. var n = await _db.Ado.ExecuteCommandAsync(
  107. @"UPDATE qms_ccjygfzb
  108. SET lrlx=@lrlx, UpperToleranceLimit=@upper, LowerToleranceLimit=@lower
  109. WHERE id=@id AND tenant_id=@t",
  110. new SugarParameter("@lrlx", (object?)lrlx ?? DBNull.Value),
  111. new SugarParameter("@upper", (object?)NullIfEmpty(upper) ?? DBNull.Value),
  112. new SugarParameter("@lower", (object?)NullIfEmpty(lower) ?? DBNull.Value),
  113. new SugarParameter("@id", input.Id),
  114. new SugarParameter("@t", tenantId));
  115. return new { ok = n > 0, judgmentType = DeriveJudgmentType(lrlx, upper, lower) };
  116. }
  117. /// <summary>清空某明细的录入类型与上下限(→ 未配置)。</summary>
  118. [DisplayName("清空检规判定条件")]
  119. [HttpPost("clear-rule")]
  120. public async Task<object> ClearRule([FromBody] FqcSpecRuleClearInput input)
  121. {
  122. var tenantId = ResolveTenantOrThrow();
  123. if (input == null || input.Id <= 0) throw Oops.Oh("明细 id 非法");
  124. var n = await _db.Ado.ExecuteCommandAsync(
  125. @"UPDATE qms_ccjygfzb SET lrlx=NULL, UpperToleranceLimit=NULL, LowerToleranceLimit=NULL
  126. WHERE id=@id AND tenant_id=@t",
  127. new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId));
  128. return new { ok = n > 0, judgmentType = "NONE" };
  129. }
  130. // ===== 辅助 =====
  131. /// <summary>判定方式推导:显式 lrlx 优先;未设置时有上下限视为 NUMERIC,否则 NONE。
  132. /// (不从"上下限为空"反推 NON_NUMERIC,非数值型必须显式 lrlx=NON_NUMERIC 标注。)</summary>
  133. private static string DeriveJudgmentType(string? lrlx, string? upper, string? lower)
  134. {
  135. if (lrlx == TypeNumeric) return TypeNumeric;
  136. if (lrlx == TypeNonNumeric) return TypeNonNumeric;
  137. var hasLimit = !string.IsNullOrWhiteSpace(upper) || !string.IsNullOrWhiteSpace(lower);
  138. return hasLimit ? TypeNumeric : "NONE";
  139. }
  140. private static string? NormalizeType(string? t)
  141. {
  142. if (string.IsNullOrWhiteSpace(t)) return null;
  143. var v = t.Trim().ToUpperInvariant();
  144. if (v == TypeNumeric || v == TypeNonNumeric) return v;
  145. throw Oops.Oh("录入类型仅支持 NUMERIC / NON_NUMERIC");
  146. }
  147. private static string Trim(string? s) => (s ?? string.Empty).Trim();
  148. private static string? NullIfEmpty(string s) => string.IsNullOrEmpty(s) ? null : s;
  149. private static decimal? ParseDec(string s) => decimal.TryParse(s, out var d) ? d : null;
  150. }
  151. public class FqcSpecRulePageInput
  152. {
  153. public string? Wjbh { get; set; }
  154. public string? Cpmc { get; set; }
  155. public int Page { get; set; } = 1;
  156. public int PageSize { get; set; } = 20;
  157. }
  158. public class FqcSpecHeadRow
  159. {
  160. public long Id { get; set; }
  161. public string? Wjbh { get; set; }
  162. public string? Bb { get; set; }
  163. public string? Cpmc { get; set; }
  164. public string? Cpxh { get; set; }
  165. public string? Sxrq { get; set; }
  166. }
  167. public class FqcSpecRuleRow
  168. {
  169. public long Id { get; set; }
  170. public string? Xh { get; set; }
  171. public string? Jyxm { get; set; }
  172. public string? Jsyq { get; set; }
  173. public string? Jsbz { get; set; }
  174. public string? Cyfa { get; set; }
  175. /// <summary>录入类型 NUMERIC / NON_NUMERIC / null</summary>
  176. public string? Lrlx { get; set; }
  177. public string? UpperLimit { get; set; }
  178. public string? LowerLimit { get; set; }
  179. /// <summary>推导判定方式 NUMERIC / NON_NUMERIC / NONE</summary>
  180. public string? JudgmentType { get; set; }
  181. }
  182. public class FqcSpecRuleSaveInput
  183. {
  184. public long Id { get; set; }
  185. /// <summary>录入类型 NUMERIC / NON_NUMERIC(可空=未设置)</summary>
  186. public string? Lrlx { get; set; }
  187. public string? UpperLimit { get; set; }
  188. public string? LowerLimit { get; set; }
  189. }
  190. public class FqcSpecRuleClearInput
  191. {
  192. public long Id { get; set; }
  193. }