141 lines
3.4 KiB
Go
141 lines
3.4 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync"
|
||
"time"
|
||
|
||
"pay-bridge/internal/channel"
|
||
"pay-bridge/internal/model"
|
||
"pay-bridge/internal/repository"
|
||
"pay-bridge/pkg/config"
|
||
"pay-bridge/pkg/crypto"
|
||
)
|
||
|
||
const channelCacheTTL = 5 * time.Minute
|
||
|
||
type cachedChannel struct {
|
||
ch channel.PaymentChannel
|
||
expiresAt time.Time
|
||
}
|
||
|
||
// ChannelService 渠道服务(负责加载渠道配置并获取渠道实例)
|
||
type ChannelService struct {
|
||
repo *repository.ChannelConfigRepository
|
||
encKey string
|
||
urlsCfg config.ChannelsConfig
|
||
mu sync.Mutex
|
||
cache map[string]*cachedChannel
|
||
}
|
||
|
||
func NewChannelService(repo *repository.ChannelConfigRepository, encKey string, urlsCfg config.ChannelsConfig) *ChannelService {
|
||
return &ChannelService{
|
||
repo: repo,
|
||
encKey: encKey,
|
||
urlsCfg: urlsCfg,
|
||
cache: make(map[string]*cachedChannel),
|
||
}
|
||
}
|
||
|
||
// GetChannel 根据 appID 和渠道码获取渠道适配器实例(5 分钟内存缓存)
|
||
func (s *ChannelService) GetChannel(ctx context.Context, appID, channelCode string) (channel.PaymentChannel, error) {
|
||
cacheKey := appID + ":" + channelCode
|
||
|
||
s.mu.Lock()
|
||
if entry, ok := s.cache[cacheKey]; ok && time.Now().Before(entry.expiresAt) {
|
||
ch := entry.ch
|
||
s.mu.Unlock()
|
||
return ch, nil
|
||
}
|
||
s.mu.Unlock()
|
||
|
||
cfg, err := s.repo.GetByAppChannel(ctx, appID, channelCode)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if cfg == nil {
|
||
return nil, fmt.Errorf("channel config not found: app=%s channel=%s", appID, channelCode)
|
||
}
|
||
|
||
decCfg, err := s.decryptConfig(cfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
ch, err := channel.Get(channelCode, decCfg, s.urlsFor(channelCode))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
s.mu.Lock()
|
||
s.cache[cacheKey] = &cachedChannel{ch: ch, expiresAt: time.Now().Add(channelCacheTTL)}
|
||
s.mu.Unlock()
|
||
|
||
return ch, nil
|
||
}
|
||
|
||
// InvalidateCache 使指定渠道的缓存失效(配置变更时调用)
|
||
func (s *ChannelService) InvalidateCache(appID, channelCode string) {
|
||
s.mu.Lock()
|
||
delete(s.cache, appID+":"+channelCode)
|
||
s.mu.Unlock()
|
||
}
|
||
|
||
// ListChannelCodes 获取应用下所有渠道码
|
||
func (s *ChannelService) ListChannelCodes(ctx context.Context, appID string) ([]string, error) {
|
||
cfgs, err := s.repo.ListByApp(ctx, appID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
codes := make([]string, 0, len(cfgs))
|
||
for _, c := range cfgs {
|
||
codes = append(codes, c.ChannelCode)
|
||
}
|
||
return codes, nil
|
||
}
|
||
|
||
// GetChannelConfig 获取渠道配置(已解密)
|
||
func (s *ChannelService) GetChannelConfig(ctx context.Context, appID, channelCode string) (*model.ChannelConfig, error) {
|
||
cfg, err := s.repo.GetByAppChannel(ctx, appID, channelCode)
|
||
if err != nil || cfg == nil {
|
||
return cfg, err
|
||
}
|
||
return s.decryptConfig(cfg)
|
||
}
|
||
|
||
// urlsFor 根据渠道码返回对应的网关地址配置
|
||
func (s *ChannelService) urlsFor(channelCode string) channel.URLs {
|
||
switch channelCode {
|
||
case "HEEPAY":
|
||
return channel.URLs{
|
||
PayURL: s.urlsCfg.Heepay.PayURL,
|
||
MerchantURL: s.urlsCfg.Heepay.MerchantURL,
|
||
}
|
||
default:
|
||
return channel.URLs{}
|
||
}
|
||
}
|
||
|
||
func (s *ChannelService) decryptConfig(cfg *model.ChannelConfig) (*model.ChannelConfig, error) {
|
||
copied := *cfg
|
||
|
||
if cfg.APIKey != "" {
|
||
dec, err := crypto.Decrypt(cfg.APIKey, s.encKey)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("decrypt api_key: %w", err)
|
||
}
|
||
copied.APIKey = dec
|
||
}
|
||
|
||
if cfg.PrivateKey != "" {
|
||
dec, err := crypto.Decrypt(cfg.PrivateKey, s.encKey)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("decrypt private_key: %w", err)
|
||
}
|
||
copied.PrivateKey = dec
|
||
}
|
||
|
||
return &copied, nil
|
||
}
|