12 KiB
“我的”账户充值弹窗落地设计
日期:2026-04-25
1. 范围
本轮在“我的”页面的“会员充值”入口落地账户充值弹窗,包含两个页签:
泥点充值会员卡充值
前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 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泥点 |
泥点充值默认初始化 ¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328 六个档位。默认档位只作为空库种子写入 profile_recharge_product_config,运行时充值中心展示、下单校验和支付确认结算都以 SpacetimeDB 配置表为准,不再把代码中的商品目录作为业务真相源。
全部泥点档位参与档位首充双倍:首充资格按 productId 独立判断。用户购买过 points_60 后,再次购买 points_60 只到账基础泥点;但 points_180、points_300 等未购买过的档位仍保留各自的首充赠送。实际到账泥点写入 profile_recharge_order.points_delta 与钱包流水,余额以 SpacetimeDB projection 为准。
充值中心返回的 pointProducts 已由后端按当前账号和每个 productId 计算有效展示状态:已完成首充的档位清空 bonusPoints、badgeLabel 与首充说明,未完成首充的档位继续显示 首充双倍 和 基础+赠送。hasPointsRecharged 仅保留为兼容字段,表示账号是否发生过任一泥点充值,不再作为隐藏所有档位首充或计算结算金额的依据。前端不得再用 hasPointsRecharged 对所有泥点商品做统一屏蔽。
后台通过“充值商品”页维护 profile_recharge_product_config,字段包括 productId、标题、商品类型、金额分、基础泥点、首充赠送泥点、会员天数、徽标、说明、会员层级、启用状态和排序。保存后新的充值中心快照、下单与支付确认立即读取配置表;历史订单继续保留下单当时写入的商品标题、金额和状态。
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。返回:
- 当前泥点余额、会员状态、到期时间
- 泥点套餐与会员套餐
- 会员权益表
- 最近订单摘要
兼容路径:GET /api/runtime/profile/recharge-center
3.2 POST /api/profile/recharge/orders
需要 Bearer JWT。请求:
{
"productId": "points_300",
"paymentChannel": "mock"
}
行为:
- 校验
productId paymentChannel = "mock"时后端创建已支付订单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”。
- JSAPI 下单请求必须显式携带
- mock 泥点套餐立即写入钱包余额与流水,mock 会员套餐立即写入会员状态
- wechat_mp 订单不提前发泥点或会员,只返回待支付订单、账户中心快照与
wechatMiniProgramPayParams
兼容路径:POST /api/runtime/profile/recharge/orders
响应里的 wechatMiniProgramPayParams 只在微信小程序支付渠道返回,字段直接对应 wx.requestPayment:
{
"wechatMiniProgramPayParams": {
"timeStamp": "1777110165",
"nonceStr": "nonce",
"package": "prepay_id=wx201410272009395522657a690389285100",
"signType": "RSA",
"paySign": "..."
}
}
3.3 POST /api/profile/recharge/orders/{order_id}/wechat/confirm
需要 Bearer JWT。该接口用于小程序支付页返回 web-view 后的主动查单确认,不替代微信支付通知:
- 后端读取本地
profile_recharge_order并校验订单归属、支付渠道和当前状态。 - 若订单已是
paid,直接返回订单与账户中心快照。 - 若订单仍是
pending,后端调用微信支付按商户订单号查单接口。 - 只有微信查单返回
trade_state = "SUCCESS"时,才调用统一入账 procedure 把订单改为paid并写入钱包流水或会员状态。 - 如果微信查单仍不是
SUCCESS,接口返回当前 pending 订单与账户中心快照;前端只在全局支付结果模态显示“支付已提交”,不提前发放泥点或会员。
响应结构:
{
"order": {
"orderId": "rcg...",
"status": "paid"
},
"center": {
"walletBalance": 120
}
}
3.4 POST /api/profile/recharge/wechat/notify
微信支付通知地址,无需 Bearer JWT。行为:
- 真实渠道使用微信支付平台公钥和
Wechatpay-*请求头验签;验签必须使用原始 HTTP body bytes 构造timestamp\nnonce\nbody\n,不能先把 body 转成字符串再重建。 - 使用
WECHAT_PAY_API_V3_KEY解密通知resource。 - 仅当
trade_state = "SUCCESS"时确认订单支付。 - 使用微信通知里的
out_trade_no查本地profile_recharge_order.order_id,把订单从pending改为paid。 - 将微信平台订单号写入
provider_transaction_id,用于对账、查单、退款和客服排障。 - 在同一 SpacetimeDB procedure 内写入钱包流水或会员到期时间,确保重复通知幂等。
- 验签、解密和业务确认通过后返回 HTTP
204 No Content;不要返回 V2 XML。 - 微信支付公钥模式下,真实请求会携带
Wechatpay-Serial: PUB_KEY_ID_...,通知验签必须要求回调头Wechatpay-Serial与WECHAT_PAY_PLATFORM_SERIAL_NO对应;若不匹配应返回401并在日志里记录 reason。
关键环境变量:
| 变量 | 说明 |
|---|---|
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. 前端交互
- “我的”页会员充值按钮打开独立弹窗,不在当前面板下方展开。
- 弹窗顶部标题为
账户充值,右上角关闭。 - 默认打开
泥点充值,可切换到会员卡充值。 - 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起
wx.requestPayment,支付页返回后刷新profileDashboard。- 小程序 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-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只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过
- 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
- 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
- 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
5. 验收
- 普通用户打开弹窗能看到泥点与会员套餐。
- 泥点购买后余额增加,流水来源为
points_recharge。 - 首充赠送按泥点档位独立生效。
- 某个
productId已成功完成泥点充值后,再打开充值弹窗时仅该档位不再展示“首充双倍”徽标或60+60等赠送泥点组合,其他未购买过的泥点档位仍展示各自首充权益。 - 会员购买后会员状态与到期时间立即更新。
- 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。