This commit is contained in:
2026-06-20 20:49:21 +08:00
16 changed files with 254 additions and 153 deletions

View File

@@ -370,10 +370,9 @@ func extractParentAccessID(params map[string]interface{}) (string, bool) {
// callExternalApi 同步调用外部API // callExternalApi 同步调用外部API
func (s *ApiApplicationServiceImpl) callExternalApi(ctx context.Context, cmd *commands.ApiCallCommand, validation *dto.ApiCallValidationResult) (string, error) { func (s *ApiApplicationServiceImpl) callExternalApi(ctx context.Context, cmd *commands.ApiCallCommand, validation *dto.ApiCallValidationResult) (string, error) {
// 查询白名单拦截:命中则返回「查询为空」,不调用上游、不扣费 // 查询白名单:应用层判断入参是否命中,并将命中的 api_codes 写入 context
if s.queryWhitelistSvc != nil && if s.queryWhitelistSvc != nil {
s.queryWhitelistSvc.ShouldReturnEmpty(ctx, validation.GetUserID(), cmd.ApiName, validation.RequestParams) { ctx = s.queryWhitelistSvc.EnrichContext(ctx, validation.GetUserID(), validation.RequestParams)
return "", ErrQueryEmpty
} }
// 创建CallContext // 创建CallContext

View File

@@ -427,13 +427,26 @@ func registerAllProcessors(combService *comb.CombService) {
"PDFG01GZ": pdfg.ProcessPDFG01GZRequest, "PDFG01GZ": pdfg.ProcessPDFG01GZRequest,
} }
// 批量注册到组合包服务 // 批量注册到组合包服务(包装白名单:读 ctx 中命中的 api_code嵌套子调用按子 api_code 判断)
for apiCode, processor := range processorMap { for apiCode, processor := range processorMap {
combService.RegisterProcessor(apiCode, processor) wrapped := wrapProcessorWithWhitelist(apiCode, processor)
combService.RegisterProcessor(apiCode, wrapped)
} }
// 同时设置全局处理器映射 // 同时设置全局处理器映射
RequestProcessors = processorMap RequestProcessors = make(map[string]processors.ProcessorFunc, len(processorMap))
for apiCode, processor := range processorMap {
RequestProcessors[apiCode] = wrapProcessorWithWhitelist(apiCode, processor)
}
}
func wrapProcessorWithWhitelist(apiCode string, processor processors.ProcessorFunc) processors.ProcessorFunc {
return func(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
if processors.WhitelistShouldReturnEmpty(ctx, apiCode) {
return nil, processors.ErrNotFound
}
return processor(ctx, params, deps)
}
} }
// 注册API处理器 - 现在通过registerAllProcessors统一管理 // 注册API处理器 - 现在通过registerAllProcessors统一管理

View File

@@ -255,28 +255,17 @@ func collectAPIData(ctx context.Context, params dto.DWBG8B4DReq, deps *processor
return apiData return apiData
} }
// callProcessor 调用指定的处理器 // callProcessor 调用指定的处理器(走注册表,含白名单包装)
func callProcessor(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) (interface{}, error) { func callProcessor(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) (interface{}, error) {
// 通过CombService获取处理器 respBytes, err := processors.InvokeRegisteredProcessor(ctx, apiCode, params, deps)
if combSvc, ok := deps.CombService.(interface { if err != nil {
GetProcessor(apiCode string) (processors.ProcessorFunc, bool) return nil, err
}); ok {
processor, exists := combSvc.GetProcessor(apiCode)
if !exists {
return nil, fmt.Errorf("未找到处理器: %s", apiCode)
}
respBytes, err := processor(ctx, params, deps)
if err != nil {
return nil, err
}
var data interface{}
if err := json.Unmarshal(respBytes, &data); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
return data, nil
} }
var data interface{}
return nil, fmt.Errorf("无法获取处理器: %sCombService不支持GetProcessor方法", apiCode) if err := json.Unmarshal(respBytes, &data); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
return data, nil
} }
// exportAPIDataToJSON 将API数据导出为JSON文件方便调试 // exportAPIDataToJSON 将API数据导出为JSON文件方便调试

View File

@@ -0,0 +1,32 @@
package processors
import (
"context"
"fmt"
)
// ProcessorRegistry 已注册处理器查询(由 CombService 实现)
type ProcessorRegistry interface {
GetProcessor(apiCode string) (ProcessorFunc, bool)
}
// InvokeRegisteredProcessor 通过注册表调用处理器(含白名单包装),聚合处理器内部转接应使用此方法。
func InvokeRegisteredProcessor(
ctx context.Context,
apiCode string,
params []byte,
deps *ProcessorDependencies,
) ([]byte, error) {
if deps == nil || deps.CombService == nil {
return nil, fmt.Errorf("CombService 未配置,无法调用处理器: %s", apiCode)
}
registry, ok := deps.CombService.(ProcessorRegistry)
if !ok {
return nil, fmt.Errorf("CombService 不支持 GetProcessor无法调用处理器: %s", apiCode)
}
processor, exists := registry.GetProcessor(apiCode)
if !exists {
return nil, fmt.Errorf("未找到处理器: %s", apiCode)
}
return processor(ctx, params, deps)
}

View File

@@ -349,29 +349,17 @@ func collectAPIData(ctx context.Context, params dto.PDFG01GZReq, deps *processor
return apiData return apiData
} }
// callProcessor 调用指定的处理器 // callProcessor 调用指定的处理器(走注册表,含白名单包装)
func callProcessor(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) (interface{}, error) { func callProcessor(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) (interface{}, error) {
// 通过CombService获取处理器 respBytes, err := processors.InvokeRegisteredProcessor(ctx, apiCode, params, deps)
if combSvc, ok := deps.CombService.(interface { if err != nil {
GetProcessor(apiCode string) (processors.ProcessorFunc, bool) return nil, err
}); ok {
processor, exists := combSvc.GetProcessor(apiCode)
if !exists {
return nil, fmt.Errorf("未找到处理器: %s", apiCode)
}
respBytes, err := processor(ctx, params, deps)
if err != nil {
return nil, err
}
var data interface{}
if err := json.Unmarshal(respBytes, &data); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
return data, nil
} }
var data interface{}
// 如果无法通过CombService获取返回错误 if err := json.Unmarshal(respBytes, &data); err != nil {
return nil, fmt.Errorf("无法获取处理器: %sCombService不支持GetProcessor方法", apiCode) return nil, fmt.Errorf("解析响应失败: %w", err)
}
return data, nil
} }
// formatDataForPDF 格式化数据为PDF生成服务需要的格式 // formatDataForPDF 格式化数据为PDF生成服务需要的格式

View File

@@ -25,7 +25,7 @@ func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) raw, err := processors.InvokeRegisteredProcessor(ctx, "QCXGM4CL", m4clParams, deps)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -28,7 +28,7 @@ func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) raw, err := processors.InvokeRegisteredProcessor(ctx, "QCXGM4CL", m4clParams, deps)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -28,7 +28,7 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) raw, err := processors.InvokeRegisteredProcessor(ctx, "QCXGM4CL", m4clParams, deps)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -50,7 +50,7 @@ func ProcessQYGL3F8ERequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
b4c0Response, err := ProcessQYGL6S1BRequest(ctx, b4c0ParamsBytes, deps) b4c0Response, err := processors.InvokeRegisteredProcessor(ctx, "QYGL6S1B", b4c0ParamsBytes, deps)
if err != nil { if err != nil {
log.Error("QYGL3F8E调用QYGL6S1B失败", zap.Error(err)) log.Error("QYGL3F8E调用QYGL6S1B失败", zap.Error(err))
return nil, err // 错误已经是处理器标准错误,直接返回 return nil, err // 错误已经是处理器标准错误,直接返回
@@ -620,25 +620,7 @@ func callProcessorSafely(ctx context.Context, processorType, entCode string, dep
} }
var response []byte var response []byte
switch processorType { response, err = processors.InvokeRegisteredProcessor(ctx, processorType, paramsBytes, deps)
case "QYGL5A3C":
response, err = ProcessQYGL5A3CRequest(ctx, paramsBytes, deps)
case "QYGL8B4D":
response, err = ProcessQYGL8B4DRequest(ctx, paramsBytes, deps)
case "QYGL9E2F":
response, err = ProcessQYGL9E2FRequest(ctx, paramsBytes, deps)
case "QYGL7C1A":
response, err = ProcessQYGL7C1ARequest(ctx, paramsBytes, deps)
case "QYGL7D9A":
response, err = ProcessQYGL7D9ARequest(ctx, paramsBytes, deps)
case "QYGL4B2E":
response, err = ProcessQYGL4B2ERequest(ctx, paramsBytes, deps)
default:
log.Warn("QYGL3F8E未知的处理器类型",
zap.String("processor_type", processorType),
)
return map[string]interface{}{}
}
if err != nil { if err != nil {
// 如果是查询为空错误,返回空对象 // 如果是查询为空错误,返回空对象
@@ -681,7 +663,7 @@ func callQYGL5S1IProcessorSafely(ctx context.Context, entCode string, entName st
if err != nil { if err != nil {
return map[string]interface{}{} return map[string]interface{}{}
} }
response, err := ProcessQYGL5S1IRequest(ctx, paramsBytes, deps) response, err := processors.InvokeRegisteredProcessor(ctx, "QYGL5S1I", paramsBytes, deps)
if err != nil { if err != nil {
return map[string]interface{}{} return map[string]interface{}{}
} }

View File

@@ -39,7 +39,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
resultsCh := make(chan apiResult, 7) resultsCh := make(chan apiResult, 7)
var wg sync.WaitGroup var wg sync.WaitGroup
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) { call := func(key, apiCode string, req interface{}) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
@@ -48,7 +48,7 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
resultsCh <- apiResult{key: key, err: err} resultsCh <- apiResult{key: key, err: err}
return return
} }
resp, err := fn(ctx, b, deps) resp, err := processors.InvokeRegisteredProcessor(ctx, apiCode, b, deps)
if err != nil { if err != nil {
resultsCh <- apiResult{key: key, err: err} resultsCh <- apiResult{key: key, err: err}
return return
@@ -70,46 +70,46 @@ func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors
} }
// 企业全量信息核验V2QYGLUY3S // 企业全量信息核验V2QYGLUY3S
call("jiguangFull", map[string]interface{}{ call("jiguangFull", "QYGLUY3S", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
"ent_code": p.EntCode, "ent_code": p.EntCode,
}, ProcessQYGLUY3SRequest) })
// 企业股权结构全景QYGLJ0Q1 // 企业股权结构全景QYGLJ0Q1
call("equityPanorama", map[string]interface{}{ call("equityPanorama", "QYGLJ0Q1", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
}, ProcessQYGLJ0Q1Request) })
// 企业司法涉诉V2QYGL5S1I // 企业司法涉诉V2QYGL5S1I
call("judicialCertFull", map[string]interface{}{ call("judicialCertFull", "QYGL5S1I", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
"ent_code": p.EntCode, "ent_code": p.EntCode,
}, ProcessQYGL5S1IRequest) })
// 企业年报信息核验QYGLDJ12 // 企业年报信息核验QYGLDJ12
call("annualReport", map[string]interface{}{ call("annualReport", "QYGLDJ12", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
"ent_code": p.EntCode, "ent_code": p.EntCode,
}, ProcessQYGLDJ12Request) })
// 企业税收违法核查QYGL8848 // 企业税收违法核查QYGL8848
call("taxViolation", map[string]interface{}{ call("taxViolation", "QYGL8848", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
"ent_code": p.EntCode, "ent_code": p.EntCode,
}, ProcessQYGL8848Request) })
// 欠税公告QYGL7D9A天眼查 OwnTaxkeyword 为统一社会信用代码) // 欠税公告QYGL7D9A天眼查 OwnTaxkeyword 为统一社会信用代码)
call("taxArrears", map[string]interface{}{ call("taxArrears", "QYGL7D9A", map[string]interface{}{
"ent_code": p.EntCode, "ent_code": p.EntCode,
"page_size": 20, "page_size": 20,
"page_num": 1, "page_num": 1,
}, ProcessQYGL7D9ARequest) })
// 企业进出口信用核查QYGLDJ33 // 企业进出口信用核查QYGLDJ33
call("customsCredit", map[string]interface{}{ call("customsCredit", "QYGLDJ33", map[string]interface{}{
"ent_name": p.EntName, "ent_name": p.EntName,
"ent_code": p.EntCode, "ent_code": p.EntCode,
}, ProcessQYGLDJ33Request) })
wg.Wait() wg.Wait()
close(resultsCh) close(resultsCh)

View File

@@ -0,0 +1,58 @@
package processors
import "context"
type whitelistContextKey struct{}
// WhitelistMatch 请求级白名单命中项(身份匹配,不含当前顶层 api_code 过滤)
type WhitelistMatch struct {
ID string
APICodes []string
IsGlobal bool
}
// WhitelistContext 写入 context 的白名单状态,供各处理器按 api_code 判断是否返回查询为空
type WhitelistContext struct {
Matches []WhitelistMatch
}
// WithWhitelistContext 将白名单命中结果写入 context
func WithWhitelistContext(ctx context.Context, matches []WhitelistMatch) context.Context {
if len(matches) == 0 {
return ctx
}
return context.WithValue(ctx, whitelistContextKey{}, &WhitelistContext{Matches: matches})
}
// WhitelistFromContext 从 context 读取白名单状态
func WhitelistFromContext(ctx context.Context) *WhitelistContext {
wc, ok := ctx.Value(whitelistContextKey{}).(*WhitelistContext)
if !ok || wc == nil {
return nil
}
return wc
}
// WhitelistShouldReturnEmpty 根据 context 中的白名单与当前 api_code 判断是否应返回查询为空。
// 入参是否命中、命中哪些 api_code 由应用层 EnrichContext 写入;此处仅读 ctx。
func WhitelistShouldReturnEmpty(ctx context.Context, apiCode string) bool {
wc := WhitelistFromContext(ctx)
if wc == nil {
return false
}
for _, m := range wc.Matches {
if matchesWhitelistAPICode(m.APICodes, apiCode) {
return true
}
}
return false
}
func matchesWhitelistAPICode(apiCodes []string, apiCode string) bool {
for _, code := range apiCodes {
if code == "*" || code == apiCode {
return true
}
}
return false
}

View File

@@ -0,0 +1,32 @@
package processors
import (
"context"
"testing"
)
func TestWhitelistShouldReturnEmpty_PerAPICode(t *testing.T) {
ctx := WithWhitelistContext(context.Background(), []WhitelistMatch{
{ID: "1", APICodes: []string{"FLXG0V4B"}},
})
if !WhitelistShouldReturnEmpty(ctx, "FLXG0V4B") {
t.Fatal("FLXG0V4B should hit")
}
if WhitelistShouldReturnEmpty(ctx, "JRZQ8A2D") {
t.Fatal("JRZQ8A2D should not hit entry scoped to FLXG0V4B")
}
}
func TestWhitelistShouldReturnEmpty_WildcardMatchesAnyAPICode(t *testing.T) {
ctx := WithWhitelistContext(context.Background(), []WhitelistMatch{
{ID: "1", APICodes: []string{"*"}},
})
if !WhitelistShouldReturnEmpty(ctx, "QYGL8261") {
t.Fatal("wildcard should match any api_code in ctx")
}
if !WhitelistShouldReturnEmpty(ctx, "YYSY8B1C") {
t.Fatal("wildcard should match mobile-only api_code when in ctx")
}
}

View File

@@ -9,6 +9,6 @@ import (
// ProcessYYSY8C2DRequest YYSY8C2D API处理方法 - 运营商三要素查询 // ProcessYYSY8C2DRequest YYSY8C2D API处理方法 - 运营商三要素查询
func ProcessYYSY8C2DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { func ProcessYYSY8C2DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
return ProcessYYSY9A1BRequest(ctx, params, deps) return processors.InvokeRegisteredProcessor(ctx, "YYSY9A1B", params, deps)
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"tyapi-server/internal/domains/api/entities" "tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/domains/api/services/processors"
) )
func TestRequiresIdentityInput_FLXG0V4B(t *testing.T) { func TestRequiresIdentityInput_FLXG0V4B(t *testing.T) {
@@ -29,7 +30,7 @@ func TestRequiresIdentityInput_COMB(t *testing.T) {
} }
} }
func TestShouldReturnEmpty_SkipsNonIdentityAPIEvenWithWildcard(t *testing.T) { func TestEnrichContext_WildcardAppliesToConfiguredAPICodesInProcessor(t *testing.T) {
idCard := "350681198611130611" idCard := "350681198611130611"
hash := HashIDCard(idCard) hash := HashIDCard(idCard)
svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{ svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{
@@ -43,10 +44,11 @@ func TestShouldReturnEmpty_SkipsNonIdentityAPIEvenWithWildcard(t *testing.T) {
Status: entities.QueryWhitelistStatusEnabled, Status: entities.QueryWhitelistStatusEnabled,
}, },
}, },
}, false) })
params := map[string]interface{}{"id_card": idCard, "name": "张三"} params := map[string]interface{}{"id_card": idCard, "name": "张三"}
if svc.ShouldReturnEmpty(context.Background(), "user-a", "QYGL8261", params) { ctx := svc.EnrichContext(context.Background(), "user-a", params)
t.Fatal("non-identity API should not be intercepted even with api_codes=*") if !processors.WhitelistShouldReturnEmpty(ctx, "YYSY8B1C") {
t.Fatal("processor layer should honor wildcard api_codes from ctx")
} }
} }

View File

@@ -8,12 +8,14 @@ import (
"tyapi-server/internal/domains/api/entities" "tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/domains/api/repositories" "tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/domains/api/services/processors"
"go.uber.org/zap" "go.uber.org/zap"
) )
type QueryWhitelistService interface { type QueryWhitelistService interface {
ShouldReturnEmpty(ctx context.Context, userID, apiCode string, params map[string]interface{}) bool // EnrichContext 入参(姓名+身份证)命中白名单时,将命中的 api_codes 写入 context
EnrichContext(ctx context.Context, userID string, params map[string]interface{}) context.Context
InvalidateCache(userID, idCardHash string) InvalidateCache(userID, idCardHash string)
InvalidateAllCache() InvalidateAllCache()
} }
@@ -24,84 +26,66 @@ type queryWhitelistSnapshot struct {
} }
type QueryWhitelistServiceImpl struct { type QueryWhitelistServiceImpl struct {
repo repositories.QueryWhitelistRepository repo repositories.QueryWhitelistRepository
formConfigService FormConfigService logger *zap.Logger
logger *zap.Logger
snapshot atomic.Pointer[queryWhitelistSnapshot] snapshot atomic.Pointer[queryWhitelistSnapshot]
snapshotMu sync.Mutex snapshotMu sync.Mutex
// apiCode -> 是否要求身份证入参FormConfig 反射结果,进程内永久缓存)
identityAPICache sync.Map
} }
func NewQueryWhitelistService( func NewQueryWhitelistService(
repo repositories.QueryWhitelistRepository, repo repositories.QueryWhitelistRepository,
formConfigService FormConfigService, _ FormConfigService,
logger *zap.Logger, logger *zap.Logger,
) QueryWhitelistService { ) QueryWhitelistService {
s := &QueryWhitelistServiceImpl{ s := &QueryWhitelistServiceImpl{
repo: repo, repo: repo,
formConfigService: formConfigService, logger: logger,
logger: logger,
} }
return s return s
} }
// ShouldReturnEmpty 检查是否应返回「查询为空」 // EnrichContext 判断入参是否命中白名单,并将命中的 api_codes 写入 context不拦截请求
// 热路径:入参提取 → API 类型缓存 → 内存快照匹配,不逐请求查库 // 热路径:姓名+身份证提取 → 内存快照匹配 → 写入 ctx由各处理器按 api_code 返回查询为空
func (s *QueryWhitelistServiceImpl) ShouldReturnEmpty( func (s *QueryWhitelistServiceImpl) EnrichContext(
ctx context.Context, ctx context.Context,
userID, apiCode string, userID string,
params map[string]interface{}, params map[string]interface{},
) bool { ) context.Context {
identity := ExtractIdentityParams(params) identity := ExtractIdentityParams(params)
if !identity.OK { if !identity.OK {
return false return ctx
}
if !s.requiresIdentityInput(ctx, apiCode) {
return false
} }
idCardHash := HashIDCard(identity.IDCard) idCardHash := HashIDCard(identity.IDCard)
entries, err := s.lookupEntries(ctx, userID, idCardHash) entries, err := s.lookupEntries(ctx, userID, idCardHash)
if err != nil { if err != nil {
s.logger.Error("查询白名单快照失败", zap.Error(err), zap.String("user_id", userID)) s.logger.Error("查询白名单快照失败", zap.Error(err), zap.String("user_id", userID))
return false return ctx
} }
matches := make([]processors.WhitelistMatch, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
if !entry.IsEnabled() { if !entry.IsEnabled() {
continue continue
} }
if !entry.MatchesAPICode(apiCode) {
continue
}
if !entry.MatchesName(identity.Name) { if !entry.MatchesName(identity.Name) {
continue continue
} }
s.logger.Info("命中查询白名单", s.logger.Info("命中查询白名单",
zap.String("user_id", userID), zap.String("user_id", userID),
zap.String("api_code", apiCode),
zap.String("whitelist_id", entry.ID), zap.String("whitelist_id", entry.ID),
zap.Bool("is_global", entry.IsGlobal()), zap.Bool("is_global", entry.IsGlobal()),
zap.Strings("api_codes", entry.APICodes),
) )
return true matches = append(matches, processors.WhitelistMatch{
ID: entry.ID,
APICodes: entry.APICodes,
IsGlobal: entry.IsGlobal(),
})
} }
return false return processors.WithWhitelistContext(ctx, matches)
}
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) { func (s *QueryWhitelistServiceImpl) lookupEntries(ctx context.Context, userID, idCardHash string) ([]*entities.QueryWhitelistEntry, error) {

View File

@@ -6,6 +6,7 @@ import (
"tyapi-server/internal/domains/api/entities" "tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/domains/api/repositories" "tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/interfaces"
"go.uber.org/zap" "go.uber.org/zap"
@@ -61,20 +62,13 @@ func (m *mockQueryWhitelistRepo) FindAllEnabled(ctx context.Context) ([]*entitie
return result, nil return result, nil
} }
type mockFormConfigService struct { func newTestQueryWhitelistService(repo repositories.QueryWhitelistRepository) QueryWhitelistService {
requiresIdentity bool return NewQueryWhitelistService(repo, nil, zap.NewNop())
} }
func (m *mockFormConfigService) GetFormConfig(ctx context.Context, apiCode string) (*FormConfig, error) { func whitelistShouldReturnEmpty(svc QueryWhitelistService, ctx context.Context, userID, apiCode string, params map[string]interface{}) bool {
return nil, nil ctx = svc.EnrichContext(ctx, userID, params)
} return processors.WhitelistShouldReturnEmpty(ctx, apiCode)
func (m *mockFormConfigService) RequiresIdentityInput(ctx context.Context, apiCode string) bool {
return m.requiresIdentity
}
func newTestQueryWhitelistService(repo repositories.QueryWhitelistRepository, requiresIdentity bool) QueryWhitelistService {
return NewQueryWhitelistService(repo, &mockFormConfigService{requiresIdentity: requiresIdentity}, zap.NewNop())
} }
func TestShouldReturnEmpty_GlobalRule(t *testing.T) { func TestShouldReturnEmpty_GlobalRule(t *testing.T) {
@@ -91,10 +85,10 @@ func TestShouldReturnEmpty_GlobalRule(t *testing.T) {
Status: entities.QueryWhitelistStatusEnabled, Status: entities.QueryWhitelistStatusEnabled,
}, },
}, },
}, true) })
params := map[string]interface{}{"id_card": idCard, "name": "任意姓名"} params := map[string]interface{}{"id_card": idCard, "name": "任意姓名"}
if !svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { if !whitelistShouldReturnEmpty(svc, context.Background(), "user-a", "FLXG0V4B", params) {
t.Fatal("global rule should hit for any user") t.Fatal("global rule should hit for any user")
} }
} }
@@ -113,16 +107,16 @@ func TestShouldReturnEmpty_UserSpecificRule(t *testing.T) {
Status: entities.QueryWhitelistStatusEnabled, Status: entities.QueryWhitelistStatusEnabled,
}, },
}, },
}, true) })
params := map[string]interface{}{"id_card": idCard, "name": "张三"} params := map[string]interface{}{"id_card": idCard, "name": "张三"}
if !svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { if !whitelistShouldReturnEmpty(svc, context.Background(), "user-a", "FLXG0V4B", params) {
t.Fatal("user-a should hit") t.Fatal("user-a should hit")
} }
if svc.ShouldReturnEmpty(context.Background(), "user-b", "FLXG0V4B", params) { if whitelistShouldReturnEmpty(svc, context.Background(), "user-b", "FLXG0V4B", params) {
t.Fatal("user-b should not hit user-a rule") t.Fatal("user-b should not hit user-a rule")
} }
if svc.ShouldReturnEmpty(context.Background(), "user-a", "JRZQ8A2D", params) { if whitelistShouldReturnEmpty(svc, context.Background(), "user-a", "JRZQ8A2D", params) {
t.Fatal("wrong api code should not hit") t.Fatal("wrong api code should not hit")
} }
} }
@@ -141,10 +135,38 @@ func TestShouldReturnEmpty_NameMismatch(t *testing.T) {
Status: entities.QueryWhitelistStatusEnabled, Status: entities.QueryWhitelistStatusEnabled,
}, },
}, },
}, true) })
params := map[string]interface{}{"id_card": idCard, "name": "李四"} params := map[string]interface{}{"id_card": idCard, "name": "李四"}
if svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { ctx := svc.EnrichContext(context.Background(), "user-a", params)
t.Fatal("name mismatch should not hit") if processors.WhitelistFromContext(ctx) != nil {
t.Fatal("name mismatch should not enrich context")
}
}
func TestEnrichContext_PartialAPICodeForComb(t *testing.T) {
idCard := "350681198611130611"
hash := HashIDCard(idCard)
svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{
entries: []*entities.QueryWhitelistEntry{
{
ID: "4",
UserID: "user-a",
Name: "张三",
IDCardHash: hash,
APICodes: entities.APICodeList{"FLXG0V4B"},
Status: entities.QueryWhitelistStatusEnabled,
},
},
})
params := map[string]interface{}{"id_card": idCard, "name": "张三"}
ctx := svc.EnrichContext(context.Background(), "user-a", params)
if !processors.WhitelistShouldReturnEmpty(ctx, "FLXG0V4B") {
t.Fatal("FLXG0V4B should return empty in comb sub-call")
}
if processors.WhitelistShouldReturnEmpty(ctx, "JRZQ8A2D") {
t.Fatal("JRZQ8A2D should still call upstream in comb sub-call")
} }
} }