Files
flow-center-service/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java
T
chenxing 4293c9628a feat(def): updateMeta 元数据原地编辑——已发布可改 name/appKey 不升版本(议题2)
- 独立于 save 的升版本路径:不碰 code/flow_guid/version/status/contentJson 结构
- contentJson 顶层 name 同步(ObjectNode 保序;消除行 name 与 JSON name 脏窗口,拍板 D4)
- appKey 校验同 save 口径(显式传入须 flw_app 启用集合;可置空)
- 单测 5 例(三不变断言/白名单/补写/跨校)+ E2E 14/14(含升版本路径回归与 oapi 摘要)
2026-10-10 19:23:44 +08:00

387 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.flowcenter.controller.api;
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
import com.yunhai.flowcenter.entity.FlwCallbackLog;
import com.yunhai.flowcenter.entity.FlwCallbackReg;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.service.FlowAdminGuard;
import com.yunhai.flowcenter.service.FlowCallbackAdminService;
import com.yunhai.flowcenter.service.FlowDefAdminService;
import com.yunhai.flowcenter.service.FlowDiagramService;
import com.yunhai.flowcenter.service.FlowMapAdminService;
import com.yunhai.platform.utils.model.BasePageResult;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 管理端(/api/flow/admin/**,CAS + 管理员角色,FlowAdminGuard 校验):
* 定义 JSON 编辑/发布/停用、办理人映射 CRUD(成员检索调 UserBase)、
* 回调注册管理/推送日志/死信重推、办件查询。
*/
@RestController
@RequestMapping("/api/flow/admin")
@RequiredArgsConstructor
public class FlowAdminController {
private final FlowAdminGuard adminGuard;
private final FlowDefAdminService defAdminService;
private final FlowMapAdminService mapAdminService;
private final FlowCallbackAdminService callbackAdminService;
private final com.yunhai.flowcenter.service.FlowRouterAdminService routerAdminService;
private final FlwInstanceRepository instanceRepository;
private final FlowDiagramService diagramService;
private final com.yunhai.flowcenter.service.DiagramImageService imageService;
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService;
// ==================== 流程定义 ====================
@GetMapping("/def/list")
public List<FlwDef> defList(BoaUser user,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "appKey", required = false) String appKey) {
return defAdminService.list(adminGuard.requireAdmin(user, null), status, appKey);
}
/** 应用清单("按应用筛选"下拉):本校 flw_def 已标注 ∪ 已注册回调的应用 */
@GetMapping("/def/appkeys")
public List<String> defAppKeys(BoaUser user) {
return defAdminService.distinctAppKeys(adminGuard.requireAdmin(user, null));
}
// ==================== 应用设置(2026-10-10 四议题) ====================
/**
* 应用管理列表:本校"可对接流程"白名单(flw_app)。附带一次学校池静默同步
* (UserBase list-by-school;不可达降级本地行)。开通过程在 UserBase 管理端做。
*/
@GetMapping("/app/list")
public List<com.yunhai.flowcenter.entity.FlwApp> appList(BoaUser user) {
return appAdminService.list(adminGuard.requireAdmin(user, null));
}
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用) */
@PostMapping("/app/enable")
public void appEnable(BoaUser user,
@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
appAdminService.enable(schId,
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
Boolean.parseBoolean(String.valueOf(body.get("enabled"))));
}
@GetMapping("/def/get")
public FlwDef defGet(BoaUser user, @RequestParam("id") Long id) {
return defAdminService.get(adminGuard.requireAdmin(user, null), id);
}
/** 保存(新建草稿/编辑;已发布定义保存=升版本,flowGuid 不变;baseGuid=在指定流程线上开新版本草稿) */
@PostMapping("/def/save")
public FlwDef defSave(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
Long id = body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id")));
return defAdminService.save(schId, id,
(String) body.get("code"), (String) body.get("name"),
(String) body.get("contentJson"), (String) body.get("baseGuid"),
(String) body.get("appKey"));
}
/**
* 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本(version/flowGuid/status/contentJson
* 结构均不动,仅同步 contentJson 顶层 name);已发布亦可。结构改动请走编辑器升版本。
*/
@PostMapping("/def/updateMeta")
public FlwDef defUpdateMeta(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
return defAdminService.updateMeta(schId,
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
(String) body.get("name"), (String) body.get("appKey"));
}
@PostMapping("/def/publish/{id}")
public FlwDef defPublish(BoaUser user, @PathVariable("id") Long id) {
return defAdminService.publish(adminGuard.requireAdmin(user, null), id);
}
@PostMapping("/def/offline/{id}")
public void defOffline(BoaUser user, @PathVariable("id") Long id) {
defAdminService.offline(adminGuard.requireAdmin(user, null), id);
}
/** 启用(与 offline 对称):仅停用版 →已发布,重跑发布期静态校验 */
@PostMapping("/def/enable/{id}")
public FlwDef defEnable(BoaUser user, @PathVariable("id") Long id) {
return defAdminService.enable(adminGuard.requireAdmin(user, null), id);
}
@GetMapping("/def/versions")
public List<FlwDef> defVersions(BoaUser user, @RequestParam("flowGuid") String flowGuid) {
return defAdminService.versions(adminGuard.requireAdmin(user, null), flowGuid);
}
/** 流程定义示意图(方案 §3.1):requireAdmin 后按 defId 任意版本出图(草稿可预览) */
@GetMapping("/def/diagram")
public FlowModels.DiagramVo defDiagram(BoaUser user, @RequestParam("id") Long id,
@RequestParam(value = "mode", required = false) String mode) {
Long schId = adminGuard.requireAdmin(user, null);
FlwDef def = defAdminService.get(schId, id);
return diagramService.build(def, null, mode);
}
/**
* 流程定义示意图 PNG 流(2026-09-17,示意图页"进度图"视图):&lt;img&gt; 直嵌走同域 CAS 会话,
* 可见性与 /def/diagram 同口径(requireAdmin)。
*/
@GetMapping("/def/diagram.png")
public ResponseEntity<byte[]> defDiagramPng(BoaUser user, @RequestParam("id") Long id,
@RequestParam(value = "mode", required = false) String mode) {
Long schId = adminGuard.requireAdmin(user, null);
FlwDef def = defAdminService.get(schId, id);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(org.springframework.http.CacheControl.noStore())
.body(imageService.renderPng(diagramService.build(def, null, mode)));
}
// ==================== 办理人映射 ====================
@GetMapping("/map/list")
public List<FlwAssigneeMap> mapList(BoaUser user,
@RequestParam(value = "deptKey", required = false) String deptKey) {
return mapAdminService.listByDept(adminGuard.requireAdmin(user, null), deptKey);
}
@PostMapping("/map/save")
public FlwAssigneeMap mapSave(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
return mapAdminService.save(schId,
(String) body.get("deptKey"), (String) body.get("deptName"),
body.get("userId") == null ? null : Integer.valueOf(String.valueOf(body.get("userId"))),
(String) body.get("userName"));
}
@PostMapping("/map/disable/{id}")
public void mapDisable(BoaUser user, @PathVariable("id") Long id) {
mapAdminService.disable(adminGuard.requireAdmin(user, null), id);
}
/** 启用映射(停用行的恢复入口;DEPT_KEY 解析只取启用行) */
@PostMapping("/map/enable/{id}")
public FlwAssigneeMap mapEnable(BoaUser user, @PathVariable("id") Long id) {
return mapAdminService.enable(adminGuard.requireAdmin(user, null), id);
}
/** 成员检索(UserBase /oapi/user/byId) */
@GetMapping("/map/lookupUser")
public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user,
@RequestParam("userId") Integer userId) {
adminGuard.requireAdmin(user, null);
return mapAdminService.lookupUser(userId);
}
/**
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(deptKey=UserBase 组 id)。
* 导入前先展示给管理员确认,不落库。
*/
@GetMapping("/map/groupPreview")
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
}
/** 一键拉取教师组·导入:批量落映射(已存在且启用中的 sch+dept+user 跳过),返回计数 */
@PostMapping("/map/importGroups")
public com.yunhai.flowcenter.service.FlowMapAdminService.GroupImportResultVo importGroups(BoaUser user) {
return mapAdminService.importGroups(adminGuard.requireAdmin(user, null));
}
// ==================== 选人组件数据源 ====================
/** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */
@GetMapping("/user/tree")
public List<FlowMapAdminService.SelectUserNodeVo> userTree(BoaUser user) {
return mapAdminService.userTree(adminGuard.requireAdmin(user, null));
}
/**
* 选人组件树·教师版(2026-09-17,仿 Notice /api/base/GetSchoolAll):组树骨架(剔除学生组)+
* userclass 教师按 GroupIds 挂组,管理端「选择用户」统一走它。
*/
@GetMapping("/user/teacherTree")
public List<FlowMapAdminService.SelectUserNodeVo> teacherTree(BoaUser user) {
return mapAdminService.teacherTree(adminGuard.requireAdmin(user, null));
}
// ==================== 回调注册/推送日志/死信 ====================
@GetMapping("/callback/regList")
public List<FlwCallbackReg> regList(BoaUser user) {
return callbackAdminService.listRegs(adminGuard.requireAdmin(user, null));
}
@PostMapping("/callback/regStatus/{id}")
public void regStatus(BoaUser user, @PathVariable("id") Long id,
@RequestParam("enabled") boolean enabled) {
callbackAdminService.setRegStatus(adminGuard.requireAdmin(user, null), id, enabled);
}
/** secret 重置(新 secret 返回给前端展示一次) */
@PostMapping("/callback/resetSecret/{id}")
public Map<String, String> resetSecret(BoaUser user, @PathVariable("id") Long id) {
String secret = callbackAdminService.resetSecret(adminGuard.requireAdmin(user, null), id);
return java.util.Collections.singletonMap("secret", secret);
}
@GetMapping("/callback/logs")
public BasePageResult<FlwCallbackLog> logs(BoaUser user,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "event", required = false) String event,
@RequestParam(value = "instanceId", required = false) Long instanceId,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size) {
return callbackAdminService.pageLogs(adminGuard.requireAdmin(user, null), status, event, instanceId, page, size);
}
/** 挂起列表(待推且重试达阈值;持续重试中疑似消费方停机的可视入口) */
@GetMapping("/callback/hung")
public BasePageResult<FlwCallbackLog> hung(BoaUser user,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size) {
return callbackAdminService.pageHung(adminGuard.requireAdmin(user, null), page, size);
}
/** 管理端手工标记死信(死信唯一人工入口;标记后可 rePush 复活) */
@PostMapping("/callback/markDead/{id}")
public void markDead(BoaUser user, @PathVariable("id") Long id) {
callbackAdminService.markDead(adminGuard.requireAdmin(user, null), id);
}
@GetMapping("/callback/dead")
public BasePageResult<FlwCallbackLog> dead(BoaUser user,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size) {
return callbackAdminService.pageDead(adminGuard.requireAdmin(user, null), page, size);
}
@PostMapping("/callback/repush")
public void repush(BoaUser user, @RequestBody List<Long> logIds) {
callbackAdminService.rePush(adminGuard.requireAdmin(user, null), logIds);
}
// ==================== 条件路由注册 / 重跑路由 ====================
/** 判断服务注册列表(flw_router 全局表;编辑器 routerKey 下拉与管理页共用) */
@GetMapping("/router/list")
public List<com.yunhai.flowcenter.entity.FlwRouter> routerList(BoaUser user) {
adminGuard.requireAdmin(user, null);
return routerAdminService.list();
}
/** 保存注册(id 空=新建;serviceKey=服务目录键,path=判断端点,basePath=对端 context-path 生产必核对) */
@PostMapping("/router/save")
public com.yunhai.flowcenter.entity.FlwRouter routerSave(BoaUser user,
@RequestBody Map<String, Object> body) {
adminGuard.requireAdmin(user, null);
return routerAdminService.save(
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
(String) body.get("routerKey"), (String) body.get("name"),
(String) body.get("serviceKey"), (String) body.get("path"),
(String) body.get("basePath"));
}
/** 启用/停用注册(停用后发布校验拦截新引用) */
@PostMapping("/router/status/{id}")
public void routerStatus(BoaUser user, @PathVariable("id") Long id,
@RequestParam("enabled") boolean enabled) {
adminGuard.requireAdmin(user, null);
routerAdminService.setStatus(id, enabled);
}
/**
* 重跑路由(判断失败挂起的恢复入口,2026-09-24 §4.4):重新调判断服务,成功即按出口续走;
* 仍失败回业务错(办件继续挂起,route_log 已落新行)。
*/
@PostMapping("/instance/{id}/reroute")
public void instanceReroute(BoaUser user, @PathVariable("id") Long id) {
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id);
}
/**
* 表达式调试求值(2026-10-10 四议题):body {expression, variablesJson},与运行时同一求值核心
* (ExpressionEvaluator);不落库、不走流程,发布前/挂起排查用。内置变量需在 variablesJson 里
* 自行提供样例值(线上是引擎注入的办件上下文)。
*/
@PostMapping("/router/eval")
public Map<String, Object> routerEval(BoaUser user, @RequestBody Map<String, Object> body) {
adminGuard.requireAdmin(user, null);
String expression = body.get("expression") == null ? "" : String.valueOf(body.get("expression")).trim();
String variablesJson = body.get("variablesJson") == null ? "" : String.valueOf(body.get("variablesJson")).trim();
Map<String, Object> result = new java.util.HashMap<>();
if (expression.isEmpty()) {
result.put("result", false);
result.put("error", "表达式为空");
return result;
}
Map<String, Object> env;
if (variablesJson.isEmpty()) {
env = new java.util.HashMap<>();
} else {
try {
env = objectMapper.readValue(variablesJson,
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {
});
} catch (Exception e) {
result.put("result", false);
result.put("error", "样例变量不是合法 JSON 对象:" + e.getMessage());
return result;
}
}
try {
Object value = com.yunhai.flowcenter.engine.ExpressionEvaluator.evaluate(expression, env);
result.put("result", Boolean.TRUE.equals(value));
result.put("value", String.valueOf(value));
} catch (Exception e) {
result.put("result", false);
result.put("error", String.valueOf(e.getMessage() != null ? e.getMessage() : e.toString()));
}
return result;
}
// ==================== 办件查询 ====================
@GetMapping("/instance/page")
public BasePageResult<FlwInstance> instancePage(
BoaUser user,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size) {
Long schId = adminGuard.requireAdmin(user, null);
Page<FlwInstance> p = instanceRepository.pageAdmin(schId, status, keyword,
PageRequest.of(page == null || page < 1 ? 0 : page - 1,
size == null || size < 1 || size > 100 ? 10 : size,
Sort.by(Sort.Order.desc("id"))));
int pi = page == null || page < 1 ? 1 : page;
int ps = size == null || size < 1 || size > 100 ? 10 : size;
return new BasePageResult<>(p.getContent(), (int) p.getTotalElements(), pi, ps,
p.getTotalPages(), p.hasNext());
}
}