feat(i7): async webhook delivery queue, OPS RBAC, UI role routing; docs and runbook

- Architect: I7_DESIGN.md, I7_IMPLEMENTATION_REVIEW.md; parallel index + track B
- Backend: @EnableMethodSecurity; OPS login; CallbackInbox PreAuthorize; IntegrationCatalog triple role
- Webhook: V2 webhook_platform_delivery; planner + scheduler + single-shot forwarder; tests
- Frontend: Pinia hasAnyRole; MainLayout/HomeView/router for OPS vs dev
- Runbook §10.5 delivery config

Made-with: Cursor
This commit is contained in:
2026-04-06 23:01:10 +08:00
parent ce49fe143c
commit 5fe7181b35
33 changed files with 936 additions and 200 deletions
@@ -6,28 +6,28 @@
<el-menu-item index="/">
<span>首页</span>
</el-menu-item>
<el-menu-item index="/customers">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER'])" index="/customers">
<span>客户管理</span>
</el-menu-item>
<el-menu-item index="/projects">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER'])" index="/projects">
<span>项目管理</span>
</el-menu-item>
<el-menu-item index="/contracts">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER'])" index="/contracts">
<span>合同管理</span>
</el-menu-item>
<el-menu-item index="/deliveries">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER'])" index="/deliveries">
<span>交付管理</span>
</el-menu-item>
<el-menu-item index="/licenses/sn">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER'])" index="/licenses/sn">
<span>许可 SN</span>
</el-menu-item>
<el-menu-item index="/callbacks">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'OPS'])" index="/callbacks">
<span>Callback 收件箱</span>
</el-menu-item>
<el-menu-item index="/integration/environments">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER', 'OPS'])" index="/integration/environments">
<span>集成环境</span>
</el-menu-item>
<el-menu-item index="/integration/product-lines">
<el-menu-item v-if="auth.hasAnyRole(['SYS_ADMIN', 'DEVELOPER', 'OPS'])" index="/integration/product-lines">
<span>产品线</span>
</el-menu-item>
</el-menu>
+5 -5
View File
@@ -12,7 +12,7 @@ const routes = [
path: "",
name: "home",
component: () => import("../views/HomeView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER"] },
meta: { roles: ["SYS_ADMIN", "DEVELOPER", "OPS"] },
},
{
path: "customers",
@@ -66,25 +66,25 @@ const routes = [
path: "integration/environments",
name: "integration-environments",
component: () => import("../views/IntegrationEnvironmentsView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "集成环境" },
meta: { roles: ["SYS_ADMIN", "DEVELOPER", "OPS"], title: "集成环境" },
},
{
path: "integration/product-lines",
name: "integration-product-lines",
component: () => import("../views/IntegrationProductLinesView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "产品线" },
meta: { roles: ["SYS_ADMIN", "DEVELOPER", "OPS"], title: "产品线" },
},
{
path: "callbacks/:id",
name: "callback-inbox-detail",
component: () => import("../views/CallbackInboxDetailView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "Callback 详情" },
meta: { roles: ["SYS_ADMIN", "OPS"], title: "Callback 详情" },
},
{
path: "callbacks",
name: "callback-inbox",
component: () => import("../views/CallbackInboxView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "Callback 收件箱" },
meta: { roles: ["SYS_ADMIN", "OPS"], title: "Callback 收件箱" },
},
{
path: "contracts/new",
@@ -9,6 +9,15 @@ export const useAuthStore = defineStore("auth", {
displayName: "",
roles: [],
}),
getters: {
hasAnyRole: (state) => {
return (roleList) => {
const need = roleList || [];
const have = state.roles || [];
return need.some((r) => have.includes(r));
};
},
},
actions: {
async login(username, password) {
const { data } = await axios.post("/api/v1/auth/login", { username, password });
+15 -13
View File
@@ -2,14 +2,14 @@
<div class="home">
<el-card class="block">
<el-alert
title="交付平台(I6 UAT):以下为已实现模块的快速入口;登录态为 JWT Bearer"
title="交付平台(I7):按角色展示入口;Callback 仅 OPS / SYS_ADMIN"
type="info"
show-icon
:closable="false"
/>
<p class="meta">用户{{ auth.displayName }}角色{{ auth.roles.join(", ") || "—" }}</p>
<div class="quick-links" aria-label="模块导航">
<router-link v-for="l in moduleLinks" :key="l.to" class="ql" :to="l.to">
<router-link v-for="l in visibleModuleLinks" :key="l.to" class="ql" :to="l.to">
{{ l.label }}
</router-link>
</div>
@@ -23,7 +23,7 @@
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import axios from "axios";
import { useAuthStore } from "../stores/auth";
@@ -31,18 +31,20 @@ const auth = useAuthStore();
const pingBody = ref("");
const pingLoading = ref(false);
/** I6:全链路导航锚点,与 MainLayout 菜单一致 */
const moduleLinks = [
{ to: "/customers", label: "客户" },
{ to: "/projects", label: "项目" },
{ to: "/contracts", label: "合同" },
{ to: "/deliveries", label: "交付" },
{ to: "/licenses/sn", label: "许可 SN" },
{ to: "/callbacks", label: "Callback 收件箱" },
{ to: "/integration/environments", label: "集成环境" },
{ to: "/integration/product-lines", label: "产品线" },
/** I7与 MainLayout / 路由 meta 一致 */
const allModuleLinks = [
{ to: "/customers", label: "客户", roles: ["SYS_ADMIN", "DEVELOPER"] },
{ to: "/projects", label: "项目", roles: ["SYS_ADMIN", "DEVELOPER"] },
{ to: "/contracts", label: "合同", roles: ["SYS_ADMIN", "DEVELOPER"] },
{ to: "/deliveries", label: "交付", roles: ["SYS_ADMIN", "DEVELOPER"] },
{ to: "/licenses/sn", label: "许可 SN", roles: ["SYS_ADMIN", "DEVELOPER"] },
{ to: "/callbacks", label: "Callback 收件箱", roles: ["SYS_ADMIN", "OPS"] },
{ to: "/integration/environments", label: "集成环境", roles: ["SYS_ADMIN", "DEVELOPER", "OPS"] },
{ to: "/integration/product-lines", label: "产品线", roles: ["SYS_ADMIN", "DEVELOPER", "OPS"] },
];
const visibleModuleLinks = computed(() => allModuleLinks.filter((l) => auth.hasAnyRole(l.roles)));
onMounted(() => auth.restoreAxiosAuth());
async function ping() {
@@ -11,7 +11,7 @@
</el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" block>登录</el-button>
</el-form>
<p class="hint">演示admin / adminSYS_ADMINdev / devDEVELOPER</p>
<p class="hint">演示admin / adminSYS_ADMINdev / devDEVELOPERops / opsOPSCallback 运营</p>
</el-card>
</div>
</template>