This commit is contained in:
2026-05-14 13:42:12 +08:00
112 changed files with 5766 additions and 626 deletions

View File

@@ -10,6 +10,7 @@
2. 当前设备识别方式与 `isCurrent` 语义
3. 多端登录识别字段如何从 `refresh_session` 派生到 DTO
4. Rust 首版在 Axum + 进程内 `module-auth` 下的最小实现边界
5. `2026-05-13` 会话组合并展示与远端踢下线闭环修复口径
## 2. 当前基线
@@ -46,11 +47,16 @@
3. 登录创建 session 时落库结构化客户端身份字段
4. 会话列表返回多端识别所需字段,并兼容旧 `clientLabel`
本阶段明确不包含
`2026-05-13` 起,本接口同时承担账号安全页的会话组读模型
1. `/api/auth/sessions/:sessionId/revoke`
2. 前端完整消费全部新增字段
3. SpacetimeDB reducer / view 正式读表
1. 后端按“同设备 + 同 IP”聚合活跃 `refresh_session`
2. 前端只消费后端聚合结果,不自行推断合并
3. `POST /api/auth/sessions/{sessionId}/revoke` 已纳入 Rust 实现,用于踢下线非当前会话
本阶段仍明确不包含:
1. SpacetimeDB reducer / view 正式读表
2. 登录方式、refresh token 轮换策略或账号安全页整体重设计
## 5. 请求与响应 contract
@@ -70,6 +76,8 @@
"sessions": [
{
"sessionId": "usess_xxx",
"sessionIds": ["usess_xxx", "usess_yyy"],
"sessionCount": 2,
"clientType": "web_browser",
"clientRuntime": "chrome",
"clientPlatform": "windows",
@@ -90,9 +98,12 @@
字段说明:
1. `clientLabel` 当前阶段继续兼容旧前端字段,值固定与 `deviceDisplayName` 保持一致
2. `clientRuntime``clientPlatform``deviceDisplayName` 是多端识别首版最小新增字段
3. 小程序来源额外暴露 `miniProgramAppId``miniProgramEnv`
1. `sessionId` 是聚合组代表会话 ID若组内包含当前 `sid`,代表 ID 必须使用当前会话 ID
2. `sessionIds` 是该聚合组内全部活跃 session ID前端批量踢下线时逐个调用 revoke
3. `sessionCount` 是聚合组内 session 数量
4. `clientLabel` 当前阶段继续兼容旧前端字段,值固定与 `deviceDisplayName` 保持一致
5. `clientRuntime``clientPlatform``deviceDisplayName` 是多端识别首版最小新增字段
6. 小程序来源额外暴露 `miniProgramAppId``miniProgramEnv`
### 5.3 失败响应
@@ -110,12 +121,25 @@
1. 从 refresh cookie 读取当前原始 refresh token
2. 在 Axum 侧计算 `sha256(refresh_token)`
3. 与会话列表中的 `refresh_token_hash` 比较
4. 命中则 `isCurrent = true`
4. 同时读取 Bearer access token claims 中的 `sid`
5. 聚合组内任意 session 命中当前 refresh hash 或当前 `sid`,则整组 `isCurrent = true`
说明:
1. 如果请求没有携带 refresh cookie本接口仍可返回会话列表
2. 此时全部会话的 `isCurrent` 都为 `false`
2. 此时仍可通过 Bearer `sid` 标记当前组
3. 当前组不允许在前端显示“踢下线”,当前设备退出必须走 `/api/auth/logout`
## 6.1 会话组合并规则
同设备同 IP 的 active refresh sessions 在后端合并为一条 DTO
1. 优先使用 `device_fingerprint + ip` 作为聚合 key
2.`device_fingerprint` 时退化为 `client_type + client_runtime + client_platform + device_display_name + user_agent + ip`
3. `createdAt` 取组内最早 `created_at`
4. `lastSeenAt` 取组内最新 `last_seen_at`
5. `expiresAt` 取组内最新 `expires_at`
6. `ipMasked` 仍只返回脱敏 IP
## 7. 多端标识派生规则
@@ -161,8 +185,21 @@
负责:
1. 读取 Bearer JWT 与 refresh cookie
2. 把活跃会话映射成旧接口兼容 DTO
3. 派生 `ipMasked``isCurrent`
2. 按同设备同 IP 聚合活跃会话
3. 把活跃会话组映射成旧接口兼容 DTO
4. 派生 `ipMasked``isCurrent`
5. 暴露 `POST /api/auth/sessions/{sessionId}/revoke`
## 8.3 指定会话吊销接口
`POST /api/auth/sessions/{sessionId}/revoke` 固定规则:
1. Bearer JWT 必填
2. 仅允许吊销当前用户自己的非当前会话
3. 当前会话自吊销返回业务错误,提示使用退出登录
4. 只撤销目标 `refresh_session`,不递增 `token_version`
5. 撤销后同步 auth store 到 SpacetimeDB
6. 认证中间件会校验 access token `sid` 对应 active `refresh_session`,因此被踢设备已有 access token 会立即失效
## 9. 测试策略
@@ -172,6 +209,9 @@
2. 微信内 H5 登录后,会话列表返回 `wechat_h5 + wechat_embedded_browser`
3. 显式小程序头优先于 `User-Agent` 判断
4. 请求携带当前 refresh cookie 时,只有当前会话 `isCurrent = true`
5. 同设备同 IP 会话会合并,并返回 `sessionIds/sessionCount`
6. 合并组包含当前 `sid` 或当前 refresh hash 时,整组 `isCurrent = true`
7. 指定远端会话吊销后,被踢设备 access token 立即无法通过认证
## 10. 完成定义
@@ -181,4 +221,6 @@
2. 会话列表可区分普通浏览器、微信内 H5、小程序来源
3. 同设备不同浏览器可在会话列表中清晰区分
4. `clientLabel` 与新增多端字段都已稳定返回
5. 文档、任务清单与测试已同步更新
5. 同设备同 IP 的重复 active refresh sessions 已合并展示
6. 非当前会话可通过真实 revoke 接口踢下线
7. 文档、任务清单与测试已同步更新

View File

@@ -40,12 +40,12 @@ HTTP status server error (503 Service Unavailable)
### 3.1 认证快照同步改为非阻断
`AppState::sync_auth_store_snapshot_to_spacetime` 保持导出本地快照、写入 SpacetimeDB、导入正式表的顺序但当远端写入或导入失败时只写 warn 日志并返回 `Ok(())`
`AppState::sync_auth_store_snapshot_to_spacetime` 保持导出本地认证快照,但运行期会直接调用 `import_auth_store_snapshot_json` 覆盖导入 SpacetimeDB 正式认证表,不再刷新 `auth_store_snapshot/default`;当远端导入失败时只写 warn 日志并返回 `Ok(())`
设计边界:
1. 当前认证请求的即时真相源是本地 `auth_store`
2. SpacetimeDB 认证快照用于跨进程恢复和正式表投影
2. SpacetimeDB 正式认证表用于跨进程恢复`auth_store_snapshot/default` 只保留为历史迁移和兜底恢复记录
3. 远端库挂起或网络异常只降级远端恢复能力,不回滚已经成功的登录、刷新、退出和资料更新。
### 3.2 Vite 补齐创作接口代理
@@ -98,7 +98,7 @@ npm run dev:web
1. `GET http://127.0.0.1:3000/api/auth/login-options` 返回 `["phone","password"]`
2. `GET http://127.0.0.1:3000/api/runtime/match3d/gallery` 返回 `{"items":[]}`,不再返回 SpacetimeDB 503。
3. 未登录请求 `POST http://127.0.0.1:3000/api/creation/match3d/sessions` 返回 `401`,说明同源请求已进入 Rust 鉴权层,不再被 Vite `404`
4. 隔离端口指向挂起的远端库并使用 mock 短信时,手机号验证码登录返回 `200` 和 token日志只记录“认证快照入 SpacetimeDB 失败,当前认证流程继续”。
4. 隔离端口指向挂起的远端库并使用 mock 短信时,手机号验证码登录返回 `200` 和 token日志只记录“认证快照入 SpacetimeDB 正式表失败,当前认证流程继续”。
## 6. 后续

View File

@@ -6,8 +6,9 @@
落地口径:
- `user_account``auth_identity``refresh_session` 作为 SpacetimeDB 中的正式认证持久化表。
- `auth_store_projection_meta` 只记录正式认证表最近一次由认证快照导入的时间,不保存用户快照内容。
- API 启动时优先从正式表导出兼容 `module-auth` 的认证快照,再恢复到内存认证服务。
- 运行期认证变更仍先复用现有 `module-auth` 逻辑生成一致快照,随后同步快照并导入正式表,保证正式表与快照一致
- 运行期认证变更仍先复用现有 `module-auth` 逻辑生成一致快照,随后调用 `import_auth_store_snapshot_json` 直接覆盖导入正式表;不再继续刷新 `auth_store_snapshot/default`
- 本阶段不重写登录、刷新、登出内部业务规则,避免在 JWT、refresh rotation、微信绑定合并等复杂语义中引入行为漂移。
## 2. 非目标
@@ -21,7 +22,7 @@
### 3.1 启动恢复
1. API 调用 `export_auth_store_snapshot_from_tables`
2. 若正式表已有用户、身份或会话数据,则返回兼容 `module-auth` 的 JSON 快照。
2. 若正式表已有用户、身份或会话数据,则返回兼容 `module-auth` 的 JSON 快照,并带上 `auth_store_projection_meta/default.updated_at`
3. API 用 `InMemoryAuthStore::from_snapshot_json` 恢复认证服务。
4. 若正式表为空或调用失败,则回退到 Stage 1 的 `auth_store_snapshot`
5. 若 Stage 1 也不可用,则回退本地 JSON 热修复文件。
@@ -29,9 +30,10 @@
### 3.2 运行期同步
1. 登录、刷新、登出等路径继续调用当前内存认证服务。
2. 每次认证状态变更后调用 `upsert_auth_store_snapshot`
3. 快照写入成功后调用 `import_auth_store_snapshot`,覆盖导入正式表
4. 导入失败时返回错误,避免用户误以为状态已经持久化
2. 每次认证状态变更后导出当前内存认证快照 JSON
3. API 调用 `import_auth_store_snapshot_json`,在同一 SpacetimeDB transaction 中清空并重建 `user_account/auth_identity/refresh_session`,同时更新 `auth_store_projection_meta/default.updated_at`
4. `upsert_auth_store_snapshot``import_auth_store_snapshot` 保留为旧库迁移入口,只服务 `auth_store_snapshot/default` 到正式认证表的历史导入,不作为运行期同步路径
5. 远端导入失败只记录 warn 并继续当前认证响应,避免远端库挂起时回滚已经成功的登录、刷新、退出和资料更新。
## 4. 数据重建规则

View File

@@ -23,7 +23,7 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot`
1. `POST /api/auth/refresh` 改写 `refresh_session` 表。
2. 登录成功写 `user_account/auth_identity/refresh_session`
3. `logout/logout-all/revoke-session` 改写细粒度表。
4. `auth_store_snapshot` 退化为迁移备份。
4. `auth_store_snapshot` 退化为迁移备份;运行期若仍复用内存认证快照,也应通过 `import_auth_store_snapshot_json` 直接导入正式认证表,不再刷新 `auth_store_snapshot/default`
## 3. 表设计落地口径
@@ -94,4 +94,3 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot`
2. Rust bindings 已刷新。
3. `spacetime-client` 暴露导入 procedure facade。
4. `api-server/spacetime-client/module-auth` 定向检查通过。

View File

@@ -94,6 +94,7 @@ API Server 新增统一 helper
| `auth_phone_login_success` | `POST /api/auth/phone/login` |
| `auth_me_view` | `GET /api/auth/me` |
| `auth_sessions_view` | `GET /api/auth/sessions` |
| `auth_revoke_session` | `POST /api/auth/sessions/{session_id}/revoke` |
| `auth_refresh_success` | `POST /api/auth/refresh` |
| `auth_logout` | `POST /api/auth/logout` |
| `auth_logout_all` | `POST /api/auth/logout-all` |

View File

@@ -9,30 +9,30 @@
1. `光点充值`
2. `会员卡充值`
前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。当前没有真实支付网关,本轮采用服务端模拟支付成功:创建订单后立即写入余额或会员状态,并返回最新账户中心快照。后续接入真实支付时,只替换订单支付状态推进,不改前端套餐与账户快照 contract
前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。普通 H5 / 本地联调继续使用 `mock` 渠道:创建订单后立即写入余额或会员状态,并返回最新账户中心快照。微信小程序 web-view 使用 `wechat_mp` 渠道:创建订单时只写入 `pending` 订单并返回小程序 `wx.requestPayment` 参数,真实到账以后端微信支付通知为准
## 2. 产品规则
### 2.1 光点充值套餐
| productId | 光点 | 金额分 | 徽标 | 说明 |
| --- | ---: | ---: | --- | --- |
| `points_60` | 60 | 600 | 首充双倍 | 首充送60光点 |
| `points_180` | 180 | 1800 | 首充双倍 | 首充送180光点 |
| `points_300` | 300 | 3000 | 首充双倍 | 首充送300光点 |
| `points_680` | 680 | 6800 | 首充双倍 | 首充送680光点 |
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280光点 |
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280光点 |
| productId | 光点 | 金额分 | 徽标 | 说明 |
| ------------- | ---: | -----: | -------- | -------------- |
| `points_60` | 60 | 600 | 首充双倍 | 首充送60光点 |
| `points_180` | 180 | 1800 | 首充双倍 | 首充送180光点 |
| `points_300` | 300 | 3000 | 首充双倍 | 首充送300光点 |
| `points_680` | 680 | 6800 | 首充双倍 | 首充送680光点 |
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280光点 |
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280光点 |
光点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账光点为基础光点与等额赠送光点之和;已有充值流水后只到账基础光点。实际到账光点写入交易流水,余额以 SpacetimeDB projection 为准。
### 2.2 会员卡套餐
| productId | 类型 | 天数 | 金额分 | 权益 |
| --- | --- | ---: | ---: | --- |
| `member_month` | 月卡 | 30 | 2800 | 免光点回合数100每日签到加成0% |
| `member_season` | 季卡 | 90 | 7800 | 免光点回合数100每日签到加成100% |
| `member_year` | 年卡 | 365 | 24800 | 免光点回合数100每日签到加成210% |
| productId | 类型 | 天数 | 金额分 | 权益 |
| --------------- | ---- | ---: | -----: | --------------------------------- |
| `member_month` | 月卡 | 30 | 2800 | 免光点回合数100每日签到加成0% |
| `member_season` | 季卡 | 90 | 7800 | 免光点回合数100每日签到加成100% |
| `member_year` | 年卡 | 365 | 24800 | 免光点回合数100每日签到加成210% |
购买会员时,如果当前会员仍有效,则从当前到期时间顺延;如果已过期或从未购买,则从当前服务端时间开始计算。状态只区分 `普通` 与已生效会员,前端不自行推断。
@@ -63,19 +63,58 @@
行为:
1. 校验 `productId`
2. 后端创建已支付订单
3. 光点套餐写入钱包余额与流水
4. 会员套餐写入会员状态
5. 返回最新账户中心快照与订单摘要
2. `paymentChannel = "mock"`后端创建已支付订单
3. `paymentChannel = "wechat_mp"` 时后端创建待支付订单,并调用微信支付 JSAPI 下单生成小程序支付参数
4. mock 光点套餐立即写入钱包余额与流水mock 会员套餐立即写入会员状态
5. wechat_mp 订单不提前发光点或会员,只返回待支付订单、账户中心快照与 `wechatMiniProgramPayParams`
兼容路径:`POST /api/runtime/profile/recharge/orders`
响应里的 `wechatMiniProgramPayParams` 只在微信小程序支付渠道返回,字段直接对应 `wx.requestPayment`
```json
{
"wechatMiniProgramPayParams": {
"timeStamp": "1777110165",
"nonceStr": "nonce",
"package": "prepay_id=wx201410272009395522657a690389285100",
"signType": "RSA",
"paySign": "..."
}
}
```
### 3.3 `POST /api/profile/recharge/wechat/notify`
微信支付通知地址,无需 Bearer JWT。行为
1. 真实渠道使用微信支付平台公钥和 `Wechatpay-*` 请求头验签。
2. 使用 `WECHAT_PAY_API_V3_KEY` 解密通知 `resource`
3. 仅当 `trade_state = "SUCCESS"` 时确认订单支付。
4. 使用微信通知里的 `out_trade_no` 查本地 `profile_recharge_order.order_id`,把订单从 `pending` 改为 `paid`
5. 将微信平台订单号写入 `provider_transaction_id`,用于对账、查单、退款和客服排障。
6. 在同一 SpacetimeDB procedure 内写入钱包流水或会员到期时间,确保重复通知幂等。
关键环境变量:
| 变量 | 说明 |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `WECHAT_PAY_ENABLED` | 是否启用微信支付客户端 |
| `WECHAT_PAY_PROVIDER` | `mock``real` |
| `WECHAT_PAY_MCH_ID` | 微信支付商户号 |
| `WECHAT_PAY_MERCHANT_SERIAL_NO` | 商户 API 证书序列号,用于请求微信支付签名头 |
| `WECHAT_PAY_PRIVATE_KEY_PEM` / `WECHAT_PAY_PRIVATE_KEY_PATH` | 商户 API 私钥 |
| `WECHAT_PAY_PLATFORM_PUBLIC_KEY_PEM` / `WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH` | 微信支付平台公钥或平台证书公钥,用于回调验签 |
| `WECHAT_PAY_PLATFORM_SERIAL_NO` | 微信支付通知头里的平台证书/公钥序列号 |
| `WECHAT_PAY_API_V3_KEY` | 32 字节 API v3 密钥,用于解密通知资源 |
| `WECHAT_PAY_NOTIFY_URL` | 公网 HTTPS 通知地址,通常为 `/api/profile/recharge/wechat/notify` |
## 4. 前端交互
1. “我的”页会员充值按钮打开独立弹窗,不在当前面板下方展开。
2. 弹窗顶部标题为 `账户充值`,右上角关闭。
3. 默认打开 `光点充值`,可切换到 `会员卡充值`
4. 点击套餐后调用下单接口,按钮进入处理中状态,成功后刷新 `profileDashboard`
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`
5. 弹窗内不写大段说明文案,只保留必要金额、光点、会员权益和状态反馈。
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。

View File

@@ -32,7 +32,8 @@
2. 请求字段:`currentPassword``newPassword`
3. 若账号未设置过密码,允许 `currentPassword` 为空并设置首个密码。
4. 若账号已有密码,必须校验 `currentPassword` 后才能写入 `newPassword`
5. 修改成功后递增用户 `token_version`,使旧 access token 失效;前端沿用当前 refresh 会话刷新登录态
5. 修改成功后递增用户 `token_version`,使旧 access token 失效。
6. `2026-05-13` 起,修改密码成功后必须撤销该用户全部 active `refresh_session`,并在响应中清除当前 refresh cookie前端清空本地 access token 并回到未登录态。用户需要使用新密码重新登录。
### 2.3 重置密码
@@ -79,7 +80,7 @@
## 5. 2026-05-12 快照同步修复
重置密码和修改密码都会改变认证真相:`password_hash``password_login_enabled``token_version`,重置密码还会立即创建新的 refresh session。因此 API 层在 `POST /api/auth/password/change``POST /api/auth/password/reset` 成功后,必须和密码登录、手机号登录、刷新、退出一样调用 `sync_auth_store_snapshot_to_spacetime()`
重置密码和修改密码都会改变认证真相:`password_hash``password_login_enabled``token_version`,重置密码还会立即创建新的 refresh session修改密码还会撤销全部旧 refresh session。因此 API 层在 `POST /api/auth/password/change``POST /api/auth/password/reset` 成功后,必须和密码登录、手机号登录、刷新、退出一样调用 `sync_auth_store_snapshot_to_spacetime()`
若只更新本地 `InMemoryAuthStore` 而不同步 SpacetimeDB 认证快照,`api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态,表现为用户已通过忘记密码重设成功,但再次密码登录仍返回“手机号或密码错误”。启动恢复时应从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照;当本地文件更新且远端表无更新时间戳时,优先使用本地文件并尝试回写 SpacetimeDB避免旧远端状态覆盖刚重设的密码。

View File

@@ -132,8 +132,8 @@ SpacetimeDB 公网路由默认保持收敛,只按实际前端 SDK 需要暴露
Nginx 配置文件分为两类:
- `deploy/nginx/genarrative.conf`:生产正式域名 HTTPS 配置,`genarrative.example.com` 只是占位域名,安装时必须替换为真实 `SERVER_NAME`,并要求 `/etc/letsencrypt/live/<SERVER_NAME>/fullchain.pem``privkey.pem` 已存在。
- `deploy/nginx/genarrative-dev-http.conf`:开发服无域名时的 HTTP-only 配置,只允许 `DEPLOY_TARGET=development` 使用。没有域名时,`SERVER_NAME` 填开发机 IP 或临时主机名。它仍复用同一套静态目录、后台 API 反代、临时主站 `/api/*` 反代和 SpacetimeDB SDK 最小公网路由,不恢复旧 `/generated-*` 或公网 `/healthz`
- `deploy/nginx/genarrative.conf`:生产正式域名 HTTPS 配置,`genarrative.example.com` 只是占位域名,安装时必须替换为真实 `SERVER_NAME`,并要求 `/etc/letsencrypt/live/<SERVER_NAME>/fullchain.pem``privkey.pem` 已存在。`SERVER_NAME` 只填证书主目录名对应的单个域名;`www` 等额外域名通过 `SERVER_ALIASES` 写入 Nginx `server_name`,不参与证书目录拼接。
- `deploy/nginx/genarrative-dev-http.conf`:开发服无域名时的 HTTP-only 配置,只允许 `DEPLOY_TARGET=development` 使用。没有域名时,`SERVER_NAME` 填开发机 IP 或临时主机名;如有多个入口,额外域名或 IP 填 `SERVER_ALIASES`。它仍复用同一套静态目录、后台 API 反代、临时主站 `/api/*` 反代和 SpacetimeDB SDK 最小公网路由,不恢复旧 `/generated-*` 或公网 `/healthz`
## 维护模式
@@ -272,13 +272,14 @@ journalctl -u 'jenkins-agent@*.service' -f
Jenkins controller 与 Linux agent 看到的 Git 服务地址不同,必须拆成两层配置:
- Jenkins Job 的 `Pipeline script from SCM` 由 controller 执行SCM URL 使用 controller 可访问的公网地址`http://82.157.175.59:3000/GenarrativeAI/Genarrative.git`
- Jenkinsfile 内部的源码、脚本 checkout 在 Linux agent 上执行,`GIT_REMOTE_URL` 使用 agent 本机可访问地址:`http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`
- Jenkins Job 的 `Pipeline script from SCM` 由 controller 执行SCM URL 使用 controller 可访问的公网域名`https://git.genarrative.world/GenarrativeAI/Genarrative.git`
- Jenkinsfile 内部的源码、脚本 checkout 在 Linux agent 上执行,`GIT_REMOTE_URL` 优先使用 agent 本机可访问地址:`http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`
-`127.0.0.1` Git 服务在当前 Linux agent 上不可达,发布、数据库和服务器配置类 Jenkinsfile 会用 `GIT_REMOTE_FALLBACK_URL=https://git.genarrative.world/GenarrativeAI/Genarrative.git` 重新 checkout。该首次 checkout 只拉 `SOURCE_BRANCH` 单分支、`depth=1` 且不拉 tags避免 release agent 通过公网备用地址拉取全仓库历史时被 Jenkins Git checkout timeout 杀掉;`scripts/jenkins-checkout-source.sh` 后续 fetch 也会按主地址、域名备用地址顺序重试,并在日志中输出最终使用的远端。
- 这里的 `3000` 是 Git/Web 服务端口,不是 SpacetimeDB 端口;生产 SpacetimeDB 固定使用 `http://127.0.0.1:3101`,避免流水线部署时与本机 Git 服务抢端口。
因此生产 Jenkinsfile 不使用 `checkout scm` 作为构建源码入口,而是显式 `checkout([$class: 'GitSCM', userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]], ...])`后续 `scripts/jenkins-checkout-source.sh` 会继续把 `origin` 设置为 `GIT_REMOTE_URL`,并按 `SOURCE_BRANCH` / `COMMIT_HASH` 拉取和校验目标提交。
因此生产 Jenkinsfile 不使用 `checkout scm` 作为构建源码入口,而是显式 `checkout([$class: 'GitSCM', userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/<SOURCE_BRANCH>:refs/remotes/origin/<SOURCE_BRANCH>"]], ...])`。首次 checkout 先尝试 `GIT_REMOTE_URL`,失败后尝试 `GIT_REMOTE_FALLBACK_URL`,两次都必须保持单分支浅克隆和 `noTags=true`后续 `scripts/jenkins-checkout-source.sh` 会继续把 `origin` 设置为实际可用远端,并按 `SOURCE_BRANCH` / `COMMIT_HASH` 拉取和校验目标提交。
`127.0.0.1` 只代表当前执行该阶段的 Linux agent 自身;如果 release agent 与 Git 服务不在同一台机器,必须把对应 Jenkinsfile 的 `GIT_REMOTE_URL` 改成 release agent 可访问的内网地址,不能让 release 发布阶段回退到 controller 公网拉取
`127.0.0.1` 只代表当前执行该阶段的 Linux agent 自身;如果 release agent 与 Git 服务不在同一台机器,`GIT_REMOTE_FALLBACK_URL` 统一使用 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`,不要再配置内网 IP 备用地址
### SSH PEM 凭证
@@ -323,7 +324,7 @@ Jenkins controller 与 Linux agent 看到的 Git 服务地址不同,必须拆
构建流水线运行在当前 Linux agent 的脱敏 label expression `linux && genarrative-build`。发布、导入导出和服务器配置流水线通过 `DEPLOY_TARGET` 映射到 Linux-only 脱敏部署表达式;其中 `development` 映射到当前 Linux 开发/构建/开发部署 agent 的 `linux && genarrative-build``release` 映射到独立 Linux 生产部署 agent 的 `linux && genarrative-release-deploy`,不能复用当前开发/构建/开发部署 agent。真实机器名、IP 和带 IP 的 Jenkins label 只允许留在 Jenkins 节点连接配置中,不能暴露为 Job 参数默认值、调度标签或文档推荐值。
发布流水线通过 Jenkins `copyArtifacts(...)` 从对应构建 Job 获取归档产物,因此 Jenkins 需要安装并启用 Copy Artifact 插件。Web 大包例外:`Genarrative-Web-Build` 只把轻量元数据归档到 Jenkins controller`web.tar.gz` 保留在 Linux 构建机稳定目录 `/var/cache/genarrative-build/web-artifacts/``Genarrative-Web-Deploy` 在部署目标机器上按构建 Job、构建号和版本号读取该目录。development 目标天然共享当前 Linux 开发/构建/开发部署机release 目标若不是同一台机器,必须先把该目录通过共享存储、rsync 或其它内网同步方式提供给 release 部署 agent。数据库导入流水线的手动上传模式使用 `stashedFile` 文件参数,因此 Jenkins 还需要安装并启用 File Parameter 插件。所有生产 Pipeline 日志必须带时间戳以便审计Jenkins 需要安装 Timestamper 插件,并在全局配置中启用 `Enabled for all Pipeline builds`。邮件通知流水线使用 Jenkins Pipeline `mail` stepJenkins 需要安装/启用 Mailer 能力,并在系统配置中配置 SMTP。生产发布不能退回到读取构建 workspace 本地目录的旧模式。
发布流水线通过 Jenkins `copyArtifacts(...)` 从对应构建 Job 获取归档产物,因此 Jenkins 需要安装并启用 Copy Artifact 插件。Web 大包例外:`Genarrative-Web-Build` 只把轻量元数据归档到 Jenkins controller`web.tar.gz` 保留在 Linux 构建机稳定目录 `/var/cache/genarrative-build/web-artifacts/``Genarrative-Web-Deploy` 在部署目标机器上按构建 Job、构建号和版本号读取该目录。development 目标天然共享当前 Linux 开发/构建/开发部署机release 目标若不是同一台机器,发布流水线默认在本地缓存缺少 `web.tar.gz` 时通过 `rsync` 从 SSH Host `genarrative-build-internal` 拉取同一路径内容。该 Host 必须配置在 release 服务器上 Jenkins 运行用户的 SSH config 中,真实内网 IP、用户和私钥路径只保存在服务器本机如需改名或指定 config可通过 `WEB_ARTIFACT_SYNC_HOST` / `WEB_ARTIFACT_SYNC_SSH_CONFIG` 参数覆盖。也可以提前通过共享存储或其它内网同步方式提供该目录。数据库导入流水线的手动上传模式使用 `stashedFile` 文件参数,因此 Jenkins 还需要安装并启用 File Parameter 插件。所有生产 Pipeline 日志必须带时间戳以便审计Jenkins 需要安装 Timestamper 插件,并在全局配置中启用 `Enabled for all Pipeline builds`。邮件通知流水线使用 Jenkins Pipeline `mail` stepJenkins 需要安装/启用 Mailer 能力,并在系统配置中配置 SMTP。生产发布不能退回到读取构建 workspace 本地目录的旧模式。
邮件通知的持久收件人不写入 Git由 Jenkins `Secret text` 凭据 `genarrative-notification-emails` 保存,凭据内容为逗号分隔邮箱。所有生产流水线仍提供 `NOTIFICATION_EMAILS` 参数作为本次运行的追加收件人;通知 Job 会把持久收件人凭据与本次 `NOTIFICATION_EMAILS` 合并去重后发送,参数留空时只发送给持久收件人。流水线结束时在 `post { always { ... } }` 中异步触发 `Genarrative-Notify-Email`,把来源 Job、构建号、构建 URL、结果、源码分支、源码 commit、发布版本、部署目标和数据库名传给通知 Job。通知 Job 失败不能反向改变业务流水线结果,只在来源流水线日志中记录触发失败。
@@ -424,8 +425,8 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
执行规则:
- 流水线先按 Jenkins SCM 配置 checkout 仓库,再执行 `git fetch --tags --prune origin "+refs/heads/<SOURCE_BRANCH>:refs/remotes/origin/<SOURCE_BRANCH>"`
- 如果工作区是浅克隆,流水线必须尝试 `git fetch --unshallow --tags`,确保能验证目标 commit 与分支关系
- 流水线先按 Jenkins SCM 配置 checkout 仓库,再执行单分支 `git fetch --no-tags --prune origin "+refs/heads/<SOURCE_BRANCH>:refs/remotes/origin/<SOURCE_BRANCH>"``COMMIT_HASH` 为空时追加 `--depth=1`
- 如果工作区是浅克隆,只有在 `COMMIT_HASH` 非空、需要验证指定提交属于目标分支时,流水线尝试 `git fetch --unshallow --no-tags``COMMIT_HASH` 为空时只需要目标分支 HEAD必须保持 `--depth=1 --no-tags`,避免普通发布或服务器配置任务拉取全仓库历史
- `COMMIT_HASH` 为空时detached checkout 到 `refs/remotes/origin/<SOURCE_BRANCH>` 当前最新 commit。
- `COMMIT_HASH` 非空时,先解析到完整 commit再用 `git merge-base --is-ancestor <commit> refs/remotes/origin/<SOURCE_BRANCH>` 校验该提交属于指定分支,校验通过后 detached checkout。
- 流水线日志必须输出最终 `SOURCE_BRANCH` 与实际 `SOURCE_COMMIT`
@@ -462,7 +463,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
该流水线属于高风险操作,默认要求人工确认后执行。
已落地的 Jenkinsfile 为 `jenkins/Jenkinsfile.production-server-provision`。该流水线默认 `DRY_RUN=true`只打印将执行的初始化动作真正写入系统用户、目录、systemd、环境文件并启动服务时必须设置 `DRY_RUN=false` 且勾选 `CONFIRM_PROVISION`。当 `DEPLOY_TARGET=release` 时,还必须勾选 `CONFIRM_RELEASE_DEPLOY_AGENT`,并通过 `linux && genarrative-release-deploy` 调度到独立 release 部署 agent。
首次真实初始化默认保持 `NGINX_CONFIG_MODE=none`先完成系统用户、目录、SpacetimeDB、systemd unit 与 `/etc/genarrative/api-server.env` 落盘。开发服没有域名时,使用 `DEPLOY_TARGET=development` + `NGINX_CONFIG_MODE=development-http` 安装 `deploy/nginx/genarrative-dev-http.conf`,并把 `SERVER_NAME` 填为开发机 IP 或临时主机名。等正式域名确定,并且目标机已经存在 `/etc/letsencrypt/live/<SERVER_NAME>/fullchain.pem``/etc/letsencrypt/live/<SERVER_NAME>/privkey.pem` 后,再把 `SERVER_NAME` 改成真实域名,并设置 `NGINX_CONFIG_MODE=production-https` 安装 Nginx HTTPS 配置。流水线会拒绝 release 目标安装 `development-http`,也会拒绝用占位域名或缺失证书安装 `production-https`。Nginx 配置写入后必须先 `nginx -t`,再 `nginx -s reload`,不能只验证配置而不重载当前进程。
首次真实初始化默认保持 `NGINX_CONFIG_MODE=none`先完成系统用户、目录、SpacetimeDB、systemd unit 与 `/etc/genarrative/api-server.env` 落盘。开发服没有域名时,使用 `DEPLOY_TARGET=development` + `NGINX_CONFIG_MODE=development-http` 安装 `deploy/nginx/genarrative-dev-http.conf`,并把 `SERVER_NAME` 填为开发机 IP 或临时主机名。等正式域名确定,并且目标机已经存在 `/etc/letsencrypt/live/<SERVER_NAME>/fullchain.pem``/etc/letsencrypt/live/<SERVER_NAME>/privkey.pem` 后,再把 `SERVER_NAME` 改成证书主域名,并设置 `NGINX_CONFIG_MODE=production-https` 安装 Nginx HTTPS 配置。如果同一张证书同时覆盖根域名和 `www` 域名,`SERVER_NAME` 仍只填证书目录名,例如 `genarrative.world``SERVER_ALIASES``www.genarrative.world`流水线会拒绝 release 目标安装 `development-http`,也会拒绝用占位域名或缺失证书安装 `production-https`。Nginx 配置写入后必须先 `nginx -t`,再 `nginx -s reload`,不能只验证配置而不重载当前进程。
若误用占位域名执行过真实初始化,失败通常发生在 `nginx -t`,错误表现为找不到 `/etc/letsencrypt/live/genarrative.example.com/fullchain.pem``privkey.pem`。新版初始化在 `NGINX_CONFIG_MODE=none` 时会检测并禁用上一轮留下的占位域名 Nginx 配置,避免它继续影响后续 `nginx -t`
@@ -482,6 +483,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
- 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 部署脚本源码,默认使用 `origin/master` 最新 commit上游构建触发时使用上游传入的实际构建 commit。
- 通过 Jenkins 归档获取 `web.tar.gz.sha256``release-manifest.json``web-artifact-pointer.txt`,再从 `/var/cache/genarrative-build/web-artifacts/<job>/<build>/<version>/` 读取 `web.tar.gz`;先校验 checksum再解压到 `/opt/genarrative/releases/<version>/web`
-`DEPLOY_TARGET=release` 且 release 服务器本地缓存缺少 `web.tar.gz` 时,默认先执行 `rsync -av --progress <WEB_ARTIFACT_SYNC_HOST>:/var/cache/genarrative-build/web-artifacts/<job>/<build>/<version>/ /var/cache/genarrative-build/web-artifacts/<job>/<build>/<version>/`,再继续校验 checksum默认 Host 为 `genarrative-build-internal`,由 release 服务器本机 SSH config 解析。
- 更新 `/opt/genarrative/current``/srv/genarrative/web` 指向。
- 执行 Nginx 配置测试和静态页面 smoke test。
- 不进入维护模式。

View File

@@ -175,7 +175,7 @@
- [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 首版接口边界。
- [AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](./AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)`/api/auth/sessions` 会话列表设计,冻结当前设备识别、多端登录字段映射、同设备同 IP 会话组合并、`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。
- [PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md](./PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md):冻结 Rust `api-server + module-auth + platform-auth` 接入真实阿里云短信 provider 的 crate 边界、发送与校验职责、配置项和错误语义。
@@ -207,7 +207,7 @@
- [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md)`M2` 第六张短信鉴权统计表 `sms_auth_event` 的事件范围、统计口径、索引与和风控/审计表的协作边界。
- [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md)`M2` 第五张风控状态表 `auth_risk_block` 的作用域、活跃态、刷新/解除规则与读取派生约束。
- [SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md)`M2` 第四张鉴权审计表 `auth_audit_log` 的事件范围、追加写规则、索引与对外 DTO 派生约束。
- [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换吊销语义、索引与迁移规则。
- [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换、logout fallback、指定会话吊销语义、索引与迁移规则。
- [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` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。

View File

@@ -1,6 +1,6 @@
# Rust API Server 路由索引2026-04-23
更新时间:`2026-05-01`
更新时间:`2026-05-13`
> 2026-04-29 补充本文件保留为迁移期路由快照。DDD G1 后续并行工作的契约冻结口径以 [`SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md) 为准,尤其是新增的 Big Fish、Puzzle、profile、runtime chat、story facade 和兼容路由删除计划。
>
@@ -20,7 +20,7 @@
2. 内部鉴权调试接口:`2` 条。
3. AI task 接口:`9` 条。
4. assets / OSS 接口:`15` 条。
5. auth 接口:`12` 条。
5. auth 接口:`13` 条。
6. custom world / agent 接口:`23` 条。
7. match3d creation / runtime 接口:`14` 条。
8. llm proxy 接口:`1` 条。
@@ -84,13 +84,14 @@
3. `POST /api/auth/logout`
4. `POST /api/auth/logout-all`
5. `GET /api/auth/sessions`
6. `POST /api/auth/refresh`
7. `POST /api/auth/phone/send-code`
8. `POST /api/auth/phone/login`
9. `GET /api/auth/wechat/start`
10. `GET /api/auth/wechat/callback`
11. `POST /api/auth/wechat/bind-phone`
12. `POST /api/auth/entry`
6. `POST /api/auth/sessions/{session_id}/revoke`
7. `POST /api/auth/refresh`
8. `POST /api/auth/phone/send-code`
9. `POST /api/auth/phone/login`
10. `GET /api/auth/wechat/start`
11. `GET /api/auth/wechat/callback`
12. `POST /api/auth/wechat/bind-phone`
13. `POST /api/auth/entry`
### 3.6 Custom World / Agent

View File

@@ -31,7 +31,7 @@ G1 单 owner 文件范围:
| 管理兑换码 | `POST /admin/api/profile/redeem-codes``POST /admin/api/profile/redeem-codes/disable` | 收敛 | 继续走 admin 路由DTO 归入 profile/runtime 管理命令组 | WP-RT、WP-API |
| 内部鉴权调试 | `GET /_internal/auth/claims``GET /_internal/auth/refresh-cookie` | 删除 | 只允许本地诊断脚本或 admin debug 能力使用,不作为前端契约 | WP-DEL |
| 鉴权公开查询 | `GET /api/auth/login-options``GET /api/auth/public-users/by-code/{code}``GET /api/auth/public-users/by-id/{user_id}` | 保留 | `AuthLoginOptionsResponse``PublicUserSearchResponse` | WP-A |
| 鉴权会话 | `GET /api/auth/me``GET /api/auth/sessions``POST /api/auth/refresh``POST /api/auth/logout``POST /api/auth/logout-all` | 保留 | `AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``LogoutResponse``LogoutAllResponse` | WP-A |
| 鉴权会话 | `GET /api/auth/me``GET /api/auth/sessions``POST /api/auth/sessions/{session_id}/revoke``POST /api/auth/refresh``POST /api/auth/logout``POST /api/auth/logout-all` | 保留 | `AuthMeResponse``AuthSessionsResponse``RevokeAuthSessionResponse``RefreshSessionResponse``LogoutResponse``LogoutAllResponse` | WP-A |
| 鉴权登录 | `POST /api/auth/phone/send-code``POST /api/auth/phone/login``GET /api/auth/wechat/start``GET /api/auth/wechat/callback``POST /api/auth/wechat/bind-phone``POST /api/auth/entry``POST /api/auth/password/change``POST /api/auth/password/reset` | 保留 | TS 命名统一使用 `Auth*` 前缀Rust 命名维持领域语义 | WP-A |
| 旧本地生成资产代理 | `GET /generated-character-drafts/{*path}``/generated-characters/{*path}``/generated-animations/{*path}``/generated-big-fish-assets/{*path}``/generated-puzzle-assets/{*path}``/generated-custom-world-scenes/{*path}``/generated-custom-world-covers/{*path}``/generated-qwen-sprites/{*path}` | 已删除 | 正式读取统一走 `GET /api/assets/read-url` 或 asset object projection`/generated-*` 仅允许作为 legacyPublicPath/object key 标识,不再作为可裸读路由 | WP-AS、WP-FE、WP-DEL |
| LLM 代理 | `POST /api/llm/chat/completions` | 收敛 | 仅作为平台能力代理;玩法 prompt 不允许由前端直接传入 | WP-PF、WP-API |
@@ -59,7 +59,7 @@ G1 单 owner 文件范围:
| --- | --- |
| `shared-contracts/src/api.rs` | `ApiResponseMeta``ApiErrorPayload``ApiSuccessEnvelope<T>``ApiErrorEnvelope` |
| `shared-contracts/src/admin.rs` | `AdminLoginRequest/Response``AdminSessionPayload``AdminMeResponse``AdminOverviewResponse``AdminDebugHttpRequest/Response` |
| `shared-contracts/src/auth.rs` | `AuthLoginOptionsResponse``AuthUserPayload``PublicUserSummaryPayload``PublicUserSearchResponse``PasswordEntry*``PasswordChange*``PasswordReset*``AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``Logout*``Phone*``Wechat*` |
| `shared-contracts/src/auth.rs` | `AuthLoginOptionsResponse``AuthUserPayload``PublicUserSummaryPayload``PublicUserSearchResponse``PasswordEntry*``PasswordChange*``PasswordReset*``AuthMeResponse``AuthSessionsResponse``RevokeAuthSessionResponse``RefreshSessionResponse``Logout*``Phone*``Wechat*` |
| `shared-contracts/src/ai.rs` | `CreateAiTaskRequest``AppendAiTextChunkRequest``CompleteAiStageRequest``AttachAiResultReferenceRequest``FailAiTaskRequest``AiTask*Payload``AiTaskMutationResponse``AiTaskAcceptedResponse` |
| `shared-contracts/src/assets.rs` | Direct upload、read url、asset object、asset binding、asset history、character visual/animation、workflow cache、role asset workflow 相关 DTO |
| `shared-contracts/src/creation_agent_document_input.rs` | `ParseCreationAgentDocumentInputRequest/Response``CreationAgentDocumentInputPayload` |

View File

@@ -115,6 +115,8 @@
1. 从 cookie 读出原始 refresh token
2. 计算 hash
3.`refresh_session.refresh_token_hash` 比较
4. 若 refresh cookie 缺失或不可用,再使用 Bearer access token claims 中的 `sid``refresh_session.session_id` 比较
5. 会话列表按“同设备 + 同 IP”聚合时组内任一 session 命中当前 hash 或当前 `sid`,整组都视为当前设备组
## 5. 表访问级别
@@ -228,9 +230,10 @@
写入规则:
1. 按当前 cookie 找 session
2. `revoked_at = now`
3.`revoked_reason_code = logout`
4. 同时提升 `user_account.token_version`
2. 如果 refresh cookie 缺失,则回退用 Bearer access token claims 中的 `sid` 找当前 session
3.`revoked_at = now`
4. `revoked_reason_code = logout`
5. 同时提升 `user_account.token_version`
### 8.4 吊销全部会话
@@ -248,7 +251,7 @@
触发点:
1. `POST /api/auth/sessions/:sessionId/revoke`
1. `POST /api/auth/sessions/{sessionId}/revoke`
写入规则:
@@ -257,6 +260,13 @@
3. 只改目标 `refresh_session`
4. `revoked_reason_code = session_revoke`
5. 不提升 `token_version`
6. 撤销后必须同步 auth store 到 SpacetimeDB
读取约束:
1. Bearer JWT 中的 `sid` 必须对应 active `refresh_session`
2. 被该接口撤销的设备即使 access token 未过期,后续请求也必须立刻返回未授权
3. 该接口不承担当前设备退出语义;当前设备退出固定走 `/api/auth/logout`
### 8.6 账号被禁用或并入
@@ -315,13 +325,18 @@
1. `clientLabel` 当前阶段继续兼容保留,但固定与 `deviceDisplayName` 对齐。
2. `ipMasked``isCurrent` 继续在 Axum 侧派生。
3. 同设备同 IP 的 active sessions 由 Axum 聚合后返回一条记录。
4. `sessionId` 是代表 ID当前组代表 ID 使用当前 `sid` 对应 session。
5. `sessionIds` 返回组内全部 active session ID`sessionCount` 返回组内数量。
6. 聚合组时间语义:`createdAt` 取最早创建时间,`lastSeenAt``expiresAt` 取最新值。
### 10.3 `POST /api/auth/logout`
依赖:
1. 当前 cookie 命中的 `refresh_session`
2. `user_account.token_version`
2. cookie 缺失时 Bearer `sid` 命中的 `refresh_session`
3. `user_account.token_version`
### 10.4 `POST /api/auth/logout-all`
@@ -330,6 +345,22 @@
1. 当前 `user_id` 下全部活跃 `refresh_session`
2. `user_account.token_version`
### 10.5 `POST /api/auth/sessions/{sessionId}/revoke`
依赖:
1. 当前 Bearer JWT 的 `user_id`
2. 当前 Bearer JWT 的 `sid`
3. 目标 `refresh_session.session_id`
4. `refresh_session.revoked_at`
5. `refresh_session.expires_at`
固定行为:
1. 目标 session 必须属于当前用户
2. 目标 session 不能是当前 `sid`
3. 成功只撤销目标 session不递增 `token_version`
## 11. 与当前 Node `user_sessions` 的映射关系
| Node `user_sessions` 列 | 新 `refresh_session` 字段 | 迁移规则 |

View File

@@ -23,7 +23,7 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
| 领域 | 表 |
| --- | --- |
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
| 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` |
| 认证 | `auth_store_snapshot`, `auth_store_projection_meta`, `user_account`, `auth_identity`, `refresh_session` |
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `analytics_date_dimension`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_feedback_submission`, `profile_save_archive` |
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
@@ -60,7 +60,7 @@ SELECT * FROM database_migration_operator WHERE operator_identity = '<identity>'
### `auth_store_snapshot`
- 作用:保存旧内存认证仓储的整份 JSON 快照,用于迁移和恢复;后续正式表拆分后仍可作为导入/导出桥
- 作用:保存旧内存认证仓储的整份 JSON 快照,用于历史迁移和兜底恢复;运行期认证同步不再继续刷新 `snapshot_id = 'default'`,而是直接导入正式认证表
- 结构:`snapshot_id PK: String`, `snapshot_json: String`, `updated_at: Timestamp`
- 索引:主键 `snapshot_id`
@@ -69,6 +69,17 @@ SELECT * FROM auth_store_snapshot;
SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
```
### `auth_store_projection_meta`
- 作用:记录正式认证表最近一次由认证快照导入的时间,避免启动恢复时旧 `auth_store_snapshot/default` 因带有时间戳而覆盖较新的正式认证表。
- 结构:`meta_id PK: String`, `updated_at: Timestamp`
- 索引:主键 `meta_id`
```sql
SELECT * FROM auth_store_projection_meta;
SELECT * FROM auth_store_projection_meta WHERE meta_id = 'default';
```
### `user_account`
- 作用用户账号主表保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关、token 版本和默认不前端展示的运营标签。
@@ -328,7 +339,8 @@ SELECT * FROM profile_membership WHERE user_id = '<user_id>';
### `profile_recharge_order`
- 作用:充值订单表,记录用户购买光点或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。
- 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Timestamp`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option<Timestamp>`
- 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Option<Timestamp>`, `provider_transaction_id: Option<String>`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option<Timestamp>`
- 支付口径:`mock` 渠道创建后立即 `paid` 并入账;微信小程序 `wechat_mp` 渠道创建时为 `pending`,微信支付通知确认后改为 `paid``provider_transaction_id` 保存微信支付平台订单号。
- 索引:`user_id`, `(user_id, created_at)`
```sql

View File

@@ -290,6 +290,8 @@ POST /api/auth/wechat/miniprogram-login
4.`auth_binding_status=pending_bind_phone`,页面必须进入绑定手机号界面
5. 绑定成功后,应切回正常已登录状态
小程序原生手机号授权链路中,请求体应携带 `wechatPhoneCode`。后端调用微信 `getuserphonenumber` 后,需要按微信原始响应字段 `phoneNumber` / `purePhoneNumber` / `countryCode` 解析手机号;如果误按 Rust 字段名 `phone_number` / `pure_phone_number` / `country_code` 解析,会出现已传 `wechatPhoneCode` 但返回“微信手机号授权失败:缺少手机号”的假失败。
## 10. 后端验收点
当前后端至少应满足以下检查:

View File

@@ -132,7 +132,7 @@ Content-Type: application/json
}
```
9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。成功后重新签发 `active` 系统 token。
9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。微信返回的手机号字段使用 `phoneNumber` / `purePhoneNumber` / `countryCode`,后端解析时必须兼容这些原始 camelCase 字段;否则会在已收到 `wechatPhoneCode` 的情况下误报“微信手机号授权失败:缺少手机号”。成功后重新签发 `active` 系统 token。
10. H5 复用 `consumeAuthCallbackResult()` 消费 `auth_token` 并进入现有登录态恢复流程。
补充H5 里的旧短信验证码绑定页继续保留为非小程序环境兜底;小程序原生手机号授权只替代“手动输入手机号 + 短信验证码”这一步,不代表后台静默读取本机号码。