Files
pay-bridge/backend/internal/service/channel.go
2026-03-13 15:51:59 +08:00

141 lines
3.4 KiB
Go
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.
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
}