forked from wangziqi/gongxue-base
362 lines
18 KiB
Markdown
362 lines
18 KiB
Markdown
# 国内认证与支付接入方案
|
||
|
||
## Supabase 边界
|
||
|
||
Supabase 适合承担 PostgreSQL、RLS、Auth、Edge Functions、Webhook/Hooks 等底座能力,但它不是中国大陆支付网关,也不会内置微信支付、支付宝、阿里云短信、腾讯云短信这一整套商用配置。
|
||
|
||
对本项目更稳妥的落位是:
|
||
|
||
- Supabase/PostgreSQL:保存多租户、订单、支付事件、权益、审计、登录事件。
|
||
- `apps/api`:实现业务 API、短信 provider、OAuth provider、支付 provider、回调验签和幂等。
|
||
- `app_private.tenant_secrets` 或生产 Vault/KMS:保存租户级密钥。
|
||
- `tenant_auth_providers`、`tenant_payment_accounts`:只保存非敏感公开配置。
|
||
|
||
Supabase Auth 可继续作为最终 JWT 用户体系目标;本地重构期先用 `app_private.auth_sessions` 签发 `tk_` session,保证 Web/Taro 能跑通端到端流程。
|
||
|
||
## 本地验收边界
|
||
|
||
当前阶段没有真实阿里云/腾讯云/微信/QQ/支付宝密钥时,不阻塞后端编码和本地联调。验收重点放在 provider adapter 之外的商用关键链路:
|
||
|
||
- 短信:使用 `AUTH_SMS_PROVIDER=mock` 或本地 fake endpoint 验证验证码生成、HMAC 存储、冷却、过期、登录事件和 session 签发。生产环境仍为 mock 会 fail-fast。
|
||
- 微信小程序/微信网页/QQ 登录:使用本地 fake endpoint 或测试桩覆盖 code 换身份之后的账号创建/合并、`user_identities`、登录审计和 session 签发;真实 `code2Session`、授权回调域名和开放平台错误码到云服务器解析完成后再验收。
|
||
- 微信/支付宝支付:本地优先验证下单参数生成、回调验签/解密的错误处理、幂等键、`payment_events`、订单状态、权益开通、退款状态机、补偿 worker 和对账工单。真实商户号、API v3 key、证书、支付宝公私钥和回调域名上线后再做官方联调。
|
||
- 密钥:本地可以只写测试密钥或 fake secret,但必须走 `app_private.tenant_secrets` / `app_private.platform_secrets` 引用链;前端和 public schema 不允许出现明文密钥。
|
||
|
||
因此本地“跑通”不等于真实平台验收完成。上线门禁必须再补真实生产账号、真实回调域名、官方账单抽样和生产 provider 配置证据。
|
||
|
||
## 当前已实现
|
||
|
||
- `POST /api/auth/sms/send`:手机号验证码发送,验证码只保存 HMAC hash。
|
||
- `POST /api/auth/sms/verify`:验证码登录,自动创建或复用 `platform_users`。
|
||
- `GET /api/auth/me`:通过 Bearer token 获取当前用户。
|
||
- `POST /api/auth/logout`:吊销迁移期 session。
|
||
- `POST /api/auth/oauth/wechat-miniapp`:微信小程序 `code2Session` 登录,后端换取 openid/unionid,签发 `tk_` session。
|
||
- `POST /api/auth/oauth/wechat`:微信网页登录,后端用 code 换 access_token/openid,再拉取 userinfo,按 unionid 合并微信身份并签发 `tk_` session。
|
||
- `POST /api/auth/oauth/qq`:QQ 登录,后端完成 code 换 token、token 换 openid、拉取 userinfo,并签发 `tk_` session。
|
||
- `tenant_auth_providers`:租户级公开认证配置。
|
||
- `sms_verification_codes`:验证码审计表,不保存明文 code。
|
||
- `auth_login_events`:登录事件审计。
|
||
- `app_private.auth_sessions`:迁移期 session token hash。
|
||
- 阿里云短信 `SendSms` provider:使用租户级 AccessKey、签名、模板发送。
|
||
- 腾讯云短信 `SendSms` provider:使用租户级 SecretId/SecretKey、SdkAppId、签名、模板发送。
|
||
- `POST /api/commerce/payments/create`:按订单创建微信支付 JSAPI 或支付宝 WAP 支付参数。
|
||
- `POST /api/commerce/payments/notify/wechat_pay`:微信支付 API v3 通知验签、AES-GCM 解密、幂等落库和权益开通。
|
||
- `POST /api/commerce/payments/notify/alipay`:支付宝 RSA2 通知验签、幂等落库和权益开通。
|
||
|
||
## 短信 Provider
|
||
|
||
本地默认是 `AUTH_SMS_PROVIDER=mock`,仅开发环境返回 `debugCode`。生产环境只允许 `aliyun/aliyun-sms` 或 `tencent/tencent-sms`;如果仍为 mock 或写成未知 provider,API 启动和 `readiness:production` 都会直接失败。
|
||
|
||
真实 provider:
|
||
|
||
- `aliyun` / `aliyun-sms`:阿里云短信 `SendSms`,需要 AccessKey、签名、模板 ID。
|
||
- `tencent` / `tencent-sms`:腾讯云短信 `SendSms`,需要 SecretId、SecretKey、SdkAppId、签名、模板 ID。
|
||
|
||
密钥策略:
|
||
|
||
- AccessKey/SecretKey 不进入 `tenant_settings.public_config`。
|
||
- 租户级密钥写 `app_private.tenant_secrets(secret_scope='sms')` 或生产 Vault。
|
||
- 前端只能看到 provider 是否启用、签名展示名、隐私协议链接等非敏感配置。
|
||
- provider endpoint 默认使用官方域名,生产环境只允许 HTTPS 官方域名;本地测试可使用 `localhost/127.0.0.1` fake server。
|
||
- `readiness:production:db` 会检查 active/testing 短信 provider 的 `signName/templateCode` 或 `smsSdkAppId/signName/templateId`,并阻断公开配置里的 secret-like 字段。
|
||
|
||
### 阿里云短信配置示例
|
||
|
||
```json
|
||
{
|
||
"provider": "aliyun",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"signName": "工学教育",
|
||
"templateCode": "SMS_123456789",
|
||
"regionId": "cn-hangzhou"
|
||
},
|
||
"secret": {
|
||
"secretScope": "sms",
|
||
"secretKey": "aliyun",
|
||
"secretJson": {
|
||
"accessKeyId": "LTAI...",
|
||
"accessKeySecret": "..."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 腾讯云短信配置示例
|
||
|
||
```json
|
||
{
|
||
"provider": "tencent",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"smsSdkAppId": "1400000000",
|
||
"signName": "工学教育",
|
||
"templateId": "123456",
|
||
"region": "ap-guangzhou",
|
||
"templateParamSet": ["{code}"]
|
||
},
|
||
"secret": {
|
||
"secretScope": "sms",
|
||
"secretKey": "tencent",
|
||
"secretJson": {
|
||
"secretId": "AKID...",
|
||
"secretKey": "..."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 微信/QQ 登录
|
||
|
||
微信小程序登录应由前端传 `wx.login` code 到 `/api/auth/oauth/wechat-miniapp`,后端调用微信 `code2Session` 换取 openid/session_key/unionid,再落 `user_identities`。
|
||
|
||
已实现微信小程序登录主链路:
|
||
|
||
- 前端传 `code` 和可选 `profile`。
|
||
- 后端读取租户 `wechat-miniapp` / `wechat_miniapp` provider 配置。
|
||
- `appId` 存在 `config_public`,`appSecret` 存在 `app_private.tenant_secrets(secret_scope='oauth')`。
|
||
- `provider_subject` 使用 `appId:openid`,避免不同小程序 openid 碰撞。
|
||
- `session_key` 不返回前端,不写入公开 `raw_profile`;当前只记录 `hasSessionKey` 和更新时间标记。
|
||
- 登录成功写 `auth_login_events`,并签发 `tk_` session。
|
||
|
||
配置示例:
|
||
|
||
```json
|
||
{
|
||
"provider": "wechat-miniapp",
|
||
"displayName": "微信小程序登录",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"appId": "wx...",
|
||
"envVersion": "release"
|
||
},
|
||
"secret": {
|
||
"secretScope": "oauth",
|
||
"secretKey": "wechat-miniapp",
|
||
"secretValue": "小程序 AppSecret"
|
||
}
|
||
}
|
||
```
|
||
|
||
微信网页登录和 QQ 登录已按同样原则落地:前端只提交授权 code,后端完成 code 换 token、获取 openid/unionid、验错、账号合并和登录事件审计。响应不会返回 `access_token`、`refresh_token`、`session_key` 或租户密钥。旧 PocketBase hooks 中的邀请码/销售归属逻辑后续应拆到 `referral` feature,不继续堆在 auth 模块里。
|
||
|
||
微信网页登录配置示例:
|
||
|
||
```json
|
||
{
|
||
"provider": "wechat-web",
|
||
"displayName": "微信网页登录",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"appId": "wx...",
|
||
"endpoint": "https://api.weixin.qq.com/sns/oauth2/access_token",
|
||
"userInfoEndpoint": "https://api.weixin.qq.com/sns/userinfo"
|
||
},
|
||
"secret": {
|
||
"secretScope": "oauth",
|
||
"secretKey": "wechat-web",
|
||
"secretValue": "网站应用 AppSecret"
|
||
}
|
||
}
|
||
```
|
||
|
||
QQ 登录配置示例:
|
||
|
||
```json
|
||
{
|
||
"provider": "qq-oauth",
|
||
"displayName": "QQ登录",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"appId": "101xxxx",
|
||
"redirectUri": "https://h5.example.com/auth/qq/callback",
|
||
"endpoint": "https://graph.qq.com/oauth2.0/token",
|
||
"openIdEndpoint": "https://graph.qq.com/oauth2.0/me",
|
||
"userInfoEndpoint": "https://graph.qq.com/user/get_user_info"
|
||
},
|
||
"secret": {
|
||
"secretScope": "oauth",
|
||
"secretKey": "qq-oauth",
|
||
"secretValue": "QQ互联 AppKey"
|
||
}
|
||
}
|
||
```
|
||
|
||
前端调用:
|
||
|
||
```text
|
||
POST /api/auth/oauth/wechat
|
||
body: { "code": "<wechat oauth code>", "lang": "zh_CN" }
|
||
|
||
POST /api/auth/oauth/qq
|
||
body: { "code": "<qq oauth code>", "redirectUri": "https://h5.example.com/auth/qq/callback" }
|
||
```
|
||
|
||
生产注意:
|
||
|
||
- `endpoint`、`userInfoEndpoint`、`openIdEndpoint` 默认使用官方 HTTPS 域名;生产环境不允许本地 HTTP fake endpoint。
|
||
- `appSecret/clientSecret/AppKey` 必须存入 `app_private.tenant_secrets(secret_scope='oauth')`,不能进入 `configPublic`。
|
||
- QQ 的 `redirectUri` 必须与 QQ 互联后台登记的一致;多租户自定义域名上线前需要逐个配置或设计统一授权中转域名。
|
||
- `readiness:production:db` 会阻断 OAuth endpoint/redirectUri 为 HTTP、localhost 或非官方 provider endpoint,也会阻断 active/testing provider 缺少对应私密 `tenant_secrets`。
|
||
|
||
## 手机号绑定/换绑
|
||
|
||
已实现 `POST /api/auth/phone/bind`。适用场景包括微信/QQ 登录后强制绑定手机号,以及个人中心更换手机号。
|
||
|
||
流程:
|
||
|
||
1. 前端调用 `POST /api/auth/sms/send`,`purpose` 必须是 `bind_phone`。
|
||
2. 前端在登录态下调用 `POST /api/auth/phone/bind`,提交 `phone` 和验证码。
|
||
3. 后端校验当前 session/JWT 属于当前租户,不信任 `x-user-id`。
|
||
4. 后端校验手机号未被其它账号占用,成功后更新 `platform_users.phone` 和 `user_identities(provider='phone')`。
|
||
5. 换绑成功会删除当前用户旧手机号 identity,并撤销其它迁移期 `tk_` session;当前 session 保持可用。
|
||
|
||
前端不能用 `login` 用途验证码绑定手机号;接口会返回 `PHONE_BIND_PURPOSE_REQUIRED`。手机号已被其它账号占用时返回 `PHONE_ALREADY_BOUND`。
|
||
|
||
## 支付 Provider
|
||
|
||
支付不走 Supabase 内置能力。推荐继续扩展 `commerce`:
|
||
|
||
- `POST /api/commerce/orders` 只负责创建订单,金额以后端套餐为准。
|
||
- `POST /api/commerce/payments/create` 按 provider 创建支付参数或收银台地址。
|
||
- `POST /api/commerce/payments/notify/<provider>` 统一落 `payment_events`,先验签、再幂等、再更新订单和权益。
|
||
- 支付成功继续复用 `grantSvipEntitlement`,避免微信/支付宝/XPay 各写一套开通逻辑。
|
||
|
||
当前支持:
|
||
|
||
- `wechat_pay`:微信支付 API v3 JSAPI 下单;通知验签后用 API v3 key 解密 `resource`。
|
||
- `alipay`:支付宝 WAP/H5 支付参数生成;通知按 RSA2 验签。
|
||
- `manual`:仅本地/运营手工确认,不作为生产自动支付。
|
||
|
||
### 微信支付配置示例
|
||
|
||
```json
|
||
{
|
||
"provider": "wechat_pay",
|
||
"mode": "tenant_collect",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"appId": "wx...",
|
||
"merchantId": "1900000001",
|
||
"merchantSerialNo": "商户证书序列号",
|
||
"notifyUrl": "https://api.example.com/api/commerce/payments/notify/wechat_pay?tenantId=<tenantId>",
|
||
"wechatpayPublicKey": "微信支付平台证书公钥或平台公钥"
|
||
},
|
||
"secret": {
|
||
"secretScope": "payment",
|
||
"secretKey": "wechat_pay",
|
||
"secretJson": {
|
||
"privateKey": "商户 API 证书私钥 PEM",
|
||
"apiV3Key": "32位 API v3 key"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 支付宝配置示例
|
||
|
||
```json
|
||
{
|
||
"provider": "alipay",
|
||
"mode": "tenant_collect",
|
||
"status": "active",
|
||
"configPublic": {
|
||
"appId": "2021000000000000",
|
||
"notifyUrl": "https://api.example.com/api/commerce/payments/notify/alipay?tenantId=<tenantId>",
|
||
"returnUrl": "https://h5.example.com/pay/success"
|
||
},
|
||
"secret": {
|
||
"secretScope": "payment",
|
||
"secretKey": "alipay",
|
||
"secretJson": {
|
||
"privateKey": "应用私钥 PEM",
|
||
"alipayPublicKey": "支付宝公钥 PEM"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
支付回调处理规则:
|
||
|
||
- `payment_events(provider,event_id)` 幂等。
|
||
- 通知验签/解密失败直接拒绝,不更新订单。
|
||
- 通知金额必须等于后端订单金额。
|
||
- 订单已支付时重复通知只返回幂等成功,不重复开通权益。
|
||
- 支付成功事务内更新 `orders`、`payments`、`payment_events`、`entitlements`。
|
||
- 支付密钥只允许放在 `app_private.tenant_secrets` 或生产 KMS/Vault。
|
||
- `readiness:production:db` 会阻断微信支付缺 `appId/merchantId/merchantSerialNo/notifyUrl`、支付宝缺 `appId/notifyUrl`、notifyUrl/returnUrl/退款回调为 HTTP 或 localhost、provider endpoint 非官方域名、公开配置混入 `privateKey/apiV3Key/secret/token` 等密钥字段。
|
||
|
||
### 资金对账
|
||
|
||
当前后端已提供租户级资金对账基础能力:
|
||
|
||
- `POST /api/commerce/reconciliation/preview`:预览供应商账单行和本地订单/支付/退款的匹配结果。
|
||
- `POST /api/commerce/reconciliation/import`:确认导入对账批次和明细,写入审计。
|
||
- `GET /api/commerce/reconciliation/batches`:查询对账批次。
|
||
- `GET /api/commerce/reconciliation/items`:查询逐行结果。
|
||
- `GET /api/commerce/reconciliation/anomalies`:查询金额不一致、状态不一致、供应商有本地无、本地有供应商无、重复行等异常。
|
||
- `POST /api/commerce/reconciliation/provider-bills/request`:创建微信/支付宝官方账单下载任务,body 为 `{ provider, billDate, billType }`。
|
||
- `GET /api/commerce/reconciliation/provider-bills/jobs`:查看官方账单下载任务状态、下载域名、hash、行数和生成的对账批次 ID。
|
||
- `POST /api/commerce/reconciliation/issues/create`:从异常明细创建差错工单,重复创建同一未关闭明细会返回现有工单。
|
||
- `GET /api/commerce/reconciliation/issues`:按状态、严重级别、负责人、批次、订单号筛选差错工单。
|
||
- `POST /api/commerce/reconciliation/issues/status`:执行 `start/assign/resolve/ignore/escalate/reopen` 状态流转。
|
||
- `GET /api/commerce/reconciliation/issues/events`:查看工单创建、分配、处理、解决、忽略、重开等事件轨迹。
|
||
- `GET /api/commerce/operations/anomalies`:聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款,供租户财务/售后运营台使用。
|
||
- `GET /api/commerce/adjustment-vouchers`:查询人工调整凭证。
|
||
- `POST /api/commerce/adjustment-vouchers`:创建人工调整凭证,可关联差错工单、对账明细、订单、支付或退款。
|
||
- `POST /api/commerce/adjustment-vouchers/status`:执行 `submitted/approved/rejected/voided` 复核流转。
|
||
- `GET /api/commerce/adjustment-vouchers/events`:查看凭证事件轨迹。
|
||
- `GET /api/commerce/adjustment-vouchers/report`:按日期输出凭证状态、类型、来源和日趋势统计。
|
||
|
||
权限点:
|
||
|
||
- `tenant:reconciliation:read`:查看/预览对账。
|
||
- `tenant:reconciliation:write`:导入对账批次、创建/处理差错工单。
|
||
- `tenant:reconciliation:review`:审批、驳回或作废人工调整凭证。
|
||
- `tenant:reconciliation:download`:创建微信/支付宝官方账单下载任务。
|
||
|
||
官方账单下载由 `apps/worker --job provider-bills` 执行。API 只创建 `commerce_bill_download_jobs`,不会在前端返回供应商 `download_url`、商户私钥、微信 API v3 key 或支付宝应用私钥。worker 使用租户 `tenant_payment_accounts.config_public.secretRef` 找到 `app_private.tenant_secrets(secret_scope='payment')`,在后端签名申请下载 URL,校验微信返回的 `hash_type/hash_value`,解析 JSON/CSV/ZIP 账单后复用 `importReconciliationBatch` 写入 `commerce_reconciliation_batches/items`,`source='provider_download'`。
|
||
|
||
对账、差错工单和人工调整凭证只生成差异台账、处理记录、凭证附件引用和审计,不自动修改订单、支付、退款和权益。`resolve/ignore` 只是财务审核结论,例如 `manual_adjustment`、`provider_confirmed`、`false_positive`;调整凭证审批也只表示财务复核通过。最终落账仍必须走退款状态机、支付补偿、手工支付确认或后续专门的落账命令。真实生产账单格式抽样验收和前端财务操作台仍需要继续补。
|
||
|
||
B 端合作商年费、服务费、服务器资源费不走学生端 `orders`,而是走平台账务:
|
||
|
||
- `platform_saas_plans`:平台售卖给合作商的 SaaS 套餐。
|
||
- `tenant_subscriptions`:合作商当前订阅状态。
|
||
- `tenant_invoices`、`tenant_invoice_items`:合作商账单与明细。
|
||
- `tenant_invoice_payments`:合作商账单收款记录。
|
||
- `tenant_usage_records`:学生数、题量、存储等用量指标。
|
||
|
||
支持策略:
|
||
|
||
- 平台代收:平台商户号收款,再给租户结算。
|
||
- 租户自收:每个租户配置自己的商户号和密钥。
|
||
- 服务商模式:平台服务商统一管理子商户。
|
||
|
||
真实接入前需要先明确微信支付、支付宝或聚合支付是否允许你们销售的题库会员形态,以及小程序端是否涉及虚拟支付限制。
|
||
|
||
本地开发可以先把这些配置留空或配置为 inactive。租户后台保存 provider 配置时只写非敏感公开字段和 `secretRef`;等云服务器域名、备案、官方应用/商户申请完成后,再在租户后台或平台后台写入真实密钥并执行生产联调。
|
||
|
||
## 需要准备的资料
|
||
|
||
- 阿里云或腾讯云短信:签名、模板 ID、AccessKey/SecretKey、短信用途文案。
|
||
- 微信小程序:AppID、AppSecret、主体信息、合法域名、用户手机号授权能力。
|
||
- 微信网页/公众号:AppID、AppSecret、授权回调域名。
|
||
- QQ 互联:AppID、AppKey、回调域名。
|
||
- 微信支付:商户号、API v3 key、商户证书/平台证书、回调域名、AppID 绑定关系。
|
||
- 支付宝:AppID、应用私钥、支付宝公钥、回调地址、网页/手机网站/当面付产品开通情况。
|
||
- 每个合作商租户的收款模式:平台代收、租户自收或服务商子商户。
|
||
|
||
## 参考资料
|
||
|
||
- Supabase Phone Login: https://supabase.com/docs/guides/auth/phone-login
|
||
- Supabase Auth Hooks: https://supabase.com/docs/guides/auth/auth-hooks
|
||
- Supabase Social Login: https://supabase.com/docs/guides/auth/social-login
|
||
- 阿里云短信 SendSms: https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms
|
||
- 腾讯云短信 SendSms: https://cloud.tencent.com/document/api/382/55981
|
||
- 微信小程序登录 code2Session: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
|
||
- 微信开放平台网站应用微信登录: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
|
||
- QQ 互联 OAuth: https://wiki.connect.qq.com/oauth2-0简介
|
||
- 微信支付 API v3: https://pay.weixin.qq.com/doc/v3/merchant/4012791855
|
||
- 支付宝开放平台: https://opendocs.alipay.com/
|