From 858d287880bea34c8cf48aff97ec634d3c5db91e Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 16 Sep 2026 04:46:17 +0800 Subject: [PATCH] Add application event dictionary and rule management console --- README.md | 8 ++++ src/App.tsx | 6 ++- src/EventRegistry.tsx | 90 +++++++++++++++++++++++++++++++++++++++++++ src/styles.css | 16 ++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/EventRegistry.tsx diff --git a/README.md b/README.md index 0045f2a..80d7a09 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,11 @@ npm run preview ```sh CORS_ORIGINS=http://localhost:5173,http://localhost:5174 uv run uvicorn adcloud_server.main:app --reload ``` + +## 事件字典管理 + +新增“事件字典”页面,连接真实后端:浏览 33 个标准事件的必选/可选字段、查看标准特征窗口与上限、加载应用事件、保存 JSON 规则草稿、示例试算、发布、停用和历史版本回滚。 +当前尚未接入客户账号登录。平台受控后端调用 POST /v1/registry/apps 创建应用后,将对应 management_key 提供给管理操作者;该页面凭据仅存当前内存,不使用平台 SERVICE_TOKEN,不写入 localStorage。 +正式管理操作使用 HTTPS;当前公网 HTTP 后端只用于合成测试数据。VITE_API_BASE_URL 设置连接后端,页面也可手动输入测试地址。CORS 需允许控制台实际来源。 +规则试算按钮会先保存当前草稿,再执行无埋点/特征写入的试算;发布按钮会保存并产生不可变规则版本。事件参数结构修改需创建新的 event_version。 +本页已接入注册表业务 API,其他收入、应用草稿与登录页面仍是原有脚手架。本仓库没有新增公网前端部署。 diff --git a/src/App.tsx b/src/App.tsx index 64e4bc2..623dc19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ +import EventRegistry from "./EventRegistry"; import { useEffect, useState, type FormEvent } from "react"; -const tabs = ["总览", "应用管理", "接入数据"]; +const tabs = ["总览", "应用管理", "接入数据", "事件字典"]; const metrics = ["预估收益", "广告请求", "展示次数", "填充率"]; const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || "http://localhost:8000").replace(/\/+$/, ""); @@ -43,7 +44,7 @@ export default function App() {
开发者控制台 / {tab}开发预览 · 登录待接入

ADCLOUD WORKSPACE

{tab}

管理应用接入、广告位与流量收益。

-

当前为前端脚手架。业务 API、账号登录和真实数据尚未接入。后端健康检查:{connection}

+

事件注册表已接入;账号登录、收益和投放数据尚未接入。后端健康检查:{connection}

{tab === tabs[0] && <>
{metrics.map((metric) =>

{metric}

—等待业务数据接入
)}

开始使用

在{tabs[1]}中创建本地应用草稿,预览工作流程。

@@ -53,6 +54,7 @@ export default function App() {
setName(event.target.value)} maxLength={100} required placeholder="输入名称" />
{drafts.length === 0 ?

暂无草稿

: } } + {tab === tabs[3] && } {tab === tabs[2] &&

暂无数据

连接业务数据接口后,将在这里展示应用请求、广告展示与收益数据。

}
diff --git a/src/EventRegistry.tsx b/src/EventRegistry.tsx new file mode 100644 index 0000000..632c63f --- /dev/null +++ b/src/EventRegistry.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; + +type Field = { type: string; enum?: string[] }; +type Standard = { title: string; category: string; source: string; required: string[]; fields: Record; business_keys: string[] }; +type Catalog = { events: Record; features: Record }; +type Config = { standard_event: string; description: string; extra_fields?: Record; required_extra?: string[]; rules: unknown[]; required_fields?: string[] }; +type Definition = { name: string; version: number; status: string; revision: number; draft: Config }; +const initialRules = [{ conditions: [{ field: "properties.category_id", operator: "eq", value: "digital" }], effects: [{ feature: "intent.purchase.digital", operation: "increment", value: 1 }] }]; + +export default function EventRegistry({ defaultBaseUrl }: { defaultBaseUrl: string }) { + const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); + const [appId, setAppId] = useState(""); + const [key, setKey] = useState(""); + const [catalog, setCatalog] = useState(null); + const [items, setItems] = useState([]); + const [name, setName] = useState("add_cart"); + const [version, setVersion] = useState(1); + const [standardName, setStandardName] = useState("commerce.cart.add"); + const [description, setDescription] = useState("用户将数码商品加入购物车"); + const [rules, setRules] = useState(JSON.stringify(initialRules, null, 2)); + const [extensions, setExtensions] = useState('{"extra_fields": {}, "required_extra": []}'); + const [sample, setSample] = useState('{"product_id": "camera", "quantity": 1, "category_id": "digital"}'); + const [revision, setRevision] = useState(1); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(""); + const [preview, setPreview] = useState(null); + + const server = baseUrl.replace(/\/+$/, ""); + const prefix = `${server}/v1/registry/apps/${encodeURIComponent(appId)}`; + const definitionPath = `${prefix}/events/${encodeURIComponent(name)}/versions/${version}`; + const standard = catalog?.events[standardName]; + + useEffect(() => { + const controller = new AbortController(); + setCatalog(null); + fetch(`${server}/v1/registry/catalog`, { signal: controller.signal }) + .then(async (response) => { if (!response.ok) throw new Error(`字典加载失败 HTTP ${response.status}`); return response.json() as Promise; }) + .then(setCatalog) + .catch((error: unknown) => { if (!controller.signal.aborted) setMessage(error instanceof Error ? error.message : "字典加载失败"); }); + return () => controller.abort(); + }, [server]); + + async function request(url: string, method = "GET", body?: unknown) { + if (!appId || !key) throw new Error("请填写应用 ID 和应用管理凭据"); + const response = await fetch(url, { method, credentials: "include", headers: { "Content-Type": "application/json", "X-Adcloud-Management-Key": key }, body: body === undefined ? undefined : JSON.stringify(body), cache: "no-store" }); + const data = await response.json(); + if (!response.ok) throw new Error(`HTTP ${response.status}: ${typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail)}`); + return data; + } + async function run(operation: () => Promise) { + setBusy(true); setMessage(""); setPreview(null); + try { await operation(); } catch (error) { setMessage(error instanceof Error ? error.message : "操作失败"); } + finally { setBusy(false); } + } + async function refresh() { const data = await request(`${prefix}/events`); setItems(data.items); } + async function save() { + const extension = JSON.parse(extensions) as { extra_fields: Record; required_extra: string[] }; + await request(definitionPath, "PUT", { standard_event: standardName, description, rules: JSON.parse(rules), extra_fields: extension.extra_fields, required_extra: extension.required_extra }); + await refresh(); setMessage("草稿已保存到服务器,尚未改变已发布规则。"); + } + function edit(item: Definition) { + setName(item.name); setVersion(item.version); setStandardName(item.draft.standard_event); setDescription(item.draft.description); + setRules(JSON.stringify(item.draft.rules, null, 2)); setExtensions(JSON.stringify({ extra_fields: item.draft.extra_fields || {}, required_extra: item.draft.required_extra || [] }, null, 2)); + setRevision(item.revision || 1); setPreview(null); setMessage(""); + } + + return
+

应用连接

使用平台服务端创建应用时返回的 management_key。凭据仅保存在当前页面内存,刷新后需重新输入。客户账号登录尚未接入。

+
+ +
+

标准事件目录

按业务选择标准事件,自定义事件名称与它建立映射。支付、注册结果和投递结果等事件仅允许服务端上报。

+
{catalog && Object.entries(catalog.events).map(([id, item]) => )}
分类 / 事件必选参数可选参数来源 / 去重键
{item.category}
{item.title}
{id}
{item.required.join("、")}{Object.keys(item.fields).filter((field) => !item.required.includes(field)).join("、") || "—"}{item.source === "server" ? "可信服务端" : "SDK / 服务端"}
{item.business_keys.join(" + ") || "event_id"}
+
+

注册事件与规则

+
+ + {standard &&

必选:{standard.required.join("、")}。规则条件引用的可选字段也会成为必选参数。金额为最小货币单位整数,并需提供 currency。

} +