This commit is contained in:
2026-05-15 02:41:43 +08:00
39 changed files with 2539 additions and 200 deletions

View File

@@ -64,7 +64,8 @@
1. 校验 `productId`
2. `paymentChannel = "mock"` 时后端创建已支付订单
3. `paymentChannel = "wechat_mp"` 时后端创建待支付订单,并调用微信支付 JSAPI 下单生成小程序支付参数
3. `paymentChannel = "wechat_mp"` 时后端创建待支付订单,并调用微信支付 JSAPI 下单生成小程序支付参数;本地 `orderId` 会作为微信 `out_trade_no` 传递,格式固定为 `rcg` 前缀 + 小写字母数字,长度在 6-32 字符内,满足微信支付 JSAPI 下单文档对商户订单号的限制。商品描述限制为 127 字符内,回调地址限制为 HTTPS、255 字符内且不携带 query/fragment。
- JSAPI 下单请求必须显式携带 `Accept: application/json``Content-Type: application/json``User-Agent: Genarrative-WechatPay/1.0`;微信侧会把缺少 `User-Agent` 的请求返回为“Http头缺少Accept或User-Agent”。
4. mock 泥点套餐立即写入钱包余额与流水mock 会员套餐立即写入会员状态
5. wechat_mp 订单不提前发泥点或会员,只返回待支付订单、账户中心快照与 `wechatMiniProgramPayParams`
@@ -84,16 +85,42 @@
}
```
### 3.3 `POST /api/profile/recharge/wechat/notify`
### 3.3 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm`
需要 Bearer JWT。该接口用于小程序支付页返回 web-view 后的主动查单确认,不替代微信支付通知:
1. 后端读取本地 `profile_recharge_order` 并校验订单归属、支付渠道和当前状态。
2. 若订单已是 `paid`,直接返回订单与账户中心快照。
3. 若订单仍是 `pending`,后端调用微信支付按商户订单号查单接口。
4. 只有微信查单返回 `trade_state = "SUCCESS"` 时,才调用统一入账 procedure 把订单改为 `paid` 并写入钱包流水或会员状态。
5. 如果微信查单仍不是 `SUCCESS`,接口返回当前 pending 订单与账户中心快照;前端只在全局支付结果模态显示“支付已提交”,不提前发放泥点或会员。
响应结构:
```json
{
"order": {
"orderId": "rcg...",
"status": "paid"
},
"center": {
"walletBalance": 120
}
}
```
### 3.4 `POST /api/profile/recharge/wechat/notify`
微信支付通知地址,无需 Bearer JWT。行为
1. 真实渠道使用微信支付平台公钥和 `Wechatpay-*` 请求头验签。
1. 真实渠道使用微信支付平台公钥和 `Wechatpay-*` 请求头验签;验签必须使用原始 HTTP body bytes 构造 `timestamp\nnonce\nbody\n`,不能先把 body 转成字符串再重建
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 内写入钱包流水或会员到期时间,确保重复通知幂等。
7. 验签、解密和业务确认通过后返回 HTTP `204 No Content`;不要返回 V2 XML。
8. 微信支付公钥模式下,真实请求会携带 `Wechatpay-Serial: PUB_KEY_ID_...`,通知验签必须要求回调头 `Wechatpay-Serial``WECHAT_PAY_PLATFORM_SERIAL_NO` 对应;若不匹配应返回 `401` 并在日志里记录 reason。
关键环境变量:
@@ -115,8 +142,14 @@
2. 弹窗顶部标题为 `账户充值`,右上角关闭。
3. 默认打开 `泥点充值`,可切换到 `会员卡充值`
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`
5. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和状态反馈
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-viewH5 在 `hashchange``focus``pageshow``visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
- 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。
- `cancel``fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
7. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
## 5. 验收

View File

@@ -49,7 +49,7 @@ npm run dev:rust
5. 如果确认需要新启动 SpacetimeDB脚本会先检测 `127.0.0.1:3101` 是否可监听;若已占用,输出占用进程并选择从 `3101` 起向后的最近可用端口,再执行 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr <实际地址>`。启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`,后续 publish 与 `api-server` 都使用同一个实际 URL。
6. 等待 SpacetimeDB 就绪:优先接受 `spacetime server ping http://127.0.0.1:<spacetime-port>` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:<spacetime-port>/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`
7. 执行 `spacetime publish <本地数据库名> --server <实际 SpacetimeDB URL> --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
8. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
8. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。本地启动器会保留终端实时输出,并把同一份 `cargo` / `api-server` 输出持久化到 `logs/api-server/`
9. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
10. 注入 `RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
11. 任一子进程退出时,脚本回收其余子进程。
@@ -120,6 +120,12 @@ npm run dev:rust:logs -- --follow
3. 默认输出到 `logs/spacetime/<database>-<timestamp>.log`,并通过 `tee` 同步显示在终端。
4. `--follow` 仅用于本地追踪,会持续追加到同一个输出文件;停止时用 `Ctrl+C`
api-server 本地持久化日志:
1. `npm run api-server` 默认写入 `logs/api-server/api-server-<timestamp>.log`,同时继续把同一份输出显示在当前终端。
2. `npm run dev` / `npm run dev:rust` 中由脚本启动的 Rust `api-server` 默认写入 `logs/api-server/api-server-dev-rust-<timestamp>.log`;等待 `/healthz` 失败时,脚本会自动输出该日志最后 80 行。
3. 如需固定日志文件,可设置 `GENARRATIVE_API_SERVER_LOG_FILE=logs/api-server/local.log`;如只需更换目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR=logs/api-server`。相对路径都按仓库根目录解析。
联调排错补充:
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。

View File

@@ -58,6 +58,14 @@ host 会比较新模块声明的 schema 和旧数据库 schema然后尝试自
4. 等数据迁移完成、旧客户端完成升级、旧表数据清空后,再移除旧表。
5. 开发环境可以使用 `--delete-data` 重建数据库,生产环境不要用它作为数据迁移方案。
## 字段级硬性约束
- 对已有 SpacetimeDB 表新增字段时,必须把新字段追加在 Rust 表结构体的最后,不能插入已有字段中间。
- 新增字段必须设置明确默认值,例如 Rust `#[default(...)]`;复杂集合默认值如果无法作为编译期常量表达,应优先使用 `Option<T>``#[default(None::<T>)]`,并在业务层归一化。
- 修改已有字段名属于高风险 schema 变更。编码前必须先询问用户,确认旧字段名、新字段名、数据保留方式、客户端兼容窗口和发布顺序,并让用户准备或确认迁移计划。
- 字段改名或任何 row shape 迁移都必须同步更新 `server-rs/crates/spacetime-module/src/migration.rs``SPACETIMEDB_TABLE_CATALOG.md` 和生成绑定;不要只改表结构体。
- 修改 SpacetimeDB schema 后必须运行 `npm run check:spacetime-schema`。该检查会对比当前工作区与基准分支的 Rust table 字段,自动拦截新增字段缺 default、字段插入中间、字段删除/改名/重排/改类型,以及漏改 `migration.rs`、表目录或生成绑定。
## 通常安全的变更
这些变更一般可以自动迁移,并且通常不会破坏现有客户端:
@@ -223,6 +231,8 @@ fn migrate_character_batch(ctx: &ReducerContext, limit: u32) {
- 是否删除、改名、重排或修改了已有列。
- 新增列是否位于表定义末尾,并且是否有 default value。
- 如需改字段名,是否已先询问用户并确认迁移计划,且已同步更新 `migration.rs`
- 是否已运行 `npm run check:spacetime-schema` 并通过。
- 是否给已有表新增了 unique 或 primary key 约束。
- 是否删除了非空表。
- 是否修改了 event table、schedule table、RLS 或 view。