Commit 429bef5fee197ebc5c8d0f2e46a00c67e4c57954
1 parent
96dcd92f
docs: ERP 侧字段候选接口任务书(AI 表单 FK 选择器接入 ERP 下拉配置)
Showing
1 changed file
with
160 additions
and
0 deletions
docs/erp-tasks-field-options.md
0 → 100644
| 1 | +# 任务:ERP 侧为 AI 表单提供「字段候选值」统一接口 | |
| 2 | + | |
| 3 | +给 ERP 侧的第二份任务书(第一份 = `rearch3-erp-tasks.md`,已交付验收通过)。 | |
| 4 | +本次只做**只读查询**,不涉及写入、不改状态协议。 | |
| 5 | + | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## 背景:xlyAi 现在自己查候选值,和 ERP 网页不一致 | |
| 9 | + | |
| 10 | +xlyAi 的表单卡上,外键字段(客户/产品/物料…)旁边有个【选择】按钮,点开是候选列表。 | |
| 11 | +现在这条路是 **xlyAi 自己直查共享库**:从字段字典视图 `viw_kg_field_dict` 推断外键目标表(如 | |
| 12 | +`eleproduct`),自己挑一个"名称列",拼 `WHERE sBrandsId=? AND 名称列 LIKE ?` 分页返回。 | |
| 13 | + | |
| 14 | +这套能跑,但和 ERP 网页的同一个下拉相比缺三样东西,**其中第二条是数据正确性问题**: | |
| 15 | + | |
| 16 | +1. **行级数据权限**:ERP 的下拉 SQL 里有 `[sId, sLookCustomer]` 这类宏,经 | |
| 17 | + `BusinessCommonServiceImpl.getDataFilterAuth` 展开成权限子查询(业务员只看自己的客户)。 | |
| 18 | + xlyAi 这条路只有"该表是否被你有权的表单引用过"的**表级**判断,**没有行级过滤**。 | |
| 19 | +2. **级联过滤(重要)**:报价单的产品下拉配置是 | |
| 20 | + `... FROM eleproduct A ... where a.bInvalid=0 and sCustomerId=#sCustomerId# #sKeyUpFilter# #A.companyId#`, | |
| 21 | + 配套 `sSqlCondition = master.sCustomerId.sCustomerId` —— 即**产品候选必须限定在已选客户名下**。 | |
| 22 | + xlyAi 现在会把该品牌下所有产品都列出来,用户可以选到一个不属于该客户的产品。 | |
| 23 | +3. **联动回填**:客户控件的 `sAssignField` 有近 30 组映射 | |
| 24 | + (`sCustomerId:sId, sCustomerName:sCustomerName, sCustomerNo:sCustomerNo, sContacts:..., sGetPayId:...`), | |
| 25 | + ERP 网页选中客户后会把这些列一起带进表单。xlyAi 只写外键 id,其余留空。 | |
| 26 | + | |
| 27 | +## 好消息:映射键已经找到了,不需要新建任何映射表 | |
| 28 | + | |
| 29 | +实测确认(本地 saaslocal 库): | |
| 30 | + | |
| 31 | +- xlyAi 表单卡里的 `formId`(如报价 = `101251240115016076506222750`) | |
| 32 | + **就是 `gdsconfigformmaster.sId`**(该行 `sTbName='QuoQuotationmaster'`、`sParentId=` 模块 id `...222050`)。 | |
| 33 | +- ERP 的控件配置 `gdsconfigformslave` 正是以 `sParentId = 这个 formId` 挂在下面(报价主表共 146 行控件)。 | |
| 34 | +- 所以 **(xlyAi 的 formId, 字段) → ERP 控件 sId** 是一次直接查表,无需新建映射。 | |
| 35 | + | |
| 36 | +已验证的两个控件(报价主表): | |
| 37 | + | |
| 38 | +| 控件 sId | sName | sKeyUpFilter | 级联 sSqlCondition | | |
| 39 | +| --- | --- | --- | --- | | |
| 40 | +| `16427541500006326873721849694000` | sCustomerName | sCustomerName | 无 | | |
| 41 | +| `16427542760005958727285709417000` | sProductName | sProductName | `master.sCustomerId.sCustomerId` | | |
| 42 | + | |
| 43 | +我已用普通用户 token 实测过现成端点 | |
| 44 | +`POST /business/getSelectDataBysControlId/{控件sId}?sModelsId={模块id}`: | |
| 45 | + | |
| 46 | +- 客户控件 + `{"sKeyUpFilterName":"中科"}` → `code=1`,3 条候选,列齐全(含 sId / sCustomerNo / sSalesManName / dTaxRate …); | |
| 47 | +- 产品控件不传级联值 → `code=-1`,msg 是 | |
| 48 | + `=============下拉SQL中包含需要替换的字符,前台未传入替换的值,sSql:master.sCustomerId.sCustomerId========`; | |
| 49 | + 传 `{"sSqlCondition":{"sCustomerId":"<客户id>"}}` → `code=1`,只列该客户的产品。**级联行为正确。** | |
| 50 | + | |
| 51 | +--- | |
| 52 | + | |
| 53 | +## 要你们做的事:新增一个 AI 专用的字段候选接口 | |
| 54 | + | |
| 55 | +**不要**让 xlyAi 直接调 `getSelectDataBysControlId`——控件解析规则(个性化表覆盖、语言列、 | |
| 56 | +`sql`/`const`/`popup` 三种类型、`sRelation` 伪列反推)是你们的领域知识,散到 AI 侧会持续错位。 | |
| 57 | +请封一个端点,把"解析 + 取数 + 元数据"一次给我们。 | |
| 58 | + | |
| 59 | +### 接口契约(建议,可按你们习惯调整,但字段语义要覆盖到) | |
| 60 | + | |
| 61 | +``` | |
| 62 | +POST /ai/fieldOptions | |
| 63 | +Authorization: <用户 token> // 与现有 /ai/* 一致,@Authorization + @CurrentUser | |
| 64 | +{ | |
| 65 | + "sFormId": "101251240115016076506222750", // = gdsconfigformmaster.sId,xlyAi 表单卡里带的 formId | |
| 66 | + "sField": "sCustomerId", // xlyAi 用的是外键 id 列名(见下方「字段名对齐」) | |
| 67 | + "q": "中科", // 搜索词,可空 | |
| 68 | + "pageNum": 1, | |
| 69 | + "pageSize": 20, | |
| 70 | + "context": { "sCustomerId": "1752644777..." } // 已在表单上选好的其它字段值,供级联使用;可空 | |
| 71 | +} | |
| 72 | +``` | |
| 73 | + | |
| 74 | +返回(外层沿用你们的 `Feedback` 信封即可): | |
| 75 | + | |
| 76 | +```json | |
| 77 | +{ | |
| 78 | + "mode": "list", // list=有候选可列;none=该字段不是可选字段 | |
| 79 | + "nameField": "sCustomerName", // 哪一列当"名字"显示 | |
| 80 | + "valueField": "sId", // 哪一列是要存进业务表的值 | |
| 81 | + "columns": [{"col":"sCustomerName","label":"客户名称"}, | |
| 82 | + {"col":"sCustomerNo","label":"客户编号"}], // 建议展示的列,按重要性排序 | |
| 83 | + "rows": [{"sId":"...","sCustomerName":"中科袜业","sCustomerNo":"KH0012", ...}], | |
| 84 | + "total": 3, "pageNum": 1, "pageSize": 20, | |
| 85 | + "assign": {"sCustomerName":"sCustomerName","sCustomerNo":"sCustomerNo","sContacts":"sContacts"}, | |
| 86 | + "requires": [] | |
| 87 | +} | |
| 88 | +``` | |
| 89 | + | |
| 90 | +### 必须满足的几点 | |
| 91 | + | |
| 92 | +1. **字段名对齐(这条最容易踩坑)** | |
| 93 | + ERP 的控件挂在**显示名列**上(`sCustomerName`),而 xlyAi 传过来的是**外键 id 列**(`sCustomerId`)。 | |
| 94 | + 请在解析时两边都认:先按 `sName = sField` 找控件;找不到就找 `sAssignField` 里 | |
| 95 | + **把结果列赋给 `sField` 的那个控件**(客户控件的 `sAssignField` 含 `sCustomerId:sId`,正是这条线)。 | |
| 96 | + 两种都找不到 → 返回 `mode:"none"`,我们回落到自己的字典查询。 | |
| 97 | + | |
| 98 | +2. **级联依赖要结构化,不要抛内部报文** | |
| 99 | + 配置了 `sSqlCondition` 但 `context` 里缺值时,**不要**返回现在那句 | |
| 100 | + `下拉SQL中包含需要替换的字符...sSql:master.sCustomerId.sCustomerId`(泄漏内部实现且用户看不懂)。 | |
| 101 | + 请返回: | |
| 102 | + | |
| 103 | + ```json | |
| 104 | + {"mode":"need_context", "requires":["sCustomerId"], | |
| 105 | + "msg":"请先选择客户,再选产品"} | |
| 106 | + ``` | |
| 107 | + | |
| 108 | + xlyAi 会据此先让用户选父字段,而不是把候选列表拉空或报错。 | |
| 109 | + | |
| 110 | +3. **`popup` 类型也要能列数据** | |
| 111 | + 全库有 25 个 `sDropDownType='popup'` 的控件,网页上是"弹出另一个业务窗体去挑"。 | |
| 112 | + AI 没有"打开另一个窗体"的能力,请在这个端点里把 popup 也归一成 `rows` 列表返回 | |
| 113 | + (内部走 `sActiveId` 对应窗体的 `getBusinessDataByFormcustomId` 即可), | |
| 114 | + 并且**不要**返回 `id[-]名称` 这种拼接值,`rows` 里请拆成独立列。 | |
| 115 | + | |
| 116 | +4. **租户隔离要在代码层兜底(我方红线)** | |
| 117 | + 现状是靠配置里手写 `#companyId#`,漏写就跨租户。这个 AI 端点请**强制**附加 | |
| 118 | + 品牌/分公司条件;若某控件配置无法安全附加,宁可对 AI 通道返回 `mode:"none"`(我们回落), | |
| 119 | + **不要**返回一个没有租户过滤的结果集。 | |
| 120 | + | |
| 121 | +5. **行级数据权限必须照常生效** | |
| 122 | + 即保留 `getDataFilterAuth` 那一层(`[列, 权限key]` 宏展开)。这正是我们想要这个接口的主因之一。 | |
| 123 | + | |
| 124 | +6. **纯只读、无副作用**,不写日志表/不发消息;Redis 缓存 key 请确认仍含 userId(避免串号)。 | |
| 125 | + | |
| 126 | +7. **`assign` 回填映射要吐给我们** | |
| 127 | + 把该控件的 `sAssignField` 解析成 `目标字段 → 结果列` 的 map 返回。 | |
| 128 | + xlyAi 会在用户选中一行后,按它把联动列(客户编号、联系人、税率、业务员…)一并写进创建载荷, | |
| 129 | + 让 AI 建的单和网页建的单字段完整度一致。**只回填目标表真实存在的列**—— | |
| 130 | + 实测报价主表只存在 `sCustomerName` / `sCustomerNo` / `sMemo` 三个目标列, | |
| 131 | + 其余映射项在该表上是空转(可能是跨表单复用的配置),请你们过滤掉不存在的列,别让我们写脏字段。 | |
| 132 | + | |
| 133 | +### 验收(希望你们跑给我们看) | |
| 134 | + | |
| 135 | +- 客户字段:`{sFormId:"...222750", sField:"sCustomerId", q:"中科"}` → 有候选、`nameField=sCustomerName`、 | |
| 136 | + `assign` 含 `sCustomerName`/`sCustomerNo`。 | |
| 137 | +- 产品字段不给 context → `mode:"need_context"`, `requires:["sCustomerId"]`, 中文 msg 可读。 | |
| 138 | +- 产品字段给 context → 只返回该客户名下的产品(与网页下拉结果一致)。 | |
| 139 | +- 换一个**只能看自己客户**的业务员 token 调同一个客户字段 → 候选条数明显少于管理员(证明行级权限生效)。 | |
| 140 | +- 一个 `popup` 类型字段 → 也能返回 `rows`(不含 `[-]`)。 | |
| 141 | +- 一个纯文本字段(如备注)→ `mode:"none"`,不报错。 | |
| 142 | + | |
| 143 | +--- | |
| 144 | + | |
| 145 | +## 顺带两个问题 | |
| 146 | + | |
| 147 | +1. **报价单的 `sCustomerName` 冗余列**:205 张原生报价里只有 95 张填了这一列,其余为空。 | |
| 148 | + 这列到底该不该填?我们建的单目前不填(只写 `sCustomerId`),想跟你们保持一致。 | |
| 149 | +2. **`sRelation` 伪列反查**:xlyAi 现在自己实现了"按 id 反查显示名"。 | |
| 150 | + 如果你们愿意顺手在同一个端点上加个 `POST /ai/fieldDisplay`(给 formId+字段+id,返回显示名), | |
| 151 | + 我们就能把这块也交回给你们统一。**非必需,看你们成本。** | |
| 152 | + | |
| 153 | +--- | |
| 154 | + | |
| 155 | +## 我方对应改动(供你们了解,不用你们做) | |
| 156 | + | |
| 157 | +拿到这个端点后,xlyAi 的 `/api/agent/form/options` 会改成: | |
| 158 | +优先按 `(formId, 字段)` 走你们的新端点;`mode:"none"` 或网络异常时回落到现有的字典直查(保底不影响可用性)。 | |
| 159 | +`need_context` 会转成"请先选客户"的提示。`assign` 会并入创建载荷。 | |
| 160 | +涉及文件:`FormController` / `FormResolverService.fkOptionPage` / `FormRenderService.buildQuote`。 | ... | ... |