Files
wangjia fd5f56f7de feat: 微信V3 + 支付宝手机站/UA自适应 + 业务对接(签名下单/webhook) + 通用多业务
- 微信支付 V3 Native 渠道 (wechat.go):Native下单/回调AES-GCM解密验签/查单
- 支付宝:手机网站支付 wap.pay + 按 UA 自适应(PC page.pay扫码 / 手机拉App);qr_pay_mode=2 完整扫码收银台
- 业务对接:下单接口扩展(biz_system/biz_ref/return_url)+ HMAC 签名鉴权;支付成功 webhook 主动推送业务方 + 60s 重试 + BizNotifyLog
- 通用多业务:config.biz 改 map,加业务只改配置(BIZ_<SYS>_SECRET/_CALLBACK_URL)
- seedPlans:四档真实套餐 + promo_first_month(¥1) + test_liandiao(0.01),均挂 biz_code;/products 暴露 biz_code
- 删除沙箱 pay.html;对接设计文档入 docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
2026-07-03 22:44:34 +08:00

83 lines
2.4 KiB
Go
Raw Permalink 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.
package channel
import (
"fmt"
"sync"
"gorm.io/gorm"
"github.com/wangjia/pay/internal/model"
)
// Registry 按商户缓存已构造的渠道实例,避免每次下单都重新加载密钥。
type Registry struct {
db *gorm.DB
mu sync.RWMutex
cache map[uint64]Channel
}
func NewRegistry(db *gorm.DB) *Registry {
return &Registry{db: db, cache: make(map[uint64]Channel)}
}
// ByMerchantID 取(或构造)指定商户的渠道。
func (r *Registry) ByMerchantID(id uint64) (Channel, *model.Merchant, error) {
var m model.Merchant
if err := r.db.First(&m, "id = ? AND enabled = ?", id, true).Error; err != nil {
return nil, nil, fmt.Errorf("商户不存在或已停用: %w", err)
}
ch, err := r.get(&m)
return ch, &m, err
}
// AlipayByAppID 异步回调时用 app_id 反查商户并取其渠道(用于验签)。
func (r *Registry) AlipayByAppID(appID string) (Channel, *model.Merchant, error) {
if appID == "" {
return nil, nil, fmt.Errorf("回调缺少 app_id")
}
var m model.Merchant
if err := r.db.First(&m, "channel = ? AND app_id = ? AND enabled = ?", "alipay", appID, true).Error; err != nil {
return nil, nil, fmt.Errorf("找不到 app_id=%s 对应的支付宝商户: %w", appID, err)
}
ch, err := r.get(&m)
return ch, &m, err
}
// FirstWechat 取一个启用中的微信商户及其渠道,用于回调解密验签。
// 微信 V3 回调正文加密,需先用商户凭证(平台证书 + APIv3 密钥)解密才能拿到 out_trade_no
// 同一公司主体共用一个 mch_id/apiv3_key/证书,故取任一启用的微信商户即可解密,
// 解密后再按 out_trade_no 定位到订单真正归属的商户入账。
func (r *Registry) FirstWechat() (Channel, *model.Merchant, error) {
var m model.Merchant
if err := r.db.First(&m, "channel = ? AND enabled = ?", "wechat", true).Error; err != nil {
return nil, nil, fmt.Errorf("没有启用的微信商户: %w", err)
}
ch, err := r.get(&m)
return ch, &m, err
}
func (r *Registry) get(m *model.Merchant) (Channel, error) {
r.mu.RLock()
if ch, ok := r.cache[m.ID]; ok {
r.mu.RUnlock()
return ch, nil
}
r.mu.RUnlock()
ch, err := Build(m)
if err != nil {
return nil, err
}
r.mu.Lock()
r.cache[m.ID] = ch
r.mu.Unlock()
return ch, nil
}
// Invalidate 商户凭证变更后清缓存(预留给管理接口)。
func (r *Registry) Invalidate(id uint64) {
r.mu.Lock()
delete(r.cache, id)
r.mu.Unlock()
}