fix(w05): 两颗料不能是同一颗 —— 起单这一刻立墙,因为路只在这里

用户:「创建活动的启动参数里面不应该能选到同样的两个芯片」。
查下来这不只是没意义,是**把单子做死**:sub_eval.part 的主键是 eval_ref+internal_pn
(role 不在主键里),S1 配方铸的两行撞成同一个主键、后一笔覆盖前一笔,两行收成一行 ——
sub_eval_part_exists_status_found = 2 永不成立,S1 收不了步,而件上那两行人加不出来
(行源 mine:,没有「+加一行」),唯一出路是终止重开。与「两格留空」是同一条链。

平台加互斥声明 x-distinct(契约 distinctGroup)——与 x-link 正相反:那个是引导,这个是墙。
· 前端:别的格占着的候选这一格不列(自己填着的那个永远留着,否则 select 当场变空白),
  并说清是谁占了哪个;走「新增」自己写一个同样的 → 小红字 + 启动按不下去;
· 服务端 validateLaunchParams 独立拦(脚本 / API 绕不过),400 点名后填的那格;
· 「算不算同一个」= 去空白 + 不分大小写(launchDistinctKey,前后端同一把尺)——
  只挡精确相等的话 lm2596s-5.0 照样进去,混一颗垃圾主数据出来。
W05 两格挂 x-distinct: 'part',2.14.0 已装库核过。

测:服务端 6 条 + 前端 8 条(含联动与互斥合起来的真形状)+ 包内 1 条,三处变异验证过;
四道门禁绿(2548 测)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 22:03:56 +08:00
co-authored by Claude Fable 5
parent 8c5f3a7736
commit e47ce92702
11 changed files with 331 additions and 30 deletions
+7 -1
View File
@@ -386,6 +386,7 @@ window.addEventListener('message', (e) => {
"desc": "差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。",
"placeholder": "LM2596S-5.0",
"linkGroup": "chip-type",
"distinctGroup": "part",
"options": [{ "value": "LM2596S-5.0", "label": "LM2596S-5.0", "group": "1", "groupLabel": "DC-DC 降压稳压器" }]
},
{
@@ -396,6 +397,7 @@ window.addEventListener('message', (e) => {
"desc": "被评估物料;库里有就直接选,没有就自己填料号,启动后经数据工单创建。",
"placeholder": "XL2596S-5.0",
"linkGroup": "chip-type",
"distinctGroup": "part",
"options": [
{ "value": "LM2596S-5.0", "label": "LM2596S-5.0", "group": "1", "groupLabel": "DC-DC 降压稳压器" },
{ "value": "AMS1117-3.3", "label": "AMS1117-3.3", "group": "2", "groupLabel": "LDO 线性稳压器" },
@@ -416,7 +418,11 @@ W05 的候选内部料号(2026-08-27 用户定)与原内部料号**同款下拉
> **这是表单的引导,不是墙**:服务端不据 `linkGroup` 校验 —— 两格填不同 `group` 的值照样收得下(combo 本就允许候选之外的值)。类型一致性由活动自己的收步判据管(W05 是 S1 的 `sub_eval_part_distinct_chip_type_id = 1`)。
> 活动清单侧怎么声明(前端不用管,给写活动包的人看):`properties.<key>` 上 `title`→label、`description`→desc、`examples[0]`→placeholder、`default`、`required[]`;`enum`+`enumNames`→select;`'x-control'` 定标量控件(`text|textarea|select|combo`);`'x-options': { ledger:'域.对象', value:'列', label:'列' }` 让候选从正式台账现读;`'x-options'.group` 让候选带上分组键 —— 只写列名(`'chip_type_id'`)= 有键没名,整份写 `{ column, ledger, value, label }` 再从字典表换人读名;`'x-link': '<组名>'` 把几格结成联动组(同组按分组键互相收窄,见上);`type:'array'` 且元素是字符串→list,**数组字段标 `x-control: combo|select` 就摆成单选**(提交单个字符串,后端包成一元数组);**`'x-order': <数字>` 定表单次序**(清单存在 jsonb 里,书写顺序到不了这里);`format:'file'` 的字段本期不上表单;select 没候选降成 text。
**互斥(2026-08-31 用户定:「不应该能选到同样的两个芯片」)**:参数可带 `distinctGroup`(互斥组名)。**同一 `distinctGroup` 的几格不能填同一个值** —— 前端把**别的格此刻占着的候选**从这一格的下拉里抽掉(唯独这一格自己填着的那个永远留着,否则 `<select>` 会当场变空白),并在控件下说清是谁占了哪个;人若走「新增」自己写了一个同样的值,小红字出来、「启动活动」按不下去。**「算不算同一个」= 去空白 + 不分大小写**(契约的 `launchDistinctKey`,前后端同一把尺;那句话是 `launchDistinctMessage`)。只管标量格(text / select / combo),数组格不参与。
> **这一条是墙,与 `linkGroup` 正相反**:服务端 `POST …/instances` 独立校验,撞上就是 400 `invalid`、`detail=[{key,message}]` 点名后填的那格 —— 脚本 / API 绕过表单一样过不去。为什么非拦不可:W05 两格填同一颗料会把单子**做死** —— `sub_eval.part` 的主键是 `eval_ref + internal_pn`,配方铸的两行撞成同一个主键、后一笔覆盖前一笔,两行收成一行,S1 的收步判据 `sub_eval_part_exists_status_found = 2` 永不成立,而件上那两行人加不出来,唯一出路是终止重开。**墙立在起单这一刻,是因为路只在这里**(那时换一格就好了)。
> 活动清单侧怎么声明(前端不用管,给写活动包的人看):`properties.<key>` 上 `title`→label、`description`→desc、`examples[0]`→placeholder、`default`、`required[]`;`enum`+`enumNames`→select;`'x-control'` 定标量控件(`text|textarea|select|combo`);`'x-options': { ledger:'域.对象', value:'列', label:'列' }` 让候选从正式台账现读;`'x-options'.group` 让候选带上分组键 —— 只写列名(`'chip_type_id'`)= 有键没名,整份写 `{ column, ledger, value, label }` 再从字典表换人读名;`'x-link': '<组名>'` 把几格结成联动组(同组按分组键互相收窄,见上);`'x-distinct': '<组名>'` 把几格结成互斥组(同组不能填同一个值 —— **这条是墙**,见上);`type:'array'` 且元素是字符串→list,**数组字段标 `x-control: combo|select` 就摆成单选**(提交单个字符串,后端包成一元数组);**`'x-order': <数字>` 定表单次序**(清单存在 jsonb 里,书写顺序到不了这里);`format:'file'` 的字段本期不上表单;select 没候选降成 text。
## 5. 启动活动(创建实例)—— `POST /api/gov/activities/:activityId/instances`(`launchOpsInstanceRoute`)