FqcSpecMaterialMapService.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. using System.Text.RegularExpressions;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Yitter.IdGenerator;
  4. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  5. /// <summary>
  6. /// S7 FQC 检规↔标准物料 派生映射服务(G1)。
  7. ///
  8. /// 目的:解决"报检物料 → 检规"精确匹配。源 qms_ccjygf.wlbm 是多物料脏串(分隔符不统一 + 粘连 + 横杠区间),
  9. /// 直接 LIKE 会误配(实证 31 token 互为子串)。本服务把 wlbm 清洗成 token,与 ItemMaster.ItemNum 精确匹配,
  10. /// 落派生桥表 ado_s7_fqc_spec_material_map,供"报检物料→检规"稳定查询与人工订正。
  11. ///
  12. /// 硬边界:① 绝不修改原始 qms_ccjygf.wlbm/sxrq;② 禁止 LIKE 子串匹配;③ 粘连不自动拆(词典缺片段),标 GLUE_UNRESOLVED;
  13. /// ④ 全部按 ResolveTenantOrThrow 登录租户隔离,禁透传源行租户。
  14. /// </summary>
  15. [ApiDescriptionSettings(Order = 340, Description = "FQC检规物料映射")]
  16. [Route("api/S7FqcSpecMap")]
  17. [NonUnify]
  18. public class FqcSpecMaterialMapService : IDynamicApiController, ITransient
  19. {
  20. private readonly ISqlSugarClient _db;
  21. private readonly UserManager _userManager;
  22. public FqcSpecMaterialMapService(ISqlSugarClient db, UserManager userManager)
  23. {
  24. _db = db;
  25. _userManager = userManager;
  26. }
  27. private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
  28. /// <summary>wlbm 拆分正则:半/全角分号逗号、换行回车 tab、斜杠、半/全角冒号、空白。</summary>
  29. private static readonly Regex TokenSplitter = new(@"[;;,,\r\n\t/::\s]+", RegexOptions.Compiled);
  30. /// <summary>纯物料码形态(字母数字,长度 4-12);用于粘连/异常识别(非白名单,仅分级)。</summary>
  31. private static readonly Regex PlainCodeShape = new(@"^[A-Za-z0-9]{4,12}$", RegexOptions.Compiled);
  32. // ============================ 清洗重建 ============================
  33. /// <summary>
  34. /// 重建当前租户的检规↔物料映射(清洗 wlbm → 精确匹配 ItemMaster → 落桥表)。
  35. /// 只删除并重建自动派生行(is_manual=0);人工绑定行(is_manual=1)原样保留。
  36. /// </summary>
  37. [DisplayName("重建检规物料映射")]
  38. [HttpPost("rebuild")]
  39. public async Task<object> Rebuild(CancellationToken ct)
  40. {
  41. var tenantId = ResolveTenantOrThrow();
  42. var now = DateTime.Now;
  43. var userId = _userManager.UserId;
  44. // 1) 读检规主表
  45. var specs = await _db.Ado.SqlQueryAsync<SpecRow>(
  46. "SELECT id AS Id, wjbh AS Wjbh, bb AS Bb, sxrq AS Sxrq, wlbm AS Wlbm FROM qms_ccjygf WHERE tenant_id=@t",
  47. new SugarParameter("@t", tenantId));
  48. // 2) 物料词典(当前租户 ItemMaster.ItemNum 精确集合)
  49. var itemSet = new HashSet<string>(
  50. await _db.Ado.SqlQueryAsync<string>(
  51. "SELECT DISTINCT ItemNum FROM ItemMaster WHERE tenant_id=@t AND ItemNum IS NOT NULL",
  52. new SugarParameter("@t", tenantId)),
  53. StringComparer.OrdinalIgnoreCase);
  54. var rows = new List<Dictionary<string, object>>();
  55. int exact = 0, notFound = 0, glue = 0, total = 0;
  56. foreach (var s in specs)
  57. {
  58. var norm = NormalizeDate(s.Sxrq);
  59. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  60. var tokens = SplitTokens(s.Wlbm);
  61. int seq = 0;
  62. foreach (var raw in tokens)
  63. {
  64. var cleaned = raw.Trim();
  65. if (cleaned.Length == 0) continue;
  66. if (!seen.Add(cleaned)) continue; // 同检规内去重
  67. seq++;
  68. total++;
  69. string status;
  70. string? matched = null;
  71. if (itemSet.Contains(cleaned)) { status = "EXACT"; matched = cleaned; exact++; }
  72. else if (!PlainCodeShape.IsMatch(cleaned)) { status = "GLUE_UNRESOLVED"; glue++; } // 含区间/异常字符/超长=粘连或非单码
  73. else { status = "NOT_FOUND"; notFound++; }
  74. rows.Add(new Dictionary<string, object>
  75. {
  76. ["id"] = YitIdHelper.NextId(),
  77. ["tenant_id"] = tenantId,
  78. ["jygfid"] = s.Id,
  79. ["wjbh"] = s.Wjbh,
  80. ["bb"] = s.Bb,
  81. ["seq"] = seq,
  82. ["raw_token"] = raw,
  83. ["cleaned_token"] = cleaned,
  84. ["material_code"] = (object?)matched ?? DBNull.Value,
  85. ["match_status"] = status,
  86. ["normalized_effective_date"] = (object?)norm ?? DBNull.Value,
  87. ["note"] = DBNull.Value,
  88. ["is_manual"] = 0,
  89. ["status"] = 1,
  90. ["create_time"] = now,
  91. ["create_user_id"] = (object?)userId ?? DBNull.Value,
  92. });
  93. }
  94. }
  95. var tran = await _db.Ado.UseTranAsync(async () =>
  96. {
  97. // 只清自动派生行,人工绑定(is_manual=1)保留
  98. await _db.Ado.ExecuteCommandAsync(
  99. "DELETE FROM ado_s7_fqc_spec_material_map WHERE tenant_id=@t AND is_manual=0",
  100. new SugarParameter("@t", tenantId));
  101. if (rows.Count > 0)
  102. await BulkInsertMapRowsAsync(rows);
  103. });
  104. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("重建映射失败");
  105. return new
  106. {
  107. ok = true,
  108. specCount = specs.Count,
  109. token = total,
  110. exact,
  111. notFound,
  112. glueUnresolved = glue,
  113. exactRate = total == 0 ? 0 : Math.Round(exact * 100.0 / total, 1)
  114. };
  115. }
  116. // ============================ 查询/维护 ============================
  117. /// <summary>映射列表(维护用,可按物料/状态筛选)。</summary>
  118. [DisplayName("检规物料映射列表")]
  119. [HttpGet("page")]
  120. public async Task<object> Page([FromQuery] FqcSpecMapPageInput input)
  121. {
  122. var tenantId = ResolveTenantOrThrow();
  123. var page = input.Page <= 0 ? 1 : input.Page;
  124. var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
  125. var where = new List<string> { "m.tenant_id=@t" };
  126. var pars = new List<SugarParameter> { new("@t", tenantId) };
  127. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  128. {
  129. where.Add("m.cleaned_token=@mc"); // 精确,禁 LIKE
  130. pars.Add(new SugarParameter("@mc", input.MaterialCode.Trim()));
  131. }
  132. if (!string.IsNullOrWhiteSpace(input.MatchStatus))
  133. {
  134. where.Add("m.match_status=@ms");
  135. pars.Add(new SugarParameter("@ms", input.MatchStatus.Trim()));
  136. }
  137. if (!string.IsNullOrWhiteSpace(input.Wjbh))
  138. {
  139. where.Add("m.wjbh=@wj");
  140. pars.Add(new SugarParameter("@wj", input.Wjbh.Trim()));
  141. }
  142. var whereSql = string.Join(" AND ", where);
  143. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM ado_s7_fqc_spec_material_map m WHERE {whereSql}", pars);
  144. var list = await _db.Ado.SqlQueryAsync<FqcSpecMapRow>(
  145. $@"SELECT m.id AS Id, m.jygfid AS Jygfid, m.wjbh AS Wjbh, m.bb AS Bb, m.raw_token AS RawToken,
  146. m.cleaned_token AS CleanedToken, m.material_code AS MaterialCode, m.match_status AS MatchStatus,
  147. m.normalized_effective_date AS NormalizedEffectiveDate, m.is_manual AS IsManual, m.note AS Note
  148. FROM ado_s7_fqc_spec_material_map m WHERE {whereSql}
  149. ORDER BY m.match_status='EXACT' DESC, m.wjbh, m.seq
  150. LIMIT {pageSize} OFFSET {(page - 1) * pageSize}", pars);
  151. return new { total, page, pageSize, list };
  152. }
  153. /// <summary>人工绑定:把一个标准物料码绑定到指定检规(is_manual=1,脱敏环境订正用;不改源 wlbm)。</summary>
  154. [DisplayName("人工绑定检规物料")]
  155. [HttpPost("bind")]
  156. public async Task<object> Bind([FromBody] FqcSpecMapBindInput input)
  157. {
  158. var tenantId = ResolveTenantOrThrow();
  159. if (input == null || input.Jygfid <= 0 || string.IsNullOrWhiteSpace(input.MaterialCode))
  160. throw Oops.Oh("检规 id 与物料编码必填");
  161. var materialCode = input.MaterialCode.Trim();
  162. var spec = await _db.Ado.SqlQuerySingleAsync<SpecRow>(
  163. "SELECT id AS Id, wjbh AS Wjbh, bb AS Bb, sxrq AS Sxrq FROM qms_ccjygf WHERE id=@id AND tenant_id=@t LIMIT 1",
  164. new SugarParameter("@id", input.Jygfid), new SugarParameter("@t", tenantId));
  165. if (spec == null) throw Oops.Oh("检规不存在或不属于当前租户");
  166. var itemExists = await _db.Ado.GetIntAsync(
  167. "SELECT COUNT(1) FROM ItemMaster WHERE tenant_id=@t AND ItemNum=@mc",
  168. new SugarParameter("@t", tenantId), new SugarParameter("@mc", materialCode));
  169. if (itemExists == 0) throw Oops.Oh($"物料 {materialCode} 不在当前租户 ItemMaster,无法绑定");
  170. // 幂等:同租户+检规+物料+人工 唯一(raw_token 用 materialCode 承载)
  171. var dup = await _db.Ado.GetIntAsync(
  172. "SELECT COUNT(1) FROM ado_s7_fqc_spec_material_map WHERE tenant_id=@t AND jygfid=@j AND cleaned_token=@mc AND is_manual=1",
  173. new SugarParameter("@t", tenantId), new SugarParameter("@j", input.Jygfid), new SugarParameter("@mc", materialCode));
  174. if (dup > 0) return new { ok = true, message = "已存在该人工绑定" };
  175. await BulkInsertMapRowsAsync(new List<Dictionary<string, object>>
  176. {
  177. new()
  178. {
  179. ["id"] = YitIdHelper.NextId(),
  180. ["tenant_id"] = tenantId,
  181. ["jygfid"] = input.Jygfid,
  182. ["wjbh"] = spec.Wjbh,
  183. ["bb"] = spec.Bb,
  184. ["seq"] = 0,
  185. ["raw_token"] = materialCode,
  186. ["cleaned_token"] = materialCode,
  187. ["material_code"] = materialCode,
  188. ["match_status"] = "MANUAL",
  189. ["normalized_effective_date"] = (object?)NormalizeDate(spec.Sxrq) ?? DBNull.Value,
  190. ["note"] = input.Note,
  191. ["is_manual"] = 1,
  192. ["status"] = 1,
  193. ["create_time"] = DateTime.Now,
  194. ["create_user_id"] = _userManager.UserId,
  195. }
  196. });
  197. return new { ok = true };
  198. }
  199. /// <summary>
  200. /// 单检规同步(S0 CRUD 后调用):只重建**这一条检规**的自动派生映射,不触碰同租户其他检规。
  201. ///
  202. /// 与 <see cref="Rebuild"/> 的区别:Rebuild 是「单租户全量」——会 DELETE 该租户所有 is_manual=0 行
  203. /// 再整体重建;一次 S0 保存去跑全量既慢又会把其他检规的映射卷进同一事务。本方法把粒度收到
  204. /// jygfid,用于 Create / Update(wlbm|sxrq|bb 变更) 后的即时一致。
  205. ///
  206. /// 硬边界(与 Rebuild 完全一致,不得放宽):
  207. /// ① DELETE 条件必须同时带 tenant_id + jygfid + **is_manual=0**——人工绑定行(is_manual=1)
  208. /// 是脏 wlbm 的人工订正结果,优先级高于源串,绝不能被自动同步删掉;
  209. /// ② 禁止 LIKE 子串匹配,token 与 ItemMaster.ItemNum 精确比对;
  210. /// ③ 绝不修改源 qms_ccjygf.wlbm / sxrq。
  211. ///
  212. /// 调用方需自行保证 jygfid 属于 tenantId(本方法只按传入租户过滤,不做归属反查)。
  213. /// </summary>
  214. public async Task<object> SyncSpecAsync(long tenantId, long jygfid)
  215. {
  216. if (tenantId <= 0 || jygfid <= 0) return new { ok = false, reason = "invalid-args" };
  217. var spec = await _db.Ado.SqlQuerySingleAsync<SpecRow>(
  218. "SELECT id AS Id, wjbh AS Wjbh, bb AS Bb, sxrq AS Sxrq, wlbm AS Wlbm FROM qms_ccjygf WHERE id=@id AND tenant_id=@t LIMIT 1",
  219. new SugarParameter("@id", jygfid), new SugarParameter("@t", tenantId));
  220. // 检规已不存在(例如 Delete 后调用)→ 只清自动行,人工行保留待人工处置
  221. var norm = spec == null ? null : NormalizeDate(spec.Sxrq);
  222. var rows = new List<Dictionary<string, object>>();
  223. int exact = 0, notFound = 0, glue = 0, total = 0;
  224. if (spec != null)
  225. {
  226. var itemSet = new HashSet<string>(
  227. await _db.Ado.SqlQueryAsync<string>(
  228. "SELECT DISTINCT ItemNum FROM ItemMaster WHERE tenant_id=@t AND ItemNum IS NOT NULL",
  229. new SugarParameter("@t", tenantId)),
  230. StringComparer.OrdinalIgnoreCase);
  231. var now = DateTime.Now;
  232. var userId = _userManager.UserId;
  233. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  234. int seq = 0;
  235. foreach (var raw in SplitTokens(spec.Wlbm))
  236. {
  237. var cleaned = raw.Trim();
  238. if (cleaned.Length == 0) continue;
  239. if (!seen.Add(cleaned)) continue;
  240. seq++; total++;
  241. string status;
  242. string? matched = null;
  243. if (itemSet.Contains(cleaned)) { status = "EXACT"; matched = cleaned; exact++; }
  244. else if (!PlainCodeShape.IsMatch(cleaned)) { status = "GLUE_UNRESOLVED"; glue++; }
  245. else { status = "NOT_FOUND"; notFound++; }
  246. rows.Add(new Dictionary<string, object>
  247. {
  248. ["id"] = YitIdHelper.NextId(),
  249. ["tenant_id"] = tenantId,
  250. ["jygfid"] = spec.Id,
  251. ["wjbh"] = spec.Wjbh,
  252. ["bb"] = spec.Bb,
  253. ["seq"] = seq,
  254. ["raw_token"] = raw,
  255. ["cleaned_token"] = cleaned,
  256. ["material_code"] = (object?)matched ?? DBNull.Value,
  257. ["match_status"] = status,
  258. ["normalized_effective_date"] = (object?)norm ?? DBNull.Value,
  259. ["note"] = DBNull.Value,
  260. ["is_manual"] = 0,
  261. ["status"] = 1,
  262. ["create_time"] = now,
  263. ["create_user_id"] = (object?)userId ?? DBNull.Value,
  264. });
  265. }
  266. }
  267. var tran = await _db.Ado.UseTranAsync(async () =>
  268. {
  269. await _db.Ado.ExecuteCommandAsync(
  270. "DELETE FROM ado_s7_fqc_spec_material_map WHERE tenant_id=@t AND jygfid=@j AND is_manual=0",
  271. new SugarParameter("@t", tenantId), new SugarParameter("@j", jygfid));
  272. if (rows.Count > 0) await BulkInsertMapRowsAsync(rows);
  273. // 人工行随主表 wjbh/bb/sxrq 变更同步元数据(不改归属、不改 is_manual、不删除)
  274. if (spec != null)
  275. {
  276. await _db.Ado.ExecuteCommandAsync(
  277. @"UPDATE ado_s7_fqc_spec_material_map
  278. SET wjbh=@w, bb=@b, normalized_effective_date=@d
  279. WHERE tenant_id=@t AND jygfid=@j AND is_manual=1",
  280. new SugarParameter("@w", (object?)spec.Wjbh ?? DBNull.Value),
  281. new SugarParameter("@b", (object?)spec.Bb ?? DBNull.Value),
  282. new SugarParameter("@d", (object?)norm ?? DBNull.Value),
  283. new SugarParameter("@t", tenantId), new SugarParameter("@j", jygfid));
  284. }
  285. });
  286. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("同步检规物料映射失败");
  287. return new { ok = true, jygfid, token = total, exact, notFound, glueUnresolved = glue };
  288. }
  289. /// <summary>解绑人工绑定。</summary>
  290. [DisplayName("解绑检规物料")]
  291. [HttpPost("unbind")]
  292. public async Task<object> Unbind([FromBody] FqcSpecMapUnbindInput input)
  293. {
  294. var tenantId = ResolveTenantOrThrow();
  295. if (input == null || input.Id <= 0) throw Oops.Oh("id 必填");
  296. var n = await _db.Ado.ExecuteCommandAsync(
  297. "DELETE FROM ado_s7_fqc_spec_material_map WHERE id=@id AND tenant_id=@t AND is_manual=1",
  298. new SugarParameter("@id", input.Id), new SugarParameter("@t", tenantId));
  299. return new { ok = n > 0 };
  300. }
  301. /// <summary>
  302. /// 按物料精确解析检规候选(供生成检验单调用)。返回 EXACT/MANUAL 命中的检规,多命中/多版本由调用方转人工选。
  303. /// </summary>
  304. [DisplayName("按物料解析检规")]
  305. [HttpGet("resolve")]
  306. public async Task<List<FqcSpecCandidate>> Resolve([FromQuery] string materialCode)
  307. {
  308. var tenantId = ResolveTenantOrThrow();
  309. if (string.IsNullOrWhiteSpace(materialCode)) throw Oops.Oh("物料编码必填");
  310. return await ResolveInternalAsync(tenantId, materialCode.Trim());
  311. }
  312. /// <summary>内部复用:按物料精确取检规候选(EXACT/MANUAL),按生效日期新→旧排序。</summary>
  313. public async Task<List<FqcSpecCandidate>> ResolveInternalAsync(long tenantId, string materialCode)
  314. {
  315. return await _db.Ado.SqlQueryAsync<FqcSpecCandidate>(
  316. @"SELECT g.id AS Jygfid, g.wjbh AS Wjbh, g.bb AS Bb, g.sxrq AS Sxrq,
  317. m.normalized_effective_date AS NormalizedEffectiveDate, m.match_status AS MatchStatus
  318. FROM ado_s7_fqc_spec_material_map m
  319. JOIN qms_ccjygf g ON g.id=m.jygfid AND g.tenant_id=m.tenant_id
  320. WHERE m.tenant_id=@t AND m.status=1 AND m.material_code=@mc
  321. AND m.match_status IN ('EXACT','MANUAL')
  322. ORDER BY m.normalized_effective_date DESC, g.id DESC",
  323. new SugarParameter("@t", tenantId), new SugarParameter("@mc", materialCode));
  324. }
  325. // ============================ 辅助 ============================
  326. private static IEnumerable<string> SplitTokens(string? wlbm)
  327. {
  328. if (string.IsNullOrWhiteSpace(wlbm)) return Array.Empty<string>();
  329. return TokenSplitter.Split(wlbm).Where(x => !string.IsNullOrWhiteSpace(x));
  330. }
  331. /// <summary>
  332. /// 参数化分块批插桥表行。替代 db.Insertable(List&lt;Dictionary&gt;).AS(表)——后者对 nullable datetime 列
  333. /// (normalized_effective_date) 的 DBNull 会被 SqlSugar 字典批插按空串渲染,触发 MySQL
  334. /// "Incorrect datetime value: ''"。改用显式参数化多行 INSERT(SugarParameter 正确处理 DBNull→NULL)。
  335. /// </summary>
  336. private static readonly string[] MapColumns =
  337. {
  338. "id", "tenant_id", "jygfid", "wjbh", "bb", "seq", "raw_token", "cleaned_token", "material_code",
  339. "match_status", "normalized_effective_date", "note", "is_manual", "status", "create_time", "create_user_id"
  340. };
  341. private async Task BulkInsertMapRowsAsync(List<Dictionary<string, object>> rows)
  342. {
  343. if (rows == null || rows.Count == 0) return;
  344. const int chunkSize = 200;
  345. var colList = string.Join(", ", MapColumns);
  346. for (var start = 0; start < rows.Count; start += chunkSize)
  347. {
  348. var slice = rows.GetRange(start, Math.Min(chunkSize, rows.Count - start));
  349. var valueClauses = new List<string>(slice.Count);
  350. var pars = new List<SugarParameter>(slice.Count * MapColumns.Length);
  351. for (var r = 0; r < slice.Count; r++)
  352. {
  353. var placeholders = new string[MapColumns.Length];
  354. for (var c = 0; c < MapColumns.Length; c++)
  355. {
  356. var name = $"@{MapColumns[c]}_{r}";
  357. placeholders[c] = name;
  358. var val = slice[r].TryGetValue(MapColumns[c], out var v) ? (v ?? DBNull.Value) : DBNull.Value;
  359. pars.Add(new SugarParameter(name, val));
  360. }
  361. valueClauses.Add("(" + string.Join(", ", placeholders) + ")");
  362. }
  363. var sql = $"INSERT INTO ado_s7_fqc_spec_material_map ({colList}) VALUES {string.Join(", ", valueClauses)}";
  364. await _db.Ado.ExecuteCommandAsync(sql, pars);
  365. }
  366. }
  367. /// <summary>规范化 sxrq(容错 dot/双点/横杠/带时分秒/非日期),失败返回 null(不覆盖源)。</summary>
  368. private static DateTime? NormalizeDate(string? sxrq)
  369. {
  370. if (string.IsNullOrWhiteSpace(sxrq)) return null;
  371. var s = sxrq.Trim().Replace("..", ".").Replace('.', '-').Replace('/', '-');
  372. var datePart = s.Split(' ')[0];
  373. if (DateTime.TryParse(datePart, out var d)) return d.Date;
  374. return null;
  375. }
  376. private sealed class SpecRow
  377. {
  378. public long Id { get; set; }
  379. public string? Wjbh { get; set; }
  380. public string? Bb { get; set; }
  381. public string? Sxrq { get; set; }
  382. public string? Wlbm { get; set; }
  383. }
  384. }
  385. public class FqcSpecMapPageInput
  386. {
  387. public string? MaterialCode { get; set; }
  388. public string? MatchStatus { get; set; }
  389. public string? Wjbh { get; set; }
  390. public int Page { get; set; } = 1;
  391. public int PageSize { get; set; } = 20;
  392. }
  393. public class FqcSpecMapRow
  394. {
  395. public long Id { get; set; }
  396. public long Jygfid { get; set; }
  397. public string? Wjbh { get; set; }
  398. public string? Bb { get; set; }
  399. public string? RawToken { get; set; }
  400. public string? CleanedToken { get; set; }
  401. public string? MaterialCode { get; set; }
  402. public string? MatchStatus { get; set; }
  403. public DateTime? NormalizedEffectiveDate { get; set; }
  404. public int IsManual { get; set; }
  405. public string? Note { get; set; }
  406. }
  407. public class FqcSpecMapBindInput
  408. {
  409. public long Jygfid { get; set; }
  410. public string MaterialCode { get; set; } = string.Empty;
  411. public string? Note { get; set; }
  412. }
  413. public class FqcSpecMapUnbindInput
  414. {
  415. public long Id { get; set; }
  416. }
  417. public class FqcSpecCandidate
  418. {
  419. public long Jygfid { get; set; }
  420. public string? Wjbh { get; set; }
  421. public string? Bb { get; set; }
  422. public string? Sxrq { get; set; }
  423. public DateTime? NormalizedEffectiveDate { get; set; }
  424. public string? MatchStatus { get; set; }
  425. }