Add versioned event registry and transactional feature rules
Docker test deployment / test-build-deploy (push) Successful in 4m44s

This commit is contained in:
2026-09-16 04:46:17 +08:00
parent 51d5a06cd7
commit b43043255c
31 changed files with 2015 additions and 30 deletions
+6 -4
View File
@@ -8,8 +8,8 @@
2. 使用 Gitea 自动签发的短期作业 token 下载精确提交的源码,不将认证信息放入 URL、命令行或日志。
3. 用 uv 和 uv.lock 构建 test 镜像,执行 Ruff 与 pytest。外部数据库测试未配置环境时跳过。
4. 构建不含开发依赖的非 root runtime 镜像,包含 PostgreSQL / Cassandra Python 驱动,允许以后配置存储;不运行数据库服务。
5. 通过独立 Compose 项目启动临时 PostgreSQL / Cassandra,执行全部 54 个测试(包含 6 个真实数据库测试);结束后清理该 CI 项目的容器和数据卷。
6. 用 Compose 部署持久化 PostgreSQL / Cassandra、API 和画像 worker,检查 JSONB 存储、画像投影与宿主机 HTTP 端口。
5. 通过独立 Compose 项目启动临时 PostgreSQL / Cassandra,执行全部数据库与注册表测试(包含 6 个真实数据库测试);结束后清理该 CI 项目的容器和数据卷。
6. 用 Compose 部署持久化 PostgreSQL / Cassandra、API、特征 worker 和画像 worker,检查 JSONB 存储、画像投影与宿主机 HTTP 端口。
7. 如提供公网 URL,再探测公网 `/health`。后续可从外部执行匿名和埋点测试。
Docker 镜像直接保存在该 daemon 上,本版不依赖镜像仓库登录。
@@ -68,9 +68,9 @@ docker build --target runtime --tag adcloud-server:test .
DEPLOY_IMAGE=adcloud-server:test bash scripts/compose_stack.sh deploy
```
当前公网环境使用 PostgreSQL 16 保存身份图谱和 JSONB 权威画像,Cassandra 4.1 保存画像投影,worker 持续消费 outbox。数据库不发布宿主机端口,只能通过项目 Docker 内网连接。
当前公网环境使用 PostgreSQL 16 保存身份图谱和 JSONB 权威画像,Cassandra 4.1 保存画像投影,adcloud-events 消费事件规则任务,adcloud-profiles 消费画像 outbox。数据库不发布宿主机端口,只能通过项目 Docker 内网连接。
Cassandra 在此合成测试环境内关闭 TLS / 认证,单节点 RF=1;生产环境需另外配置认证、TLS、备份和多节点副本。
数据库和应用合计内存限制约 3.5 GiB,服务器应留出至少 4 GiB 可用内存,首次启动 Cassandra 可能需要几分钟。
数据库和应用合计内存限制约 4 GiB,服务器应留出至少 4.5 GiB 可用内存(CI 临时数据库会额外占用内存),首次启动 Cassandra 可能需要几分钟。
`compose.yml` 使用健康检查和一次性 CQL 初始化任务控制启动顺序。
临时 CI 使用 `adcloud-ci-<commit>-<run>` 独立项目;PostgreSQL 密码随机生成。Cassandra 测试同时连接 CI PostgreSQL,验证双存储组合。失败时也会清理该项目。
@@ -109,3 +109,5 @@ uv run python scripts/smoke_api.py --base-url http://SERVER_IP:18080 --public-on
不要将 SERVICE_TOKEN 放入浏览器或 SDK。需要管理接口测试时,在服务器的受控环境读取部署配置,或配置你自己的受保护测试凭据。
参考:[Docker 端口映射](https://docs.docker.com/engine/network/port-publishing/)、[runner v0.6.1 配置](https://gitea.com/gitea/act_runner/src/tag/v0.6.1/internal/pkg/config/config.example.yaml)、[uv Docker 集成](https://docs.astral.sh/uv/guides/integration/docker/)。
注册表默认强制应用凭据和已发布事件版本;smoke 会创建合成应用、注册 add_cart、重试同一 event_id 并验证数码意向 +1,只输出验证结论。公网 public-only 检查健康、匿名 Cookie 和未注册事件拒绝,不携带管理凭据。
+122
View File
@@ -0,0 +1,122 @@
# 事件注册表与标准特征
## 身份与凭据
本版沿用现有部署 TENANT_ID:一个部署上下文对应一个租户,该租户内按 app_id 隔离注册表和接入凭据。尚未实现客户账号登录、多租户请求路由或客户角色权限;不能把不同客户的应用都放进 default 租户并声称彼此身份图谱隔离。
平台受控后端使用 SERVICE_TOKEN 调用 POST /v1/registry/apps 创建应用,返回 collection_key、server_key、management_key(仅此时返回原值);数据库只保存带租户密钥的 HMAC。
collection_key 是 SDK 使用的受限写入凭据,不是用户认证或广告反作弊凭据。server_key 只在客户可信后端保存,用于上报服务端专属事件。management_key 用于本版控制台注册管理,页面内存保存,刷新清空;不使用 SERVICE_TOKEN,不存 localStorage。正式管理操作需 HTTPS,并最终接入客户登录。
事件请求头 X-Adcloud-Key 必须匹配 app_id;管理请求头 X-Adcloud-Management-Key 必须属于同一应用。
匿名 token 与 Master ID 沿用身份图谱;事件本身不验证邮箱、不执行登录态身份缝合。
## 公共请求字段
必选:app_id、name、event_version、event_id(UUID)、occurred_at(带时区,不能为未来或超过 90 天的历史事件)、properties。
可选:session_id、context、anonymous_token。浏览器可使用 Cookie,SDK 也可显式发送此前签发的匿名 token。
context 为有 32 KiB 上限的可选 JSON 上下文;properties 为有 32 KiB 上限的注册参数对象。业务身份/密码/设备推送 token 不应作为埋点字段;query、URL 等可能包含隐私数据,应由客户在上报前清理。
匿名签发 POST /v1/identity/anonymous 也接受可选 anonymous_token,用于 SDK 在 Cookie 不可用时恢复此前签发的匿名身份。
## 标准事件目录
下表参数都位于 properties。默认拒绝未注册参数、空字符串、类型不符和超出范围的值。quantity 为正整数;金额为最小货币单位非负整数,并要求三位大写 currency。
items 为 1–100 个商品对象,每项必选 product_id / quantity,可选 category_id / unit_price。
content.engage.action: like / save / share / comment;不采集评论正文。
content.progress 是累计进度信号,本版没有将其自动累加成时长特征。
ad.response.result: filled / no_fill / error;filled 需 delivery_id。ad.error 需 request_id 或 delivery_id。
ad.impression / ad.click 是客户端信号,本版不实现广告归属验证、有效曝光认定、反作弊或计费。
delivery_id 是单次广告投递 ID;message_id 必须是单个接收方的一次消息投递 ID,群发活动使用 campaign_id。push.open 按 message_id 统计首次打开,非每次重复打开。
push.delivery.result.status: accepted / delivered / failed,只有供应商确实提供的状态才上报。
| 标准事件 | 分类 / 含义 | 必选参数 | 可选参数 | 来源 | 业务去重键 |
|---|---|---|---|---|---|
| `page.view` | 访问与内容 / 页面访问 | page_id | referrer, title, url | SDK / 服务端 | event_id |
| `content.view` | 访问与内容 / 内容查看 | content_id, content_type | author_id, category_id | SDK / 服务端 | event_id |
| `content.engage` | 访问与内容 / 内容互动 | content_id, action | category_id, content_type | SDK / 服务端 | event_id |
| `content.progress` | 访问与内容 / 内容消费进度 | content_id, consumed_ms | duration_ms, progress_percent | SDK / 服务端 | event_id |
| `search.submit` | 访问与内容 / 发起搜索 | query | category_id, result_count | SDK / 服务端 | event_id |
| `navigation.click` | 访问与内容 / 导航点击 | target_id | destination, placement_id | SDK / 服务端 | event_id |
| `commerce.product.view` | 电商与交易 / 商品查看 | product_id | category_id, currency, price | SDK / 服务端 | event_id |
| `commerce.cart.add` | 电商与交易 / 加入购物车 | product_id, quantity | cart_id, category_id, currency, price | SDK / 服务端 | event_id |
| `commerce.cart.remove` | 电商与交易 / 移出购物车 | product_id, quantity | cart_id, category_id | SDK / 服务端 | event_id |
| `commerce.checkout.start` | 电商与交易 / 开始结算 | checkout_id, items | amount, coupon_id, currency | SDK / 服务端 | event_id |
| `commerce.order.create` | 电商与交易 / 创建订单 | order_id, items, amount, currency | checkout_id, coupon_id | 服务端 | order_id |
| `commerce.payment.success` | 电商与交易 / 支付成功 | order_id, payment_id, amount, currency | items, payment_method | 服务端 | payment_id |
| `commerce.order.cancel` | 电商与交易 / 订单取消 | order_id | reason_code | 服务端 | order_id |
| `commerce.refund.success` | 电商与交易 / 退款成功 | order_id, refund_id, amount, currency | items, reason_code | 服务端 | refund_id |
| `account.signup.success` | 注册与线索 / 注册成功 | registration_id, method | campaign_id | 服务端 | registration_id |
| `account.login.success` | 注册与线索 / 登录成功 | method | login_id | 服务端 | event_id |
| `lead.form.start` | 注册与线索 / 开始填写表单 | form_id | campaign_id, placement_id | SDK / 服务端 | event_id |
| `lead.submit.success` | 注册与线索 / 留资成功 | lead_id, form_id | campaign_id, lead_type | 服务端 | lead_id |
| `conversion.complete` | 注册与线索 / 业务转化完成 | conversion_id, conversion_type | currency, related_object_id, value | 服务端 | conversion_id |
| `ad.request` | 广告 / 请求广告 | request_id, placement_id | format, page_id | SDK / 服务端 | request_id |
| `ad.response` | 广告 / 广告返回 | request_id, result | delivery_id, latency_ms, reason_code | 服务端 | request_id |
| `ad.render` | 广告 / 广告渲染 | delivery_id | render_ms | SDK / 服务端 | delivery_id |
| `ad.impression` | 广告 / 广告展示信号 | delivery_id, impression_id | visible_ms, visible_ratio | SDK / 服务端 | impression_id |
| `ad.click` | 广告 / 广告点击信号 | delivery_id, click_id | destination_id, impression_id | SDK / 服务端 | click_id |
| `ad.video.progress` | 广告 / 广告视频进度 | delivery_id, milestone | played_ms | SDK / 服务端 | delivery_id, milestone |
| `ad.dismiss` | 广告 / 关闭广告 | delivery_id, action | reason_code | SDK / 服务端 | event_id |
| `ad.error` | 广告 / 广告错误 | stage, error_code | delivery_id, request_id | SDK / 服务端 | event_id |
| `push.permission.result` | 推送 / 推送权限结果 | status, channel | prompt_id | SDK / 服务端 | event_id |
| `push.send` | 推送 / 发送推送 | message_id, channel | campaign_id | 服务端 | message_id |
| `push.delivery.result` | 推送 / 推送投递结果 | message_id, status | failure_reason, provider_code | 服务端 | message_id, status |
| `push.open` | 推送 / 打开推送 | message_id | campaign_id | SDK / 服务端 | message_id |
| `push.click` | 推送 / 点击推送入口 | message_id, action_id | destination_id | SDK / 服务端 | message_id, action_id |
| `push.unsubscribe` | 推送 / 取消推送订阅 | channel | reason_code, subscription_id | SDK / 服务端 | event_id |
## 注册、发布、试算
公共 GET /v1/registry/catalog 返回事件字段类型、必选字段、枚举、范围,以及标准特征字典。
应用管理接口路径前缀 /v1/registry/apps/{app_id}:
- GET /events:查看应用所有事件参数版本和草稿。
- PUT /events/{name}/versions/{version}:新增或保存草稿。
- POST 同一路径 /preview:传入 properties,显示命中规则和特征增量,无事件或特征写入。
- POST /publish:产生不可变 rule_revision 并激活。
- POST /disable:拒绝新事件;已经接受的任务继续执行,原 event_id 的幂等重试仍返回原回执。
- GET /publications:列出不可变历史发布配置。
- POST /rollback:传入 revision,激活该历史规则;不重算此前接受的事件。
配置示例:
```json
{
"standard_event": "commerce.cart.add",
"description": "用户将数码商品加入购物车",
"extra_fields": {},
"required_extra": [],
"rules": [{
"conditions": [{"field": "properties.category_id", "operator": "eq", "value": "digital"}],
"effects": [{"feature": "intent.purchase.digital", "operation": "increment", "value": 1}]
}]
}
```
name 可以是客户自己的 add_cart;参数仍使用标准字段名。本版不做参数字段重命名或商品目录补全。
规则条件引用可选字段时,该字段会成为该事件定义的必选字段;上例因此必须提供 category_id。
扩展字段支持 string / integer / number / boolean;不能覆盖标准字段。发布过的参数结构不可变,改结构需要新 event_version;只改规则增量或描述可发布新 rule_revision。
规则内条件全部满足才执行;多条规则独立匹配。支持 eq / in / contains / gt / gte / lt / lte,contains 只支持字符串。
仅支持正数 increment,增量精度最多 6 位小数,单个增量 ≤100。规则只能引用平台已有特征;不运行任意代码、脚本或正则表达式。
最多 20 条规则、每条 20 个条件和 20 个增量、30 个扩展字段,配置正文最大 64 KiB。
## 持久化与计算
PostgreSQL 新增 event_apps、event_definitions、event_rule_publications、event_receipts、event_feature_contributions。元数据与冻结的请求/规则快照使用 JSONB,本地使用 SQLite JSON;已有表不改列,初始化时增加新表。
POST /v1/events 在同一事务中保存原始事件和规则任务;adcloud-events worker 在同一事务中写特征贡献、更新画像并入画像 outbox、标记任务完成。失败回滚,任务仍可重试。
同一租户下 event_id 不能用于不同事件;有业务去重键的事件还会按应用和标准事件业务键去重,避免同一支付、广告点击或推送投递重复计算。
贡献绑定原始身份节点。读取时按 Master ID 对应的所有图谱成员汇总;缝合前后不复制贡献,不重复加分。GET /v1/users/{master_id} 提供 features;GET /v1/users/{master_id}/features 提供最近 100 个贡献及其 rule_revision,使用现有可信服务鉴权。
标准特征值按事件发生时间计算窗口和上限,而非简单覆盖 attributes;不能通过画像 PATCH 修改标准特征。
adcloud-profiles 将含特征快照的画像同步 Cassandra。画像读取的 features 按关系库贡献实时计算,避免过期分数被 Cassandra 缓存延长;Cassandra 画像仍保持其版本回退机制。
没有自动打标签、历史事件重放、商品类目查询、通用累积观看时长或客户自建标准特征功能。购买次数等特征按客户映射的业务事件计数,映射时应明确最终成交的业务含义。
| 标准特征 | 展示名称 | 窗口 | 上限 |
|---|---|---|---|
| `intent.purchase.digital` | 数码购买意向 | 7 天 | 100 |
| `interest.category.digital` | 数码兴趣 | 30 天 | 100 |
| `engagement.ad.click` | 广告点击活跃度 | 7 天 | 1000 |
| `engagement.push.open` | 推送打开活跃度 | 30 天 | 1000 |
| `commerce.purchase.count` | 购买次数 | 30 天 | 10000 |
| `conversion.lead.count` | 留资次数 | 30 天 | 10000 |
## 兼容与部署
EVENT_REGISTRY_REQUIRED 默认 true。旧版未带应用字段的埋点将返回 422,应先创建应用、注册并发布事件、更新 SDK。仅本地旧代码联调可设置 false;一旦带 app_id 仍必须通过注册表校验。平台专属结果事件不会因为兼容开关而允许 collection_key 上报。
Compose 已增加常驻 event-worker;部署 smoke 创建合成应用、注册加购、发送与重试事件,并验证 +1、身份缝合及 Cassandra 当前版本。临时 CI 对 SQLite 与真实 PostgreSQL 都测试注册、参数、去重、规则版本、worker 回滚和窗口。
+4 -3
View File
@@ -83,14 +83,15 @@ await fetch("https://YOUR_API_HOST/v1/identity/anonymous", {
});
```
后端第一次收到匿名请求就签发 ID;第三方 React SDK 的自动初始化集成尚未修改。
后端第一次收到匿名请求就签发 ID;React SDK 的 AdCloudProvider 在挂载时初始化,track 也会等待初始化完成。
浏览器请求需使用 `credentials: "include"`。匿名写接口检查 Origin;CORS 使用明确允许来源并启用凭据。
Cookie 默认 Secure、HttpOnly、SameSite=Lax,有效期默认一年;本地 HTTP 开发明确设置 `COOKIE_SECURE=false`。
跨站点 Cookie 如需 SameSite=None,必须同时启用 Secure。浏览器第三方 Cookie 策略可能阻止 Cookie,应优先将 API 放在第一方域名下。
不设置父域 Cookie,不通过第三方设备指纹跨站点识别用户。
事件可提供 UUID `event_id` 进行幂等重试。相同 ID 的不同主体或内容返回 409;省略 event_id 时每次生成新事件。
`occurred_at` 如果提供,必须含时区且不能晚于服务端当前时间。
默认事件接收要求注册并发布对应 app_id / name / event_version,携带应用 collection 或 server 凭据,以及 UUID event_id 和带时区的 occurred_at。
相同 ID 的不同主体或内容返回 409;未注册、未发布、参数不符或来源不合法会明确拒绝。详见 [事件字典与规则](events.md)。
普通画像属性继续按目标账号优先合并;标准特征由独立贡献记录按图谱成员汇总,因此身份缝合不覆盖数值、不重复加分。
Stitch 示例(只在 OTP/OAuth 验证后由可信后端执行):