Files
NianAIGC/docs/PAYMENTS.md
T
2026-10-12 13:57:23 +08:00

80 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 微信 Native 扫码充值
电脑账单页面生成支付二维码,用户用手机微信扫码,支付确认后金额进入当前组织钱包。支持固定档位和自定义金额;组织管理员可以发起和查看本组织的支付订单,普通成员继续共用组织额度。超级管理员可在计费页查看所有组织的充值订单、按订单发起微信原路退款,也保留原有人工上账入口。
## 微信商户侧准备
- 公司商户号已开通 **Native 支付**。小程序支付权限不能替代 Native 权限。
- 已认证的小程序或服务号 AppID 已绑定此商户号。已有小程序 AppID 可用,网页无需跳转小程序,也不需要用户微信登录。
- 商户经营资料、网站和实际 AI 服务相符,并完成适用的审核及网站运营手续。
- 取得商户 API 证书序列号、商户私钥、32 字节 APIv3 密钥,以及微信支付公钥和公钥 ID。这里使用微信支付公钥模式。
参考:[Native 接入准备](https://pay.wechatpay.cn/doc/v3/merchant/4015614538)、[开发参数](https://pay.wechatpay.cn/doc/v3/merchant/4013070756)。
## 部署配置
1. ACK 发布的新 Go 镜像开启了启动前迁移,会自动应用尚未执行的 `0007_wechat_native_payments.sql` 和 `0008_wechat_refunds.sql`,并通过 `DATABASE_APP_ROLE` 给应用账号授权。构建镜像前运行 `npm run db:bundle` 并提交同步后的 Go 迁移包;不要修改已执行的历史迁移。其他部署方式可手工运行 `npm run db:migrate`,或按 [`部署说明`](./DEPLOYMENT.md) 配置同一启动迁移流程。
2. 在 **Go 后端** 配置下表变量;配置项不进入前端静态构建或网页设置接口。私钥文件来自微信商户平台的商户 API 证书生成流程,格式为 PKCS#8 `-----BEGIN PRIVATE KEY-----`。
3. 将公开网站地址 `ZHINIAN_PUBLIC_BASE_URL`(或已设置的 `NEXT_PUBLIC_APP_URL`)配置为正确的 HTTPS 网站地址。回调路径由已有 `/api` 反向代理转发给 Go,公网微信服务器应能访问,且该路径不应被网站登录网关、验证码或 WAF 挑战页拦截。
4. 配置完整后设置 `WECHAT_PAY_ENABLED=true` 并重启 Go。支付开启时强制要求 PostgreSQL 持久化和有效的强制登录配置;缺少密钥会启动失败。默认关闭时不展示充值入口。
| 变量 | 含义 |
| --- | --- |
| `WECHAT_PAY_MCH_ID` | 普通商户号 |
| `WECHAT_PAY_APP_ID` | 已认证且绑定商户号的小程序/服务号 AppID |
| `WECHAT_PAY_NOTIFY_URL` | `https://正式域名/api/payments/wechat/notify` |
| `WECHAT_PAY_MERCHANT_SERIAL` | 商户 API 证书序列号 |
| `WECHAT_PAY_PRIVATE_KEY_FILE` | 商户 PKCS#8 私钥 PEM 文件路径 |
| `WECHAT_PAY_API_V3_KEY` | 32 字节 APIv3 密钥,用于回调解密 |
| `WECHAT_PAY_PUBLIC_KEY_ID` | 微信支付公钥 ID,通常以 `PUB_KEY_ID_` 开头 |
| `WECHAT_PAY_PUBLIC_KEY_FILE` | 微信支付公钥 PEM 文件路径 |
| `WECHAT_PAY_MIN_AMOUNT_FEN` | 自定义充值最低金额,默认 `100`(1 元) |
| `WECHAT_PAY_MAX_AMOUNT_FEN` | 自定义充值最高金额,默认 `1000000`(10000 元),不得超过此上限 |
| `WECHAT_PAY_PRESET_AMOUNTS_FEN` | 固定档位,默认 `10000,50000,100000`(100、500、1000 元) |
文件变量也支持对应的 `WECHAT_PAY_PRIVATE_KEY_PEM` / `WECHAT_PAY_PUBLIC_KEY_PEM`,同时配置时文件优先。金额均为整数分,最多配置 8 个不重复且在上下限内的固定档位。
ACK 的 `secrets.example.yaml` 已提供 `zhinian-go-wechat-pay` 模板,只有 Go 工作负载加载它。模板保留占位符;实际 Secret 通过部署环境注入。Docker Compose 可通过已有 `.env.local` 注入 PEM 字符串(换行可写作 `\n`),或在 Go 容器挂载只读密钥文件并填写容器内路径。若同时配置两个公开网站地址,`NEXT_PUBLIC_APP_URL` 优先,需确保它不是旧的 localhost 地址。
微信退款通知使用 `https://正式域名/api/payments/wechat/refund-notify`。后端从已配置的 `WECHAT_PAY_NOTIFY_URL` 派生同一 HTTPS 域名,无需新增环境变量。两个回调路径都需由公网 `/api` 路由转发给 Go。
## 订单与到账
- 服务端从会话确定组织和发起人,校验整数金额及范围。每次充值尝试带 `requestKey`,重试返回同一订单;更换金额应创建新尝试。
- 默认支付有效期 15 分钟。二维码在浏览器本地生成,不向第三方二维码服务传递支付链接。
- 回调通过官方 SDK 验签、检查时间戳并解密,然后核对商户、AppID、订单、币种、金额和支付状态。
- 支付成功状态、交易号和钱包账本在同一 PostgreSQL 事务提交。重复回调、页面查单和后台补单并发到达仍只入账一次。
- 每 30 秒后台扫描待支付订单,结合微信查单补齐漏通知。超时订单查询后向微信关单;回调晚到仍可确认有效付款。网络失败保留待确认状态继续重试。
- 钱包流水的充值类型为 `recharge`,发起人在支付订单和账本 metadata 里记录。财务可使用本地订单号及微信交易号与微信商户平台核对。
## 原路退款与余额
只有超级管理员可在「计费 → 微信充值」查看跨组织订单并发起退款;组织管理员和普通成员不能发起或读取后台退款接口。订单列表可按组织筛选,展示已退款、退款中、可退金额。仅已支付且属于当前商户号的订单可退,单次可退一部分或整笔剩余可退金额,累计不得超过原支付金额。退款原因必填,去除首尾空白后最多 80 个 UTF-8 字节。
创建退款时,系统先为该笔订单保留可退额度,并以 `requestKey` 保证重复提交仍指向同一笔退款。微信受理或退款处理中**不会立即扣除组织余额**;只有经微信退款查询或签名通知确认成功,才在同一数据库事务中写入成功状态并从组织钱包扣款。该笔扣款允许钱包变为负数,避免已消费充值金额阻止真实原路退款;后续计费仍按原余额规则处理。退款账本流水使用独立的 `recharge_refund` 类型,不与生成任务失败返还的退款流水混用。
退款状态包括 `pending`、`processing`、`success`、`closed`、`abnormal`、`rejected`。微信明确拒绝申请时,记录 `rejected` 和可读原因,释放可退额度;网络超时、限流或服务端错误仍保留待确认,沿用同一微信商户退款单号查询及重试。已关闭退款也会释放预留额度,可重新申请;异常退款继续占用额度,需核对微信商户平台及后台查询结果,避免重复退款。后台定期查询处理中订单,修复遗漏回调。退款通知无需用户登录,但必须经过微信官方签名验证、解密,以及商户号、交易号、金额和订单核对。关闭充值开关后仍可查看历史订单和退款记录,不能新建退款;待处理退款的通知和对账需保持微信支付配置开启。
## API
| 接口 | 用途 |
| --- | --- |
| `GET /api/payments/options` | 登录后读取开启状态、权限、固定金额和上下限 |
| `GET /api/payments/orders` | 组织管理员读取本组织最近 100 个订单 |
| `POST /api/payments/orders` | `{ "amountFen": 10000, "requestKey": "客户端生成的UUID" }` 创建或重试订单 |
| `GET /api/payments/orders/{id}` | 本组织订单状态;适时向微信补查 |
| `POST /api/payments/wechat/notify` | 微信签名通知,成功持久化后返回 204;不依赖用户 Cookie |
| `GET /api/payments/admin/orders?organizationId=&limit=50&offset=0` | 超管分页查看全部组织订单;组织筛选可选,包含可退与已退金额 |
| `GET /api/payments/admin/orders/{id}/refunds` | 超管查看指定订单的退款记录 |
| `POST /api/payments/admin/orders/{id}/refunds` | 超管发起部分或剩余金额退款;请求体 `{ "amountFen": 5000, "requestKey": "客户端生成的UUID", "reason": "客户申请" }` |
| `GET /api/payments/admin/refunds/{id}` | 超管查询单笔退款状态 |
| `POST /api/payments/wechat/refund-notify` | 微信退款签名通知;不依赖用户 Cookie |
## 上线联调
在测试商户或受控正式环境中先核对配置和回调可达性,再由业务负责人发起一笔小额支付,确认网页成功状态、组织余额、唯一充值流水及微信商户平台交易号一致。必要时可暂时把最低金额设为 `1` 分,并同时调整固定档位。检查重复回调不重复加钱、关闭网页后仍自动到账、刷新后能恢复待支付订单以及二维码失效后可新建订单。退款需另行用受控小额订单核对微信成功状态、退款通知、组织余额只扣一次和 `recharge_refund` 流水;切勿为测试而对真实客户订单发起退款。
此版本提供收款充值、查询、关单、自动补单和超管原路退款;尚无普通用户自助退款或微信账单下载。正式收款前仍应确定退款审批与财务核对流程。若绕过平台直接在微信商户平台退款,平台余额不会自动同步,需要人工核对并处理账本。现有生成任务失败退回平台余额的逻辑继续独立运行。
真实商户权限、证书和线上微信回调需要部署后联调,本地模拟测试不能确认这些外部条件。