diff --git a/src/api/module/flow.ts b/src/api/module/flow.ts index 448ecee..000bb0e 100644 --- a/src/api/module/flow.ts +++ b/src/api/module/flow.ts @@ -137,6 +137,10 @@ class FlowApi { appEnable(id: number | string, enabled: boolean) { return post(`${ROOT}/admin/app/enable`, { id, enabled }); } + /** 应用辅助统计:appKey → 回调注册数(本校) */ + appStats() { + return get(`${ROOT}/admin/app/stats`); + } defGet(id: number | string) { return get(`${ROOT}/admin/def/get`, { id }); } @@ -223,6 +227,10 @@ class FlowApi { regResetSecret(id: number | string) { return post(`${ROOT}/admin/callback/resetSecret/${id}`); } + /** 回调探测:对该注册同步推一条 flow.ping(未知事件码),返回 {success,httpStatus,elapsedMs,respSnippet,error} */ + callbackProbe(id: number | string) { + return post(`${ROOT}/admin/callback/probe/${id}`); + } callbackLogs(params: { status?: number; event?: string; instanceId?: number; page?: number; size?: number }) { return get(`${ROOT}/admin/callback/logs`, params); } diff --git a/src/views/flow/admin/AppSetting.vue b/src/views/flow/admin/AppSetting.vue index 4cf2618..317145d 100644 --- a/src/views/flow/admin/AppSetting.vue +++ b/src/views/flow/admin/AppSetting.vue @@ -15,13 +15,29 @@ const tab = ref("apps"); // ==================== tab1 应用管理 ==================== const loading = ref(false); const rows = ref([]); +const searchKey = ref(""); +const regStats = ref>({}); const appRegionRef = ref(null); const appH = useElementHeight(appRegionRef); +// 搜索过滤(应用名/appKey 子串,纯前端) +const filteredRows = () => { + const k = searchKey.value.trim().toLowerCase(); + if (!k) return rows.value; + return rows.value.filter((r: any) => + String(r.appName || "").toLowerCase().includes(k) || String(r.appKey || "").includes(k)); +}; + const columns = [ { title: "序号", type: "index", width: 70, align: "center" }, { title: "应用", key: "appName", minWidth: 200 }, { title: "appKey", key: "appKey", width: 120, align: "center" }, + { + title: "回调注册数", + width: 110, + align: "center", + render: (h: any, p: any) => h("span", String(regStats.value[String(p.row.appKey)] ?? 0)), + }, { title: "状态", width: 90, @@ -43,23 +59,42 @@ async function load() { loading.value = true; try { rows.value = ((await flowApi.appList().send()) as any[]) || []; + try { + regStats.value = ((await flowApi.appStats().send()) as any) || {}; + } catch (e) { + regStats.value = {}; + } } finally { loading.value = false; } } async function onToggle(row: any, enabled: boolean) { - row._switching = true; - try { - await flowApi.appEnable(row.id, enabled).send(); - row.enabled = enabled ? 1 : 0; - Message.success(enabled ? `已开启「${row.appName}」` : `已停用「${row.appName}」`); - } catch (e: any) { - Message.error(e?.message || "操作失败"); - load(); - } finally { - row._switching = false; + const doToggle = async () => { + row._switching = true; + try { + await flowApi.appEnable(row.id, enabled).send(); + row.enabled = enabled ? 1 : 0; + Message.success(enabled ? `已开启「${row.appName}」` : `已停用「${row.appName}」`); + } catch (e: any) { + Message.error(e?.message || "操作失败"); + load(); + } finally { + row._switching = false; + } + }; + if (enabled) { + doToggle(); + return; } + MyConfirm({ + title: "停用应用", + msg: `确定停用「${row.appName}(${row.appKey})」吗?`, + info: "停用后新流程定义不能再归属到该应用(编辑器归属下拉与 defSave 校验拦截);存量定义不受影响", + async onOk() { + await doToggle(); + }, + }); } // ==================== tab2 科室管理 ==================== @@ -322,13 +357,15 @@ onMounted(() => {
+ 应用清单来自本校已开通应用(UserBase 学校组开通,含学校别名);开启后可用于流程归属与筛选。 新开通/改名在 UserBase 管理端操作后回本页刷新即可
- diff --git a/src/views/flow/admin/Callbacks.vue b/src/views/flow/admin/Callbacks.vue index 73e3a55..b92f716 100644 --- a/src/views/flow/admin/Callbacks.vue +++ b/src/views/flow/admin/Callbacks.vue @@ -33,11 +33,12 @@ const regColumns = [ { title: "状态", width: 80, align: "center", render: (h: any, p: any) => h(Tag, { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) }, actionColumn( [ + { key: "probe", label: "探测", icon: "icon-fasong", on: (r: any) => onProbe(r) }, { key: "disable", label: "停用", icon: "icon-stop", on: (r: any) => onRegStatus(r, true), disabledWhen: (r: any) => r.status !== 0, danger: true }, { key: "enable", label: "启用", icon: "icon-shiyong", on: (r: any) => onRegStatus(r, false), disabledWhen: (r: any) => r.status === 0 }, { key: "resetSecret", label: "重置secret", icon: "icon-shezhi", on: (r: any) => onResetSecret(r) }, ], - { mode: "text", width: 140, fixed: "right" }, + { mode: "text", width: 240, fixed: "right" }, ), ]; @@ -57,8 +58,27 @@ const logColumns = [ { title: "重试", key: "retryCount", width: 60, align: "center" }, { title: "末次结果", key: "lastError", minWidth: 200, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") }, { title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") }, + actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openLogDetail(r) }], { mode: "text", width: 100, fixed: "right" }), ]; +// ---- 推送日志详情抽屉(2026-10-11:完整请求体/响应摘要/重试链可视) ---- +const logDetail = ref(false); +const logDetailRow = ref(null); + +function openLogDetail(row: any) { + logDetailRow.value = row; + logDetail.value = true; +} + +function prettyJson(raw: any): string { + if (raw === null || raw === undefined || raw === "") return "-"; + try { + return JSON.stringify(JSON.parse(raw), null, 2); + } catch (e) { + return String(raw); + } +} + // ---- 挂起列表(持续重试中:待推且重试达阈值;人工判定无意义 → 标记死信止血) ---- const hungs = ref([]); const hungLoading = ref(false); @@ -133,6 +153,22 @@ function onResetSecret(row: any) { }); } +// ---- 回调探测(2026-10-11):签一条 flow.ping 走真实管道,结果弹窗展示并落推送日志 ---- +const probeResult = ref(null); +const probeShowing = ref(false); +const probingId = ref(null); + +async function onProbe(row: any) { + probingId.value = row.id; + try { + const d: any = await flowApi.callbackProbe(row.id).send(); + probeResult.value = { ...d, appKey: row.appKey, callbackUrl: row.callbackUrl }; + probeShowing.value = true; + } finally { + probingId.value = null; + } +} + function onMarkDead(row: any) { MyConfirm({ title: "标记死信", @@ -224,9 +260,49 @@ onMounted(loadRegs); + + + +
+ + {{ probeResult.success ? "接收方可达且验签通过(HTTP 2xx)" : "探测失败:接收方不可达或验签被拒" }} + +
回调地址{{ probeResult.callbackUrl }}
+
HTTP 状态{{ probeResult.httpStatus ?? "-" }}
+
耗时{{ probeResult.elapsedMs }} ms
+
错误{{ probeResult.error }}
+
+ 响应摘要 +
{{ probeResult.respSnippet }}
+
+
探测事件 flow.ping 已落「推送日志」(探测结果以接收方按规范记日志后 200 为准)
+
+
+ + + +
+
事件ID{{ logDetailRow.eventId }}
+
办件 / biz{{ logDetailRow.instanceId }} / {{ logDetailRow.bizType }}/{{ logDetailRow.bizId }}
+
标题{{ logDetailRow.bizTitle || "-" }}
+
状态{{ LOG_STATUS[logDetailRow.status] }}
+
重试次数{{ logDetailRow.retryCount }}
+
末次 HTTP{{ logDetailRow.lastHttpStatus || "-" }}
+
末次错误{{ logDetailRow.lastError || "-" }}
+
推送时间{{ (logDetailRow.pushTime || "-").replace("T", " ") }}
+
下次重推{{ (logDetailRow.nextRetryTime || "-").replace("T", " ") }}
+
响应摘要
{{ logDetailRow.respBody || "-" }}
+
请求体(完整快照)
{{ prettyJson(logDetailRow.payloadJson) }}
+
+