mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat(i8-i9): webhook DEAD replay, read-only delivery status, and callback UI
I8: platform proxies replay to webhook; webhook ops token filter and internal replay endpoint; delivery service supports read/replay flows. I9: platform GET callback webhook delivery status by inbox id; UI shows read-only status block and handles load errors without blocking the page. Also refresh OpenAPI, Runbook notes, test fixtures and YAML; fix Vite dev axios baseURL so /api uses proxy; improve login error messaging. Made-with: Cursor
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
<el-descriptions-item label="事件类型">{{ row.eventType ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Schema 版本">{{ row.schemaVersion ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="幂等键">{{ row.idempotencyKey ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Webhook 收据 ID">{{ row.webhookReceiptId ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="SN">{{ row.snCode ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目 ID">{{ row.projectId ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同 ID">{{ row.contractId ?? "—" }}</el-descriptions-item>
|
||||
@@ -35,6 +36,28 @@
|
||||
<h3 class="section-title">Payload(脱敏预览)</h3>
|
||||
<pre class="payload-pre">{{ payloadDisplay }}</pre>
|
||||
|
||||
<h3 v-if="canReplayWebhook" class="section-title">Webhook 平台投递状态(I9)</h3>
|
||||
<template v-if="canReplayWebhook">
|
||||
<el-skeleton v-if="webhookDeliveryLoading" :rows="2" animated />
|
||||
<p v-else-if="webhookDeliveryError" class="hint webhook-err">{{ webhookDeliveryError }}</p>
|
||||
<el-descriptions v-else-if="webhookDeliveryStatus" :column="2" border class="block block-tight">
|
||||
<el-descriptions-item label="出库状态">{{ webhookDeliveryStatus.status ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="尝试次数">{{ webhookDeliveryStatus.attempts ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上次错误" :span="2">{{ webhookDeliveryStatus.lastError ?? "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下次重试">{{ formatDateTime(webhookDeliveryStatus.nextRetryAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出库更新时间">{{ formatDateTime(webhookDeliveryStatus.updatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<h3 v-if="canReplayWebhook" class="section-title">Webhook 出库(I8)</h3>
|
||||
<p v-if="canReplayWebhook" class="hint">
|
||||
若 Webhook 侧平台投递为 DEAD,可将该收据对应任务重新入队;需在平台配置 <code>LICENSE_WEBHOOK_BASE_URL</code> 与
|
||||
<code>LICENSE_WEBHOOK_OPS_TOKEN</code>。
|
||||
</p>
|
||||
<div v-if="canReplayWebhook" class="status-row">
|
||||
<el-button type="warning" :loading="replaying" @click="replayWebhook">重新入队出库(DEAD→待投递)</el-button>
|
||||
</div>
|
||||
|
||||
<h3 v-if="isPending" class="section-title">状态处置</h3>
|
||||
<div v-if="isPending" class="status-row">
|
||||
<el-button type="success" :loading="patchingStatus" @click="setStatus('PROCESSED')">标为已处理</el-button>
|
||||
@@ -68,7 +91,13 @@ import { ref, reactive, computed, watch, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
import { getCallbackInbox, patchCallbackInboxStatus, patchCallbackInboxLink } from "../api/platform";
|
||||
import {
|
||||
getCallbackInbox,
|
||||
getCallbackWebhookDelivery,
|
||||
patchCallbackInboxStatus,
|
||||
patchCallbackInboxLink,
|
||||
replayCallbackWebhookDelivery,
|
||||
} from "../api/platform";
|
||||
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||
import { formatRedactedPayloadJson } from "../utils/redactPayload";
|
||||
|
||||
@@ -78,8 +107,12 @@ const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const patchingStatus = ref(false);
|
||||
const replaying = ref(false);
|
||||
const savingLink = ref(false);
|
||||
const row = ref(null);
|
||||
const webhookDeliveryStatus = ref(null);
|
||||
const webhookDeliveryLoading = ref(false);
|
||||
const webhookDeliveryError = ref(null);
|
||||
|
||||
const linkForm = reactive({
|
||||
licenseSnId: "",
|
||||
@@ -91,6 +124,11 @@ const inboxId = computed(() => route.params.id);
|
||||
|
||||
const isPending = computed(() => String(row.value?.status ?? "").toUpperCase() === "PENDING");
|
||||
|
||||
const canReplayWebhook = computed(() => {
|
||||
const id = row.value?.webhookReceiptId ?? row.value?.webhook_receipt_id;
|
||||
return id != null && String(id).trim() !== "";
|
||||
});
|
||||
|
||||
const payloadDisplay = computed(() => {
|
||||
const r = row.value;
|
||||
if (!r) return "—";
|
||||
@@ -147,6 +185,27 @@ function goList() {
|
||||
router.push({ name: "callback-inbox" });
|
||||
}
|
||||
|
||||
async function loadWebhookDelivery() {
|
||||
const id = inboxId.value;
|
||||
const rid = row.value?.webhookReceiptId ?? row.value?.webhook_receipt_id;
|
||||
if (id == null || id === "" || rid == null || String(rid).trim() === "") {
|
||||
webhookDeliveryStatus.value = null;
|
||||
webhookDeliveryError.value = null;
|
||||
return;
|
||||
}
|
||||
webhookDeliveryLoading.value = true;
|
||||
webhookDeliveryError.value = null;
|
||||
try {
|
||||
const { data } = await getCallbackWebhookDelivery(id);
|
||||
webhookDeliveryStatus.value = data && typeof data === "object" ? data : null;
|
||||
} catch (e) {
|
||||
webhookDeliveryStatus.value = null;
|
||||
webhookDeliveryError.value = apiErrorMessage(e, "加载 Webhook 出库状态失败");
|
||||
} finally {
|
||||
webhookDeliveryLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const id = inboxId.value;
|
||||
if (id == null || id === "") return;
|
||||
@@ -160,6 +219,12 @@ async function load() {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
if (row.value) {
|
||||
await loadWebhookDelivery();
|
||||
} else {
|
||||
webhookDeliveryStatus.value = null;
|
||||
webhookDeliveryError.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setStatus(status) {
|
||||
@@ -183,6 +248,30 @@ async function setStatus(status) {
|
||||
}
|
||||
}
|
||||
|
||||
async function replayWebhook() {
|
||||
const id = inboxId.value;
|
||||
if (id == null) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
"确认向 Webhook 请求将该收据的 DEAD 出库重新入队?(若平台未配置 Webhook 地址或出库非 DEAD 将失败)",
|
||||
"重新入队",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
replaying.value = true;
|
||||
try {
|
||||
await replayCallbackWebhookDelivery(id);
|
||||
ElMessage.success("已请求重新入队,请稍后在 Webhook 库表或收件箱确认");
|
||||
await loadWebhookDelivery();
|
||||
} catch (e) {
|
||||
ElMessage.error(apiErrorMessage(e, "重新入队失败"));
|
||||
} finally {
|
||||
replaying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLink() {
|
||||
const id = inboxId.value;
|
||||
if (id == null) return;
|
||||
@@ -239,6 +328,18 @@ async function saveLink() {
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hint {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.webhook-err {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.block-tight {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.payload-pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ref, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||
|
||||
const username = ref("admin");
|
||||
const password = ref("admin");
|
||||
@@ -39,7 +40,11 @@ async function onSubmit() {
|
||||
const redirect = route.query.redirect || "/";
|
||||
await router.replace(typeof redirect === "string" ? redirect : "/");
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || "登录失败");
|
||||
const hasResponse = e && typeof e === "object" && "response" in e && e.response;
|
||||
const fallback = hasResponse
|
||||
? "登录失败"
|
||||
: "无法连接登录接口,请确认 delivery-platform-api 已在本机 8080 运行(npm run dev 前启动后端,见 README)。";
|
||||
ElMessage.error(apiErrorMessage(e, fallback));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user