Files
gongxue-base/docs/refactor/auth-payment-provider-plan.md

339 lines
15 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.

# 国内认证与支付接入方案
## 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 能跑通端到端流程。
## 当前已实现
- `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`。生产环境如果仍为 mock会直接拒绝发送。
真实 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。
### 阿里云短信配置示例
```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 互联后台登记的一致;多租户自定义域名上线前需要逐个配置或设计统一授权中转域名。
## 手机号绑定/换绑
已实现 `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。
### 资金对账
当前后端已提供租户级资金对账基础能力:
- `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`:查看工单创建、分配、处理、解决、忽略、重开等事件轨迹。
权限点:
- `tenant:reconciliation:read`:查看/预览对账。
- `tenant:reconciliation:write`:导入对账批次、创建/处理差错工单。
- `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`:学生数、题量、存储等用量指标。
支持策略:
- 平台代收:平台商户号收款,再给租户结算。
- 租户自收:每个租户配置自己的商户号和密钥。
- 服务商模式:平台服务商统一管理子商户。
真实接入前需要先明确微信支付、支付宝或聚合支付是否允许你们销售的题库会员形态,以及小程序端是否涉及虚拟支付限制。
## 需要准备的资料
- 阿里云或腾讯云短信:签名、模板 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/