package services import ( "context" "sync" "sync/atomic" "time" "tyapi-server/internal/domains/api/entities" "tyapi-server/internal/domains/api/repositories" "go.uber.org/zap" ) type QueryWhitelistService interface { ShouldReturnEmpty(ctx context.Context, userID, apiCode string, params map[string]interface{}) bool InvalidateCache(userID, idCardHash string) InvalidateAllCache() } // queryWhitelistSnapshot 全量 enabled 规则快照,按 id_card_hash 索引,热路径只读内存。 type queryWhitelistSnapshot struct { byHash map[string][]*entities.QueryWhitelistEntry } type QueryWhitelistServiceImpl struct { repo repositories.QueryWhitelistRepository formConfigService FormConfigService logger *zap.Logger snapshot atomic.Pointer[queryWhitelistSnapshot] snapshotMu sync.Mutex // apiCode -> 是否要求身份证入参(FormConfig 反射结果,进程内永久缓存) identityAPICache sync.Map } func NewQueryWhitelistService( repo repositories.QueryWhitelistRepository, formConfigService FormConfigService, logger *zap.Logger, ) QueryWhitelistService { s := &QueryWhitelistServiceImpl{ repo: repo, formConfigService: formConfigService, logger: logger, } return s } // ShouldReturnEmpty 检查是否应返回「查询为空」。 // 热路径:入参提取 → API 类型缓存 → 内存快照匹配,不逐请求查库。 func (s *QueryWhitelistServiceImpl) ShouldReturnEmpty( ctx context.Context, userID, apiCode string, params map[string]interface{}, ) bool { identity := ExtractIdentityParams(params) if !identity.OK { return false } if !s.requiresIdentityInput(ctx, apiCode) { return false } idCardHash := HashIDCard(identity.IDCard) entries, err := s.lookupEntries(ctx, userID, idCardHash) if err != nil { s.logger.Error("查询白名单快照失败", zap.Error(err), zap.String("user_id", userID)) return false } for _, entry := range entries { if !entry.IsEnabled() { continue } if !entry.MatchesAPICode(apiCode) { continue } if !entry.MatchesName(identity.Name) { continue } s.logger.Info("命中查询白名单", zap.String("user_id", userID), zap.String("api_code", apiCode), zap.String("whitelist_id", entry.ID), zap.Bool("is_global", entry.IsGlobal()), ) return true } return false } func (s *QueryWhitelistServiceImpl) requiresIdentityInput(ctx context.Context, apiCode string) bool { if s.formConfigService == nil { return false } if cached, ok := s.identityAPICache.Load(apiCode); ok { return cached.(bool) } result := s.formConfigService.RequiresIdentityInput(ctx, apiCode) s.identityAPICache.Store(apiCode, result) return result } func (s *QueryWhitelistServiceImpl) lookupEntries(ctx context.Context, userID, idCardHash string) ([]*entities.QueryWhitelistEntry, error) { snap, err := s.getSnapshot(ctx) if err != nil { return nil, err } candidates := snap.byHash[idCardHash] if len(candidates) == 0 { return nil, nil } result := make([]*entities.QueryWhitelistEntry, 0, len(candidates)) for _, entry := range candidates { if entry.UserID == userID || entry.UserID == entities.QueryWhitelistGlobalUserID { result = append(result, entry) } } return result, nil } func (s *QueryWhitelistServiceImpl) getSnapshot(ctx context.Context) (*queryWhitelistSnapshot, error) { if snap := s.snapshot.Load(); snap != nil { return snap, nil } return s.reloadSnapshot(ctx) } func (s *QueryWhitelistServiceImpl) reloadSnapshot(ctx context.Context) (*queryWhitelistSnapshot, error) { s.snapshotMu.Lock() defer s.snapshotMu.Unlock() if snap := s.snapshot.Load(); snap != nil { return snap, nil } entries, err := s.repo.FindAllEnabled(ctx) if err != nil { return nil, err } byHash := make(map[string][]*entities.QueryWhitelistEntry, len(entries)) for _, entry := range entries { byHash[entry.IDCardHash] = append(byHash[entry.IDCardHash], entry) } snap := &queryWhitelistSnapshot{byHash: byHash} s.snapshot.Store(snap) s.logger.Info("查询白名单快照已加载", zap.Int("entries", len(entries)), zap.Int("hash_buckets", len(byHash))) return snap, nil } // refreshSnapshotAsync 管理端变更后异步刷新,避免阻塞写请求。 func (s *QueryWhitelistServiceImpl) refreshSnapshotAsync() { go func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() s.snapshotMu.Lock() defer s.snapshotMu.Unlock() s.snapshot.Store(nil) entries, err := s.repo.FindAllEnabled(ctx) if err != nil { s.logger.Error("刷新查询白名单快照失败", zap.Error(err)) return } byHash := make(map[string][]*entities.QueryWhitelistEntry, len(entries)) for _, entry := range entries { byHash[entry.IDCardHash] = append(byHash[entry.IDCardHash], entry) } s.snapshot.Store(&queryWhitelistSnapshot{byHash: byHash}) s.logger.Info("查询白名单快照已刷新", zap.Int("entries", len(entries))) }() } func (s *QueryWhitelistServiceImpl) InvalidateCache(_ string, _ string) { s.refreshSnapshotAsync() } func (s *QueryWhitelistServiceImpl) InvalidateAllCache() { s.refreshSnapshotAsync() }