Files
flow-center-service/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java
T

504 lines
26 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.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
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;
private final com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
private final com.yunhai.flowcenter.repository.FlwRouteLogRepository routeLogRepository;
// ==================== 流程定义 ====================
@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"))));
}
/** 应用辅助统计(2026-10-11 增强):appKey → 回调注册数(本校),应用设置页"对接深度"列 */
@GetMapping("/app/stats")
public Map<String, Long> appStats(BoaUser user) {
return appAdminService.stats(adminGuard.requireAdmin(user, null));
}
@GetMapping("/def/get")
public FlwDef defGet(BoaUser user, @RequestParam("id") Long id) {
return defAdminService.get(adminGuard.requireAdmin(user, null), id);
}
/** 保存(新建草稿/编辑;已发布定义保存=升版本,flowGuid 不变;baseGuid=在指定流程线上开新版本草稿;changeLog=升版本改动说明,仅新版本行落库,>500 截断;formUuid=关联表单定义 uuid(C1,2026-10-20):null=保持、空串=取消关联、发布后不可改改=升版) */
@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"), (String) body.get("changeLog"),
(String) body.get("formUuid"));
}
/**
* 元数据原地编辑(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);
}
/** 在途办件数(议题二④):flowGuids 逗号分隔,去重后最多 100;返回 flowGuid → 审批中办件数(无在途也返 0 键) */
@GetMapping("/def/inflight")
public Map<String, Long> defInflight(BoaUser user, @RequestParam("flowGuids") String flowGuids) {
Long schId = adminGuard.requireAdmin(user, null);
List<String> guids = java.util.Arrays.stream(flowGuids.split(","))
.map(String::trim).filter(s -> !s.isEmpty()).distinct().limit(100)
.collect(java.util.stream.Collectors.toList());
return defAdminService.inflightCounts(schId, guids);
}
/** 流程定义示意图(方案 §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)));
}
// ==================== 科室管理(2026-10-10 四议题·议题3,取代 /map/* 映射 CRUD) ====================
/** 科室列表(编辑器 ctx.depts 同结构:deptKey=id 字符串 + 启用成员数) */
@GetMapping("/dept/list")
public List<com.yunhai.flowcenter.service.FlowDeptAdminService.DeptVo> deptList(BoaUser user) {
return deptAdminService.list(adminGuard.requireAdmin(user, null));
}
@PostMapping("/dept/create")
public com.yunhai.flowcenter.entity.FlwDept deptCreate(BoaUser user, @RequestBody Map<String, Object> body) {
return deptAdminService.create(adminGuard.requireAdmin(user, null), (String) body.get("deptName"));
}
@PostMapping("/dept/rename")
public com.yunhai.flowcenter.entity.FlwDept deptRename(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
return deptAdminService.rename(schId, requireId(body.get("id")), (String) body.get("deptName"));
}
/** 停用/启用科室(停用=解析不出办理人、发布校验拦新引用;成员不动) */
@PostMapping("/dept/status")
public void deptStatus(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.setStatus(schId, requireId(body.get("id")), boolVal(body.get("enabled")));
}
/** 删除科室(级联删成员;被流程引用的 deptKey 将解析失败——发布校验/办理时会拦) */
@PostMapping("/dept/delete")
public void deptDelete(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.delete(schId, requireId(body.get("id")));
}
@GetMapping("/dept/members")
public List<com.yunhai.flowcenter.entity.FlwDeptMember> deptMembers(BoaUser user,
@RequestParam("deptId") Long deptId) {
return deptAdminService.members(adminGuard.requireAdmin(user, null), deptId);
}
/** 批量加成员(已存在跳过;姓名经 UserBase 补全),返回新增数 */
@PostMapping("/dept/memberAdd")
public int deptMemberAdd(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
List<Integer> userIds = new java.util.ArrayList<>();
Object raw = body.get("userIds");
if (raw instanceof List) {
for (Object o : (List<?>) raw) {
if (o != null) {
userIds.add(Integer.valueOf(String.valueOf(o)));
}
}
}
return deptAdminService.memberAdd(schId, requireId(body.get("deptId")), userIds);
}
@PostMapping("/dept/memberRemove")
public void deptMemberRemove(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.memberRemove(schId, requireId(body.get("memberId")));
}
/**
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(UserBase 组 id 即将落
* flw_dept.source_group_id)。导入前先展示给管理员确认,不落库。
*/
@GetMapping("/dept/groupPreview")
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
}
/** 一键拉取教师组·导入:upsert flw_dept(source_group_id 幂等,重复拉取=刷新组名与成员集) */
@PostMapping("/dept/importGroups")
public com.yunhai.flowcenter.service.FlowDeptAdminService.GroupImportResultVo importGroups(BoaUser user) {
return deptAdminService.importGroups(adminGuard.requireAdmin(user, null));
}
private static Long longVal(Object o) {
return o == null ? null : Long.valueOf(String.valueOf(o));
}
/** 科室/成员行 id 缺失=调用方bug/脏请求:回业务错提示,不让 findById(null) 炸成笼统 500 */
private static Long requireId(Object o) {
Long v = longVal(o);
if (v == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "缺少 id");
}
return v;
}
private static boolean boolVal(Object o) {
return o == null ? false : Boolean.parseBoolean(String.valueOf(o));
}
/** 成员检索(UserBase /oapi/user/byId;编辑器选人/姓名补全共用,非映射 CRUD,保留) */
@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);
}
// ==================== 选人组件数据源 ====================
/** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */
@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);
}
/**
* 回调探测(2026-10-11 管理端增强):对该注册同步推送一条 flow.ping(未知事件码,接收方按
* 规范 §6.4 记日志后 200),返回 {success, httpStatus, elapsedMs, respSnippet, error};
* 结果同时落推送日志一行(event=flow.ping)。
*/
@PostMapping("/callback/probe/{id}")
public Map<String, Object> probe(BoaUser user, @PathVariable("id") Long id) {
return callbackAdminService.probe(adminGuard.requireAdmin(user, null), id);
}
@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);
}
/**
* 对接健康度体检(2026-10-15 议题二 P2):每应用一行六项检查 + 综合评分。
* 数据全部来自已有资产(flw_callback_reg / flw_callback_log),零新增表;
* 应用名从 flw_app 白名单补齐(缺失回落 appKey)。
*/
@GetMapping("/callback/health")
public List<com.yunhai.flowcenter.model.AppHealthVo> callbackHealth(BoaUser user) {
Long schId = adminGuard.requireAdmin(user, null);
java.util.Map<String, String> appNames = new java.util.HashMap<>();
for (com.yunhai.flowcenter.entity.FlwApp app : appAdminService.list(schId)) {
if (app.getAppKey() != null) {
appNames.put(app.getAppKey(), app.getAppName());
}
}
return callbackAdminService.health(schId, appNames);
}
// ==================== 条件路由注册 / 重跑路由 ====================
/** 判断服务注册列表(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 已落新行)。
* useLatestDef=true(F5-4 僵尸逃生门):按该 flowGuid 当前已发布最高版本重跑并刷新办件定义快照。
*/
@PostMapping("/instance/{id}/reroute")
public void instanceReroute(BoaUser user, @PathVariable("id") Long id,
@RequestParam(value = "useLatestDef", required = false, defaultValue = "false")
boolean useLatestDef) {
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id, useLatestDef);
}
/**
* 表达式调试求值(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;
}
// ==================== 办件查询 ====================
/**
* 管理端办件分页(F11-4 增 hung 筛选与行级红标):hung=true 只看路由挂起办件
* (口径见 {@link com.yunhai.flowcenter.repository.FlwInstanceRepository#pageAdmin});
* 每行回填 {@code routeHung}(按页一次 {@code findHungInstanceIds} 查询,非逐行 N+1)。
*/
@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 = "hung", required = false) Boolean hung,
@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, hung,
PageRequest.of(page == null || page < 1 ? 0 : page - 1,
size == null || size < 1 || size > 100 ? 10 : size,
Sort.by(Sort.Order.desc("id"))));
List<Long> pageIds = new java.util.ArrayList<>();
for (FlwInstance i : p.getContent()) {
pageIds.add(i.getId());
}
// 行级红标:不在挂起集合内的行显式置 false(前端只判 truthy,null 也行,但显式更可读)
java.util.Set<Long> hungIds = pageIds.isEmpty() ? java.util.Collections.emptySet()
: new java.util.HashSet<>(routeLogRepository.findHungInstanceIds(pageIds));
for (FlwInstance i : p.getContent()) {
i.setRouteHung(hungIds.contains(i.getId()));
}
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());
}
}