fd5f56f7de
- 微信支付 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
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
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()
|
||
}
|