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 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")))); } /** 应用辅助统计(2026-10-11 增强):appKey → 回调注册数(本校),应用设置页"对接深度"列 */ @GetMapping("/app/stats") public Map 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 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 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); } /** 在途办件数(议题二④):flowGuids 逗号分隔,去重后最多 100;返回 flowGuid → 审批中办件数(无在途也返 0 键) */ @GetMapping("/def/inflight") public Map defInflight(BoaUser user, @RequestParam("flowGuids") String flowGuids) { Long schId = adminGuard.requireAdmin(user, null); List 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,示意图页"进度图"视图):<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))); } // ==================== 科室管理(2026-10-10 四议题·议题3,取代 /map/* 映射 CRUD) ==================== /** 科室列表(编辑器 ctx.depts 同结构:deptKey=id 字符串 + 启用成员数) */ @GetMapping("/dept/list") public List deptList(BoaUser user) { return deptAdminService.list(adminGuard.requireAdmin(user, null)); } @PostMapping("/dept/create") public com.yunhai.flowcenter.entity.FlwDept deptCreate(BoaUser user, @RequestBody Map 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 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 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 body) { Long schId = adminGuard.requireAdmin(user, null); deptAdminService.delete(schId, requireId(body.get("id"))); } @GetMapping("/dept/members") public List 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 body) { Long schId = adminGuard.requireAdmin(user, null); List 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 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 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 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); } /** * 回调探测(2026-10-11 管理端增强):对该注册同步推送一条 flow.ping(未知事件码,接收方按 * 规范 §6.4 记日志后 200),返回 {success, httpStatus, elapsedMs, respSnippet, error}; * 结果同时落推送日志一行(event=flow.ping)。 */ @PostMapping("/callback/probe/{id}") public Map probe(BoaUser user, @PathVariable("id") Long id) { return callbackAdminService.probe(adminGuard.requireAdmin(user, null), id); } @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); } /** * 对接健康度体检(2026-10-15 议题二 P2):每应用一行六项检查 + 综合评分。 * 数据全部来自已有资产(flw_callback_reg / flw_callback_log),零新增表; * 应用名从 flw_app 白名单补齐(缺失回落 appKey)。 */ @GetMapping("/callback/health") public List callbackHealth(BoaUser user) { Long schId = adminGuard.requireAdmin(user, null); java.util.Map 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 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 已落新行)。 * 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 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; } // ==================== 办件查询 ==================== /** * 管理端办件分页(F11-4 增 hung 筛选与行级红标):hung=true 只看路由挂起办件 * (口径见 {@link com.yunhai.flowcenter.repository.FlwInstanceRepository#pageAdmin}); * 每行回填 {@code routeHung}(按页一次 {@code findHungInstanceIds} 查询,非逐行 N+1)。 */ @GetMapping("/instance/page") public BasePageResult 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 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 pageIds = new java.util.ArrayList<>(); for (FlwInstance i : p.getContent()) { pageIds.add(i.getId()); } // 行级红标:不在挂起集合内的行显式置 false(前端只判 truthy,null 也行,但显式更可读) java.util.Set 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()); } }