重写
This commit is contained in:
140
docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md
Normal file
140
docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 资产对象业务实体绑定 reducer 设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 `M6` 中“对象绑定业务实体 reducer”的首版落地方案。
|
||||
|
||||
当前已经完成:
|
||||
|
||||
1. 浏览器可通过 `PostObject` 把文件直传到私有 OSS。
|
||||
2. `POST /api/assets/objects/confirm` 已能确认对象存在。
|
||||
3. `asset_object` 已按 `bucket + object_key` 写入 SpacetimeDB。
|
||||
|
||||
下一步要补上的最小闭环是:
|
||||
|
||||
1. 已确认的 `asset_object` 能绑定到某个业务实体。
|
||||
2. 绑定关系由 SpacetimeDB 持久化。
|
||||
3. Axum 提供最小 HTTP facade,避免前端直接拼 SpacetimeDB reducer 参数。
|
||||
|
||||
## 2. 当前阶段不直接创建强业务表的原因
|
||||
|
||||
当前先落通用 `asset_entity_binding`,不直接创建 `character_visual_asset / scene_image_asset / sprite_sheet_asset`。
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. 角色、场景、精灵等强业务表的完整字段还没有冻结。
|
||||
2. 当前最紧急的工程闭环是“确认后的对象能被实体引用”,不是完整发布模型。
|
||||
3. 通用绑定表可以先承接旧接口迁移中的 `entityId + slot` 关系,后续再由强业务表逐步替换或派生。
|
||||
|
||||
## 3. 表设计
|
||||
|
||||
首版新增 private table:
|
||||
|
||||
1. `asset_entity_binding`
|
||||
|
||||
字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `binding_id` | `String` | 是 | 主键,固定 `assetbind_` 前缀。 |
|
||||
| `asset_object_id` | `String` | 是 | 被绑定的 `asset_object.asset_object_id`。 |
|
||||
| `entity_kind` | `String` | 是 | 业务实体类型,例如 `character`、`scene`、`profile`。 |
|
||||
| `entity_id` | `String` | 是 | 业务实体 ID。 |
|
||||
| `slot` | `String` | 是 | 实体上的资产槽位,例如 `primary_visual`、`cover`、`sprite_sheet`。 |
|
||||
| `asset_kind` | `String` | 是 | 资产类型,例如 `character_visual`。 |
|
||||
| `owner_user_id` | `Option<String>` | 否 | 归属用户,当前仅作为服务端传入的记录字段。 |
|
||||
| `profile_id` | `Option<String>` | 否 | 归属 profile。 |
|
||||
| `created_at` | `Timestamp` | 是 | 首次绑定时间。 |
|
||||
| `updated_at` | `Timestamp` | 是 | 最近绑定更新时间。 |
|
||||
|
||||
索引如下:
|
||||
|
||||
1. `entity_kind + entity_id + slot`
|
||||
用于按实体槽位查当前绑定。
|
||||
2. `asset_object_id`
|
||||
用于按对象反查被哪些业务实体引用。
|
||||
|
||||
## 4. 幂等规则
|
||||
|
||||
绑定写入按以下规则执行:
|
||||
|
||||
1. `asset_object_id` 必须已存在于 `asset_object`。
|
||||
2. `entity_kind + entity_id + slot` 作为首版幂等定位键。
|
||||
3. 同一实体槽位重复绑定时,不新增第二行。
|
||||
4. 重复绑定会复用原 `binding_id` 与 `created_at`,更新 `asset_object_id / asset_kind / owner_user_id / profile_id / updated_at`。
|
||||
5. 不同槽位可以绑定同一个 `asset_object_id`。
|
||||
|
||||
## 5. reducer / procedure 设计
|
||||
|
||||
SpacetimeDB 新增:
|
||||
|
||||
1. `bind_asset_object_to_entity`
|
||||
reducer,只返回 `Result<(), String>`,供后续模块内部复用。
|
||||
2. `bind_asset_object_to_entity_and_return`
|
||||
procedure,面向 Axum 同步接口返回最终绑定快照。
|
||||
|
||||
procedure 返回结构采用:
|
||||
|
||||
1. `ok`
|
||||
2. `record`
|
||||
3. `error_message`
|
||||
|
||||
与 `asset_object` 确认 procedure 保持一致,便于 `spacetime-client` 做统一错误映射。
|
||||
|
||||
## 6. Axum HTTP facade
|
||||
|
||||
首版新增接口:
|
||||
|
||||
`POST /api/assets/objects/bind`
|
||||
|
||||
请求体:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `assetObjectId` | `String` | 是 | 已确认对象 ID。 |
|
||||
| `entityKind` | `String` | 是 | 业务实体类型。 |
|
||||
| `entityId` | `String` | 是 | 业务实体 ID。 |
|
||||
| `slot` | `String` | 是 | 资产槽位。 |
|
||||
| `assetKind` | `String` | 是 | 资产类型。 |
|
||||
| `ownerUserId` | `String` | 否 | 当前阶段由后端调用方显式传入。 |
|
||||
| `profileId` | `String` | 否 | 当前阶段由后端调用方显式传入。 |
|
||||
|
||||
响应体核心字段:
|
||||
|
||||
1. `bindingId`
|
||||
2. `assetObjectId`
|
||||
3. `entityKind`
|
||||
4. `entityId`
|
||||
5. `slot`
|
||||
6. `assetKind`
|
||||
7. `ownerUserId`
|
||||
8. `profileId`
|
||||
9. `createdAt`
|
||||
10. `updatedAt`
|
||||
|
||||
## 7. 当前阶段安全边界
|
||||
|
||||
当前接口是 Axum facade,不是前端直接调用 SpacetimeDB reducer 的最终权限模型。
|
||||
|
||||
约束如下:
|
||||
|
||||
1. 当前不把长期 OSS AK/SK 下发给客户端。
|
||||
2. 当前不让客户端直接写 private table。
|
||||
3. `owner_user_id` 当前只作为记录字段,不作为可信授权依据。
|
||||
4. 后续接入 SpacetimeDB 身份透传后,绑定 reducer 的授权必须改为基于可信身份,不信任客户端传入的用户 ID。
|
||||
|
||||
## 8. 完成定义
|
||||
|
||||
首版完成条件:
|
||||
|
||||
1. `module-assets` 提供绑定输入、快照、结果结构与字段校验。
|
||||
2. `spacetime-module` 新增 `asset_entity_binding` 表与绑定 reducer/procedure。
|
||||
3. `spacetime-client` 生成最新 Rust bindings 并封装绑定 procedure。
|
||||
4. `api-server` 暴露 `POST /api/assets/objects/bind`。
|
||||
5. 本地测试覆盖字段错误与 “asset_object 不存在不能绑定”。
|
||||
|
||||
## 9. 一句话结论
|
||||
|
||||
当前阶段先用通用 `asset_entity_binding` 把已确认 OSS 对象绑定到业务实体槽位,强业务资产表等字段稳定后再继续拆分。
|
||||
185
docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md
Normal file
185
docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 资产对象上传完成确认接口设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于把 `M6` 中“上传完成后的对象确认接口”冻结到可直接编码的级别。
|
||||
|
||||
当前要解决的不是完整资产发布链,而是最小闭环:
|
||||
|
||||
1. 浏览器先通过 `PostObject` 把文件上传到私有 OSS
|
||||
2. 服务端确认对象真实存在
|
||||
3. 服务端把对象元数据写入当前阶段的 `asset_object` 真相存储
|
||||
4. 后续业务绑定与 SpacetimeDB reducer 再基于这条已确认对象继续扩展
|
||||
|
||||
## 2. 当前前提
|
||||
|
||||
已落地事实:
|
||||
|
||||
1. `POST /api/assets/direct-upload-tickets` 已能签发浏览器 `PostObject` 直传票据。
|
||||
2. `platform-oss` 已能生成私有读签名 URL。
|
||||
3. `asset_object` 已在 `spacetime-module` 中落下首版表骨架。
|
||||
|
||||
当前仍未落地:
|
||||
|
||||
1. 上传完成确认接口
|
||||
2. 对象 HEAD 校验
|
||||
3. `asset_object` 实际写入路径
|
||||
4. 业务实体绑定
|
||||
|
||||
## 3. 接口职责
|
||||
|
||||
`POST /api/assets/objects/confirm` 当前阶段只负责三件事:
|
||||
|
||||
1. 校验请求给出的 `bucket + object_key` 是否合法
|
||||
2. 调 OSS 做一次私有 `HEAD Object` 校验,确认对象真实存在
|
||||
3. 把对象元数据写入当前阶段的 `asset_object` 进程内存储,并返回确认结果
|
||||
|
||||
当前阶段明确不做:
|
||||
|
||||
1. 不做业务实体绑定
|
||||
2. 不做图片尺寸探测
|
||||
3. 不做 hash 计算
|
||||
4. 不做重复对象合并
|
||||
5. 不直接调用 SpacetimeDB reducer
|
||||
|
||||
## 4. 请求体设计
|
||||
|
||||
请求路径:
|
||||
|
||||
`POST /api/assets/objects/confirm`
|
||||
|
||||
请求体:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `bucket` | `String` | 否 | 当前阶段允许不传;不传时默认回落到服务端 OSS bucket。 |
|
||||
| `objectKey` | `String` | 是 | 正式对象路径真相字段。 |
|
||||
| `contentType` | `String` | 否 | 客户端已知 MIME,可回写到对象元数据。 |
|
||||
| `contentLength` | `u64` | 否 | 客户端可传期望大小;当前仅用于一致性校验,不作为唯一真相来源。 |
|
||||
| `contentHash` | `String` | 否 | 后续内容摘要预留字段。 |
|
||||
| `assetKind` | `String` | 是 | 业务资产类型,例如 `character_visual`。 |
|
||||
| `accessPolicy` | `String` | 否 | 默认 `private`。 |
|
||||
| `sourceJobId` | `String` | 否 | 来源任务 ID。 |
|
||||
| `ownerUserId` | `String` | 否 | 归属用户 ID。 |
|
||||
| `profileId` | `String` | 否 | 归属 profile ID。 |
|
||||
| `entityId` | `String` | 否 | 归属业务实体 ID。 |
|
||||
|
||||
补充约束:
|
||||
|
||||
1. `bucket` 当前若传入,必须与服务端已配置 bucket 一致。
|
||||
2. `objectKey` 必须落在受支持的 `generated-*` 前缀下。
|
||||
3. `assetKind` 当前不能为空。
|
||||
|
||||
## 5. 校验顺序
|
||||
|
||||
接口校验顺序固定如下:
|
||||
|
||||
1. 检查 OSS 配置是否存在
|
||||
2. 校验请求参数基础合法性
|
||||
3. 校验 `bucket` 与服务端配置 bucket 是否一致
|
||||
4. 调用 OSS `HEAD Object`
|
||||
5. 若客户端传了 `contentLength`,则与 OSS 返回的真实 `Content-Length` 做一致性校验
|
||||
6. 通过后写入 `asset_object`
|
||||
|
||||
## 6. OSS 校验结果口径
|
||||
|
||||
OSS `HEAD Object` 当前至少回填:
|
||||
|
||||
1. `content_length`
|
||||
2. `content_type`
|
||||
3. `last_modified_at`
|
||||
4. `etag`
|
||||
|
||||
当前阶段以 OSS 返回值为准:
|
||||
|
||||
1. `content_length` 真相取 OSS
|
||||
2. `content_type` 优先取 OSS,OSS 未返回时再回退请求体
|
||||
3. `content_hash` 暂不强行等于 `etag`
|
||||
|
||||
原因:
|
||||
|
||||
1. `etag` 对 multipart 上传和不同上传模式并不总等价于内容 hash
|
||||
2. 当前阶段先留出字段,不把错误假设固化进 schema
|
||||
|
||||
## 7. 写入规则
|
||||
|
||||
确认成功后写入 `asset_object`:
|
||||
|
||||
1. `asset_object_id` 由服务端生成,固定 `assetobj_` 前缀
|
||||
2. `bucket` 与 `object_key` 按正式真相写入
|
||||
3. `access_policy` 当前默认 `private`
|
||||
4. `content_length` 以 OSS HEAD 为准
|
||||
5. `content_type` 优先 OSS HEAD
|
||||
6. `version` 当前固定为 `1`
|
||||
7. `created_at` / `updated_at` 在确认时写当前 UTC 时间
|
||||
|
||||
当前阶段重复确认同一 `bucket + object_key` 的行为固定为:
|
||||
|
||||
1. 若已存在,则返回已存在记录并更新 `updated_at`
|
||||
2. 不生成第二条重复对象记录
|
||||
|
||||
## 8. 响应体设计
|
||||
|
||||
成功响应核心字段:
|
||||
|
||||
1. `assetObjectId`
|
||||
2. `bucket`
|
||||
3. `objectKey`
|
||||
4. `accessPolicy`
|
||||
5. `contentType`
|
||||
6. `contentLength`
|
||||
7. `contentHash`
|
||||
8. `assetKind`
|
||||
9. `sourceJobId`
|
||||
10. `ownerUserId`
|
||||
11. `profileId`
|
||||
12. `entityId`
|
||||
13. `version`
|
||||
14. `createdAt`
|
||||
15. `updatedAt`
|
||||
|
||||
## 9. 错误口径
|
||||
|
||||
### 9.1 请求参数错误
|
||||
|
||||
返回 `400`:
|
||||
|
||||
1. `objectKey` 为空
|
||||
2. `objectKey` 不在受支持前缀下
|
||||
3. `assetKind` 为空
|
||||
4. `bucket` 与当前配置 bucket 不一致
|
||||
5. 客户端声明的 `contentLength` 与 OSS HEAD 不一致
|
||||
|
||||
### 9.2 OSS 未配置
|
||||
|
||||
返回 `503`
|
||||
|
||||
### 9.3 OSS 对象不存在
|
||||
|
||||
返回 `404`
|
||||
|
||||
### 9.4 OSS 探测失败
|
||||
|
||||
返回 `502`
|
||||
|
||||
## 10. 当前阶段实现边界
|
||||
|
||||
当前阶段实现固定为:
|
||||
|
||||
1. `platform-oss` 增加服务端 `HEAD Object` helper
|
||||
2. `module-assets` 提供进程内 `asset_object` 确认服务
|
||||
3. `api-server` 接入 `POST /api/assets/objects/confirm`
|
||||
|
||||
下一阶段再继续:
|
||||
|
||||
1. 对接真实 SpacetimeDB reducer
|
||||
2. 业务实体绑定 reducer
|
||||
3. 更细的元数据探测
|
||||
|
||||
## 11. 关联文档
|
||||
|
||||
1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
|
||||
2. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md)
|
||||
3. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
@@ -0,0 +1,237 @@
|
||||
# Axum 到 SpacetimeDB 的资产对象确认调用方案
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 `POST /api/assets/objects/confirm` 从当前“进程内确认写入”切换到“真实 SpacetimeDB 持久化”的最小落地方案。
|
||||
|
||||
当前要解决的问题只有一个:
|
||||
|
||||
1. `api-server` 在完成 OSS `HEAD Object` 校验后,如何把确认结果稳定写入 `spacetime-module.asset_object`。
|
||||
|
||||
这份文档需要把以下信息冻结到可以直接编码的级别:
|
||||
|
||||
1. 本地 SpacetimeDB server 口径
|
||||
2. 本地数据库名
|
||||
3. `spacetime-module` 内部 reducer / procedure 的职责分工
|
||||
4. `spacetime-client` 在当前阶段的最小实现方式
|
||||
5. `api-server` 的切换边界
|
||||
|
||||
## 2. 当前约束
|
||||
|
||||
已确认事实如下:
|
||||
|
||||
1. 阿里云 OSS 当前按私有 bucket 接入。
|
||||
2. `api-server` 当前已经完成:
|
||||
- `POST /api/assets/direct-upload-tickets`
|
||||
- `GET /api/assets/read-url`
|
||||
- `POST /api/assets/objects/confirm`
|
||||
3. `platform-oss` 已具备:
|
||||
- `PostObject` 直传签名
|
||||
- 私有 `GET` 签名 URL
|
||||
- 私有 `HEAD Object` 探测
|
||||
4. `spacetime-module` 当前已具备:
|
||||
- `asset_object` 首版表骨架
|
||||
- `bucket + object_key` 双列定位索引
|
||||
5. 当前 `module-assets` 的对象确认仍然写入进程内 store,不是正式数据库真相。
|
||||
|
||||
## 3. 当前阶段的职责拆分
|
||||
|
||||
### 3.1 Axum 负责的部分
|
||||
|
||||
`api-server` 当前阶段继续负责以下职责:
|
||||
|
||||
1. 接收 HTTP 请求
|
||||
2. 校验请求体字段
|
||||
3. 校验 `bucket` 与服务端配置的一致性
|
||||
4. 调用 OSS `HEAD Object`
|
||||
5. 组装“已确认对象元数据”
|
||||
6. 调用 SpacetimeDB 持久化入口
|
||||
7. 把持久化结果转换成当前 HTTP 响应 contract
|
||||
|
||||
### 3.2 SpacetimeDB 负责的部分
|
||||
|
||||
`spacetime-module` 当前阶段只负责以下纯状态职责:
|
||||
|
||||
1. 依据 `bucket + object_key` 查重
|
||||
2. 已存在则复用原 `asset_object_id` 与 `created_at`
|
||||
3. 已存在则更新 `updated_at` 与最新元数据
|
||||
4. 不存在则插入新对象行
|
||||
5. 返回持久化后的对象记录
|
||||
|
||||
### 3.3 不允许的职责漂移
|
||||
|
||||
当前阶段明确不允许:
|
||||
|
||||
1. 在 reducer / procedure 内直接访问 OSS
|
||||
2. 在 reducer / procedure 内直接访问 HTTP 请求头、Cookie 或 Axum context
|
||||
3. 在 `api-server` 内重新实现第二套 `asset_object` 去重规则
|
||||
4. 通过 CLI 文本解析做正式持久化主链
|
||||
|
||||
## 4. 本地开发口径冻结
|
||||
|
||||
### 4.1 本地 server 地址
|
||||
|
||||
从当前版本开始,`server-rs/scripts/spacetime-dev.ps1` 与 `server-rs/scripts/spacetime-dev.sh` 的默认监听口径统一为:
|
||||
|
||||
1. `127.0.0.1:3000`
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. `spacetime` CLI 的默认 `local` server 昵称当前指向 `http://127.0.0.1:3000`
|
||||
2. 若脚本默认改到 `3001`,则 `publish / call / generate` 与本地调试口径会长期错位
|
||||
3. `api-server` 默认占用 `3000` 仅限当前进程,不影响 SpacetimeDB 独立开发脚本通过单独终端启动
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 若需要与本地 Axum 同时运行,可显式传参改端口。
|
||||
2. 默认口径必须先回到 CLI 约定,避免文档、脚本和发布命令长期分叉。
|
||||
|
||||
### 4.2 本地数据库名
|
||||
|
||||
当前资产对象确认链路的本地数据库名固定为:
|
||||
|
||||
1. `genarrative-dev`
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. 名称满足 `spacetime publish` 的数据库命名规则
|
||||
2. 后续 auth / runtime / asset 的 schema 可以先统一聚合到同一开发数据库
|
||||
3. 当前仓库还没有按模块拆分多个独立数据库的明确方案
|
||||
|
||||
### 4.3 当前阶段的标准命令
|
||||
|
||||
本地开发标准命令固定如下:
|
||||
|
||||
```bash
|
||||
spacetime start --listen-addr 127.0.0.1:3000
|
||||
spacetime publish genarrative-dev --server local --yes --module-path server-rs/crates/spacetime-module
|
||||
spacetime generate --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module --include-private --yes
|
||||
```
|
||||
|
||||
## 5. 为什么当前阶段不用 CLI 文本解析
|
||||
|
||||
当前阶段不采用 `spacetime call / spacetime sql` 的正式主链方案,原因固定如下:
|
||||
|
||||
1. CLI 输出更适合人工调试,不适合作为稳定业务协议层
|
||||
2. `api-server` 若依赖命令行文本解析,会把错误处理、超时、返回值解析和平台兼容复杂度放大
|
||||
3. 当前 `asset_object` 是 private table,不适合继续叠加一层 CLI 查询拼装返回值
|
||||
|
||||
因此当前阶段改为:
|
||||
|
||||
1. 用 SpacetimeDB Rust SDK + codegen bindings 做正式调用
|
||||
2. 用 `procedure` 返回确认结果
|
||||
3. 用 `try_with_tx` 在 procedure 内完成原子 upsert
|
||||
|
||||
## 6. `spacetime-module` 的调用面设计
|
||||
|
||||
### 6.1 reducer
|
||||
|
||||
当前阶段保留一个内部 reducer:
|
||||
|
||||
1. `confirm_asset_object`
|
||||
|
||||
职责:
|
||||
|
||||
1. 承载真实 upsert 规则
|
||||
2. 便于后续被其他 reducer 或 scheduled logic 复用
|
||||
3. 明确 `asset_object` 的唯一写入规则在模块内收口
|
||||
|
||||
返回规则:
|
||||
|
||||
1. reducer 只返回 `Result<(), String>`
|
||||
2. 不直接返回对象记录
|
||||
|
||||
### 6.2 procedure
|
||||
|
||||
当前阶段新增一个对 Axum 友好的 procedure:
|
||||
|
||||
1. `confirm_asset_object_and_return`
|
||||
|
||||
职责:
|
||||
|
||||
1. 接收 Axum 已经确认好的对象元数据
|
||||
2. 在 `try_with_tx` 中调用共享 upsert 逻辑
|
||||
3. 返回持久化后的 `asset_object` DTO
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. `POST /api/assets/objects/confirm` 是同步确认接口,需要立即返回 `assetObjectId` 等字段
|
||||
2. reducer 本身不返回业务数据
|
||||
3. procedure 可以在不做外部 IO 的前提下返回 `SpacetimeType` 结果
|
||||
|
||||
## 7. `spacetime-client` 当前阶段设计
|
||||
|
||||
`spacetime-client` 当前阶段只实现一条最小链路:
|
||||
|
||||
1. 连接指定 SpacetimeDB server
|
||||
2. 等待 SDK `on_connect` 回调确认连接已经收到 `IdentityToken`
|
||||
3. 在 `on_connect` 后调用 `confirm_asset_object_and_return`
|
||||
4. 获取返回 DTO
|
||||
|
||||
实现约束固定如下:
|
||||
|
||||
1. 不允许在 `DbConnection::build()` 返回后立刻发 procedure,因为 build 只代表 WebSocket 初始化完成,不代表 SpacetimeDB 身份握手已经完成。
|
||||
2. procedure 调用、异步连接失败、断线和超时必须收口到同一个结果通道,避免 HTTP 请求在 SDK idle timeout 后才失败。
|
||||
3. 当前阶段每次 HTTP 确认请求可以建立一条短连接,待真实链路验证稳定后再评估连接池或长连接复用。
|
||||
|
||||
当前阶段不做:
|
||||
|
||||
1. 通用订阅框架
|
||||
2. 多数据库路由
|
||||
3. 通用 reducer / procedure 适配器
|
||||
4. 前端直连复用层
|
||||
|
||||
## 8. `api-server` 的切换规则
|
||||
|
||||
从当前版本开始,`POST /api/assets/objects/confirm` 的真实主链改为:
|
||||
|
||||
1. `api-server`
|
||||
2. `platform-oss HEAD Object`
|
||||
3. `spacetime-client`
|
||||
4. `spacetime-module.confirm_asset_object_and_return`
|
||||
5. `asset_object`
|
||||
|
||||
当前进程内 `AssetObjectService` 退化为:
|
||||
|
||||
1. 共享字段校验
|
||||
2. 共享 Axum 侧对象确认编排
|
||||
3. 本地无 SpacetimeDB 配置时的临时 fallback 不再作为默认主链
|
||||
|
||||
## 9. 环境变量口径
|
||||
|
||||
当前阶段新增以下环境变量:
|
||||
|
||||
1. `GENARRATIVE_SPACETIME_SERVER_URL`
|
||||
默认 `http://127.0.0.1:3000`
|
||||
2. `GENARRATIVE_SPACETIME_DATABASE`
|
||||
默认 `genarrative-dev`
|
||||
3. `GENARRATIVE_SPACETIME_TOKEN`
|
||||
可选;未配置时默认匿名连接
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 当前本地开发可以先匿名连接。
|
||||
2. 后续若要做 Axum -> SpacetimeDB 的身份透传,再单独冻结 JWT / OIDC token 的传递策略。
|
||||
|
||||
## 10. 当前阶段验收标准
|
||||
|
||||
当以下条件满足时,本方案视为落地完成:
|
||||
|
||||
1. `[x]` `spacetime-module` 新增 `confirm_asset_object` reducer 与 `confirm_asset_object_and_return` procedure
|
||||
2. `[x]` `spacetime-client` 能调用该 procedure 并拿到返回值
|
||||
3. `[x]` `api-server` 默认不再依赖进程内对象 store 作为正式真相
|
||||
4. `[x]` `server-rs/scripts/spacetime-dev.*` 默认端口回到 `3000`
|
||||
5. `[x]` 本地可以通过 publish + API 测试跑通真实 `asset_object` 写入
|
||||
|
||||
`2026-04-21` 已完成验收:
|
||||
|
||||
1. `cargo test -p api-server confirm_asset_object_live_roundtrip_persists_confirmed_record --manifest-path server-rs/Cargo.toml -- --ignored --nocapture --test-threads=1` 通过。
|
||||
2. `spacetime sql genarrative-dev --server local -y "SELECT asset_object_id, bucket, object_key, asset_kind, content_length FROM asset_object"` 可查到 `bucket = "xushi-dev"` 与 `generated-characters/confirm-live-test/.../master.txt` 的确认记录。
|
||||
|
||||
## 11. 一句话结论
|
||||
|
||||
当前阶段 `POST /api/assets/objects/confirm` 的正式主链应当是:
|
||||
|
||||
**Axum 完成 OSS 校验,再通过 Rust SDK 调用 SpacetimeDB procedure,在模块内部用事务 upsert `asset_object` 并把最终对象记录同步返回。**
|
||||
152
docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md
Normal file
152
docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# M6 OSS 服务端上传与 STS 接口收口设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 `M6` 中“上传与对象确认”剩余两项的工程落地口径:
|
||||
|
||||
1. `STS` 临时授权接口
|
||||
2. 服务端上传 helper
|
||||
|
||||
当前产品需求已经明确为:
|
||||
|
||||
1. 服务器向 AI 请求图片生成。
|
||||
2. 服务器把生成资源上传到 OSS。
|
||||
3. Web 端只负责访问和下载,不直接持有 OSS 写权限。
|
||||
|
||||
因此本轮不再把 STS 作为默认上传主链,而是把它实现为一个明确禁用的接口 contract,避免后续误把临时写凭证下发给浏览器。
|
||||
|
||||
## 2. 当前已具备能力
|
||||
|
||||
当前 `M6` 上传与对象确认链路已具备:
|
||||
|
||||
1. `POST /api/assets/direct-upload-tickets`
|
||||
2. OSS `PostObject` 浏览器直传签名
|
||||
3. OSS 私有 `GET` 短期签名读取 URL
|
||||
4. `GET /api/assets/read-url`
|
||||
5. OSS `HEAD Object` 校验
|
||||
6. `POST /api/assets/objects/confirm`
|
||||
7. `asset_object` 写入 SpacetimeDB
|
||||
8. `POST /api/assets/objects/bind`
|
||||
9. `asset_entity_binding` 业务实体槽位绑定
|
||||
|
||||
## 3. STS 临时授权接口口径
|
||||
|
||||
### 3.1 路径
|
||||
|
||||
`POST /api/assets/sts-upload-credentials`
|
||||
|
||||
### 3.2 当前阶段行为
|
||||
|
||||
当前阶段固定返回 `403`,不下发任何临时写凭证。
|
||||
|
||||
响应错误详情固定包含:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `provider` | 固定为 `aliyun-sts` |
|
||||
| `enabled` | 固定为 `false` |
|
||||
| `reason` | 当前不开放浏览器 OSS 写权限的中文原因 |
|
||||
|
||||
### 3.3 为什么不是 501
|
||||
|
||||
这里不用 `501 Not Implemented`,因为接口 contract 已实现;未开放的是能力开关,而不是路由缺失。
|
||||
|
||||
使用 `403` 的含义是:
|
||||
|
||||
1. 调用方不应尝试从 Web 端申请 OSS 写权限。
|
||||
2. 当前上传主链应走服务端上传或既有 `PostObject` 直传票据。
|
||||
3. 后续若确实要开放 STS,需要先补专门的权限边界设计。
|
||||
|
||||
## 4. 服务端上传 helper 口径
|
||||
|
||||
### 4.1 所属 crate
|
||||
|
||||
服务端上传能力落在:
|
||||
|
||||
`server-rs/crates/platform-oss`
|
||||
|
||||
原因:
|
||||
|
||||
1. OSS 签名、元数据、对象键归一化都属于平台适配能力。
|
||||
2. `api-server`、AI worker、后续资产任务系统都应复用同一套 helper。
|
||||
3. 不允许在各业务模块里重复实现 OSS 签名。
|
||||
|
||||
### 4.2 输入字段
|
||||
|
||||
服务端上传 helper 使用 `OssPutObjectRequest`,字段固定为:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `prefix` | `LegacyAssetPrefix` | 是 | 兼容旧 `/generated-*` 的对象前缀 |
|
||||
| `path_segments` | `Vec<String>` | 是 | 对象键中间路径片段 |
|
||||
| `file_name` | `String` | 是 | 文件名,内部会做安全归一化 |
|
||||
| `content_type` | `Option<String>` | 否 | MIME,写入 `Content-Type` |
|
||||
| `access` | `OssObjectAccess` | 是 | 当前仅作为 contract 字段,不默认改变 bucket ACL |
|
||||
| `metadata` | `BTreeMap<String, String>` | 是 | 自动归一化为 `x-oss-meta-*` |
|
||||
| `body` | `Vec<u8>` | 是 | 要写入 OSS 的对象内容 |
|
||||
|
||||
### 4.3 输出字段
|
||||
|
||||
服务端上传 helper 返回 `OssPutObjectResponse`:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `provider` | 固定为 `aliyun-oss` |
|
||||
| `bucket` | 服务端配置的 bucket |
|
||||
| `endpoint` | 服务端配置的 endpoint |
|
||||
| `host` | bucket host |
|
||||
| `objectKey` | 正式对象键 |
|
||||
| `legacyPublicPath` | 兼容旧前端路径习惯的 `/generated-*` 路径 |
|
||||
| `contentType` | 归一化后的 MIME |
|
||||
| `contentLength` | 实际上传字节数 |
|
||||
| `access` | 请求传入的访问策略 contract |
|
||||
| `etag` | OSS 响应头中的 ETag,当前不强制等同内容 hash |
|
||||
| `lastModified` | OSS 响应头中的 Last-Modified |
|
||||
|
||||
## 5. 对象确认衔接
|
||||
|
||||
服务端上传 helper 只负责把二进制写入 OSS,不直接写 SpacetimeDB。
|
||||
|
||||
调用方在上传成功后必须继续执行对象确认:
|
||||
|
||||
1. 调 `POST /api/assets/objects/confirm`
|
||||
2. 或在 Axum 内部复用同等确认逻辑
|
||||
3. 确认链路必须继续通过 `HEAD Object` 读取 OSS 真值
|
||||
4. `asset_object` 仍以 `bucket + object_key` 为正式真相
|
||||
|
||||
这样可以避免:
|
||||
|
||||
1. helper 上传成功但资产对象未落库
|
||||
2. AI worker 绕过确认链路写出不完整记录
|
||||
3. 把 OSS 响应中的派生 URL 当成对象真相
|
||||
|
||||
## 6. 与 Web 端的边界
|
||||
|
||||
Web 端当前只允许:
|
||||
|
||||
1. 请求后端业务接口触发生成
|
||||
2. 通过 `GET /api/assets/read-url` 换取私有读签名 URL
|
||||
3. 使用签名 URL 展示或下载对象
|
||||
|
||||
Web 端当前不允许:
|
||||
|
||||
1. 持有长期 `AccessKeyId / AccessKeySecret`
|
||||
2. 持有 STS 写权限
|
||||
3. 直接把 `/generated-*` 当成私有 OSS 可匿名读取 URL
|
||||
|
||||
## 7. 当前阶段完成定义
|
||||
|
||||
当以下内容落地后,本轮“上传与对象确认”剩余部分视为完成:
|
||||
|
||||
1. `POST /api/assets/sts-upload-credentials` 返回明确禁用 contract。
|
||||
2. `platform-oss` 具备服务端 `PUT Object` helper。
|
||||
3. 服务端上传 helper 复用现有对象键、元数据、签名与错误规范。
|
||||
4. 文档与 `M6` 任务清单同步更新。
|
||||
|
||||
## 8. 关联文档
|
||||
|
||||
1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
|
||||
2. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
|
||||
3. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
152
docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md
Normal file
152
docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Axum 手机验证码登录最小闭环设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 Rust `api-server + module-auth` 当前阶段手机号验证码登录的最小实现边界,避免在接入真实阿里云短信 adapter 前,把接口 contract、mock 行为和后续 SpacetimeDB 数据模型混在一起。
|
||||
|
||||
## 2. 当前阶段范围
|
||||
|
||||
本阶段只落以下内容:
|
||||
|
||||
1. `POST /api/auth/phone/send-code`
|
||||
2. `POST /api/auth/phone/login`
|
||||
3. 进程内 mock 验证码存储与校验
|
||||
4. 登录成功后签发 access token 与 refresh cookie
|
||||
|
||||
本阶段明确不包含:
|
||||
|
||||
1. 真实阿里云短信发送
|
||||
2. 图形验证码
|
||||
3. 风控封禁
|
||||
4. 手机号换绑
|
||||
5. 微信补绑手机号
|
||||
|
||||
## 3. 固定 mock 规则
|
||||
|
||||
当前阶段统一使用以下 mock 约束:
|
||||
|
||||
1. 固定验证码:`123456`
|
||||
2. 验证码 TTL:`5` 分钟
|
||||
3. 冷却秒数:`60`
|
||||
4. 手机号必须按中国大陆手机号规则校验
|
||||
|
||||
## 4. 接口 contract
|
||||
|
||||
### 4.1 `POST /api/auth/phone/send-code`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "13800138000",
|
||||
"scene": "login"
|
||||
}
|
||||
```
|
||||
|
||||
当前允许的 `scene`:
|
||||
|
||||
1. `login`
|
||||
2. `bind_phone`
|
||||
3. `change_phone`
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"cooldownSeconds": 60,
|
||||
"expiresInSeconds": 300,
|
||||
"providerRequestId": null
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 `POST /api/auth/phone/login`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "13800138000",
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "access-token",
|
||||
"user": {
|
||||
"id": "user_00000001",
|
||||
"username": "phone_00000002",
|
||||
"displayName": "138****8000",
|
||||
"phoneNumberMasked": "138****8000",
|
||||
"loginMethod": "phone",
|
||||
"bindingStatus": "active",
|
||||
"wechatBound": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
同时必须写回 refresh cookie。
|
||||
|
||||
## 5. 用户创建与复用规则
|
||||
|
||||
1. 手机号首次登录时自动建号
|
||||
2. 同一手机号再次登录时复用同一系统用户
|
||||
3. access token 的 `provider` 固定为 `phone`
|
||||
4. 用户快照中的 `loginMethod` 固定为 `phone`
|
||||
|
||||
## 6. 错误语义
|
||||
|
||||
当前阶段统一约束为:
|
||||
|
||||
1. 手机号格式错误:`400`
|
||||
2. 场景非法:`400`
|
||||
3. 验证码不存在、错误或过期:`400`
|
||||
4. 服务内部存储或签发失败:`500`
|
||||
|
||||
## 7. crate 边界
|
||||
|
||||
### 7.1 `module-auth`
|
||||
|
||||
负责:
|
||||
|
||||
1. 手机号规范化
|
||||
2. mock 验证码写入与校验
|
||||
3. 手机号用户创建与复用
|
||||
|
||||
### 7.2 `api-server`
|
||||
|
||||
负责:
|
||||
|
||||
1. HTTP 请求解析
|
||||
2. 场景字符串映射
|
||||
3. 登录成功后创建会话、签发 JWT、写回 refresh cookie
|
||||
|
||||
### 7.3 `platform-auth`
|
||||
|
||||
负责:
|
||||
|
||||
1. 签发 access token
|
||||
2. 生成 refresh cookie
|
||||
|
||||
## 8. 测试要求
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. `send-code` 成功返回 mock 冷却与过期秒数
|
||||
2. 默认错误场景返回 `400`
|
||||
3. 首次手机号登录会自动建号并写 refresh cookie
|
||||
4. 同一手机号重复登录复用同一用户
|
||||
|
||||
## 9. 完成定义
|
||||
|
||||
满足以下条件时,本任务视为完成:
|
||||
|
||||
1. Rust 侧已提供 `send-code` 与 `phone/login`
|
||||
2. mock 验证码链路可跑通
|
||||
3. 手机号登录成功后能拿到 access token 与 refresh cookie
|
||||
4. 文档、任务清单与测试已同步更新
|
||||
@@ -0,0 +1,108 @@
|
||||
# Axum 手机验证码冷却与失败次数限制设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 Rust `api-server + module-auth` 当前阶段短信验证码的最小防刷边界,避免在真实阿里云短信 adapter、图形验证码和风控封禁尚未接入前,验证码可以被无限重复发送或无限试错。
|
||||
|
||||
## 2. 当前阶段范围
|
||||
|
||||
本阶段只落以下内容:
|
||||
|
||||
1. 同一手机号、同一业务场景的发送冷却。
|
||||
2. 同一验证码快照的错误次数限制。
|
||||
3. Axum 对冷却和错误次数耗尽返回 `429`。
|
||||
4. `module-auth` 与 `api-server` 回归测试。
|
||||
|
||||
本阶段明确不包含:
|
||||
|
||||
1. 真实短信供应商发送。
|
||||
2. IP / 设备维度频率限制。
|
||||
3. 图形验证码触发。
|
||||
4. `auth_risk_block` 风控封禁。
|
||||
5. SpacetimeDB 表写入。
|
||||
|
||||
## 3. 固定规则
|
||||
|
||||
当前阶段规则冻结为:
|
||||
|
||||
1. 验证码仍使用 mock 固定值:`123456`。
|
||||
2. 验证码有效期仍为 `5` 分钟。
|
||||
3. 发送冷却仍为 `60` 秒。
|
||||
4. 同一验证码最多允许错误 `5` 次。
|
||||
5. 错误次数达到上限后立即删除该验证码快照,用户必须重新获取验证码。
|
||||
|
||||
## 4. 发送冷却语义
|
||||
|
||||
冷却维度:
|
||||
|
||||
1. 归一化后的手机号。
|
||||
2. 业务场景:`login` / `bind_phone` / `change_phone`。
|
||||
|
||||
处理规则:
|
||||
|
||||
1. 若不存在有效验证码,允许发送并写入新快照。
|
||||
2. 若已有验证码已过期,删除旧快照后允许发送。
|
||||
3. 若已有验证码未过期,且距离上次发送不足 `60` 秒,返回 `429`。
|
||||
4. 若已有验证码未过期,但已经超过 `60` 秒,允许覆盖旧快照并重置错误次数。
|
||||
|
||||
## 5. 错误次数语义
|
||||
|
||||
处理规则:
|
||||
|
||||
1. 验证码格式非法仍直接返回 `400`,不计入某个手机号快照。
|
||||
2. 验证码不存在或已过期仍返回 `400`。
|
||||
3. 验证码错误时,给当前快照的失败次数加 `1`。
|
||||
4. 错误次数未达到上限时返回 `400`。
|
||||
5. 错误次数达到 `5` 次时删除验证码快照并返回 `429`。
|
||||
6. 正确验证码登录成功后删除验证码快照。
|
||||
|
||||
## 6. HTTP contract
|
||||
|
||||
### 6.1 冷却中重复发送
|
||||
|
||||
状态码:`429`
|
||||
|
||||
响应体沿用当前统一错误体:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"message": "验证码发送过于频繁,请稍后再试",
|
||||
"details": {
|
||||
"retryAfterSeconds": 59
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
同时写回 `Retry-After` 响应头。
|
||||
|
||||
### 6.2 验证码错误次数耗尽
|
||||
|
||||
状态码:`429`
|
||||
|
||||
响应体沿用当前统一错误体:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"message": "验证码错误次数过多,请重新获取验证码"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
满足以下条件时,本任务视为完成:
|
||||
|
||||
1. 同一手机号同一场景在 `60` 秒内重复发送会返回 `429`。
|
||||
2. 同一手机号不同场景互不影响。
|
||||
3. 同一验证码错误 `5` 次后会返回 `429` 并删除验证码。
|
||||
4. 重新获取验证码后可以继续正常登录。
|
||||
5. Rust 定向测试通过。
|
||||
@@ -0,0 +1,127 @@
|
||||
# 手机验证码登录阶段 A 落地设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结当前仓库“手机验证码登录首个可用切片”的实现边界,避免继续出现:
|
||||
|
||||
1. 后端短信登录接口已经存在,但默认前台仍被游客兜底抢走入口。
|
||||
2. 平台公开浏览请求和正式登录态互相污染,导致公开首页能把已登录态误判成失效。
|
||||
3. smoke 只覆盖旧密码/游客链路,没有真正验证手机号验证码登录主链。
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
截至 `2026-04-21`,仓库里已经存在以下能力:
|
||||
|
||||
1. `server-node/src/routes/authRoutes.ts`
|
||||
- 已暴露 `/api/auth/phone/send-code`
|
||||
- 已暴露 `/api/auth/phone/login`
|
||||
2. `server-node/src/auth/authService.ts`
|
||||
- 已实现发送验证码、验证码校验、账号创建/登录、审计、风控与 refresh session 写入
|
||||
3. `src/components/auth/LoginScreen.tsx`
|
||||
- 已有手机号输入、验证码输入、获取验证码按钮与登录按钮
|
||||
4. `src/components/auth/AuthGate.tsx`
|
||||
- 已能在受保护动作上拉起登录弹窗
|
||||
|
||||
问题不在“有没有代码”,而在“默认体验和回归验证是否真的把手机号登录当成正式入口”。
|
||||
|
||||
## 3. 本阶段目标
|
||||
|
||||
阶段 A 只收敛到以下结果:
|
||||
|
||||
1. 未登录用户进入平台时,默认走公开浏览,不再默认隐式创建游客账号。
|
||||
2. 点击受保护动作时,弹出手机号验证码登录弹窗。
|
||||
3. 获取验证码后,前台给出简洁明确的发送成功反馈与冷却态。
|
||||
4. 平台公开只读请求即使返回 `401`,也不能顺手清空当前 access token。
|
||||
5. `scripts/smoke-server-node.ts` 必须实际覆盖一次手机号验证码发送与登录。
|
||||
|
||||
## 4. 明确范围
|
||||
|
||||
### 4.1 本次必须落地
|
||||
|
||||
1. `AuthGate` 中开发游客兜底改为“显式开关才允许开启”。
|
||||
2. `LoginScreen` 增加验证码发送成功反馈。
|
||||
3. `apiClient` 对 `skipAuth + skipRefresh` 的公开请求做登录态隔离。
|
||||
4. smoke 脚本补齐手机号验证码登录主链。
|
||||
5. `.env.example` 默认值与实际代码语义一致。
|
||||
|
||||
### 4.2 本次不做
|
||||
|
||||
1. 微信登录正式联调
|
||||
2. 绑定手机号链路新增产品动作
|
||||
3. account center 二期能力扩展
|
||||
4. Rust / Axum 侧手机号登录迁移
|
||||
|
||||
## 5. 关键规则
|
||||
|
||||
## 5.1 开发游客兜底
|
||||
|
||||
当前规则冻结为:
|
||||
|
||||
1. 只有 `import.meta.env.DEV === true`
|
||||
2. 且 `VITE_AUTH_ALLOW_DEV_GUEST === "true"`
|
||||
|
||||
同时满足时,才允许自动创建/恢复游客账号。
|
||||
|
||||
这意味着:
|
||||
|
||||
1. 未配置时默认关闭
|
||||
2. `.env.example` 必须体现默认关闭
|
||||
3. 正式手机号验证码登录不会再被隐式游客入口抢走
|
||||
|
||||
## 5.2 公开请求与登录态隔离
|
||||
|
||||
`listCustomWorldGallery` / `getCustomWorldGalleryDetail` 这类公开只读请求继续固定:
|
||||
|
||||
1. `skipAuth: true`
|
||||
2. `skipRefresh: true`
|
||||
|
||||
并补一条实现约束:
|
||||
|
||||
1. 若请求本身没有携带 `Authorization`,即使服务端返回 `401`,也不能清空本地 access token
|
||||
|
||||
否则会出现:
|
||||
|
||||
1. 已登录用户浏览公开首页
|
||||
2. 某次公开接口异常返回 `401`
|
||||
3. 前端误清空 token
|
||||
4. 平台突然退回未登录态
|
||||
|
||||
这与公开浏览设计直接冲突。
|
||||
|
||||
## 5.3 smoke 最小覆盖
|
||||
|
||||
`scripts/smoke-server-node.ts` 必须至少验证:
|
||||
|
||||
1. `POST /api/auth/phone/send-code`
|
||||
2. `POST /api/auth/phone/login`
|
||||
3. `GET /api/auth/me`
|
||||
4. 已登录后访问一个受保护运行时接口
|
||||
5. `POST /api/auth/logout` 后旧 token 失效
|
||||
|
||||
说明:
|
||||
|
||||
1. smoke 配置固定使用 `smsAuth.provider = mock`
|
||||
2. 使用 mock 验证码 `123456`
|
||||
3. 这条 smoke 不依赖真实阿里云短信
|
||||
|
||||
## 6. 代码落点
|
||||
|
||||
本阶段最少覆盖:
|
||||
|
||||
1. `src/components/auth/AuthGate.tsx`
|
||||
2. `src/components/auth/LoginScreen.tsx`
|
||||
3. `src/services/apiClient.ts`
|
||||
4. `scripts/smoke-server-node.ts`
|
||||
5. `.env.example`
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
做到以下几点,本阶段视为完成:
|
||||
|
||||
1. 本地未显式开启游客开关时,平台不会自动进游客账号。
|
||||
2. 未登录点击受保护动作后,出现手机号验证码登录弹窗。
|
||||
3. 点击“获取验证码”后,弹窗出现发送成功提示与倒计时。
|
||||
4. 公开广场请求不会因为 `401` 顺手把已登录态清空。
|
||||
5. smoke 脚本已经包含手机号验证码登录主链。
|
||||
@@ -4,10 +4,15 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [PHONE_SMS_LOGIN_STAGE_A_IMPLEMENTATION_2026-04-21.md](./PHONE_SMS_LOGIN_STAGE_A_IMPLEMENTATION_2026-04-21.md):冻结手机号验证码登录第一阶段的真实落地边界,明确游客兜底默认关闭、公开请求不污染登录态,以及 smoke 必须覆盖短信登录主链。
|
||||
- [AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](./AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md):`/api/auth/login-options` 首版设计,冻结登录方式列表 contract、配置开关来源与返回顺序。
|
||||
- [AUTH_ME_QUERY_DESIGN_2026-04-21.md](./AUTH_ME_QUERY_DESIGN_2026-04-21.md):`/api/auth/me` 首版查询设计,冻结 Bearer JWT 衔接、`user + availableLoginMethods` 返回 contract,以及用户不存在时的 `401` 语义。
|
||||
- [AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](./AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md):`/api/auth/logout-all` 全端登出设计,冻结全部 refresh session 吊销、`token_version` 递增、清 cookie 语义与 Rust 首版接口边界。
|
||||
- [AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](./AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md):`/api/auth/sessions` 会话列表设计,冻结当前设备识别、多端登录字段映射、`clientLabel` 兼容策略与 Rust 首版接口边界。
|
||||
- [PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](./PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md):手机号验证码登录最小闭环设计,冻结 mock 验证码规则、`send-code` / `phone/login` contract 与 crate 边界。
|
||||
- [PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](./PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md):手机号验证码冷却与失败次数限制设计,冻结同手机号同场景发送冷却、错误次数耗尽、`429` 与 `Retry-After` contract。
|
||||
- [WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](./WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md):Rust `api-server` 微信登录实现设计,冻结微信 provider 接入、系统 JWT 签发边界、`wechat/start` / `wechat/callback` / `wechat/bind-phone` 闭环,以及与后续 `SpacetimeDB` claims 透传的关系。
|
||||
- [WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md](./WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md):微信登录从本地 mock 到真实微信开放平台联调的执行手册,覆盖环境变量、回调域名、代理头要求、验证步骤与常见失败排查。
|
||||
- [PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md](./PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md):密码登录与自动建号落地设计,冻结 `/api/auth/entry`、幂等兼容策略、模块边界以及与 JWT / refresh cookie 的衔接方式。
|
||||
- [MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md](./MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md):多端登录会话身份模型设计,冻结浏览器、小程序、微信内 H5 的客户端身份字段、请求头约定与展示名派生规则。
|
||||
- [PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md](./PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md):`platform-auth` refresh cookie 适配设计,冻结 cookie 配置结构、读取规则与 `api-server` 最小读取链路。
|
||||
@@ -22,7 +27,9 @@
|
||||
- [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md):`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。
|
||||
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md):`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。
|
||||
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
|
||||
- [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSS,Web 端只负责签名读下载。
|
||||
- [AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md](./AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md):冻结 `POST /api/assets/objects/confirm` 从 Axum 通过 Rust SDK 调用 `SpacetimeDB procedure` 的最小落地方案,明确本地 server、数据库名、procedure/reducer 分工与 `spacetime-client` 边界。
|
||||
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
|
||||
- [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。
|
||||
- [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。
|
||||
- [CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md](./CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md):世界草稿生成失败后等待页误显示为“卡在编译草稿卡”的根因拆解、主链与增强链路边界,以及本次修复策略。
|
||||
@@ -44,4 +51,4 @@
|
||||
## 使用建议
|
||||
|
||||
- 做实现选型时,优先看这一组。
|
||||
- 做阶段排期时,把这一组和 `docs/planning/`、`docs/prd/` 一起看,更容易判断先后顺序。
|
||||
- 做阶段排期时,把这一组和 `docs/planning/`、`docs/prd/` 一起看,更容易判断先后顺序。
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# SpacetimeDB 资产对象存储设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结当前 `assets / OSS` 前置落地后的两个关键口径:
|
||||
|
||||
1. 阿里云 OSS 当前按**私有 bucket** 模式接入。
|
||||
2. 后续 `SpacetimeDB` 的资产对象引用统一按 `bucket` 与 `object_key` **两列存储**,而不是拼成单个路径字符串。
|
||||
|
||||
这份文档直接服务于以下工程收口动作:
|
||||
|
||||
1. `platform-oss` 的输出 contract 收敛。
|
||||
2. `api-server` 的直传票据接口收敛。
|
||||
3. 后续 `asset_object`、`asset_manifest`、业务绑定表的字段设计收敛。
|
||||
|
||||
## 2. 当前已确认事实
|
||||
|
||||
### 2.1 Bucket 访问策略
|
||||
|
||||
当前阿里云 OSS bucket:
|
||||
|
||||
- `bucket`:`xushi-dev`
|
||||
- `endpoint`:`oss-cn-beijing.aliyuncs.com`
|
||||
|
||||
已确认:
|
||||
|
||||
1. bucket 当前为**私有读写**。
|
||||
2. 服务端可通过 `AccessKeyId / AccessKeySecret` 正常访问 bucket。
|
||||
3. 浏览器可通过服务端签发的 `PostObject` 票据完成上传。
|
||||
4. 未签名的对象公开 URL 返回 `403`,不能当成正式读取链路。
|
||||
|
||||
### 2.2 当前工程能力状态
|
||||
|
||||
当前 `server-rs` 已落地:
|
||||
|
||||
1. `platform-oss`:浏览器 `PostObject` 直传签名
|
||||
2. `api-server`:`POST /api/assets/direct-upload-tickets`
|
||||
3. `platform-oss`:私有对象短期签名读取 URL
|
||||
4. `api-server`:`GET /api/assets/read-url`
|
||||
5. `server-rs/scripts/oss-smoke.ps1`:真实 OSS 联调脚本
|
||||
6. `platform-oss`:私有 `HEAD Object` 探测
|
||||
7. `api-server`:`POST /api/assets/objects/confirm`
|
||||
8. `module-assets`:进程内 `asset_object` 确认服务
|
||||
9. `platform-oss`:服务端 `PutObject` 上传 helper
|
||||
10. `api-server`:`POST /api/assets/sts-upload-credentials` 禁用式 contract
|
||||
|
||||
当前仍未落地:
|
||||
|
||||
1. `STS` 真实临时授权下发
|
||||
2. multipart 分片上传
|
||||
3. 内容 hash 自动计算与版本字段细化
|
||||
|
||||
当前上传完成确认接口的详细请求、校验顺序与写入规则见:
|
||||
|
||||
1. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
|
||||
|
||||
服务端上传与 STS 禁用式 contract 的详细设计见:
|
||||
|
||||
1. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md)
|
||||
|
||||
## 3. 正式存储口径
|
||||
|
||||
### 3.1 SpacetimeDB 中的对象引用格式
|
||||
|
||||
后续 `SpacetimeDB` 中凡是需要引用 OSS 对象的地方,统一按以下两列存储:
|
||||
|
||||
1. `bucket`
|
||||
2. `object_key`
|
||||
|
||||
不采用以下做法作为正式真相:
|
||||
|
||||
1. 不存完整公开 URL
|
||||
2. 不存 `bucket/object_key` 拼接后的单字符串
|
||||
3. 不存依赖 bucket 权限假设的匿名可读 URL
|
||||
|
||||
### 3.2 为什么必须拆成两列
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. bucket 后续可能按环境、业务线或冷热分层拆桶。
|
||||
2. `object_key` 本身才是对象路径主键;`bucket` 是对象所属仓。
|
||||
3. 后续签名下载 URL、服务端代理读取、对象迁移都需要分别拿到 `bucket` 与 `object_key`。
|
||||
4. 若只存拼接字符串,后续查询、迁移、批量回填和索引都更差。
|
||||
|
||||
## 4. 推荐表字段
|
||||
|
||||
### 4.1 `asset_object`
|
||||
|
||||
后续 `asset_object` 至少应包含:
|
||||
|
||||
1. `id`
|
||||
2. `bucket`
|
||||
3. `object_key`
|
||||
4. `access_policy`
|
||||
5. `content_type`
|
||||
6. `content_length`
|
||||
7. `content_hash`
|
||||
8. `version`
|
||||
9. `source_job_id`
|
||||
10. `owner_user_id`
|
||||
11. `profile_id`
|
||||
12. `entity_id`
|
||||
13. `asset_kind`
|
||||
14. `created_at`
|
||||
15. `updated_at`
|
||||
|
||||
当前已落到可编码级别的字段类型、索引和访问级别见:
|
||||
|
||||
1. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md)
|
||||
|
||||
### 4.2 业务资产表
|
||||
|
||||
如:
|
||||
|
||||
1. `character_visual_asset`
|
||||
2. `character_animation_asset`
|
||||
3. `scene_image_asset`
|
||||
4. `sprite_sheet_asset`
|
||||
|
||||
这些表不直接重复存 URL,而是引用:
|
||||
|
||||
1. `asset_object_id`
|
||||
2. 或明确的 `bucket + object_key`
|
||||
|
||||
优先建议:
|
||||
|
||||
1. 强业务关系表引用 `asset_object_id`
|
||||
2. `asset_object` 再统一承载 `bucket + object_key`
|
||||
|
||||
## 5. API 输出口径
|
||||
|
||||
### 5.1 直传票据接口
|
||||
|
||||
`POST /api/assets/direct-upload-tickets` 的正式输出应以以下字段为核心:
|
||||
|
||||
1. `bucket`
|
||||
2. `objectKey`
|
||||
3. `legacyPublicPath`
|
||||
4. `access`
|
||||
5. `formFields`
|
||||
6. `expiresAt`
|
||||
|
||||
补充说明:
|
||||
|
||||
1. `publicUrl` 在私有 bucket 模式下不是正式读取真相。
|
||||
2. 若当前为了调试保留 `publicUrl`,也只能视为“候选直连 URL”,不能代表对象一定可匿名读取。
|
||||
|
||||
### 5.2 私有读取链路
|
||||
|
||||
后续正式读取私有对象时,应采用以下方案之一:
|
||||
|
||||
1. `api-server` 输出短期签名 URL
|
||||
2. `api-server` 做下载代理
|
||||
3. CDN 私有回源 + 服务端签名
|
||||
|
||||
当前仓库已实现第一种最小闭环:
|
||||
|
||||
1. 服务端落库保存 `bucket + object_key`
|
||||
2. Web 端请求 `GET /api/assets/read-url`
|
||||
3. `api-server` 返回短期 `signedUrl`
|
||||
4. 浏览器再使用该 `signedUrl` 执行展示或下载
|
||||
|
||||
当前阶段不允许:
|
||||
|
||||
1. 把长期 `AccessKeyId / AccessKeySecret` 下发给客户端
|
||||
2. 让浏览器直接持有长期主凭证读取私有对象
|
||||
|
||||
## 6. `platform-oss` 的实现约束
|
||||
|
||||
从当前版本开始,`platform-oss` 需要遵守以下约束:
|
||||
|
||||
1. 默认按私有对象思维设计,不假设对象可匿名读取。
|
||||
2. `PostObject` 直传输出必须显式带 `bucket` 与 `object_key`。
|
||||
3. `publicUrl` 若存在,只能作为派生信息,不能替代 `bucket/object_key`。
|
||||
4. 后续新增签名下载能力时,输入必须是 `bucket + object_key`。
|
||||
|
||||
## 7. `SpacetimeDB` 表设计约束
|
||||
|
||||
结合 `SpacetimeDB` 的 schema 演进约束,当前先冻结:
|
||||
|
||||
1. 资产对象表必须尽早固定 `bucket` 与 `object_key` 两列。
|
||||
2. 不要先落单列字符串,后续再拆列,这会放大 schema 迁移成本。
|
||||
3. 对象 URL、签名 URL、CDN URL 都属于派生读模型,不是主存储字段。
|
||||
|
||||
## 8. 一句话结论
|
||||
|
||||
当前 `assets / OSS` 的正式真相应当是:
|
||||
|
||||
**阿里云 OSS 用私有 bucket 持有二进制对象,`SpacetimeDB` 用 `bucket + object_key` 两列持有对象引用,任何 URL 都只能是运行时派生结果。**
|
||||
@@ -0,0 +1,175 @@
|
||||
# `asset_object` 表设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于把 `asset_object` 从“概念字段列表”推进到**可以直接编码**的级别。
|
||||
|
||||
当前要冻结的不只是字段名,还包括:
|
||||
|
||||
1. 表职责边界
|
||||
2. `bucket + object_key` 的正式主存储口径
|
||||
3. 首版字段类型
|
||||
4. 当前阶段必须先落下的索引
|
||||
5. 与 `api-server / platform-oss / 后续业务绑定表` 的边界
|
||||
|
||||
## 2. 当前已确认前提
|
||||
|
||||
目前与 `asset_object` 直接相关的事实已经确认如下:
|
||||
|
||||
1. 阿里云 OSS 当前按**私有 bucket** 模式接入。
|
||||
2. 浏览器上传通过 `POST /api/assets/direct-upload-tickets` 下发的 `PostObject` 票据完成。
|
||||
3. `platform-oss` 已输出:
|
||||
- `bucket`
|
||||
- `objectKey`
|
||||
- `legacyPublicPath`
|
||||
- `access`
|
||||
4. 匿名公开 URL 当前返回 `403`,不能作为正式读取真相。
|
||||
|
||||
因此 `asset_object` 必须承担的真实职责是:
|
||||
|
||||
1. 记录对象在 OSS 中的唯一定位
|
||||
2. 记录对象元数据
|
||||
3. 为后续业务绑定表提供稳定引用
|
||||
4. 不把 URL 当成正式主键
|
||||
|
||||
## 3. 表职责边界
|
||||
|
||||
### 3.1 `asset_object` 负责的内容
|
||||
|
||||
1. `asset_object_id`:对象主键
|
||||
2. `bucket`:对象所属 bucket
|
||||
3. `object_key`:bucket 内对象路径
|
||||
4. `access_policy`:对象访问策略
|
||||
5. `content_type`
|
||||
6. `content_length`
|
||||
7. `content_hash`
|
||||
8. `version`
|
||||
9. 来源任务、归属用户、profile、业务实体等轻量关联键
|
||||
10. `asset_kind`
|
||||
11. `created_at / updated_at`
|
||||
|
||||
### 3.2 `asset_object` 不负责的内容
|
||||
|
||||
1. 不负责大对象二进制本体
|
||||
2. 不负责生成任务完整编排状态
|
||||
3. 不负责直接暴露公共 URL
|
||||
4. 不负责角色、动作、场景、精灵表等强业务关系本身
|
||||
|
||||
### 3.3 与其他表的边界
|
||||
|
||||
1. `asset_job` 负责任务态,不替代对象真相。
|
||||
2. `asset_manifest` 负责对象集合或发布清单,不替代单对象行。
|
||||
3. `character_visual_asset / scene_image_asset / sprite_sheet_asset` 这类强业务关系表优先引用 `asset_object_id`。
|
||||
|
||||
## 4. 表访问级别
|
||||
|
||||
`asset_object` 当前固定为 **private table**。
|
||||
|
||||
原因:
|
||||
|
||||
1. bucket 当前是私有读写。
|
||||
2. 对象读取需要服务端签名 URL 或下载代理。
|
||||
3. 前端不应直接把 `asset_object` 当公开订阅表使用。
|
||||
|
||||
补充口径:
|
||||
|
||||
1. 当前阶段先不把 `asset_object` 做成 public。
|
||||
2. 未来若要给客户端读对象列表,优先通过 view 或 Axum facade 输出脱敏 DTO。
|
||||
|
||||
## 5. 首版字段设计
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `asset_object_id` | `String` | 是 | 主键,固定使用 `assetobj_` 前缀的稳定字符串 ID。 |
|
||||
| `bucket` | `String` | 是 | 正式对象仓名称。 |
|
||||
| `object_key` | `String` | 是 | bucket 内对象路径,不带前导 `/`。 |
|
||||
| `access_policy` | `AssetObjectAccessPolicy` | 是 | 当前先冻结为 `private` / `public_read`。 |
|
||||
| `content_type` | `Option<String>` | 否 | 真实对象 MIME。 |
|
||||
| `content_length` | `u64` | 是 | 已确认对象大小,单位字节。 |
|
||||
| `content_hash` | `Option<String>` | 否 | 内容摘要,当前先保留为空间,不预设算法强制值。 |
|
||||
| `version` | `u32` | 是 | 对象版本号,首版默认 `1`。 |
|
||||
| `source_job_id` | `Option<String>` | 否 | 来源任务 ID。 |
|
||||
| `owner_user_id` | `Option<String>` | 否 | 归属用户 ID。 |
|
||||
| `profile_id` | `Option<String>` | 否 | 归属 profile ID。 |
|
||||
| `entity_id` | `Option<String>` | 否 | 归属业务实体 ID。 |
|
||||
| `asset_kind` | `String` | 是 | 资产业务类型,例如 `character_visual`、`scene_image`。 |
|
||||
| `created_at` | `Timestamp` | 是 | 创建时间。 |
|
||||
| `updated_at` | `Timestamp` | 是 | 最近更新时间。 |
|
||||
|
||||
补充约束:
|
||||
|
||||
1. `bucket` 与 `object_key` 是正式对象定位真相。
|
||||
2. 允许存在 `asset_object_id`,但不允许回退成单列 `storage_path` 作为真相字段。
|
||||
3. `content_hash` 先允许为空,避免在上传确认链路未落地前把 schema 卡死。
|
||||
|
||||
## 6. 索引与查询约束
|
||||
|
||||
### 6.1 当前阶段必须先落的索引
|
||||
|
||||
1. `bucket + object_key` 组合 B-Tree 索引
|
||||
作用:按真实对象定位回查对象元数据
|
||||
2. `asset_kind` 单列索引
|
||||
作用:后续按业务类型聚合或清理对象
|
||||
|
||||
### 6.2 为什么先不加更多索引
|
||||
|
||||
当前阶段只先解决:
|
||||
|
||||
1. 正式对象定位
|
||||
2. 业务类型过滤
|
||||
|
||||
而以下能力尚未落地:
|
||||
|
||||
1. `asset_job`
|
||||
2. 上传完成确认 reducer
|
||||
3. 业务绑定 reducer
|
||||
|
||||
因此暂不提前为每个可空关联键堆索引,避免在真实访问模式还没固定前把 schema 复杂度拉高。
|
||||
|
||||
## 7. 写入约束
|
||||
|
||||
### 7.1 当前阶段允许的对象写入时机
|
||||
|
||||
后续真正写 `asset_object` 时,必须满足以下前提之一:
|
||||
|
||||
1. 浏览器直传成功,服务端确认对象存在
|
||||
2. 后台 worker 成功上传对象到 OSS
|
||||
3. 旧对象迁移脚本确认对象存在并完成元数据回填
|
||||
|
||||
### 7.2 当前阶段不允许的漂移
|
||||
|
||||
1. 只根据 `legacyPublicPath` 就写入对象真相
|
||||
2. 只存完整 URL,不拆 `bucket/object_key`
|
||||
3. 在 `asset_object` 里重复塞角色、场景、动作等业务专属冗余字段
|
||||
4. 先落 `bucket/object_key` 之外的单列字符串路径,再计划后续拆列
|
||||
|
||||
## 8. 与当前代码的对接关系
|
||||
|
||||
当前工程中的直接对接关系固定如下:
|
||||
|
||||
1. `platform-oss`
|
||||
- 负责生成 `bucket + object_key` 对象定位
|
||||
2. `api-server`
|
||||
- 负责输出直传票据与私有读签名 URL
|
||||
3. `module-assets`
|
||||
- 负责沉淀 `AssetObjectAccessPolicy` 与字段校验 helper
|
||||
4. `spacetime-module`
|
||||
- 负责聚合 `asset_object` 首版表骨架
|
||||
|
||||
## 9. 当前阶段完成定义
|
||||
|
||||
当以下条件满足时,`asset_object` 的首版设计与骨架视为完成:
|
||||
|
||||
1. `bucket + object_key` 已在表结构中固定为两列
|
||||
2. 表访问级别已固定为 private
|
||||
3. 字段和索引已具体到可直接编码
|
||||
4. `module-assets` 与 `spacetime-module` 已落真实 crate scaffold
|
||||
|
||||
## 10. 相关文档
|
||||
|
||||
1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
|
||||
2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
3. [../../../server-rs/crates/module-assets/README.md](../../../server-rs/crates/module-assets/README.md)
|
||||
4. [../../../server-rs/crates/spacetime-module/README.md](../../../server-rs/crates/spacetime-module/README.md)
|
||||
@@ -222,8 +222,9 @@
|
||||
|
||||
1. 先按 `provider_unionid` 查;没有再按 `provider_uid` 查
|
||||
2. 若 identity 已存在,则更新资料快照并写 `last_login_at`
|
||||
3. 若 identity 不存在,则创建一条新的 `wechat` identity
|
||||
4. 若是首次微信登录,还要同步创建 `pending_bind_phone` 的 `user_account`
|
||||
3. 若本次是按 `provider_unionid` 命中,但 `provider_uid` 已变化,则必须把新的 `provider_uid` 一并回写为最新主映射
|
||||
4. 若 identity 不存在,则创建一条新的 `wechat` identity
|
||||
5. 若是首次微信登录,还要同步创建 `pending_bind_phone` 的 `user_account`
|
||||
|
||||
### 9.4 `POST /api/auth/wechat/bind-phone`
|
||||
|
||||
|
||||
@@ -626,7 +626,8 @@ workflow-cache/{workflow_type}/{workflow_id}.json
|
||||
- `/generated-custom-world-scenes/*`
|
||||
- `/generated-custom-world-covers/*`
|
||||
- `/generated-qwen-sprites/*`
|
||||
9. `STS`、服务端上传 helper、对象确认与业务绑定仍在后续阶段补齐。
|
||||
9. 当前 `POST /api/assets/sts-upload-credentials` 已按“服务器上传、Web 只下载”的需求固定为禁用式 contract,不向浏览器下发 OSS 写权限。
|
||||
10. 当前 `platform-oss` 已提供服务端 `PutObject` 上传 helper,供后续 AI worker 上传生成资源后继续走对象确认链路。
|
||||
|
||||
## 11.3 元数据与标签
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Axum 微信登录接入设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于把 Rust `api-server` 当前阶段的微信登录落地边界固定到可编码级别,避免在接入 `SpacetimeDB` 前把“第三方 provider 身份”和“系统内登录态 JWT”混成一层。
|
||||
|
||||
本次设计固定以下结论:
|
||||
|
||||
1. 微信不能作为 `SpacetimeDB` 直连时直接消费的 OIDC provider。
|
||||
2. 微信返回的 `code -> access_token/openid/unionid` 只能先在 `Axum` 侧完成兑换。
|
||||
3. `Axum` 在拿到微信身份后,签发本系统自己的 **OIDC 兼容 JWT**。
|
||||
4. 前端、Axum、未来 SpacetimeDB 都统一消费这枚系统 JWT,而不是直接消费微信 token。
|
||||
|
||||
## 2. 核心边界
|
||||
|
||||
### 2.1 为什么不能直接把微信 token 给 SpacetimeDB
|
||||
|
||||
原因固定为:
|
||||
|
||||
1. 当前微信开放平台登录链路不是标准 OIDC `id_token` 语义。
|
||||
2. 常见返回物是 `code`、`access_token`、`openid`、`unionid`,不是可直接按 OIDC `iss/sub/aud` 校验的标准 JWT。
|
||||
3. `SpacetimeDB` 当前直接接受的是 OIDC 兼容 JWT,而不是微信的 provider 原始返回。
|
||||
|
||||
因此正式口径固定为:
|
||||
|
||||
- 微信只负责提供三方身份。
|
||||
- 系统内部登录态统一由 `Axum` 签发。
|
||||
- 未来接入 `SpacetimeDB` 时,传给 `.withToken(...)` 的仍然是本系统 JWT。
|
||||
|
||||
### 2.2 与 SpacetimeDB 的关系
|
||||
|
||||
本阶段虽然前端仍然不直连 `SpacetimeDB`,但 JWT 语义必须从现在就和后续保持一致:
|
||||
|
||||
1. `iss` 固定为本系统网关发行者。
|
||||
2. `sub` 固定为系统内稳定用户 ID,不允许使用手机号、`openid`、`unionid`。
|
||||
3. `provider` 表示**当前会话登录来源**,允许为 `password`、`phone`、`wechat`。
|
||||
4. `binding_status` 表示当前账号是否仍处于 `pending_bind_phone`。
|
||||
|
||||
## 3. 登录主链
|
||||
|
||||
### 3.1 `GET /api/auth/wechat/start`
|
||||
|
||||
职责固定为:
|
||||
|
||||
1. 归一化 `redirectPath`
|
||||
2. 按 `User-Agent` 判断授权场景:
|
||||
- `desktop`
|
||||
- `wechat_in_app`
|
||||
3. 创建一次性 `state`
|
||||
4. 返回微信授权地址
|
||||
|
||||
关键约束:
|
||||
|
||||
1. 普通手机浏览器且非微信内打开时,不创建 state,直接报错。
|
||||
2. 每次点击微信登录都创建新 state,不复用旧 state。
|
||||
|
||||
### 3.2 `GET /api/auth/wechat/callback`
|
||||
|
||||
职责固定为:
|
||||
|
||||
1. 先消费一次性 `state`
|
||||
2. 再用 `code` 或 mock code 换取微信身份
|
||||
3. 先按 `unionid`,再按 `openid` 查系统内是否已有绑定账号
|
||||
4. 若没有账号,则创建 `pending_bind_phone` 的微信壳账号
|
||||
5. 若本次是按 `unionid` 命中,但微信回调带回了新的 `openid`,必须把新的 `openid -> user_id` 映射一并回写
|
||||
6. 签发系统 access token
|
||||
7. 创建 refresh session
|
||||
8. 以 hash 片段回跳前端:
|
||||
- `auth_provider=wechat`
|
||||
- `auth_token=...`
|
||||
- `auth_binding_status=active|pending_bind_phone`
|
||||
|
||||
### 3.3 `POST /api/auth/wechat/bind-phone`
|
||||
|
||||
职责固定为:
|
||||
|
||||
1. 仅允许当前 Bearer 用户为 `pending_bind_phone`
|
||||
2. 校验手机号验证码
|
||||
3. 若手机号已对应正式账号:
|
||||
- 把微信身份归并到该正式账号
|
||||
- 删除当前微信壳账号
|
||||
4. 若手机号尚未绑定:
|
||||
- 激活当前微信账号
|
||||
- 写入手机号与掩码
|
||||
5. 再次签发 access token 与 refresh session
|
||||
|
||||
补充约束:
|
||||
|
||||
1. 若绑定的是当前待绑定微信账号本体,则返回用户快照的 `loginMethod = wechat`。
|
||||
2. 若是并入已有手机号正式账号,则返回目标正式账号快照,当前实现会保持其账号主登录方式,例如 `loginMethod = phone`。
|
||||
3. 但 access token 中的 `provider` 仍按**本次登录来源**签发,不依赖账号主登录方式推断,因此微信绑定后的当前会话仍会签发 `provider = wechat`。
|
||||
|
||||
## 4. 当前最小实现策略
|
||||
|
||||
当前阶段为了先打通 Rust 后端闭环,采用以下最小实现:
|
||||
|
||||
1. `module-auth` 内使用进程内存仓模拟:
|
||||
- 用户
|
||||
- refresh session
|
||||
- 微信 identity
|
||||
- 微信 state
|
||||
- 手机验证码
|
||||
2. 短信验证码先走 mock:
|
||||
- 固定验证码 `123456`
|
||||
- TTL `5` 分钟
|
||||
- 冷却 `60` 秒
|
||||
3. 微信 provider 同时支持:
|
||||
- `mock`
|
||||
- `real`
|
||||
|
||||
这意味着:
|
||||
|
||||
1. 本轮目的是先把 Rust 认证主链补齐。
|
||||
2. 后续切到真实 `SpacetimeDB private table` 时,保留接口 contract,不改前端页面。
|
||||
|
||||
## 5. 当前接口影响面
|
||||
|
||||
本次接入会补齐这些 Rust 接口:
|
||||
|
||||
1. `GET /api/auth/login-options`
|
||||
2. `POST /api/auth/phone/send-code`
|
||||
3. `POST /api/auth/phone/login`
|
||||
4. `GET /api/auth/wechat/start`
|
||||
5. `GET /api/auth/wechat/callback`
|
||||
6. `POST /api/auth/wechat/bind-phone`
|
||||
|
||||
## 6. 环境变量
|
||||
|
||||
当前 Rust `api-server` 需要新增或明确这些变量:
|
||||
|
||||
1. `WECHAT_AUTH_ENABLED`
|
||||
2. `WECHAT_AUTH_PROVIDER`
|
||||
3. `WECHAT_APP_ID`
|
||||
4. `WECHAT_APP_SECRET`
|
||||
5. `WECHAT_CALLBACK_PATH`
|
||||
6. `WECHAT_REDIRECT_PATH`
|
||||
7. `WECHAT_AUTHORIZE_ENDPOINT`
|
||||
8. `WECHAT_ACCESS_TOKEN_ENDPOINT`
|
||||
9. `WECHAT_USER_INFO_ENDPOINT`
|
||||
10. `WECHAT_STATE_TTL_MINUTES`
|
||||
11. `WECHAT_MOCK_USER_ID`
|
||||
12. `WECHAT_MOCK_UNION_ID`
|
||||
13. `WECHAT_MOCK_DISPLAY_NAME`
|
||||
14. `WECHAT_MOCK_AVATAR_URL`
|
||||
|
||||
## 7. 与后续 SpacetimeDB 的衔接要求
|
||||
|
||||
未来把认证真相从内存仓切到 `SpacetimeDB` 时,必须继续保持:
|
||||
|
||||
1. `wechat_auth_state` 只做一次性 OAuth 状态,不存 provider token。
|
||||
2. `auth_identity` 负责 `openid/unionid -> user_id` 绑定。
|
||||
3. `refresh_session` 负责设备会话。
|
||||
4. `Axum` 负责签 JWT。
|
||||
5. `SpacetimeDB module` 只信系统 JWT claims,不直接解析微信回调结果。
|
||||
|
||||
## 8. 一句话结论
|
||||
|
||||
微信登录在本项目中的正确接法不是“把微信 token 直接接到 `SpacetimeDB`”,而是:
|
||||
|
||||
**微信只提供三方身份,Axum 负责完成 provider 兑换并签发系统 OIDC 兼容 JWT,再把这枚 JWT 用作前端与未来 SpacetimeDB 的统一登录态。**
|
||||
@@ -0,0 +1,317 @@
|
||||
# 微信登录真实联调手册
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于把当前仓库里的微信登录,从“代码已具备 mock / real 双模式”推进到“开发、测试、部署都能按步骤联调”的级别。
|
||||
|
||||
文档目标不是重复实现设计,而是回答以下落地问题:
|
||||
|
||||
1. 本地先怎么用 mock 跑通
|
||||
2. 什么时候切 `real`
|
||||
3. 真实微信开放平台需要配什么
|
||||
4. 回调地址到底怎么拼
|
||||
5. 前后端各自怎么验证
|
||||
6. 失败时先查哪一层
|
||||
|
||||
## 2. 当前实现结论
|
||||
|
||||
当前仓库里的微信登录实现固定为两段式:
|
||||
|
||||
1. 微信 OAuth 只负责返回第三方身份:`code -> openid / unionid`
|
||||
2. Rust `api-server` 负责把该第三方身份换成本系统 JWT
|
||||
|
||||
因此当前正式口径固定为:
|
||||
|
||||
1. 前端不直接消费微信 token
|
||||
2. `SpacetimeDB` 未来也不直接消费微信 token
|
||||
3. 统一由 `api-server` 签发系统 JWT,再回给前端和后续服务层
|
||||
|
||||
## 3. 模式说明
|
||||
|
||||
### 3.1 mock 模式
|
||||
|
||||
适用场景:
|
||||
|
||||
1. 本地前后端先验证交互链路
|
||||
2. 还没有微信开放平台配置
|
||||
3. 还没有可用公网回调域名
|
||||
|
||||
当前配置:
|
||||
|
||||
```env
|
||||
WECHAT_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_PROVIDER="mock"
|
||||
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
|
||||
WECHAT_REDIRECT_PATH="/"
|
||||
WECHAT_MOCK_USER_ID="wx-mock-user"
|
||||
WECHAT_MOCK_UNION_ID="wx-mock-union"
|
||||
WECHAT_MOCK_DISPLAY_NAME="微信旅人"
|
||||
WECHAT_MOCK_AVATAR_URL=""
|
||||
```
|
||||
|
||||
mock 模式行为固定为:
|
||||
|
||||
1. `GET /api/auth/wechat/start` 仍会创建一次性 `state`
|
||||
2. 返回的 `authorizationUrl` 不会跳去微信,而是直接指向本地 callback
|
||||
3. callback 会走 mock 身份:`mock_code + state`
|
||||
4. 后端仍会完整创建微信登录态、refresh session 与待绑定账号
|
||||
|
||||
### 3.2 real 模式
|
||||
|
||||
适用场景:
|
||||
|
||||
1. 已拿到微信开放平台应用
|
||||
2. 已有可访问的公网域名
|
||||
3. 已在微信开放平台后台配置回调域名
|
||||
|
||||
当前配置:
|
||||
|
||||
```env
|
||||
WECHAT_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_PROVIDER="real"
|
||||
WECHAT_APP_ID="你的微信开放平台 AppID"
|
||||
WECHAT_APP_SECRET="你的微信开放平台 AppSecret"
|
||||
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
|
||||
WECHAT_REDIRECT_PATH="/"
|
||||
```
|
||||
|
||||
real 模式行为固定为:
|
||||
|
||||
1. `GET /api/auth/wechat/start` 返回微信真实授权地址
|
||||
2. 用户在微信侧完成授权
|
||||
3. 微信回跳到 `WECHAT_CALLBACK_PATH`
|
||||
4. 后端用 `code` 换 `access_token/openid/unionid`
|
||||
5. 后端签发本系统 JWT,并把结果通过 URL hash 回跳给前端
|
||||
|
||||
## 4. 环境变量清单
|
||||
|
||||
当前真实联调至少需要这些变量:
|
||||
|
||||
| 变量 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `WECHAT_AUTH_ENABLED` | 是 | 是否启用微信登录 |
|
||||
| `WECHAT_AUTH_PROVIDER` | 是 | `mock` 或 `real` |
|
||||
| `WECHAT_APP_ID` | `real` 模式必填 | 微信开放平台应用 ID |
|
||||
| `WECHAT_APP_SECRET` | `real` 模式必填 | 微信开放平台应用 Secret |
|
||||
| `WECHAT_CALLBACK_PATH` | 是 | 回调路径,默认 `/api/auth/wechat/callback` |
|
||||
| `WECHAT_REDIRECT_PATH` | 是 | 登录完成后前端默认落点 |
|
||||
| `WECHAT_AUTHORIZE_ENDPOINT` | 否 | 默认桌面二维码授权地址 |
|
||||
| `WECHAT_ACCESS_TOKEN_ENDPOINT` | 否 | 默认 access_token 接口 |
|
||||
| `WECHAT_USER_INFO_ENDPOINT` | 否 | 默认用户信息接口 |
|
||||
| `WECHAT_STATE_TTL_MINUTES` | 否 | state 有效期,默认 `15` 分钟 |
|
||||
|
||||
补充说明:
|
||||
|
||||
1. `WECHAT_CALLBACK_PATH` 只是路径,不是完整 URL。
|
||||
2. 当前完整回调地址由后端在运行时按请求头拼接:
|
||||
- 优先 `x-forwarded-proto`
|
||||
- 其次 `host` / `x-forwarded-host`
|
||||
3. 因此部署到反向代理后,代理层必须正确透传 `host` 与 `x-forwarded-proto`。
|
||||
|
||||
## 5. 微信开放平台后台配置
|
||||
|
||||
真实联调前,需要先在微信开放平台完成以下配置:
|
||||
|
||||
1. 创建网站应用并拿到 `AppID / AppSecret`
|
||||
2. 配置网站授权回调域名
|
||||
3. 确保回调域名与实际访问域名一致
|
||||
|
||||
当前项目必须特别注意:
|
||||
|
||||
1. 微信后台通常配置的是“域名”,不是完整路径
|
||||
2. 但项目真正收到回调时会落到:
|
||||
|
||||
```text
|
||||
https://你的域名/api/auth/wechat/callback
|
||||
```
|
||||
|
||||
3. 如果代理层把 HTTPS 终止在上游网关,必须把 `x-forwarded-proto=https` 正确传给 `api-server`
|
||||
4. 否则后端可能生成 `http://.../api/auth/wechat/callback`,导致微信侧回调地址不匹配
|
||||
|
||||
## 6. 本地 mock 联调步骤
|
||||
|
||||
### 6.1 配置
|
||||
|
||||
在 `.env.local` 中写入:
|
||||
|
||||
```env
|
||||
SMS_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_PROVIDER="mock"
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||
```
|
||||
|
||||
### 6.2 启动
|
||||
|
||||
前端和后端分别启动当前项目自己的开发链路。
|
||||
|
||||
若只验证 Rust 后端,可直接启动 `server-rs` 的 `api-server`。
|
||||
|
||||
### 6.3 验证顺序
|
||||
|
||||
1. 请求 `GET /api/auth/login-options`
|
||||
2. 期望返回同时包含:
|
||||
- `phone`
|
||||
- `wechat`
|
||||
3. 前端点击“微信登录”
|
||||
4. 浏览器应先跳到 `/api/auth/wechat/start`
|
||||
5. 随后直接命中本地 `/api/auth/wechat/callback?...`
|
||||
6. 前端收到 hash:
|
||||
- `auth_provider=wechat`
|
||||
- `auth_token=...`
|
||||
- `auth_binding_status=pending_bind_phone`
|
||||
7. 页面进入“绑定手机号”界面
|
||||
8. 发送验证码并绑定手机号
|
||||
9. 若手机号未使用,当前账号激活
|
||||
10. 若手机号已对应正式账号,微信身份并入已有正式账号
|
||||
|
||||
## 7. 真实微信联调步骤
|
||||
|
||||
### 7.1 切换配置
|
||||
|
||||
在 `.env.local` 中切到:
|
||||
|
||||
```env
|
||||
SMS_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_ENABLED="true"
|
||||
WECHAT_AUTH_PROVIDER="real"
|
||||
WECHAT_APP_ID="你的 AppID"
|
||||
WECHAT_APP_SECRET="你的 AppSecret"
|
||||
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
|
||||
WECHAT_REDIRECT_PATH="/"
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||
```
|
||||
|
||||
### 7.2 部署要求
|
||||
|
||||
需要有一个用户浏览器能够访问的地址,例如:
|
||||
|
||||
```text
|
||||
https://game.example.com
|
||||
```
|
||||
|
||||
并且该地址最终把认证请求转发到当前 Rust `api-server`。
|
||||
|
||||
### 7.3 代理层要求
|
||||
|
||||
反向代理必须至少透传:
|
||||
|
||||
1. `Host`
|
||||
2. `X-Forwarded-Proto`
|
||||
3. `X-Forwarded-Host`(如果你的代理链路会改写 Host)
|
||||
|
||||
### 7.4 验证顺序
|
||||
|
||||
1. 浏览器访问前端页面
|
||||
2. 点击“微信登录”
|
||||
3. 观察 `/api/auth/wechat/start` 返回的 `authorizationUrl`
|
||||
4. 确认其中 `redirect_uri` 指向真实回调地址
|
||||
5. 在微信授权完成后,确认请求回到:
|
||||
|
||||
```text
|
||||
/api/auth/wechat/callback?code=...&state=...
|
||||
```
|
||||
|
||||
6. 观察回跳前端地址是否带上:
|
||||
- `auth_provider=wechat`
|
||||
- `auth_token=...`
|
||||
- `auth_binding_status=active|pending_bind_phone`
|
||||
7. 如果进入待绑定页面,继续完成手机号绑定
|
||||
8. 绑定后再请求 `GET /api/auth/me`
|
||||
9. 确认:
|
||||
- 当前用户存在
|
||||
- `wechatBound = true`
|
||||
- `bindingStatus` 已更新为目标状态
|
||||
|
||||
## 8. 账号命中规则
|
||||
|
||||
当前实现固定按以下顺序命中已有账号:
|
||||
|
||||
1. 先按 `unionid`
|
||||
2. 再按 `openid`
|
||||
3. 都没有命中时,创建 `pending_bind_phone` 微信壳账号
|
||||
|
||||
补充规则:
|
||||
|
||||
1. 若按 `unionid` 命中了已有微信身份,但本次微信回调带来了新的 `openid`,后端会把新的 `openid -> user_id` 映射补齐
|
||||
2. 若后续绑定手机号时发现该手机号已经属于正式账号,则会把微信身份并入这个正式账号
|
||||
|
||||
## 9. 前端验收点
|
||||
|
||||
前端联调时至少检查以下行为:
|
||||
|
||||
1. 登录弹窗只在 `login-options` 返回 `wechat` 时显示“微信登录”按钮
|
||||
2. 点击微信登录后应跳去后端返回的 `authorizationUrl`
|
||||
3. 回调 hash 被前端消费后,应把 `auth_token` 存入本地登录态
|
||||
4. 若 `auth_binding_status=pending_bind_phone`,页面必须进入绑定手机号界面
|
||||
5. 绑定成功后,应切回正常已登录状态
|
||||
|
||||
## 10. 后端验收点
|
||||
|
||||
当前后端至少应满足以下检查:
|
||||
|
||||
1. `GET /api/auth/wechat/start` 能返回授权地址
|
||||
2. `GET /api/auth/wechat/callback` 能创建系统会话并回跳
|
||||
3. `POST /api/auth/wechat/bind-phone` 能完成补绑
|
||||
4. `GET /api/auth/me` 能反映最新 `bindingStatus / wechatBound`
|
||||
5. access token claims 中的 `provider` 应保持当前会话来源为 `wechat`
|
||||
|
||||
## 11. 常见失败点
|
||||
|
||||
### 11.1 点微信登录后直接报“微信登录暂未启用”
|
||||
|
||||
先检查:
|
||||
|
||||
1. `WECHAT_AUTH_ENABLED` 是否为 `true`
|
||||
2. 运行的是否真是 Rust `api-server`
|
||||
3. 前端是否还在打旧 Node 服务
|
||||
|
||||
### 11.2 微信授权页能打开,但回调报错
|
||||
|
||||
先检查:
|
||||
|
||||
1. 微信后台配置的回调域名是否正确
|
||||
2. 代理层是否正确透传 `host` 与 `x-forwarded-proto`
|
||||
3. `WECHAT_CALLBACK_PATH` 是否与当前代码路径一致
|
||||
|
||||
### 11.3 按 `unionid` 命中后又出现新壳账号
|
||||
|
||||
先检查:
|
||||
|
||||
1. 微信开放平台当前应用是否真的能返回稳定 `unionid`
|
||||
2. 当前账号历史上是否只有 `openid` 没有 `unionid`
|
||||
3. 是否发生了不同开放平台主体之间的数据割裂
|
||||
|
||||
### 11.4 绑定手机号后命中了已有正式账号,但前端看到 `loginMethod=phone`
|
||||
|
||||
这是当前实现允许的结果。
|
||||
|
||||
原因是:
|
||||
|
||||
1. 返回的是目标正式账号快照
|
||||
2. 目标正式账号本身的主登录方式仍可能是 `phone`
|
||||
3. 但当前会话签发的 access token `provider` 仍然是 `wechat`
|
||||
|
||||
## 12. 当前自动化验证证据
|
||||
|
||||
当前仓库里已经有这些自动化验证:
|
||||
|
||||
1. `cargo test -p api-server`
|
||||
覆盖:
|
||||
- `wechat/start`
|
||||
- `wechat/callback`
|
||||
- `wechat/bind-phone`
|
||||
2. `cargo test -p module-auth`
|
||||
覆盖:
|
||||
- `unionid` 优先命中已有微信用户
|
||||
- 微信待绑定账号并入已有手机号正式账号
|
||||
3. `npm test -- --run src/services/authService.test.ts`
|
||||
覆盖:
|
||||
- 前端微信登录起跳
|
||||
- callback hash 消费
|
||||
|
||||
## 13. 一句话切换原则
|
||||
|
||||
本地先用 `mock` 跑通页面和会话闭环,公网域名与微信后台配置就绪后,再切 `real` 做真实 OAuth 联调。
|
||||
Reference in New Issue
Block a user