package com.xly.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
import com.xly.service.AuthzService;
import com.xly.service.ErpClient;
import com.xly.service.FormResolverService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* FormCollect 的**FK 二级选择器**数据端点:外键字段(客户/产品/物料…)的候选,
* 多列展示 + 名称搜索 + 分页。
*
*
两条取数路径,优先前者:
*
* - ERP 下拉配置({@code POST /ai/fieldOptions},需 formId+field)——候选口径与 ERP 网页
* 完全一致:带**行级数据权限**、**级联过滤**(如产品限定在已选客户名下)、**联动回填映射**。
* - 本地字段字典({@link FormResolverService#fkOptionPage})——ERP 端点未上线/无该字段配置/
* 调用失败时的保底,行为同改造前。
*
*
* 前端调 {@code GET /api/agent/form/options?table=elecustomer&formId=..&field=sCustomerId&q=..&page=1},
* 返回 {@code {columns:[{col,label}…], rows:[{sId, 列:值…}], total, page, pageSize, valueField?, assign?}};
* 级联条件未满足时返回 {@code {needContext:true, requires:[…], message:"请先选择客户"}}。
*
*
安全:需登录;本地路径的租户来自服务端内省的身份(不接受客户端传 brandsid,否则不传即可跨租户
* 全库翻页),且表名限于「在字段字典里作为外键目标出现过」并被有权表单引用(见
* {@link FormResolverService#fkTableAccessible});ERP 路径由 ERP 用用户 token 自行鉴权。
*/
@RestController
@RequestMapping("/api/agent/form")
public class FormController {
private static final Logger log = LoggerFactory.getLogger(FormController.class);
private final FormResolverService resolver;
private final AuthzService authz;
private final ErpClient erp;
private final ObjectMapper mapper;
/** ERP 字段候选接口开关(默认开:端点未上线时会自动回落,不影响可用)。 */
@Value("${erp.field-options.enabled:true}")
private boolean erpFieldOptionsEnabled;
public FormController(FormResolverService resolver, AuthzService authz, ErpClient erp, ObjectMapper mapper) {
this.resolver = resolver;
this.authz = authz;
this.erp = erp;
this.mapper = mapper;
}
@GetMapping("/options")
public Map options(@RequestParam("table") String table,
@RequestParam(value = "q", required = false) String q,
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize,
@RequestParam(value = "formId", required = false) String formId,
@RequestParam(value = "field", required = false) String field,
@RequestParam(value = "ctx", required = false) String ctxJson,
@RequestHeader(value = "Authorization", required = false) String auth) {
AgentIdentity id = authz.resolveIdentity(auth);
if (id == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "登录已过期或未登录");
}
Map viaErp = tryErp(id, formId, field, q, page, pageSize, ctxJson);
if (viaErp != null) {
return viaErp;
}
// 仅登录不够:该 FK 表必须被用户有权的某张表单引用,否则无表单权限的用户也能翻完客户名录
if (!resolver.fkTableAccessible(table, id)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权访问该数据");
}
return resolver.fkOptionPage(table, id.brandsId(), q, page, pageSize);
}
/** 走 ERP 下拉配置;不可用(未上线/该字段无配置/异常)返回 null 让调用方回落。 */
private Map tryErp(AgentIdentity id, String formId, String field, String q,
int page, int pageSize, String ctxJson) {
if (!erpFieldOptionsEnabled || isBlank(formId) || isBlank(field)) {
return null;
}
JsonNode r = erp.fieldOptions(id.token(), formId.trim(), field.trim(), q, page, pageSize, parseCtx(ctxJson));
if (r == null) {
return null;
}
String mode = r.path("mode").asText("");
if ("need_context".equals(mode)) {
Map out = new LinkedHashMap<>();
out.put("needContext", true);
out.put("requires", textList(r.path("requires")));
String msg = r.path("msg").asText("");
out.put("message", msg.isBlank() ? "请先选择上级字段,再选这一项。" : msg);
return out;
}
if (!"list".equals(mode)) {
return null; // mode=none 等:该字段在 ERP 侧没有可列的候选,回落本地字典
}
return normalize(r);
}
/** ERP 载荷 → 前端既有的 {columns, rows, total, …} 形状,额外带上 valueField/assign。 */
private Map normalize(JsonNode r) {
List