- 独立于 save 的升版本路径:不碰 code/flow_guid/version/status/contentJson 结构 - contentJson 顶层 name 同步(ObjectNode 保序;消除行 name 与 JSON name 脏窗口,拍板 D4) - appKey 校验同 save 口径(显式传入须 flw_app 启用集合;可置空) - 单测 5 例(三不变断言/白名单/补写/跨校)+ E2E 14/14(含升版本路径回归与 oapi 摘要)
387 lines
19 KiB
Java
387 lines
19 KiB
Java
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,示意图页"进度图"视图):<img> 直嵌走同域 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());
|
||
}
|
||
}
|