Files
tyapi-server/internal/domains/api/services/query_whitelist_service.go
2026-06-18 21:16:02 +08:00

186 lines
5.1 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 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()
}