feat(callback/app): 回调探测端点 /admin/callback/probe/{id}——签 flow.ping 未知事件码走真实管道同步一发(信封同构 CallbackDispatcher,结果落 flw_callback_log 一次性行+resp_body 响应摘要列);应用辅助统计 /admin/app/stats(appKey→本校回调注册数,应用设置页对接深度列)
build / build-push (push) Failing after 14s

This commit is contained in:
chenxing
2026-10-11 22:40:44 +08:00
parent cd046782d9
commit f07ac6da93
5 changed files with 142 additions and 0 deletions
@@ -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<String, Object> 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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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");
@@ -91,6 +91,12 @@ public class FlowAdminController {
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);
@@ -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<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,
@@ -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;
@@ -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<String, Long> stats(Long schId) {
Map<String, Long> out = new HashMap<>();
for (com.yunhai.flowcenter.entity.FlwCallbackReg reg : callbackRegRepository.findBySchIdOrderByUpdateTimeDesc(schId)) {
out.merge(reg.getAppKey(), 1L, Long::sum);
}
return out;
}
/**
* 列表(管理端应用设置页)。附带一次静默学校池同步:UserBase 不可达/学校未开通任何应用时
@@ -55,6 +55,15 @@ public class FlowCallbackAdminService {
return secret;
}
/**
* 回调探测(2026-10-11 管理端增强):对指定注册签发一条 flow.ping 未知事件码走真实管道
* 同步一发,验证接收方可达性与验签配置。停用注册同样允许探测(管理员排障需要)。
*/
public java.util.Map<String, Object> probe(Long schId, Long regId) {
FlwCallbackReg reg = requireReg(schId, regId);
return pushService.probe(schId, reg);
}
/** 推送日志分页(状态/事件/办件筛选) */
public BasePageResult<FlwCallbackLog> pageLogs(Long schId, Integer status, String event,
Long instanceId, Integer page, Integer size) {