Implement identity graph stitching and configurable profile storage

Issue persistent signed anonymous IDs and accept cookie/token-bound events.
Resolve verified email, phone, OAuth, and namespaced third-party IDs into
transactionally merged master identities with historical event aliases.
Protect shared-device account switching, tenant isolation, and event retries.

Store sparse canonical profiles as PostgreSQL JSONB or local SQLite JSON.
Support an optional versioned Cassandra wide-column projection through a
transactional outbox, retry worker, and immediate canonical read fallback.
Document deployment settings and provide opt-in database integration tests.

Validation: 43 local tests passed; 6 external database tests skipped.
Ruff checks, formatting, and offline uv lock verification passed.
This commit is contained in:
2026-09-15 23:49:43 +08:00
parent c30a136102
commit 0fc6c68a99
27 changed files with 2993 additions and 103 deletions
+52 -68
View File
@@ -1,86 +1,60 @@
# AdCloud Server
面向开发者和 coding agent 的广告云埋点与广告推送后端。使用 FastAPI 开发,依赖和虚拟环境统一由 **uv** 管理。
面向开发者和 coding agent 的广告云埋点与广告推送后端。使用 FastAPI 开发,Python 依赖和虚拟环境统一由 **uv** 管理。
已实现 `/v1` Gateway 路由、匿名身份签发、ID 映射图谱、可信身份 Stitch、稀疏画像及历史事件回溯。
画像可按部署设置选择关系库读取或 Cassandra 宽表投影;身份图谱保持关系库事务一致性。
广告业务目前仅有路由探测,尚未实现投放引擎。邮箱/手机号 OTP 和具体 OAuth 登录需要由调用方接入验证。
## 本地开发(无需安装数据库服务)
```sh
uv sync --locked
uv run python scripts/init_local_env.py
uv run uvicorn adcloud_server.main:app --reload --host 127.0.0.1 --port 8000
```
初始化脚本生成被 Git 忽略的 `.env`,权限 0600,包含本地开发所需随机 secret,不输出 secret 值,不覆盖已有文件。
SQLite 数据存放于 `.data/`,同样不提交。身份服务缺少 IDENTITY_SECRET 时返回 503,系统健康接口仍可用。
环境变量优先于 `.env`。生产 HTTPS 部署将 `COOKIE_SECURE` 设置为 true。
- 系统健康检查:`http://localhost:8000/health`
- API 文档:`http://localhost:8000/docs`
- OpenAPI:`http://localhost:8000/openapi.json`
另一个终端可运行 `uv run python scripts/demo_identity.py`,演示匿名访问、埋点、临时画像、模拟验证后 Stitch 及继承结果。
演示脚本使用合成的已验证身份,不代替实际 OTP/OAuth 校验。
## HTTP Gateway
```text
HTTPS :443 (Nginx,终止 TLS)
├── /v1/events/* → events 路由模块
├── /v1/users/* → users 路由模块
├── /v1/ads/* → ads 路由模块
├── /v1/identity/* → identity 路由模块
├── /v1/events/* → 事件接收、历史查询
├── /v1/users/* → 稀疏用户画像
├── /v1/ads/* → 广告业务模块(待实现)
├── /v1/identity/* → 匿名 ID、解析、拼接与图谱
└── /v1/... → 后续业务模块
↓
FastAPI 127.0.0.1:8000
```
目前使用模块化单体:四个业务模块共用一个 FastAPI 进程和版本入口。
Nginx 为各业务域定义了独立 upstream,后续拆分服务时可以替换地址。
Gateway 保留完整路径和查询参数,不剥离 `/v1` 或业务前缀。
未知路径返回 404,不支持的 HTTP 方法返回 405。
各模块当前共用一个进程,独立注册在 `api/v1/router.py` 中;Nginx 为各域预留独立 upstream。
未知路由返回 404、不支持的方法返回 405。保留各模块 `/health` 探测以及旧系统 `/health`。
默认 CORS 允许 localhost:3000、5173、5174,启用 credentials;匿名写请求检查浏览器 Origin。
| 路径 | 模块职责 | 当前已实现接口 |
| --- | --- | --- |
| `/v1/events/*` | 埋点事件接收与查询 | `GET /v1/events/health` |
| `/v1/users/*` | 平台用户资料管理 | `GET /v1/users/health` |
| `/v1/ads/*` | 广告管理与投放 | `GET /v1/ads/health` |
| `/v1/identity/*` | 登录、令牌与身份权限 | `GET /v1/identity/health` |
`deploy/nginx.conf.example` 放在 Nginx `http {}` 上下文,替换域名和证书路径后在部署环境执行 `nginx -t`。
精确匹配 `/v1/events` 防止 POST 被重定向为带尾斜杠路径,所有转发保留 `/v1`、业务前缀和查询参数。
配置未在本机部署或验证,未启动 443、未申请证书。API 文档仅通过内部地址访问。
各模块健康接口用于验证路由挂载,返回 `{"status":"ok","module":"events"}` 等响应;不代表数据库或外部依赖已就绪。
当前阶段仅实现 Gateway 和模块入口,尚未实现业务写入、数据持久化、登录或鉴权。
## Identity Resolution / Stitching
```text
src/adcloud_server/
main.py # 应用工厂、CORS、系统健康接口
schemas.py # 公共响应模型
api/v1/router.py # /v1 统一入口和模块注册
api/v1/events.py
api/v1/users.py
api/v1/ads.py
api/v1/identity.py
```
详见 [身份图谱与存储配置](docs/identity.md),包含接口、合并策略、共享设备处理和 PostgreSQL / Cassandra 部署选择。
增加新模块时,在 `api/v1/` 新增 `APIRouter`,并在 `router.py` 中注册前缀和 OpenAPI 标签。
若部署为独立服务,再增加对应的 Gateway upstream 和 location。
## 本地开发
```sh
uv sync --locked
uv run uvicorn adcloud_server.main:app --reload --host 127.0.0.1 --port 8000
```
- 系统健康检查:`http://localhost:8000/health`(保留 SDK 和控制台兼容)
- 模块探测:`http://localhost:8000/v1/events/health`
- API 文档:`http://localhost:8000/docs`
- OpenAPI:`http://localhost:8000/openapi.json`
默认允许来自 `http://localhost:3000`、`http://localhost:5173`、`http://localhost:5174` 的浏览器请求。
`CORS_ORIGINS` 接受逗号分隔的来源;环境配置示例见 `.env.example`。
```sh
CORS_ORIGINS=https://advertiser.example,https://developer.example uv run uvicorn adcloud_server.main:app
```
CORS 已配置写请求方法和 `Authorization`、`Content-Type`、`X-Request-ID` 请求头。
允许预检不代表相应业务端点已实现;CORS 也不代替身份认证和权限校验。
## HTTPS 入口配置
`deploy/nginx.conf.example` 是放入 Nginx `http {}` 上下文的配置模板。
替换 `server_name` 和证书文件路径,然后在目标服务器使用 `nginx -t` 检查,加载配置。
模板未在本机启动 443,也未申请或部署证书。API 文档仅通过内部 FastAPI 地址访问。
Nginx 在公网边缘重写转发头,FastAPI 只监听回环地址。目标服务器启动后端:
```sh
uv run uvicorn adcloud_server.main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips 127.0.0.1
```
模板允许最大 1 MiB 请求体,连接超时 5 秒、读写超时 30 秒;可按后续埋点批量策略调整。
参考:[FastAPI 多模块路由](https://fastapi.tiangolo.com/tutorial/bigger-applications/)、[Nginx proxy_pass 路径规则](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass)。
- Cookie / token 只证明匿名设备持有,不能自行提交 `verified=true` 合并账户。
- Stitch、图谱、画像读写和历史查询仅允许携带 SERVICE_TOKEN 的可信后端。
- 匿名事件保留原始 ID,通过 alias 在合并后即时归入 Master ID。
- PostgreSQL JSONB / SQLite JSON 省略空属性;Cassandra 可选宽表投影通过事务 outbox 同步和故障重试。
- 图谱主库始终是关系库,不提供纯 Cassandra、HBase 或 Bigtable 图谱实现。
## 检查
@@ -90,4 +64,14 @@ uv run ruff check .
uv run ruff format --check .
```
提交 `uv.lock`,使用 `uv sync --locked` 复现依赖。
默认测试使用临时 SQLite 和可恢复的投影替身,不安装、不启动数据库服务。
PostgreSQL、Cassandra 联调入口默认跳过,等提供专用测试环境后使用:
```sh
uv sync --locked --extra postgres --extra cassandra
```
- 设置 `ADCLOUD_TEST_POSTGRES_URL` 为专用测试 PostgreSQL 的 SQLAlchemy URL,然后运行 `uv run --extra postgres pytest tests/test_postgres.py`。
- 设置 `ADCLOUD_TEST_CASSANDRA_HOSTS`、`ADCLOUD_TEST_CASSANDRA_KEYSPACE` 和可选的 PORT、USERNAME、PASSWORD、TLS、CA_FILE,同前缀环境变量配置;先在专用集群应用 `deploy/cassandra.cql`,再运行 `uv run --extra cassandra pytest tests/test_cassandra.py`。
联调测试使用独立随机租户,不自动创建 Cassandra 集群。PostgreSQL 测试会创建初始表并保留测试租户数据,需使用专用测试数据库。