增加支付逻辑

This commit is contained in:
andy committed 2026-10-12 13:57:23 +08:00
1 parent 4ac786aafb
commit dfb2c91b8d
77 files changed
+6370 -45

No files matched your search

+12 -1
View File
@@ -86,7 +86,18 @@ curl -X POST https://你的域名/api/v1/jobs \
开放 API 仍按 API Key 和账号分区运行。当前未绑定组织的开放 API 任务保持兼容,不从组织钱包扣费;平台浏览器用户的真实图片/视频任务会按超级管理员配置的服务商标准单价、计费单位和上浮倍率,从所属组织余额冻结。普通用户余额不足时不会提交服务商并返回余额不足错误;超级管理员仍保存计算费用,但不检查、冻结或扣减组织额度,也不产生钱包扣费、退款流水。计费规则与最终金额会随任务保存,任务失败、取消或过期后由 Worker 在最终终态退款;Seedance 成功后按 `usage.completion_tokens` 多退少补,缺少该字段时保留冻结金额。MiniMax H3 按实际输出秒数及输入图片数结算;Wan 3.0 按 `usage.output_video_duration` 的实际时长结算(无视频输入时可回退 `usage.duration`),缺少有效用量时保留预扣并标记为估算。
组织余额当前只能由超级管理员通过 `/billing` 的“余额与上账”直接入账。所有充值和人工余额调整都只记入组织账本,不存在个人上账归属;组织管理员和员工共同使用组织额度。未来接入支付时,支付成功回调应使用同一幂等上账逻辑自动入账,不产生待审核申请。余额不足时,平台会拒绝创建真实计费任务并返回错误;未配置真实服务商凭据时也不会生成占位结果。
组织余额可由超级管理员通过 `/billing` 的“余额与上账”手工入账,也可在启用微信 Native 支付后由组织管理员扫码充值。所有充值和人工余额调整均计入组织账本,组织管理员和员工共同使用额度。支付回调和补单经核验后,在一个数据库事务中更新支付状态并幂等入账。超级管理员还能在「计费 → 微信充值」查看所有组织的订单并发起部分或剩余金额的微信原路退款;微信确认成功后,组织余额才会扣除,必要时允许变为负数。退款写入独立的 `recharge_refund` 账本类型,与生成失败返还余额区分。余额不足时,平台会拒绝创建真实计费任务并返回错误;未配置真实服务商凭据时也不会生成占位结果。
支付后台接口使用平台登录会话,不接受本页开放 API 的 API Key。只有超级管理员可以调用:
| 接口 | 用途 |
| --- | --- |
| `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": "唯一请求ID", "reason": "客户申请" }` |
| `GET /api/payments/admin/refunds/{id}` | 查询退款状态 |
退款原因必填、最多 80 个 UTF-8 字节;同一 `requestKey` 重试不会创建第二笔退款。`pending` 和 `processing` 状态占用可退额度,但不会立即扣除余额;`success` 才扣除,`closed` 释放额度,`abnormal` 保留占用等待核对。关闭充值开关后仍可查看历史订单和退款,不能发起新退款。微信退款通知走无需登录的 `POST /api/payments/wechat/refund-notify`,由后端验签、解密和核对;回调 URL 从支付回调域名派生,无需另设环境变量。接口、配置和财务处理见 [微信支付说明](PAYMENTS.md)。
创作页通过 `GET /api/billing/balance` 展示当前组织的可用余额。接口仅接受平台会话认证,从服务端刷新后的会话确定组织,忽略客户端组织参数,返回 `organization` 和 `wallet`,不加载账本或价格目录,并设置 `Cache-Control: no-store`。未绑定组织返回 422。页面支持手动刷新,并在提交任务、任务计费状态变化、重新切回页面及可见状态下每 30 秒刷新;超级管理员显示“超管不计额度”。余额提示供用户参考,实际扣费及余额校验仍由服务端执行。
+33 -17
View File
@@ -47,29 +47,35 @@ docker build -f backend/Dockerfile.alpine \
生产固定使用 PostgreSQL。数据库连接只通过 Go Deployment 的 Secret 注入,不得进入
Web 镜像或 ConfigMap。优先使用 RDS 内网地址,并仅对白名单中的 ACK 工作负载网段放行。
`DATABASE_URL` 必须使用 `sslmode=disable`。Go 和手工迁移使用的 Node 客户端都会强制
`DATABASE_URL` 和 `DATABASE_MIGRATION_URL` 必须使用 `sslmode=disable`。Go 和手工迁移使用的 Node 客户端都会强制
归一化为该值;即使旧 Secret 仍包含 TLS 参数,客户端也不会读取 CA 或协商 TLS。
这表示 ACK 到 RDS 的数据库流量不使用 TLS、链路内容为明文。只应使用 RDS 内网地址,
并通过 VPC、安全组和白名单严格限制访问;若未来需要链路加密,必须同时修改客户端策略
和 RDS 配置后再部署。
首次发布前,部署负责人按顺序手工执行:
ACK 模板设置 `DATABASE_AUTO_MIGRATE=true`。Go API 启动时先连接
`DATABASE_MIGRATION_URL`(`zhinian-migration-db` Secret),按版本顺序执行镜像内已
同步的 `database/migrations/*.sql`,并用 `DATABASE_APP_ROLE` 给应用账号授权;全部成功后
才启动 HTTP 服务。该变量未提供时,启动器会回退使用 `DATABASE_URL`,但 ACK 明确使用
独立迁移账号。`DATABASE_APP_ROLE` 必须填写为 `zhinian-go-db` 连接串中的 PostgreSQL
用户名,且迁移账号须具备建表、修改表和授权权限。`DATABASE_MIGRATION_URL` 与
`DATABASE_URL` 的主机、端口和数据库名必须相同,连接用户名可以不同;启动器会检查。
默认启动迁移超时 180 秒;对应的 startupProbe 提供约 300 秒预算。每个迁移版本的 DDL
与版本记录在同一事务中;失败的版本会回滚,之前已成功提交的版本保留。迁移失败时 Pod
不监听端口,查看 Go Pod 日志并修复原因后再重启。回滚镜像不会逆向撤销数据库迁移。
遇到历史重复数据,先人工审计,不能跳过或
自动删除计费与用量记录。
1. `database/migrations/0001_initial_schema.sql`
2. `database/migrations/0002_generation_lifecycle_fencing.sql`
3. `database/migrations/0003_platform_runtime_settings.sql`
4. `database/migrations/0004_seedream_billing_provider.sql`
5. `database/migrations/0005_seedream_layer_compositions.sql`
6. `database/migrations/0006_minimax_h3_billing_provider.sql`
7. 应用角色授权语句(参考 `scripts/migrate-postgres.mjs`)
每次新增迁移,在仓库根目录运行 `npm run db:bundle`,把根目录 SQL 同步成提交到 Git 的
Go 迁移包,然后构建 Go 镜像;不能修改已应用的历史 SQL。新镜像启动时会核对版本记录,
只执行未应用的迁移,重启不会重复建表。手工处理仍可从仓库根目录执行
`npm run db:migrate`;其连接账号也应具备上述权限。生产不部署迁移 Job 或 Node Worker,
Go 镜像运行时不需要 Node/npm,也不用在 ACK「启动执行」中填写命令。
升级已有生产库接入 MiniMax H3 时,只需在发布新 Go 镜像前执行尚未应用的 `0006`;
新版本 `/api/ready` 会同时检查计费服务商约束中已包含 `seedream` 和 `minimax`,未迁移的
Pod 不会进入 Ready,避免运行后才在报价阶段返回 500。
生产不部署迁移 Job。遇到历史重复数据时迁移会失败,需先人工审计,不能跳过或自动删除
计费与用量记录。仓库不再提供可误执行的 Node Migration Job 或 Node Worker 清单;后台
任务由 Go API 内嵌 WorkerLoop 处理。
虽然当前 `replicas: 1`,RollingUpdate 的 `maxSurge: 1` 可能短暂同时运行新旧两个 Go
Pod。迁移器使用 PostgreSQL 锁串行化迁移,不能以单副本假定没有并发。旧版 Pod 可能在
新版迁移期间继续接流量,因此新增 SQL 需兼容滚动期间的新旧程序。数据库迁移不能靠
回滚镜像自动撤销。
Go 首次启动会读取 `zhinian-go-bootstrap` Secret 中的
`ZHINIAN_BOOTSTRAP_ADMIN_PHONE` 和 `ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD`,仅在不存在
@@ -82,6 +88,11 @@ Go 首次启动会读取 `zhinian-go-bootstrap` Secret 中的
Git 的 `deploy/ack/secrets.production.yaml`,替换所有占位值。Web 不需要任何运行时
Secret;会话密钥只属于 Go。
将 `configmap.yaml` 中 `DATABASE_APP_ROLE: REPLACE_WITH_APP_DB_ROLE` 替换为应用数据库
用户(`zhinian-go-db` 的 `APP_USER`),并确认 `zhinian-migration-db` 的连接串指向同一
数据库;两条连接串的主机、端口和数据库名须一致。先发布 Secret/ConfigMap 再更新 Go Deployment。ACK 控制台的「启动执行」命令
与参数保持空白,镜像原本的 `zhinian-api` 启动入口会自行迁移。
仓库内静态检查:
```bash
@@ -126,6 +137,7 @@ Pod 的 `emptyDir`,Pod 重建或滚动升级同样会永久丢失上传文件
Kubernetes Secret:
- `DATABASE_URL`
- `DATABASE_MIGRATION_URL`(独立迁移账号,来自 `zhinian-migration-db`)
- `ZHINIAN_AUTH_SESSION_SECRET`(Secret 名 `zhinian-go-auth`,仅注入 Go)
- `ZHINIAN_BOOTSTRAP_ADMIN_PHONE`、`ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD`
- OSS 凭据;服务商凭据也可以先放 Secret 作为数据库尚未配置时的回退值
@@ -141,7 +153,7 @@ Secret/ConfigMap 为事实来源。设置页全部 27 项配置由超级管理
## 探针与验收
- Web `/healthz`:Nginx 静态进程存活检查。
- Go `/api/health`:Go 进程存活检查;以 250ms 上限刷新服务商状态,失败时使用进程内缓存并仍返回存活状态。
- Go `/api/health`:迁移完成且服务开始监听后的进程存活检查;以 250ms 上限刷新服务商状态,失败时使用进程内缓存并仍返回存活状态。
- Go `/api/ready`:检查 PostgreSQL schema、表权限和业务函数权限。
发布后执行:
@@ -176,6 +188,10 @@ cp .env.example .env.local
./scripts/deploy.sh
```
ACK 和 Compose 会向 Go 进程注入环境变量。若直接在本地执行 `go run` 或 Go 二进制,
需显式导出 `DATABASE_AUTO_MIGRATE`、`DATABASE_MIGRATION_URL`、`DATABASE_URL` 等
数据库启动参数;仅填写 `.env.local` 不等于已向 Go 进程注入这些变量。
根目录 Compose 会启动 `zhinian-web` 与 `zhinian-go-api`,挂载
`deploy/nginx-compose.conf` 将 `/api`、`/uploads`、`/generated-results` 转发给 Go;Go
内嵌 WorkerLoop,不再启动独立 Node Worker。ACK 生产仍使用上面的两镜像 + Ingress 拓扑,
+79
View File
@@ -0,0 +1,79 @@
# 微信 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` 流水;切勿为测试而对真实客户订单发起退款。
此版本提供收款充值、查询、关单、自动补单和超管原路退款;尚无普通用户自助退款或微信账单下载。正式收款前仍应确定退款审批与财务核对流程。若绕过平台直接在微信商户平台退款,平台余额不会自动同步,需要人工核对并处理账本。现有生成任务失败退回平台余额的逻辑继续独立运行。
真实商户权限、证书和线上微信回调需要部署后联调,本地模拟测试不能确认这些外部条件。
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.