diff --git a/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java b/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java index c386d77..e29c0dd 100644 --- a/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java +++ b/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java @@ -211,6 +211,105 @@ public class CallbackPushService { } } + /** + * 回调探测(2026-10-11 管理端增强):以该注册的 secret 按现行信封签一条未知事件码 flow.ping, + * 走真实推送管道同步一发——接收方按规范 §6.4 对未知事件码"记日志后 200",不破坏业务数据。 + * 探测是一次性行为:结果落 flw_callback_log(成功=1 / 失败=2,永不进重试队列), + * 响应体摘要落 resp_body。返回 {success, httpStatus, elapsedMs, respSnippet, error}。 + */ + public java.util.Map probe(Long schId, FlwCallbackReg reg) { + String eventId = UUID.randomUUID().toString().replace("-", ""); + String body = probePayload(schId, reg, eventId); + long start = System.currentTimeMillis(); + String ts = String.valueOf(System.currentTimeMillis()); + String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + Integer httpStatus = null; + String respSnippet = null; + String error = null; + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("X-Flw-EventId", eventId); + headers.set("X-Flw-Timestamp", ts); + headers.set("X-Flw-Nonce", nonce); + headers.set("X-Flw-Signature", sign(reg.getSecret(), ts, nonce, body)); + RestTemplate template = CallbackAppSigner.signedTemplate( + appIdentityProvider.getIfAvailable(), reg.getBasePath(), 5000, 10000); + ResponseEntity resp = template.exchange(reg.getCallbackUrl(), HttpMethod.POST, + new HttpEntity<>(body, headers), String.class); + httpStatus = resp.getStatusCodeValue(); + respSnippet = resp.getBody() == null ? null : truncate(resp.getBody()); + } catch (Exception e) { + error = rootMessage(e); + } + long elapsed = System.currentTimeMillis() - start; + boolean ok = httpStatus != null && httpStatus >= 200 && httpStatus < 300; + persistProbeLog(schId, reg, eventId, body, ok, httpStatus, respSnippet, error); + log.info("[flow-center] 回调探测:reg={} url={} ok={} http={} elapsed={}ms", + reg.getId(), reg.getCallbackUrl(), ok, httpStatus, elapsed); + java.util.Map out = new java.util.HashMap<>(); + out.put("success", ok); + out.put("httpStatus", httpStatus); + out.put("elapsedMs", elapsed); + out.put("respSnippet", respSnippet); + out.put("error", error); + return out; + } + + /** 探测信封:与 CallbackDispatcher 现行事件同构,event=flow.ping(未知码),instanceId=0 哨兵 */ + private String probePayload(Long schId, FlwCallbackReg reg, String eventId) { + java.util.Map payload = new java.util.HashMap<>(); + payload.put("eventId", eventId); + payload.put("event", "flow.ping"); + payload.put("schId", schId); + payload.put("flowGuid", reg.getFlowGuid()); + payload.put("instanceId", 0L); + payload.put("instanceVersion", 0); + payload.put("bizType", "probe"); + payload.put("bizId", String.valueOf(reg.getId())); + payload.put("bizTitle", "回调探测 flow.ping"); + payload.put("occurredTime", LocalDateTime.now().format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + payload.put("taskId", null); + payload.put("operatorUserId", null); + payload.put("operatorName", null); + java.util.Map data = new java.util.HashMap<>(); + data.put("probe", true); + data.put("message", "flow-center 回调连通性探测(未知事件码,按规范 §6.4 记日志后 200 即可)"); + payload.put("data", data); + try { + return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(payload); + } catch (Exception e) { + throw new IllegalStateException("探测信封序列化失败", e); + } + } + + /** 探测结果落日志行(一次性行为:成功=1 / 失败=2,status≠PENDING 永不被重试扫描捞起) */ + private void persistProbeLog(Long schId, FlwCallbackReg reg, String eventId, String body, + boolean ok, Integer httpStatus, String respSnippet, String error) { + try { + FlwCallbackLog row = new FlwCallbackLog(); + row.setSchId(schId); + row.setRegId(reg.getId()); + row.setEventId(eventId); + row.setEvent("flow.ping"); + row.setInstanceId(0L); + row.setBizType("probe"); + row.setBizId(String.valueOf(reg.getId())); + row.setPayloadJson(body); + row.setRespBody(respSnippet); + row.setStatus(ok ? FlwEnums.CallbackLogStatus.SUCCESS.getCode() + : FlwEnums.CallbackLogStatus.DEAD.getCode()); + row.setRetryCount(0); + row.setNextRetryTime(LocalDateTime.now()); + row.setLastHttpStatus(httpStatus == null ? null : String.valueOf(httpStatus)); + row.setLastError(error == null ? null : truncate(error)); + row.setPushTime(LocalDateTime.now()); + logRepository.save(row); + } catch (Exception e) { + log.warn("[flow-center] 探测日志落库失败:reg={} {}", reg.getId(), e.toString()); + } + } + private static String sign(String secret, String timestamp, String nonce, String body) { try { Mac mac = Mac.getInstance("HmacSHA256"); diff --git a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java index a5c91c1..d132bce 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java @@ -91,6 +91,12 @@ public class FlowAdminController { 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); @@ -302,6 +308,16 @@ public class FlowAdminController { 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, diff --git a/src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java b/src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java index 0b89667..a2cf36c 100644 --- a/src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java +++ b/src/main/java/com/yunhai/flowcenter/entity/FlwCallbackLog.java @@ -76,6 +76,10 @@ public class FlwCallbackLog extends BaseEntity { @Column(name = "last_error", length = 1000) private String lastError; + /** 响应体摘要(探测/probe 落,截 1000;常规推送不落——2xx 即成功不解析响应体) */ + @Column(name = "resp_body", columnDefinition = "TEXT") + private String respBody; + /** 成功时间 */ @Column(name = "push_time") private LocalDateTime pushTime; diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowAppAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowAppAdminService.java index 253f732..53049a3 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowAppAdminService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowAppAdminService.java @@ -2,6 +2,7 @@ package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.entity.FlwApp; import com.yunhai.flowcenter.repository.FlwAppRepository; +import com.yunhai.flowcenter.repository.FlwCallbackRegRepository; import com.yunhai.platform.openapi.base.OpenApiAppClient; import com.yunhai.platform.utils.model.BoaApp; import com.yunhai.platform.utils.model.BoaAppType; @@ -30,6 +31,19 @@ public class FlowAppAdminService { private final FlwAppRepository appRepository; private final OpenApiAppClient appClient; + private final FlwCallbackRegRepository callbackRegRepository; + + /** + * 应用维度辅助统计(2026-10-11 应用管理增强):key=appKey,value=回调注册数(本校范围)。 + * 应用设置页展示"对接深度"用;不区分注册启停(停用注册同样计入,管理员感知总量)。 + */ + public Map stats(Long schId) { + Map out = new HashMap<>(); + for (com.yunhai.flowcenter.entity.FlwCallbackReg reg : callbackRegRepository.findBySchIdOrderByUpdateTimeDesc(schId)) { + out.merge(reg.getAppKey(), 1L, Long::sum); + } + return out; + } /** * 列表(管理端应用设置页)。附带一次静默学校池同步:UserBase 不可达/学校未开通任何应用时 diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java index 0c844cb..873c69b 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowCallbackAdminService.java @@ -55,6 +55,15 @@ public class FlowCallbackAdminService { return secret; } + /** + * 回调探测(2026-10-11 管理端增强):对指定注册签发一条 flow.ping 未知事件码走真实管道 + * 同步一发,验证接收方可达性与验签配置。停用注册同样允许探测(管理员排障需要)。 + */ + public java.util.Map probe(Long schId, Long regId) { + FlwCallbackReg reg = requireReg(schId, regId); + return pushService.probe(schId, reg); + } + /** 推送日志分页(状态/事件/办件筛选) */ public BasePageResult pageLogs(Long schId, Integer status, String event, Long instanceId, Integer page, Integer size) {