# “我的”账户充值弹窗落地设计 日期:`2026-04-25` ## 1. 范围 本轮在“我的”页面的“会员充值”入口落地账户充值弹窗,包含两个页签: 1. `泥点充值` 2. `会员卡充值` 前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。支付接入固定为普通商户直连模式:小程序 web-view 使用 `wechat_mp`,移动网页使用 `wechat_h5`,桌面网页使用 `wechat_native` 二维码。生产默认永远不走 `mock`;只有自动测试或显式测试配置手动传 `paymentChannel = "mock"` 时才允许 mock 即时入账。所有真实微信渠道的到账事实以后端微信支付通知和服务端查单为准。 ## 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泥点 | 泥点充值固定为 `¥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% | 购买会员时,如果当前会员仍有效,则从当前到期时间顺延;如果已过期或从未购买,则从当前服务端时间开始计算。状态只区分 `普通` 与已生效会员,前端不自行推断。 ## 3. 后端接口 ### 3.1 `GET /api/profile/recharge-center` 需要 Bearer JWT。返回: 1. 当前泥点余额、会员状态、到期时间 2. 泥点套餐与会员套餐 3. 会员权益表 4. 最近订单摘要 兼容路径:`GET /api/runtime/profile/recharge-center` ### 3.2 `POST /api/profile/recharge/orders` 需要 Bearer JWT。请求: ```json { "productId": "points_300", "paymentChannel": "wechat_h5" } ``` 行为: 1. 校验 `productId` 2. 缺少 `paymentChannel` 或传入未知渠道时直接返回 `400`,不得再默认解释为 `mock` 3. 生产/真实支付配置拒绝 `paymentChannel = "mock"`;mock 只服务自动测试或显式 mock 测试配置 - `wechat_mp` / `wechat_h5` / `wechat_native` 必须在 `WECHAT_PAY_ENABLED=true` 且 `WECHAT_PAY_PROVIDER=real` 时才允许下单;`WECHAT_PAY_PROVIDER=mock` 不能为真实微信渠道返回 mock 支付载荷。 4. `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”。 5. `paymentChannel = "wechat_h5"` 时调用微信支付 H5 下单,返回 `wechatH5Payment.h5Url`,前端跳转到该地址 6. `paymentChannel = "wechat_native"` 时调用微信支付 Native 下单,返回 `wechatNativePayment.codeUrl`,前端在充值弹窗内把 `codeUrl` 渲染成二维码 7. 所有微信真实渠道订单不提前发泥点或会员,只返回待支付订单、账户中心快照与对应支付载荷 兼容路径:`POST /api/runtime/profile/recharge/orders` 响应里的支付字段只在对应微信支付渠道返回。`wechatMiniProgramPayParams` 字段直接对应 `wx.requestPayment`: ```json { "wechatMiniProgramPayParams": { "timeStamp": "1777110165", "nonceStr": "nonce", "package": "prepay_id=wx201410272009395522657a690389285100", "signType": "RSA", "paySign": "..." } } ``` H5 与 Native 响应: ```json { "wechatH5Payment": { "h5Url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=..." }, "wechatNativePayment": { "codeUrl": "weixin://pay.weixin.qq.com/bizpayurl/up?pr=..." } } ``` ### 3.3 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 需要 Bearer JWT。该接口用于微信支付返回页面或 Native 扫码后的主动查单确认,不替代微信支付通知: 1. 后端读取本地 `profile_recharge_order` 并校验订单归属、支付渠道和当前状态;`wechat_mp` / `wechat_h5` / `wechat_native` 都可确认,`mock` 不能走该接口。 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-*` 请求头验签;验签必须使用原始 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。 关键环境变量: | 变量 | 说明 | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `WECHAT_PAY_ENABLED` | 是否启用微信支付客户端 | | `WECHAT_PAY_PROVIDER` | `mock` 或 `real`;真实微信渠道只能使用 `real`,`mock` 只允许显式测试渠道 | | `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. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`。 - 支付渠道由 `src/services/payment/paymentPlatform.ts` 统一判断,业务 UI 不内联区分小程序、手机网页和桌面网页。 - 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。 - native 支付页通过 `wx_pay_result=:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。 - `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。 - 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。 - `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。 5. 移动网页含微信内 H5 首版统一走 `wechat_h5`:拿到 `h5Url` 后跳转;若微信内 H5 调起失败,失败态提示用户改用系统浏览器或小程序。 6. 桌面网页走 `wechat_native`:充值弹窗展示二维码,用户扫码后点击“我已支付”,前端调用 confirm 接口短轮询确认;确认前不刷新父级余额。 7. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。 8. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。 9. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。 ## 5. 验收 1. 普通用户打开弹窗能看到泥点与会员套餐。 2. 默认平台通道只会解析到 `wechat_mp` / `wechat_h5` / `wechat_native`,不会解析到 `mock`。 3. 泥点购买后余额增加,流水来源为 `points_recharge`。 4. 首充赠送只在首次泥点充值时生效。 5. 会员购买后会员状态与到期时间立即更新。 6. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。