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 }