erp-tasks-field-options.md 9.83 KB

任务:ERP 侧为 AI 表单提供「字段候选值」统一接口

给 ERP 侧的第二份任务书(第一份 = rearch3-erp-tasks.md,已交付验收通过)。 本次只做只读查询,不涉及写入、不改状态协议。


背景:xlyAi 现在自己查候选值,和 ERP 网页不一致

xlyAi 的表单卡上,外键字段(客户/产品/物料…)旁边有个【选择】按钮,点开是候选列表。 现在这条路是 xlyAi 自己直查共享库:从字段字典视图 viw_kg_field_dict 推断外键目标表(如 eleproduct),自己挑一个"名称列",拼 WHERE sBrandsId=? AND 名称列 LIKE ? 分页返回。

这套能跑,但和 ERP 网页的同一个下拉相比缺三样东西,其中第二条是数据正确性问题

  1. 行级数据权限:ERP 的下拉 SQL 里有 [sId, sLookCustomer] 这类宏,经 BusinessCommonServiceImpl.getDataFilterAuth 展开成权限子查询(业务员只看自己的客户)。 xlyAi 这条路只有"该表是否被你有权的表单引用过"的表级判断,没有行级过滤
  2. 级联过滤(重要):报价单的产品下拉配置是 ... FROM eleproduct A ... where a.bInvalid=0 and sCustomerId=#sCustomerId# #sKeyUpFilter# #A.companyId#, 配套 sSqlCondition = master.sCustomerId.sCustomerId —— 即产品候选必须限定在已选客户名下。 xlyAi 现在会把该品牌下所有产品都列出来,用户可以选到一个不属于该客户的产品。
  3. 联动回填:客户控件的 sAssignField 有近 30 组映射 (sCustomerId:sId, sCustomerName:sCustomerName, sCustomerNo:sCustomerNo, sContacts:..., sGetPayId:...), ERP 网页选中客户后会把这些列一起带进表单。xlyAi 只写外键 id,其余留空。

好消息:映射键已经找到了,不需要新建任何映射表

实测确认(本地 saaslocal 库):

  • xlyAi 表单卡里的 formId(如报价 = 101251240115016076506222750就是 gdsconfigformmaster.sId(该行 sTbName='QuoQuotationmaster'sParentId= 模块 id ...222050)。
  • ERP 的控件配置 gdsconfigformslave 正是以 sParentId = 这个 formId 挂在下面(报价主表共 146 行控件)。
  • 所以 (xlyAi 的 formId, 字段) → ERP 控件 sId 是一次直接查表,无需新建映射。

已验证的两个控件(报价主表):

控件 sId sName sKeyUpFilter 级联 sSqlCondition
16427541500006326873721849694000 sCustomerName sCustomerName
16427542760005958727285709417000 sProductName sProductName master.sCustomerId.sCustomerId

我已用普通用户 token 实测过现成端点 POST /business/getSelectDataBysControlId/{控件sId}?sModelsId={模块id}

  • 客户控件 + {"sKeyUpFilterName":"中科"}code=1,3 条候选,列齐全(含 sId / sCustomerNo / sSalesManName / dTaxRate …);
  • 产品控件不传级联值 → code=-1,msg 是 =============下拉SQL中包含需要替换的字符,前台未传入替换的值,sSql:master.sCustomerId.sCustomerId========; 传 {"sSqlCondition":{"sCustomerId":"<客户id>"}}code=1,只列该客户的产品。级联行为正确。

要你们做的事:新增一个 AI 专用的字段候选接口

不要让 xlyAi 直接调 getSelectDataBysControlId——控件解析规则(个性化表覆盖、语言列、 sql/const/popup 三种类型、sRelation 伪列反推)是你们的领域知识,散到 AI 侧会持续错位。 请封一个端点,把"解析 + 取数 + 元数据"一次给我们。

接口契约(建议,可按你们习惯调整,但字段语义要覆盖到)

POST /ai/fieldOptions
Authorization: <用户 token>        // 与现有 /ai/* 一致,@Authorization + @CurrentUser
{
  "sFormId":  "101251240115016076506222750",   // = gdsconfigformmaster.sId,xlyAi 表单卡里带的 formId
  "sField":   "sCustomerId",                   // xlyAi 用的是外键 id 列名(见下方「字段名对齐」)
  "q":        "中科",                          // 搜索词,可空
  "pageNum":  1,
  "pageSize": 20,
  "context":  { "sCustomerId": "1752644777..." }  // 已在表单上选好的其它字段值,供级联使用;可空
}

返回(外层沿用你们的 Feedback 信封即可):

{
  "mode": "list",                       // list=有候选可列;none=该字段不是可选字段
  "nameField": "sCustomerName",         // 哪一列当"名字"显示
  "valueField": "sId",                  // 哪一列是要存进业务表的值
  "columns": [{"col":"sCustomerName","label":"客户名称"},
              {"col":"sCustomerNo","label":"客户编号"}],   // 建议展示的列,按重要性排序
  "rows":   [{"sId":"...","sCustomerName":"中科袜业","sCustomerNo":"KH0012", ...}],
  "total":  3, "pageNum": 1, "pageSize": 20,
  "assign": {"sCustomerName":"sCustomerName","sCustomerNo":"sCustomerNo","sContacts":"sContacts"},
  "requires": []
}

必须满足的几点

  1. 字段名对齐(这条最容易踩坑) ERP 的控件挂在显示名列上(sCustomerName),而 xlyAi 传过来的是外键 id 列sCustomerId)。 请在解析时两边都认:先按 sName = sField 找控件;找不到就找 sAssignField把结果列赋给 sField 的那个控件(客户控件的 sAssignFieldsCustomerId:sId,正是这条线)。 两种都找不到 → 返回 mode:"none",我们回落到自己的字典查询。

  2. 级联依赖要结构化,不要抛内部报文 配置了 sSqlConditioncontext 里缺值时,不要返回现在那句 下拉SQL中包含需要替换的字符...sSql:master.sCustomerId.sCustomerId(泄漏内部实现且用户看不懂)。 请返回:

   {"mode":"need_context", "requires":["sCustomerId"],
    "msg":"请先选择客户,再选产品"}

xlyAi 会据此先让用户选父字段,而不是把候选列表拉空或报错。

  1. popup 类型也要能列数据 全库有 25 个 sDropDownType='popup' 的控件,网页上是"弹出另一个业务窗体去挑"。 AI 没有"打开另一个窗体"的能力,请在这个端点里把 popup 也归一成 rows 列表返回 (内部走 sActiveId 对应窗体的 getBusinessDataByFormcustomId 即可), 并且不要返回 id[-]名称 这种拼接值,rows 里请拆成独立列。

  2. 租户隔离要在代码层兜底(我方红线) 现状是靠配置里手写 #companyId#,漏写就跨租户。这个 AI 端点请强制附加 品牌/分公司条件;若某控件配置无法安全附加,宁可对 AI 通道返回 mode:"none"(我们回落), 不要返回一个没有租户过滤的结果集。

  3. 行级数据权限必须照常生效 即保留 getDataFilterAuth 那一层([列, 权限key] 宏展开)。这正是我们想要这个接口的主因之一。

  4. 纯只读、无副作用,不写日志表/不发消息;Redis 缓存 key 请确认仍含 userId(避免串号)。

  5. assign 回填映射要吐给我们 把该控件的 sAssignField 解析成 目标字段 → 结果列 的 map 返回。 xlyAi 会在用户选中一行后,按它把联动列(客户编号、联系人、税率、业务员…)一并写进创建载荷, 让 AI 建的单和网页建的单字段完整度一致。只回填目标表真实存在的列—— 实测报价主表只存在 sCustomerName / sCustomerNo / sMemo 三个目标列, 其余映射项在该表上是空转(可能是跨表单复用的配置),请你们过滤掉不存在的列,别让我们写脏字段。

验收(希望你们跑给我们看)

  • 客户字段:{sFormId:"...222750", sField:"sCustomerId", q:"中科"} → 有候选、nameField=sCustomerNameassignsCustomerName/sCustomerNo
  • 产品字段不给 context → mode:"need_context", requires:["sCustomerId"], 中文 msg 可读。
  • 产品字段给 context → 只返回该客户名下的产品(与网页下拉结果一致)。
  • 换一个只能看自己客户的业务员 token 调同一个客户字段 → 候选条数明显少于管理员(证明行级权限生效)。
  • 一个 popup 类型字段 → 也能返回 rows(不含 [-])。
  • 一个纯文本字段(如备注)→ mode:"none",不报错。

顺带两个问题

  1. 报价单的 sCustomerName 冗余列:205 张原生报价里只有 95 张填了这一列,其余为空。 这列到底该不该填?我们建的单目前不填(只写 sCustomerId),想跟你们保持一致。
  2. sRelation 伪列反查:xlyAi 现在自己实现了"按 id 反查显示名"。 如果你们愿意顺手在同一个端点上加个 POST /ai/fieldDisplay(给 formId+字段+id,返回显示名), 我们就能把这块也交回给你们统一。非必需,看你们成本。

我方对应改动(供你们了解,不用你们做)

已实现(2026-07-29):/api/agent/form/options 优先按 (formId, 字段) 走你们的新端点, mode:"none"/端点未上线/网络异常时回落到本地字典直查(保底不影响可用); need_context 转成"请先选择客户"的提示;FK 选择器把表单上已绑定的记录 id 作为 context 带上。

用户在选择器里点中某一行后,我们直接采信该行的 id 写入载荷(不再按名称反查)—— 即候选的权限、租户、级联判定完全以你们返回的结果为准。因此候选列表里出现的记录, 就等于允许该用户绑定的记录,请确保这两件事在你们那边是同一回事。

assign 联动回填尚未接(等端点上线后做)。 涉及文件:FormController / ErpClient.fieldOptions / FormRenderService.normalize|buildCreate / PreviewService.save|saveCreate