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 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 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 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 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 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 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 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 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 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 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 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 userTree(BoaUser user) { return mapAdminService.userTree(adminGuard.requireAdmin(user, null)); } /** * 选人组件树·教师版(2026-09-17,仿 Notice /api/base/GetSchoolAll):组树骨架(剔除学生组)+ * userclass 教师按 GroupIds 挂组,管理端「选择用户」统一走它。 */ @GetMapping("/user/teacherTree") public List teacherTree(BoaUser user) { return mapAdminService.teacherTree(adminGuard.requireAdmin(user, null)); } // ==================== 回调注册/推送日志/死信 ==================== @GetMapping("/callback/regList") public List 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 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 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 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 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 logIds) { callbackAdminService.rePush(adminGuard.requireAdmin(user, null), logIds); } // ==================== 条件路由注册 / 重跑路由 ==================== /** 判断服务注册列表(flw_router 全局表;编辑器 routerKey 下拉与管理页共用) */ @GetMapping("/router/list") public List 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 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 routerEval(BoaUser user, @RequestBody Map 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 result = new java.util.HashMap<>(); if (expression.isEmpty()) { result.put("result", false); result.put("error", "表达式为空"); return result; } Map env; if (variablesJson.isEmpty()) { env = new java.util.HashMap<>(); } else { try { env = objectMapper.readValue(variablesJson, new com.fasterxml.jackson.core.type.TypeReference>() { }); } 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 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 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()); } }