FormController.java
7.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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 二级选择器**数据端点:外键字段(客户/产品/物料…)的候选,
* 多列展示 + 名称搜索 + 分页。
*
* <p>两条取数路径,优先前者:
* <ol>
* <li><b>ERP 下拉配置</b>({@code POST /ai/fieldOptions},需 formId+field)——候选口径与 ERP 网页
* 完全一致:带**行级数据权限**、**级联过滤**(如产品限定在已选客户名下)、**联动回填映射**。</li>
* <li><b>本地字段字典</b>({@link FormResolverService#fkOptionPage})——ERP 端点未上线/无该字段配置/
* 调用失败时的保底,行为同改造前。</li>
* </ol>
*
* <p>前端调 {@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:"请先选择客户"}}。
*
* <p><b>安全</b>:需登录;本地路径的租户来自服务端内省的身份(不接受客户端传 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> normalize(JsonNode r) {
List<Map<String, Object>> columns = new ArrayList<>();
for (JsonNode c : r.path("columns")) {
String col = c.path("col").asText("");
if (!col.isBlank()) {
columns.add(Map.of("col", col, "label", c.path("label").asText(col)));
}
}
List<Map<String, Object>> rows = new ArrayList<>();
for (JsonNode row : r.path("rows")) {
Map<String, Object> m = mapper.convertValue(row, Map.class);
rows.add(m);
}
Map<String, Object> out = new LinkedHashMap<>();
out.put("columns", columns);
out.put("rows", rows);
out.put("total", r.path("total").asInt(rows.size()));
out.put("page", r.path("pageNum").asInt(1));
out.put("pageSize", r.path("pageSize").asInt(rows.size()));
out.put("nameField", r.path("nameField").asText(columns.isEmpty() ? "" : String.valueOf(columns.get(0).get("col"))));
out.put("valueField", r.path("valueField").asText("sId"));
out.put("assign", r.has("assign") ? mapper.convertValue(r.path("assign"), Map.class) : Map.of());
out.put("source", "erp");
return out;
}
/** ctx = 表单上已绑定的其它字段值(字段名→id),供 ERP 的级联条件用。解析失败按无上下文处理。 */
private Map<String, String> parseCtx(String ctxJson) {
if (isBlank(ctxJson)) {
return Map.of();
}
try {
@SuppressWarnings("unchecked")
Map<String, String> m = mapper.readValue(ctxJson, Map.class);
return m;
} catch (Exception e) {
log.debug("ctx 解析失败,按无上下文处理: {}", e.getMessage());
return Map.of();
}
}
private static List<String> textList(JsonNode arr) {
List<String> out = new ArrayList<>();
for (JsonNode n : arr) {
out.add(n.asText());
}
return out;
}
private static boolean isBlank(String s) {
return s == null || s.isBlank();
}
}