From 3a5a0d002820122f3da29ea6415454c15432664d Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 18 Jun 2026 21:16:02 +0800 Subject: [PATCH] add --- internal/app/app.go | 1 + .../api/api_application_service.go | 9 + .../api/dto/query_whitelist_dto.go | 71 ++++ .../query_whitelist_application_service.go | 309 ++++++++++++++++++ .../application/api/query_whitelist_legacy.go | 22 ++ internal/container/container.go | 15 + .../api/entities/query_whitelist_entry.go | 112 +++++++ .../query_whitelist_repository.go | 19 ++ .../api/services/form_config_service.go | 31 ++ .../processors/flxg/flxg0v4b_processor.go | 3 - .../processors/flxg/flxg2e8f_processor.go | 4 - .../processors/flxg/flxg3a9b_processor.go | 4 - .../processors/flxg/flxg5a3b_processor.go | 3 - .../processors/flxg/flxg7e8f_processor.go | 3 - .../processors/flxg/flxg9c1d_processor.go | 3 - .../processors/flxg/flxgca3d_processor.go | 3 - .../processors/flxg/flxgdea8_processor.go | 4 - .../processors/flxg/flxgdea9_processor.go | 4 - .../processors/flxg/flxghb4f_processor.go | 4 - .../processors/flxg/flxgk5d2_processor.go | 4 - .../processors/jrzq/jrzq8a2d_processor.go | 7 +- .../processors/jrzq/jrzq9d4e_transform.go | 36 +- .../jrzq/jrzq9d4e_transform_test.go | 36 ++ .../processors/jrzq/jrzqv7md_processor.go | 4 - .../processors/qygl/qygl2s0w_processor.go | 3 - .../api/services/query_whitelist_helpers.go | 65 ++++ .../services/query_whitelist_helpers_test.go | 66 ++++ .../query_whitelist_identity_api_test.go | 52 +++ .../api/services/query_whitelist_service.go | 185 +++++++++++ .../services/query_whitelist_service_test.go | 147 +++++++++ .../api/gorm_query_whitelist_repository.go | 152 +++++++++ .../handlers/admin_query_whitelist_handler.go | 149 +++++++++ .../http/handlers/user_handler.go | 19 +- .../routes/admin_query_whitelist_routes.go | 37 +++ scripts/gen_query_whitelist_hashes/main.go | 21 ++ scripts/migrate_query_whitelist.sql | 25 ++ 36 files changed, 1566 insertions(+), 66 deletions(-) create mode 100644 internal/application/api/dto/query_whitelist_dto.go create mode 100644 internal/application/api/query_whitelist_application_service.go create mode 100644 internal/application/api/query_whitelist_legacy.go create mode 100644 internal/domains/api/entities/query_whitelist_entry.go create mode 100644 internal/domains/api/repositories/query_whitelist_repository.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq9d4e_transform_test.go create mode 100644 internal/domains/api/services/query_whitelist_helpers.go create mode 100644 internal/domains/api/services/query_whitelist_helpers_test.go create mode 100644 internal/domains/api/services/query_whitelist_identity_api_test.go create mode 100644 internal/domains/api/services/query_whitelist_service.go create mode 100644 internal/domains/api/services/query_whitelist_service_test.go create mode 100644 internal/infrastructure/database/repositories/api/gorm_query_whitelist_repository.go create mode 100644 internal/infrastructure/http/handlers/admin_query_whitelist_handler.go create mode 100644 internal/infrastructure/http/routes/admin_query_whitelist_routes.go create mode 100644 scripts/gen_query_whitelist_hashes/main.go create mode 100644 scripts/migrate_query_whitelist.sql diff --git a/internal/app/app.go b/internal/app/app.go index 9ad2d9a..df24445 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -264,6 +264,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error { &apiEntities.ApiUser{}, &apiEntities.ApiCall{}, &apiEntities.Report{}, + &apiEntities.QueryWhitelistEntry{}, // 下属账号域 &subordinateEntities.SubordinateInvitation{}, diff --git a/internal/application/api/api_application_service.go b/internal/application/api/api_application_service.go index b710429..23e9d50 100644 --- a/internal/application/api/api_application_service.go +++ b/internal/application/api/api_application_service.go @@ -96,6 +96,7 @@ type ApiApplicationServiceImpl struct { subscriptionService *product_services.ProductSubscriptionService balanceAlertService finance_services.BalanceAlertService subordinateRepo subordinate_repositories.SubordinateRepository + queryWhitelistSvc services.QueryWhitelistService } func NewApiApplicationService( @@ -116,6 +117,7 @@ func NewApiApplicationService( exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, subordinateRepo subordinate_repositories.SubordinateRepository, + queryWhitelistSvc services.QueryWhitelistService, ) ApiApplicationService { service := &ApiApplicationServiceImpl{ apiCallService: apiCallService, @@ -135,6 +137,7 @@ func NewApiApplicationService( subscriptionService: subscriptionService, balanceAlertService: balanceAlertService, subordinateRepo: subordinateRepo, + queryWhitelistSvc: queryWhitelistSvc, } return service @@ -367,6 +370,12 @@ func extractParentAccessID(params map[string]interface{}) (string, bool) { // callExternalApi 同步调用外部API func (s *ApiApplicationServiceImpl) callExternalApi(ctx context.Context, cmd *commands.ApiCallCommand, validation *dto.ApiCallValidationResult) (string, error) { + // 查询白名单拦截:命中则返回「查询为空」,不调用上游、不扣费 + if s.queryWhitelistSvc != nil && + s.queryWhitelistSvc.ShouldReturnEmpty(ctx, validation.GetUserID(), cmd.ApiName, validation.RequestParams) { + return "", ErrQueryEmpty + } + // 创建CallContext callContext := &processors.CallContext{ ContractCode: validation.ContractCode, diff --git a/internal/application/api/dto/query_whitelist_dto.go b/internal/application/api/dto/query_whitelist_dto.go new file mode 100644 index 0000000..46b794e --- /dev/null +++ b/internal/application/api/dto/query_whitelist_dto.go @@ -0,0 +1,71 @@ +package dto + +import ( + "time" + + "tyapi-server/internal/domains/api/entities" +) + +type QueryWhitelistEntryRequest struct { + UserID string `json:"user_id" validate:"required"` + Name string `json:"name" validate:"required,min=1,max=100"` + IDCard string `json:"id_card" validate:"required"` + APICodes []string `json:"api_codes" validate:"required,min=1,dive,required"` + Remark string `json:"remark" validate:"max=500"` +} + +type QueryWhitelistEntryUpdateRequest struct { + Name string `json:"name" validate:"omitempty,min=1,max=100"` + IDCard string `json:"id_card" validate:"omitempty"` + APICodes []string `json:"api_codes" validate:"omitempty,min=1,dive,required"` + Remark string `json:"remark" validate:"max=500"` +} + +type QueryWhitelistStatusRequest struct { + Status string `json:"status" validate:"required,oneof=enabled disabled"` +} + +type QueryWhitelistEntryResponse struct { + ID string `json:"id"` + UserID string `json:"user_id"` + IsGlobal bool `json:"is_global"` + Name string `json:"name"` + IDCardMasked string `json:"id_card_masked"` + APICodes []string `json:"api_codes"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type QueryWhitelistListResponse struct { + Items []QueryWhitelistEntryResponse `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + Size int `json:"page_size"` +} + +type QueryWhitelistImportLegacyResponse struct { + Imported int `json:"imported"` + Skipped int `json:"skipped"` + Total int `json:"total"` +} + +func NewQueryWhitelistEntryResponse(entry *entities.QueryWhitelistEntry) QueryWhitelistEntryResponse { + apiCodes := []string(entry.APICodes) + if apiCodes == nil { + apiCodes = []string{} + } + return QueryWhitelistEntryResponse{ + ID: entry.ID, + UserID: entry.UserID, + IsGlobal: entry.IsGlobal(), + Name: entry.Name, + IDCardMasked: entry.IDCardMasked, + APICodes: apiCodes, + Status: entry.Status, + Remark: entry.Remark, + CreatedAt: entry.CreatedAt, + UpdatedAt: entry.UpdatedAt, + } +} diff --git a/internal/application/api/query_whitelist_application_service.go b/internal/application/api/query_whitelist_application_service.go new file mode 100644 index 0000000..a1ef0e8 --- /dev/null +++ b/internal/application/api/query_whitelist_application_service.go @@ -0,0 +1,309 @@ +package api + +import ( + "context" + "errors" + "fmt" + "strings" + + "tyapi-server/internal/application/api/dto" + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/domains/api/repositories" + api_services "tyapi-server/internal/domains/api/services" + "tyapi-server/internal/shared/interfaces" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type QueryWhitelistApplicationService interface { + CreateEntry(ctx context.Context, adminUserID string, req *dto.QueryWhitelistEntryRequest) (*dto.QueryWhitelistEntryResponse, error) + UpdateEntry(ctx context.Context, adminUserID, id string, req *dto.QueryWhitelistEntryUpdateRequest) (*dto.QueryWhitelistEntryResponse, error) + UpdateEntryStatus(ctx context.Context, adminUserID, id, status string) (*dto.QueryWhitelistEntryResponse, error) + DeleteEntry(ctx context.Context, id string) error + GetEntry(ctx context.Context, id string) (*dto.QueryWhitelistEntryResponse, error) + ListEntries(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*dto.QueryWhitelistListResponse, error) + ImportLegacyEntries(ctx context.Context, adminUserID string) (*dto.QueryWhitelistImportLegacyResponse, error) +} + +type QueryWhitelistApplicationServiceImpl struct { + repo repositories.QueryWhitelistRepository + queryWhitelistSvc api_services.QueryWhitelistService + logger *zap.Logger +} + +func NewQueryWhitelistApplicationService( + repo repositories.QueryWhitelistRepository, + queryWhitelistSvc api_services.QueryWhitelistService, + logger *zap.Logger, +) QueryWhitelistApplicationService { + return &QueryWhitelistApplicationServiceImpl{ + repo: repo, + queryWhitelistSvc: queryWhitelistSvc, + logger: logger, + } +} + +func (s *QueryWhitelistApplicationServiceImpl) CreateEntry( + ctx context.Context, + adminUserID string, + req *dto.QueryWhitelistEntryRequest, +) (*dto.QueryWhitelistEntryResponse, error) { + if err := validateQueryWhitelistRequest(req.UserID, req.Name, req.IDCard, req.APICodes); err != nil { + return nil, err + } + + idCardHash := api_services.HashIDCard(req.IDCard) + exists, err := s.repo.ExistsByUserIDCardHashAndName(ctx, req.UserID, idCardHash, strings.TrimSpace(req.Name), "") + if err != nil { + return nil, err + } + if exists { + return nil, fmt.Errorf("该用户下已存在相同的身份证与姓名规则") + } + + entry := &entities.QueryWhitelistEntry{ + UserID: strings.TrimSpace(req.UserID), + Name: normalizeWhitelistName(req.Name), + IDCardHash: idCardHash, + IDCardMasked: api_services.MaskIDCard(req.IDCard), + APICodes: entities.APICodeList(req.APICodes), + Status: entities.QueryWhitelistStatusEnabled, + Remark: strings.TrimSpace(req.Remark), + CreatedBy: &adminUserID, + } + if err := s.repo.Create(ctx, entry); err != nil { + return nil, err + } + s.queryWhitelistSvc.InvalidateCache(entry.UserID, idCardHash) + resp := dto.NewQueryWhitelistEntryResponse(entry) + return &resp, nil +} + +func (s *QueryWhitelistApplicationServiceImpl) UpdateEntry( + ctx context.Context, + adminUserID, id string, + req *dto.QueryWhitelistEntryUpdateRequest, +) (*dto.QueryWhitelistEntryResponse, error) { + entry, err := s.repo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("规则不存在") + } + return nil, err + } + oldHash := entry.IDCardHash + + if req.Name != "" { + entry.Name = normalizeWhitelistName(req.Name) + } + if req.IDCard != "" { + if err := validateIDCard(req.IDCard); err != nil { + return nil, err + } + entry.IDCardHash = api_services.HashIDCard(req.IDCard) + entry.IDCardMasked = api_services.MaskIDCard(req.IDCard) + } + if len(req.APICodes) > 0 { + if err := validateAPICodes(req.APICodes); err != nil { + return nil, err + } + entry.APICodes = entities.APICodeList(req.APICodes) + } + if req.Remark != "" || req.Remark == "" { + entry.Remark = strings.TrimSpace(req.Remark) + } + + exists, err := s.repo.ExistsByUserIDCardHashAndName(ctx, entry.UserID, entry.IDCardHash, entry.Name, entry.ID) + if err != nil { + return nil, err + } + if exists { + return nil, fmt.Errorf("该用户下已存在相同的身份证与姓名规则") + } + + entry.UpdatedBy = &adminUserID + if err := s.repo.Update(ctx, entry); err != nil { + return nil, err + } + s.queryWhitelistSvc.InvalidateCache(entry.UserID, oldHash) + s.queryWhitelistSvc.InvalidateCache(entry.UserID, entry.IDCardHash) + resp := dto.NewQueryWhitelistEntryResponse(entry) + return &resp, nil +} + +func (s *QueryWhitelistApplicationServiceImpl) UpdateEntryStatus( + ctx context.Context, + adminUserID, id, status string, +) (*dto.QueryWhitelistEntryResponse, error) { + entry, err := s.repo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("规则不存在") + } + return nil, err + } + entry.Status = status + entry.UpdatedBy = &adminUserID + if err := s.repo.Update(ctx, entry); err != nil { + return nil, err + } + s.queryWhitelistSvc.InvalidateCache(entry.UserID, entry.IDCardHash) + resp := dto.NewQueryWhitelistEntryResponse(entry) + return &resp, nil +} + +func (s *QueryWhitelistApplicationServiceImpl) DeleteEntry(ctx context.Context, id string) error { + entry, err := s.repo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("规则不存在") + } + return err + } + if err := s.repo.Delete(ctx, id); err != nil { + return err + } + s.queryWhitelistSvc.InvalidateCache(entry.UserID, entry.IDCardHash) + return nil +} + +func (s *QueryWhitelistApplicationServiceImpl) GetEntry(ctx context.Context, id string) (*dto.QueryWhitelistEntryResponse, error) { + entry, err := s.repo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("规则不存在") + } + return nil, err + } + resp := dto.NewQueryWhitelistEntryResponse(entry) + return &resp, nil +} + +func (s *QueryWhitelistApplicationServiceImpl) ListEntries( + ctx context.Context, + filters map[string]interface{}, + options interfaces.ListOptions, +) (*dto.QueryWhitelistListResponse, error) { + if idCard, ok := filters["id_card"].(string); ok && idCard != "" { + filters["id_card_hash"] = api_services.HashIDCard(idCard) + delete(filters, "id_card") + } + entries, total, err := s.repo.List(ctx, filters, options) + if err != nil { + return nil, err + } + items := make([]dto.QueryWhitelistEntryResponse, 0, len(entries)) + for _, entry := range entries { + items = append(items, dto.NewQueryWhitelistEntryResponse(entry)) + } + page := options.Page + if page < 1 { + page = 1 + } + size := options.PageSize + if size < 1 { + size = 20 + } + return &dto.QueryWhitelistListResponse{ + Items: items, + Total: total, + Page: page, + Size: size, + }, nil +} + +func (s *QueryWhitelistApplicationServiceImpl) ImportLegacyEntries( + ctx context.Context, + adminUserID string, +) (*dto.QueryWhitelistImportLegacyResponse, error) { + imported := 0 + skipped := 0 + for _, idCard := range LegacyHardcodedIDCards { + hash := api_services.HashIDCard(idCard) + exists, err := s.repo.ExistsByUserIDCardHashAndName( + ctx, + entities.QueryWhitelistGlobalUserID, + hash, + entities.QueryWhitelistWildcardName, + "", + ) + if err != nil { + return nil, err + } + if exists { + skipped++ + continue + } + entry := &entities.QueryWhitelistEntry{ + UserID: entities.QueryWhitelistGlobalUserID, + Name: entities.QueryWhitelistWildcardName, + IDCardHash: hash, + IDCardMasked: api_services.MaskIDCard(idCard), + APICodes: entities.APICodeList{"*"}, + Status: entities.QueryWhitelistStatusEnabled, + Remark: "自硬编码迁移-全局", + CreatedBy: &adminUserID, + } + if err := s.repo.Create(ctx, entry); err != nil { + return nil, err + } + s.queryWhitelistSvc.InvalidateCache(entities.QueryWhitelistGlobalUserID, hash) + imported++ + } + return &dto.QueryWhitelistImportLegacyResponse{ + Imported: imported, + Skipped: skipped, + Total: len(LegacyHardcodedIDCards), + }, nil +} + +func validateQueryWhitelistRequest(userID, name, idCard string, apiCodes []string) error { + userID = strings.TrimSpace(userID) + if userID == "" { + return fmt.Errorf("user_id 不能为空") + } + if strings.TrimSpace(name) == "" { + return fmt.Errorf("name 不能为空") + } + if err := validateIDCard(idCard); err != nil { + return err + } + return validateAPICodes(apiCodes) +} + +func validateIDCard(idCard string) error { + idCard = api_services.NormalizeIDCard(idCard) + if len(idCard) != 18 { + return fmt.Errorf("身份证号格式不正确") + } + return nil +} + +func validateAPICodes(apiCodes []string) error { + if len(apiCodes) == 0 { + return fmt.Errorf("api_codes 不能为空") + } + hasWildcard := false + for _, code := range apiCodes { + code = strings.TrimSpace(code) + if code == "" { + return fmt.Errorf("api_codes 不能包含空值") + } + if code == "*" { + hasWildcard = true + } + } + if hasWildcard && len(apiCodes) > 1 { + return fmt.Errorf("api_codes 包含 * 时不能与其他编码混用") + } + return nil +} + +func normalizeWhitelistName(name string) string { + name = strings.TrimSpace(name) + if name == entities.QueryWhitelistWildcardName { + return entities.QueryWhitelistWildcardName + } + return name +} diff --git a/internal/application/api/query_whitelist_legacy.go b/internal/application/api/query_whitelist_legacy.go new file mode 100644 index 0000000..f7fbb4c --- /dev/null +++ b/internal/application/api/query_whitelist_legacy.go @@ -0,0 +1,22 @@ +package api + +// LegacyHardcodedIDCards 原 processor 硬编码的身份证号,导入为全局规则(user_id=*,name=*) +var LegacyHardcodedIDCards = []string{ + "350681198611130611", + "622301200006250550", + "320682198910134998", + "640102198708020925", + "420624197310234034", + "350104198501184416", + "410521198606018056", + "410482198504029333", + "370982199012037272", + "431027198810290730", + "362502199510298017", + "340826199008250378", + "321027198304072129", + "420116198907031413", + "13032319930128263X", + "350681198412013041", + "33072619741031111X", +} diff --git a/internal/container/container.go b/internal/container/container.go index 1cc94d1..2548366 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -683,6 +683,10 @@ func NewContainer() *Container { api_repo.NewGormReportRepository, fx.As(new(domain_api_repo.ReportRepository)), ), + fx.Annotate( + api_repo.NewGormQueryWhitelistRepository, + fx.As(new(domain_api_repo.QueryWhitelistRepository)), + ), ), // 下属账号仓储 @@ -803,10 +807,15 @@ func NewContainer() *Container { // 使用带仓储注入的构造函数,支持企业报告记录持久化 api_services.NewApiRequestServiceWithRepos, api_services.NewFormConfigService, + api_services.NewQueryWhitelistService, ), // API域应用服务 fx.Provide( + fx.Annotate( + api_app.NewQueryWhitelistApplicationService, + fx.As(new(api_app.QueryWhitelistApplicationService)), + ), // API应用服务 - 绑定到接口 fx.Annotate( func( @@ -827,6 +836,7 @@ func NewContainer() *Container { exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, subordinateRepo domain_subordinate_repo.SubordinateRepository, + queryWhitelistSvc api_services.QueryWhitelistService, ) api_app.ApiApplicationService { return api_app.NewApiApplicationService( apiCallService, @@ -846,6 +856,7 @@ func NewContainer() *Container { exportManager, balanceAlertService, subordinateRepo, + queryWhitelistSvc, ) }, fx.As(new(api_app.ApiApplicationService)), @@ -1309,6 +1320,7 @@ func NewContainer() *Container { handlers.NewStatisticsHandler, // 管理员安全HTTP处理器 handlers.NewAdminSecurityHandler, + handlers.NewAdminQueryWhitelistHandler, // 文章HTTP处理器 func( appService article.ArticleApplicationService, @@ -1403,6 +1415,7 @@ func NewContainer() *Container { routes.NewStatisticsRoutes, // 管理员安全路由 routes.NewAdminSecurityRoutes, + routes.NewAdminQueryWhitelistRoutes, // PDFG路由 routes.NewPDFGRoutes, // 企业报告页面路由 @@ -1523,6 +1536,7 @@ func RegisterRoutes( apiRoutes *routes.ApiRoutes, statisticsRoutes *routes.StatisticsRoutes, adminSecurityRoutes *routes.AdminSecurityRoutes, + adminQueryWhitelistRoutes *routes.AdminQueryWhitelistRoutes, pdfgRoutes *routes.PDFGRoutes, qyglReportRoutes *routes.QYGLReportRoutes, jwtAuth *middleware.JWTAuthMiddleware, @@ -1551,6 +1565,7 @@ func RegisterRoutes( announcementRoutes.Register(router) statisticsRoutes.Register(router) adminSecurityRoutes.Register(router) + adminQueryWhitelistRoutes.Register(router) pdfgRoutes.Register(router) qyglReportRoutes.Register(router) diff --git a/internal/domains/api/entities/query_whitelist_entry.go b/internal/domains/api/entities/query_whitelist_entry.go new file mode 100644 index 0000000..ad7bc4a --- /dev/null +++ b/internal/domains/api/entities/query_whitelist_entry.go @@ -0,0 +1,112 @@ +package entities + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +const ( + QueryWhitelistGlobalUserID = "*" // 全局规则:对所有用户生效 + QueryWhitelistWildcardName = "*" // 仅匹配身份证,不校验姓名(兼容历史硬编码) + QueryWhitelistStatusEnabled = "enabled" + QueryWhitelistStatusDisabled = "disabled" + QueryWhitelistTableName = "query_whitelist_entries" +) + +// APICodeList 生效的 API 编码列表,["*"] 表示全部「身份证必填」类接口 +type APICodeList []string + +func (a APICodeList) Value() (driver.Value, error) { + if a == nil { + return "[]", nil + } + data, err := json.Marshal(a) + if err != nil { + return nil, err + } + return string(data), nil +} + +func (a *APICodeList) Scan(value interface{}) error { + if value == nil { + *a = APICodeList{} + return nil + } + var bytes []byte + switch v := value.(type) { + case []byte: + bytes = v + case string: + bytes = []byte(v) + default: + return errors.New("无法扫描 APICodeList 类型") + } + if len(bytes) == 0 || string(bytes) == "null" { + *a = APICodeList{} + return nil + } + return json.Unmarshal(bytes, a) +} + +// QueryWhitelistEntry 查询白名单:命中后返回「查询为空」,不调用上游 +type QueryWhitelistEntry struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + UserID string `gorm:"type:varchar(36);not null;index:idx_qwl_user_id_card_hash,priority:1" json:"user_id"` + Name string `gorm:"type:varchar(100);not null" json:"name"` + IDCardHash string `gorm:"type:varchar(64);not null;index:idx_qwl_user_id_card_hash,priority:2" json:"-"` + IDCardMasked string `gorm:"type:varchar(32);not null" json:"id_card_masked"` + APICodes APICodeList `gorm:"type:json;not null" json:"api_codes"` + Status string `gorm:"type:varchar(20);not null;default:'enabled'" json:"status"` + Remark string `gorm:"type:varchar(500)" json:"remark"` + CreatedBy *string `gorm:"type:varchar(36)" json:"created_by,omitempty"` + UpdatedBy *string `gorm:"type:varchar(36)" json:"updated_by,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (QueryWhitelistEntry) TableName() string { + return QueryWhitelistTableName +} + +func (e *QueryWhitelistEntry) BeforeCreate(tx *gorm.DB) error { + if e.ID == "" { + e.ID = uuid.New().String() + } + if e.Status == "" { + e.Status = QueryWhitelistStatusEnabled + } + if e.APICodes == nil { + e.APICodes = APICodeList{"*"} + } + return nil +} + +func (e *QueryWhitelistEntry) IsGlobal() bool { + return e.UserID == QueryWhitelistGlobalUserID +} + +func (e *QueryWhitelistEntry) IsEnabled() bool { + return e.Status == QueryWhitelistStatusEnabled +} + +func (e *QueryWhitelistEntry) MatchesAPICode(apiCode string) bool { + for _, code := range e.APICodes { + if code == "*" || code == apiCode { + return true + } + } + return false +} + +func (e *QueryWhitelistEntry) MatchesName(name string) bool { + if e.Name == QueryWhitelistWildcardName { + return true + } + return e.Name == name +} diff --git a/internal/domains/api/repositories/query_whitelist_repository.go b/internal/domains/api/repositories/query_whitelist_repository.go new file mode 100644 index 0000000..7df5855 --- /dev/null +++ b/internal/domains/api/repositories/query_whitelist_repository.go @@ -0,0 +1,19 @@ +package repositories + +import ( + "context" + + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/shared/interfaces" +) + +type QueryWhitelistRepository interface { + Create(ctx context.Context, entry *entities.QueryWhitelistEntry) error + Update(ctx context.Context, entry *entities.QueryWhitelistEntry) error + Delete(ctx context.Context, id string) error + FindByID(ctx context.Context, id string) (*entities.QueryWhitelistEntry, error) + FindEnabledByUserIDsAndIDCardHash(ctx context.Context, userIDs []string, idCardHash string) ([]*entities.QueryWhitelistEntry, error) + FindAllEnabled(ctx context.Context) ([]*entities.QueryWhitelistEntry, error) + List(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.QueryWhitelistEntry, int64, error) + ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name string, excludeID string) (bool, error) +} diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 8610273..9a01ae3 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -29,6 +29,8 @@ type FormConfig struct { // FormConfigService 表单配置服务接口 type FormConfigService interface { GetFormConfig(ctx context.Context, apiCode string) (*FormConfig, error) + // RequiresIdentityInput 该 API 入参是否要求身份证(id_card / idCard 为必填) + RequiresIdentityInput(ctx context.Context, apiCode string) bool } // FormConfigServiceImpl 表单配置服务实现 @@ -72,6 +74,35 @@ func (s *FormConfigServiceImpl) GetFormConfig(ctx context.Context, apiCode strin return config, nil } +// RequiresIdentityInput 判断 API 是否以身份证为必填入参。 +// api_codes 为 ["*"] 时,仅对此类接口生效;无 id_card 必填字段的接口不会被拦截。 +func (s *FormConfigServiceImpl) RequiresIdentityInput(ctx context.Context, apiCode string) bool { + if len(apiCode) >= 4 && strings.EqualFold(apiCode[:4], "COMB") { + // 组合包是否拦截由请求入参是否含 id_card 决定(在 QueryWhitelist 上层已校验) + return true + } + dtoStruct, err := s.getDTOStruct(ctx, apiCode) + if err != nil || dtoStruct == nil { + return false + } + return dtoStructRequiresIdentityInput(dtoStruct) +} + +func dtoStructRequiresIdentityInput(dtoStruct interface{}) bool { + t := reflect.TypeOf(dtoStruct).Elem() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] + if jsonTag != "id_card" && jsonTag != "idCard" { + continue + } + if strings.Contains(field.Tag.Get("validate"), "required") { + return true + } + } + return false +} + // getDTOStruct 根据API代码获取对应的DTO结构体 func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string) (interface{}, error) { // 建立API代码到DTO结构体的映射 diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index bead4ba..64ccba2 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -22,9 +22,6 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, diff --git a/internal/domains/api/services/processors/flxg/flxg2e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg2e8f_processor.go index 3788cbf..bccbbc1 100644 --- a/internal/domains/api/services/processors/flxg/flxg2e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg2e8f_processor.go @@ -21,10 +21,6 @@ func ProcessFLXG2E8FRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } - encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/flxg/flxg3a9b_processor.go b/internal/domains/api/services/processors/flxg/flxg3a9b_processor.go index 6d11a6e..05e2b2b 100644 --- a/internal/domains/api/services/processors/flxg/flxg3a9b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg3a9b_processor.go @@ -20,10 +20,6 @@ func ProcessFLXG3A9BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index 7519b68..64636a3 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -20,9 +20,6 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, diff --git a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go index 10b6a06..e64c3f2 100644 --- a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go @@ -20,9 +20,6 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, diff --git a/internal/domains/api/services/processors/flxg/flxg9c1d_processor.go b/internal/domains/api/services/processors/flxg/flxg9c1d_processor.go index 503425c..8b9608f 100644 --- a/internal/domains/api/services/processors/flxg/flxg9c1d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg9c1d_processor.go @@ -20,9 +20,6 @@ func ProcessFLXG9C1DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index c2d6e88..86a4e0b 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -20,9 +20,6 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) if err != nil { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go index b887c27..981c4e7 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go @@ -20,10 +20,6 @@ func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go index 3c404cc..6b5bd53 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -20,10 +20,6 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, "idCard": paramsDto.IDCard, diff --git a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go index dd32270..a10bfd5 100644 --- a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go @@ -25,10 +25,6 @@ func ProcessFLXGHB4FRequest(ctx context.Context, params []byte, deps *processors if deps.HaiyuapiService == nil { return nil, errors.Join(processors.ErrSystem, errors.New("海宇API服务未初始化")) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } reqParams := map[string]interface{}{ "name": paramsDto.Name, diff --git a/internal/domains/api/services/processors/flxg/flxgk5d2_processor.go b/internal/domains/api/services/processors/flxg/flxgk5d2_processor.go index bb5dc7a..36731a1 100644 --- a/internal/domains/api/services/processors/flxg/flxgk5d2_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgk5d2_processor.go @@ -20,10 +20,6 @@ func ProcessFLXGK5D2Request(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/jrzq/jrzq8a2d_processor.go b/internal/domains/api/services/processors/jrzq/jrzq8a2d_processor.go index d52e946..85dd869 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq8a2d_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq8a2d_processor.go @@ -20,10 +20,6 @@ func ProcessJRZQ8A2DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, @@ -56,7 +52,6 @@ func ProcessJRZQ8A2DRequest(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - return respBytes, nil // respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI018", reqData) @@ -73,6 +68,6 @@ func ProcessJRZQ8A2DRequest(ctx context.Context, params []byte, deps *processors // if err != nil { // return nil, errors.Join(processors.ErrSystem, err) // } - // return respBytes, nil + } diff --git a/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform.go b/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform.go index bc4798d..83c0cfc 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform.go +++ b/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform.go @@ -1,13 +1,41 @@ package jrzq +import "strings" + // mapNuoerLoanRiskTagV23ToResponse 将 nuoer data 转为 JRZQ9D4E 对外结构: -// 解包 result,score/reason/contents 原样透传。 +// 解包 result,score/reason 原样透传,contents 内 TC_ 前缀标签码映射为 BH_。 func mapNuoerLoanRiskTagV23ToResponse(data map[string]interface{}) map[string]interface{} { if data == nil { return map[string]interface{}{} } - if result, ok := data["result"].(map[string]interface{}); ok { - return result + result := data + if unwrapped, ok := data["result"].(map[string]interface{}); ok { + result = unwrapped } - return data + return mapLoanRiskTagV23ContentsKeys(result) +} + +func mapLoanRiskTagV23ContentsKeys(data map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(data)) + for key, val := range data { + if key == "contents" { + if contents, ok := val.(map[string]interface{}); ok { + out[key] = renameLoanRiskTagTCKeyPrefix(contents) + continue + } + } + out[key] = val + } + return out +} + +func renameLoanRiskTagTCKeyPrefix(contents map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(contents)) + for key, val := range contents { + if strings.HasPrefix(key, "TC_") { + key = "BH_" + key[3:] + } + out[key] = val + } + return out } diff --git a/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform_test.go b/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform_test.go new file mode 100644 index 0000000..cecc153 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq9d4e_transform_test.go @@ -0,0 +1,36 @@ +package jrzq + +import "testing" + +func TestMapNuoerLoanRiskTagV23ToResponse_RenamesTCContentsKeys(t *testing.T) { + raw := map[string]interface{}{ + "result": map[string]interface{}{ + "score": "750", + "reason": "ok", + "contents": map[string]interface{}{ + "TC_Q016_q20": "1.5", + "TC_A001": "3", + "BH_A002": "2", + }, + }, + } + + got := mapNuoerLoanRiskTagV23ToResponse(raw) + + if got["score"] != "750" { + t.Fatalf("score = %v, want 750", got["score"]) + } + contents, ok := got["contents"].(map[string]interface{}) + if !ok { + t.Fatalf("contents type = %T, want map[string]interface{}", got["contents"]) + } + if contents["BH_A001"] != "3" { + t.Fatalf("BH_A001 = %v, want 3", contents["BH_A001"]) + } + if contents["BH_A002"] != "2" { + t.Fatalf("BH_A002 = %v, want 2", contents["BH_A002"]) + } + if _, exists := contents["TC_Q016_q20"]; exists { + t.Fatal("TC_Q016_q20 should be renamed") + } +} diff --git a/internal/domains/api/services/processors/jrzq/jrzqv7md_processor.go b/internal/domains/api/services/processors/jrzq/jrzqv7md_processor.go index 75a1917..4473709 100644 --- a/internal/domains/api/services/processors/jrzq/jrzqv7md_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzqv7md_processor.go @@ -20,10 +20,6 @@ func ProcessJRZQV7MDRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" || paramsDto.IDCard == "350681198412013041" || paramsDto.IDCard == "33072619741031111X" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } body := map[string]string{ "name": paramsDto.Name, diff --git a/internal/domains/api/services/processors/qygl/qygl2s0w_processor.go b/internal/domains/api/services/processors/qygl/qygl2s0w_processor.go index 4b6dd0c..cb892c6 100644 --- a/internal/domains/api/services/processors/qygl/qygl2s0w_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl2s0w_processor.go @@ -31,9 +31,6 @@ func ProcessQYGL2S0WRequest(ctx context.Context, params []byte, deps *processors fmt.Print("个人身份证件号不能为空") return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当失信被执行人类型为个人时,身份证件号不能为空")) } - if paramsDto.IDCard == "410482198504029333" { - return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) - } } else if paramsDto.Type == "ent" { // 企业查询:name 和 entMark 两者必填其一 nameValue = paramsDto.EntName diff --git a/internal/domains/api/services/query_whitelist_helpers.go b/internal/domains/api/services/query_whitelist_helpers.go new file mode 100644 index 0000000..9b8be79 --- /dev/null +++ b/internal/domains/api/services/query_whitelist_helpers.go @@ -0,0 +1,65 @@ +package services + +import ( + "crypto/sha256" + "encoding/hex" + "strings" +) + +// NormalizeIDCard 归一化身份证号(去空格、末位 X 大写) +func NormalizeIDCard(idCard string) string { + idCard = strings.TrimSpace(idCard) + return strings.ToUpper(idCard) +} + +// HashIDCard 计算身份证号 SHA256 哈希,用于索引与匹配 +func HashIDCard(idCard string) string { + sum := sha256.Sum256([]byte(NormalizeIDCard(idCard))) + return hex.EncodeToString(sum[:]) +} + +// MaskIDCard 脱敏展示:350681********0611 +func MaskIDCard(idCard string) string { + id := NormalizeIDCard(idCard) + if len(id) <= 10 { + return id + } + return id[:6] + "********" + id[len(id)-4:] +} + +// IdentityParams 从请求参数中提取的身份证与姓名 +type IdentityParams struct { + IDCard string + Name string + OK bool +} + +// ExtractIdentityParams 从解密后的 params map 提取 id_card + name +func ExtractIdentityParams(params map[string]interface{}) IdentityParams { + idCard := firstNonEmptyString(params, "id_card", "idCard") + name := firstNonEmptyString(params, "name") + if idCard == "" { + return IdentityParams{OK: false} + } + return IdentityParams{ + IDCard: NormalizeIDCard(idCard), + Name: strings.TrimSpace(name), + OK: true, + } +} + +func firstNonEmptyString(params map[string]interface{}, keys ...string) string { + for _, key := range keys { + v, ok := params[key] + if !ok { + continue + } + if s, ok := v.(string); ok { + s = strings.TrimSpace(s) + if s != "" { + return s + } + } + } + return "" +} diff --git a/internal/domains/api/services/query_whitelist_helpers_test.go b/internal/domains/api/services/query_whitelist_helpers_test.go new file mode 100644 index 0000000..9a1485c --- /dev/null +++ b/internal/domains/api/services/query_whitelist_helpers_test.go @@ -0,0 +1,66 @@ +package services + +import ( + "testing" + + "tyapi-server/internal/domains/api/entities" +) + +func TestNormalizeIDCard(t *testing.T) { + if got := NormalizeIDCard("13032319930128263x"); got != "13032319930128263X" { + t.Fatalf("expected uppercase X, got %s", got) + } +} + +func TestMaskIDCard(t *testing.T) { + masked := MaskIDCard("350681198611130611") + if masked != "350681********0611" { + t.Fatalf("unexpected mask: %s", masked) + } +} + +func TestExtractIdentityParams(t *testing.T) { + params := map[string]interface{}{ + "id_card": "350681198611130611", + "name": "张三", + } + identity := ExtractIdentityParams(params) + if !identity.OK || identity.Name != "张三" { + t.Fatalf("unexpected identity: %+v", identity) + } +} + +func TestQueryWhitelistEntry_Matches(t *testing.T) { + entry := &entities.QueryWhitelistEntry{ + UserID: entities.QueryWhitelistGlobalUserID, + Name: entities.QueryWhitelistWildcardName, + APICodes: entities.APICodeList{"FLXG0V4B"}, + Status: entities.QueryWhitelistStatusEnabled, + } + + if !entry.MatchesAPICode("FLXG0V4B") { + t.Fatal("should match api code") + } + if entry.MatchesAPICode("JRZQ8A2D") { + t.Fatal("should not match other api code") + } + if !entry.MatchesName("任意姓名") { + t.Fatal("wildcard name should match any name") + } + + strict := &entities.QueryWhitelistEntry{ + Name: "李四", + } + if strict.MatchesName("李四") == false || strict.MatchesName("张三") { + t.Fatal("strict name matching failed") + } +} + +func TestQueryWhitelistEntry_GlobalWildcardAPICodes(t *testing.T) { + entry := &entities.QueryWhitelistEntry{ + APICodes: entities.APICodeList{"*"}, + } + if !entry.MatchesAPICode("ANY_CODE") { + t.Fatal("* should match any api code") + } +} diff --git a/internal/domains/api/services/query_whitelist_identity_api_test.go b/internal/domains/api/services/query_whitelist_identity_api_test.go new file mode 100644 index 0000000..7a0c07c --- /dev/null +++ b/internal/domains/api/services/query_whitelist_identity_api_test.go @@ -0,0 +1,52 @@ +package services + +import ( + "context" + "testing" + + "tyapi-server/internal/domains/api/entities" +) + +func TestRequiresIdentityInput_FLXG0V4B(t *testing.T) { + svc := NewFormConfigServiceWithoutDependencies() + if !svc.RequiresIdentityInput(context.Background(), "FLXG0V4B") { + t.Fatal("FLXG0V4B should require id_card") + } +} + +func TestRequiresIdentityInput_EnterpriseAPI(t *testing.T) { + svc := NewFormConfigServiceWithoutDependencies() + // QYGL8261 等企业类接口通常不要求 id_card 必填 + if svc.RequiresIdentityInput(context.Background(), "QYGL8261") { + t.Fatal("QYGL8261 should not require id_card") + } +} + +func TestRequiresIdentityInput_COMB(t *testing.T) { + svc := NewFormConfigServiceWithoutDependencies() + if !svc.RequiresIdentityInput(context.Background(), "COMBXXXX") { + t.Fatal("COMB should be eligible when params contain id_card") + } +} + +func TestShouldReturnEmpty_SkipsNonIdentityAPIEvenWithWildcard(t *testing.T) { + idCard := "350681198611130611" + hash := HashIDCard(idCard) + svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{ + entries: []*entities.QueryWhitelistEntry{ + { + ID: "1", + UserID: entities.QueryWhitelistGlobalUserID, + Name: entities.QueryWhitelistWildcardName, + IDCardHash: hash, + APICodes: entities.APICodeList{"*"}, + Status: entities.QueryWhitelistStatusEnabled, + }, + }, + }, false) + + params := map[string]interface{}{"id_card": idCard, "name": "张三"} + if svc.ShouldReturnEmpty(context.Background(), "user-a", "QYGL8261", params) { + t.Fatal("non-identity API should not be intercepted even with api_codes=*") + } +} diff --git a/internal/domains/api/services/query_whitelist_service.go b/internal/domains/api/services/query_whitelist_service.go new file mode 100644 index 0000000..b968855 --- /dev/null +++ b/internal/domains/api/services/query_whitelist_service.go @@ -0,0 +1,185 @@ +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() +} diff --git a/internal/domains/api/services/query_whitelist_service_test.go b/internal/domains/api/services/query_whitelist_service_test.go new file mode 100644 index 0000000..2fbcea3 --- /dev/null +++ b/internal/domains/api/services/query_whitelist_service_test.go @@ -0,0 +1,147 @@ +package services + +import ( + "context" + "testing" + + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/domains/api/repositories" + "tyapi-server/internal/shared/interfaces" + + "go.uber.org/zap" +) + +type mockQueryWhitelistRepo struct { + entries []*entities.QueryWhitelistEntry +} + +func (m *mockQueryWhitelistRepo) Create(ctx context.Context, entry *entities.QueryWhitelistEntry) error { + return nil +} +func (m *mockQueryWhitelistRepo) Update(ctx context.Context, entry *entities.QueryWhitelistEntry) error { + return nil +} +func (m *mockQueryWhitelistRepo) Delete(ctx context.Context, id string) error { return nil } +func (m *mockQueryWhitelistRepo) FindByID(ctx context.Context, id string) (*entities.QueryWhitelistEntry, error) { + return nil, nil +} +func (m *mockQueryWhitelistRepo) List(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.QueryWhitelistEntry, int64, error) { + return nil, 0, nil +} +func (m *mockQueryWhitelistRepo) ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name, excludeID string) (bool, error) { + return false, nil +} + +func (m *mockQueryWhitelistRepo) FindEnabledByUserIDsAndIDCardHash(ctx context.Context, userIDs []string, idCardHash string) ([]*entities.QueryWhitelistEntry, error) { + var result []*entities.QueryWhitelistEntry + for _, entry := range m.entries { + if entry.IDCardHash != idCardHash || !entry.IsEnabled() { + continue + } + for _, uid := range userIDs { + if entry.UserID == uid { + result = append(result, entry) + break + } + } + } + return result, nil +} + +func (m *mockQueryWhitelistRepo) FindAllEnabled(ctx context.Context) ([]*entities.QueryWhitelistEntry, error) { + var result []*entities.QueryWhitelistEntry + for _, entry := range m.entries { + if entry.IsEnabled() { + result = append(result, entry) + } + } + return result, nil +} + +type mockFormConfigService struct { + requiresIdentity bool +} + +func (m *mockFormConfigService) GetFormConfig(ctx context.Context, apiCode string) (*FormConfig, error) { + return nil, nil +} + +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) { + idCard := "350681198611130611" + hash := HashIDCard(idCard) + svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{ + entries: []*entities.QueryWhitelistEntry{ + { + ID: "1", + UserID: entities.QueryWhitelistGlobalUserID, + Name: entities.QueryWhitelistWildcardName, + IDCardHash: hash, + APICodes: entities.APICodeList{"*"}, + Status: entities.QueryWhitelistStatusEnabled, + }, + }, + }, true) + + params := map[string]interface{}{"id_card": idCard, "name": "任意姓名"} + if !svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { + t.Fatal("global rule should hit for any user") + } +} + +func TestShouldReturnEmpty_UserSpecificRule(t *testing.T) { + idCard := "350681198611130611" + hash := HashIDCard(idCard) + svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{ + entries: []*entities.QueryWhitelistEntry{ + { + ID: "2", + UserID: "user-a", + Name: "张三", + IDCardHash: hash, + APICodes: entities.APICodeList{"FLXG0V4B"}, + Status: entities.QueryWhitelistStatusEnabled, + }, + }, + }, true) + + params := map[string]interface{}{"id_card": idCard, "name": "张三"} + if !svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { + t.Fatal("user-a should hit") + } + if svc.ShouldReturnEmpty(context.Background(), "user-b", "FLXG0V4B", params) { + t.Fatal("user-b should not hit user-a rule") + } + if svc.ShouldReturnEmpty(context.Background(), "user-a", "JRZQ8A2D", params) { + t.Fatal("wrong api code should not hit") + } +} + +func TestShouldReturnEmpty_NameMismatch(t *testing.T) { + idCard := "350681198611130611" + hash := HashIDCard(idCard) + svc := newTestQueryWhitelistService(&mockQueryWhitelistRepo{ + entries: []*entities.QueryWhitelistEntry{ + { + ID: "3", + UserID: "user-a", + Name: "张三", + IDCardHash: hash, + APICodes: entities.APICodeList{"*"}, + Status: entities.QueryWhitelistStatusEnabled, + }, + }, + }, true) + + params := map[string]interface{}{"id_card": idCard, "name": "李四"} + if svc.ShouldReturnEmpty(context.Background(), "user-a", "FLXG0V4B", params) { + t.Fatal("name mismatch should not hit") + } +} diff --git a/internal/infrastructure/database/repositories/api/gorm_query_whitelist_repository.go b/internal/infrastructure/database/repositories/api/gorm_query_whitelist_repository.go new file mode 100644 index 0000000..ed91fc5 --- /dev/null +++ b/internal/infrastructure/database/repositories/api/gorm_query_whitelist_repository.go @@ -0,0 +1,152 @@ +package api + +import ( + "context" + "fmt" + + "tyapi-server/internal/domains/api/entities" + "tyapi-server/internal/domains/api/repositories" + "tyapi-server/internal/shared/database" + "tyapi-server/internal/shared/interfaces" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +const QueryWhitelistTable = "query_whitelist_entries" + +type GormQueryWhitelistRepository struct { + *database.BaseRepositoryImpl +} + +var _ repositories.QueryWhitelistRepository = (*GormQueryWhitelistRepository)(nil) + +func NewGormQueryWhitelistRepository(db *gorm.DB, logger *zap.Logger) repositories.QueryWhitelistRepository { + return &GormQueryWhitelistRepository{ + BaseRepositoryImpl: database.NewBaseRepositoryImpl(db, logger), + } +} + +func (r *GormQueryWhitelistRepository) Create(ctx context.Context, entry *entities.QueryWhitelistEntry) error { + return r.CreateEntity(ctx, entry) +} + +func (r *GormQueryWhitelistRepository) Update(ctx context.Context, entry *entities.QueryWhitelistEntry) error { + return r.UpdateEntity(ctx, entry) +} + +func (r *GormQueryWhitelistRepository) Delete(ctx context.Context, id string) error { + return r.GetDB(ctx).Where("id = ?", id).Delete(&entities.QueryWhitelistEntry{}).Error +} + +func (r *GormQueryWhitelistRepository) FindByID(ctx context.Context, id string) (*entities.QueryWhitelistEntry, error) { + var entry entities.QueryWhitelistEntry + if err := r.GetDB(ctx).Where("id = ?", id).First(&entry).Error; err != nil { + return nil, err + } + return &entry, nil +} + +func (r *GormQueryWhitelistRepository) FindEnabledByUserIDsAndIDCardHash( + ctx context.Context, + userIDs []string, + idCardHash string, +) ([]*entities.QueryWhitelistEntry, error) { + if len(userIDs) == 0 || idCardHash == "" { + return nil, nil + } + var entries []*entities.QueryWhitelistEntry + err := r.GetDB(ctx). + Where("user_id IN ? AND id_card_hash = ? AND status = ?", userIDs, idCardHash, entities.QueryWhitelistStatusEnabled). + Find(&entries).Error + if err != nil { + return nil, err + } + return entries, nil +} + +func (r *GormQueryWhitelistRepository) FindAllEnabled(ctx context.Context) ([]*entities.QueryWhitelistEntry, error) { + var entries []*entities.QueryWhitelistEntry + err := r.GetDB(ctx). + Where("status = ?", entities.QueryWhitelistStatusEnabled). + Find(&entries).Error + if err != nil { + return nil, err + } + return entries, nil +} + +func (r *GormQueryWhitelistRepository) List( + ctx context.Context, + filters map[string]interface{}, + options interfaces.ListOptions, +) ([]*entities.QueryWhitelistEntry, int64, error) { + query := r.GetDB(ctx).Model(&entities.QueryWhitelistEntry{}) + + if userID, ok := filters["user_id"].(string); ok && userID != "" { + query = query.Where("user_id = ?", userID) + } + if status, ok := filters["status"].(string); ok && status != "" { + query = query.Where("status = ?", status) + } + if apiCode, ok := filters["api_code"].(string); ok && apiCode != "" { + query = query.Where("api_codes::text LIKE ?", fmt.Sprintf("%%\"%s\"%%", apiCode)) + } + if idCardHash, ok := filters["id_card_hash"].(string); ok && idCardHash != "" { + query = query.Where("id_card_hash = ?", idCardHash) + } + if keyword, ok := filters["keyword"].(string); ok && keyword != "" { + like := "%" + keyword + "%" + query = query.Where("name LIKE ? OR remark LIKE ? OR id_card_masked LIKE ?", like, like, like) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + page := options.Page + if page < 1 { + page = 1 + } + pageSize := options.PageSize + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + offset := (page - 1) * pageSize + + order := "created_at DESC" + if options.Sort != "" { + dir := "ASC" + if options.Order == "desc" { + dir = "DESC" + } + order = fmt.Sprintf("%s %s", options.Sort, dir) + } + + var entries []*entities.QueryWhitelistEntry + if err := query.Order(order).Offset(offset).Limit(pageSize).Find(&entries).Error; err != nil { + return nil, 0, err + } + return entries, total, nil +} + +func (r *GormQueryWhitelistRepository) ExistsByUserIDCardHashAndName( + ctx context.Context, + userID, idCardHash, name string, + excludeID string, +) (bool, error) { + query := r.GetDB(ctx).Model(&entities.QueryWhitelistEntry{}). + Where("user_id = ? AND id_card_hash = ? AND name = ?", userID, idCardHash, name) + if excludeID != "" { + query = query.Where("id <> ?", excludeID) + } + var count int64 + if err := query.Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} diff --git a/internal/infrastructure/http/handlers/admin_query_whitelist_handler.go b/internal/infrastructure/http/handlers/admin_query_whitelist_handler.go new file mode 100644 index 0000000..0f015a9 --- /dev/null +++ b/internal/infrastructure/http/handlers/admin_query_whitelist_handler.go @@ -0,0 +1,149 @@ +package handlers + +import ( + "strconv" + "strings" + + api_app "tyapi-server/internal/application/api" + "tyapi-server/internal/application/api/dto" + "tyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AdminQueryWhitelistHandler struct { + appService api_app.QueryWhitelistApplicationService + responseBuilder interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger +} + +func NewAdminQueryWhitelistHandler( + appService api_app.QueryWhitelistApplicationService, + responseBuilder interfaces.ResponseBuilder, + validator interfaces.RequestValidator, + logger *zap.Logger, +) *AdminQueryWhitelistHandler { + return &AdminQueryWhitelistHandler{ + appService: appService, + responseBuilder: responseBuilder, + validator: validator, + logger: logger, + } +} + +func (h *AdminQueryWhitelistHandler) ListEntries(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + filters := map[string]interface{}{} + if userID := strings.TrimSpace(c.Query("user_id")); userID != "" { + filters["user_id"] = userID + } + if status := strings.TrimSpace(c.Query("status")); status != "" { + filters["status"] = status + } + if apiCode := strings.TrimSpace(c.Query("api_code")); apiCode != "" { + filters["api_code"] = apiCode + } + if idCard := strings.TrimSpace(c.Query("id_card")); idCard != "" { + filters["id_card"] = idCard + } + if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { + filters["keyword"] = keyword + } + + result, err := h.appService.ListEntries(c.Request.Context(), filters, interfaces.ListOptions{ + Page: page, + PageSize: pageSize, + }) + if err != nil { + h.logger.Error("获取查询白名单列表失败", zap.Error(err)) + h.responseBuilder.InternalError(c, "获取查询白名单列表失败") + return + } + h.responseBuilder.Success(c, result, "获取查询白名单列表成功") +} + +func (h *AdminQueryWhitelistHandler) GetEntry(c *gin.Context) { + id := c.Param("id") + result, err := h.appService.GetEntry(c.Request.Context(), id) + if err != nil { + h.logger.Error("获取查询白名单详情失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, result, "获取查询白名单详情成功") +} + +func (h *AdminQueryWhitelistHandler) CreateEntry(c *gin.Context) { + var req dto.QueryWhitelistEntryRequest + if err := h.validator.BindAndValidate(c, &req); err != nil { + h.responseBuilder.BadRequest(c, "参数校验失败") + return + } + adminUserID := c.GetString("user_id") + result, err := h.appService.CreateEntry(c.Request.Context(), adminUserID, &req) + if err != nil { + h.logger.Error("创建查询白名单失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, result, "创建查询白名单成功") +} + +func (h *AdminQueryWhitelistHandler) UpdateEntry(c *gin.Context) { + id := c.Param("id") + var req dto.QueryWhitelistEntryUpdateRequest + if err := h.validator.BindAndValidate(c, &req); err != nil { + h.responseBuilder.BadRequest(c, "参数校验失败") + return + } + adminUserID := c.GetString("user_id") + result, err := h.appService.UpdateEntry(c.Request.Context(), adminUserID, id, &req) + if err != nil { + h.logger.Error("更新查询白名单失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, result, "更新查询白名单成功") +} + +func (h *AdminQueryWhitelistHandler) UpdateEntryStatus(c *gin.Context) { + id := c.Param("id") + var req dto.QueryWhitelistStatusRequest + if err := h.validator.BindAndValidate(c, &req); err != nil { + h.responseBuilder.BadRequest(c, "参数校验失败") + return + } + adminUserID := c.GetString("user_id") + result, err := h.appService.UpdateEntryStatus(c.Request.Context(), adminUserID, id, req.Status) + if err != nil { + h.logger.Error("更新查询白名单状态失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, result, "更新查询白名单状态成功") +} + +func (h *AdminQueryWhitelistHandler) DeleteEntry(c *gin.Context) { + id := c.Param("id") + if err := h.appService.DeleteEntry(c.Request.Context(), id); err != nil { + h.logger.Error("删除查询白名单失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, nil, "删除查询白名单成功") +} + +func (h *AdminQueryWhitelistHandler) ImportLegacyEntries(c *gin.Context) { + adminUserID := c.GetString("user_id") + result, err := h.appService.ImportLegacyEntries(c.Request.Context(), adminUserID) + if err != nil { + h.logger.Error("导入历史硬编码白名单失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, err.Error()) + return + } + h.responseBuilder.Success(c, result, "导入历史硬编码白名单成功") +} diff --git a/internal/infrastructure/http/handlers/user_handler.go b/internal/infrastructure/http/handlers/user_handler.go index a8f03d5..79b97f6 100644 --- a/internal/infrastructure/http/handlers/user_handler.go +++ b/internal/infrastructure/http/handlers/user_handler.go @@ -76,7 +76,7 @@ type decodedSendCodeData struct { // @Router /api/v1/users/send-code [post] func (h *UserHandler) SendCode(c *gin.Context) { var cmd commands.SendCodeCommand - + // 绑定请求(包含data字段和可选的captchaVerifyParam字段) if err := c.ShouldBindJSON(&cmd); err != nil { h.response.BadRequest(c, "请求参数格式错误,必须提供data字段") @@ -113,7 +113,7 @@ func (h *UserHandler) SendCode(c *gin.Context) { zap.String("scene", decodedData.Scene), zap.String("client_ip", c.ClientIP()), zap.Error(err)) - + // 根据错误类型返回不同的用户友好消息(不暴露技术细节) userMessage := h.getSignatureErrorMessage(err) h.response.BadRequest(c, userMessage) @@ -123,11 +123,11 @@ func (h *UserHandler) SendCode(c *gin.Context) { // 构建SendCodeCommand用于调用应用服务 serviceCmd := &commands.SendCodeCommand{ - Phone: decodedData.Phone, - Scene: decodedData.Scene, - Timestamp: decodedData.Timestamp, - Nonce: decodedData.Nonce, - Signature: decodedData.Signature, + Phone: decodedData.Phone, + Scene: decodedData.Scene, + Timestamp: decodedData.Timestamp, + Nonce: decodedData.Nonce, + Signature: decodedData.Signature, CaptchaVerifyParam: cmd.CaptchaVerifyParam, } @@ -183,7 +183,7 @@ func (h *UserHandler) verifyDecodedSignature(ctx context.Context, data *decodedS // getSignatureErrorMessage 根据错误类型返回用户友好的错误消息(不暴露技术细节) func (h *UserHandler) getSignatureErrorMessage(err error) string { errMsg := err.Error() - + // 根据错误消息内容判断错误类型,返回通用的用户友好消息 if strings.Contains(errMsg, "请求已被使用") || strings.Contains(errMsg, "重复提交") { // 重放攻击:返回通用消息,不暴露具体原因 @@ -197,12 +197,11 @@ func (h *UserHandler) getSignatureErrorMessage(err error) string { // 签名错误:返回通用消息 return "请求验证失败,请重新操作" } - + // 其他错误:返回通用消息 return "请求验证失败,请重新操作" } - // Register 用户注册 // @Summary 用户注册 // @Description 使用手机号、密码和验证码进行用户注册,需要确认密码 diff --git a/internal/infrastructure/http/routes/admin_query_whitelist_routes.go b/internal/infrastructure/http/routes/admin_query_whitelist_routes.go new file mode 100644 index 0000000..6e10121 --- /dev/null +++ b/internal/infrastructure/http/routes/admin_query_whitelist_routes.go @@ -0,0 +1,37 @@ +package routes + +import ( + "tyapi-server/internal/infrastructure/http/handlers" + sharedhttp "tyapi-server/internal/shared/http" + "tyapi-server/internal/shared/middleware" +) + +type AdminQueryWhitelistRoutes struct { + handler *handlers.AdminQueryWhitelistHandler + admin *middleware.AdminAuthMiddleware +} + +func NewAdminQueryWhitelistRoutes( + handler *handlers.AdminQueryWhitelistHandler, + admin *middleware.AdminAuthMiddleware, +) *AdminQueryWhitelistRoutes { + return &AdminQueryWhitelistRoutes{ + handler: handler, + admin: admin, + } +} + +func (r *AdminQueryWhitelistRoutes) Register(router *sharedhttp.GinRouter) { + engine := router.GetEngine() + group := engine.Group("/api/v1/admin/query-whitelist") + group.Use(r.admin.Handle()) + { + group.GET("/entries", r.handler.ListEntries) + group.GET("/entries/:id", r.handler.GetEntry) + group.POST("/entries", r.handler.CreateEntry) + group.PUT("/entries/:id", r.handler.UpdateEntry) + group.PATCH("/entries/:id/status", r.handler.UpdateEntryStatus) + group.DELETE("/entries/:id", r.handler.DeleteEntry) + group.POST("/entries/import-legacy", r.handler.ImportLegacyEntries) + } +} diff --git a/scripts/gen_query_whitelist_hashes/main.go b/scripts/gen_query_whitelist_hashes/main.go new file mode 100644 index 0000000..abca136 --- /dev/null +++ b/scripts/gen_query_whitelist_hashes/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + + "tyapi-server/internal/domains/api/services" +) + +func main() { + ids := []string{ + "350681198611130611", "622301200006250550", "320682198910134998", "640102198708020925", + "420624197310234034", "350104198501184416", "410521198606018056", "410482198504029333", + "370982199012037272", "431027198810290730", "362502199510298017", "340826199008250378", + "321027198304072129", "420116198907031413", "13032319930128263X", "350681198412013041", + "33072619741031111X", + } + + for _, id := range ids { + fmt.Printf("%s|%s|%s\n", id, services.HashIDCard(id), services.MaskIDCard(id)) + } +} diff --git a/scripts/migrate_query_whitelist.sql b/scripts/migrate_query_whitelist.sql new file mode 100644 index 0000000..89ade2f --- /dev/null +++ b/scripts/migrate_query_whitelist.sql @@ -0,0 +1,25 @@ +-- 查询白名单迁移脚本(可选,与后台「导入历史硬编码」按钮等效) +-- 也可在管理后台 /admin/query-whitelist 点击「导入历史硬编码」完成导入 +-- name = '*' 表示仅匹配身份证,不校验姓名(兼容历史硬编码逻辑) +-- 执行前请确认表已由 AutoMigrate 创建 + +INSERT INTO query_whitelist_entries (id, user_id, name, id_card_hash, id_card_masked, api_codes, status, remark, created_at, updated_at) +VALUES + (gen_random_uuid()::text, '*', '*', '6db054d4cc4f4cadb3e08eaef45c09a9917ffb1c9baadfd9345a78744c4d5d23', '350681********0611', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '4eaa05ddb07ff799b049d52f79a4f4d6453521e4c3bae8fa3dffb3172d241d01', '622301********0550', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '4f930a44bf6246d6d358205d52e18b447e2ae83a808b8710416551dea6217b60', '320682********4998', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '87d466b44e8bc33d25d1ef9654eefa7c2d50a34a5b7c25a221eab3fe6fe7e6a9', '640102********0925', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'c27b2f57a11b37831e799f0bd18526b141a5b90519ff6d790248698b2e527c09', '420624********4034', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'e9f421ce47c3934d6312d7275afa479d263fe788222163bd45981985dba8cdf8', '350104********4416', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '5cbdcee2f32363fbbe6f7e4797285c3249355f6380b2af18c055bef97dd9c46d', '410521********8056', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'e1c8bf84aedd6ebbdeece59545bfe66cd4050373bde74fc161c0fd69034b76a6', '410482********9333', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '43b8ee09dac65311e8a5e9f5701a36d34377eb508c04d1c3695e7ba1ddbe6016', '370982********7272', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '4c041ff4bfcb75eb1fa6f2accefb3d68587f5e626f29536667d5997b3594b9b0', '431027********0730', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'e5220b9160cec7ce5f9d32bfee649e487f4ddc5e9a2b23c7b44dfc7c606bdb76', '362502********8017', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'f064061c4f7f6923bc96871bc7317131ea9c6dd1d8b6c547b6048a7fe7339dd5', '340826********0378', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', '84ed6390cb87b2142b1cf13317b252844e15237fb7317ce4dc927b33d8311e2d', '321027********2129', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'a082da9bc138c2ce91fed5d41c932dc9c5707f47cf3cca7d3169d25d971d3192', '420116********1413', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'a885a75377163280d23504fed43e7a7d3aded3c4deb94c3a83625af6a43092d2', '130323********263X', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'd9a6e6eabbc25f720f08322c82d9e0379143b093879999d2c01fa731ac97b4b9', '350681********3041', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()), + (gen_random_uuid()::text, '*', '*', 'ca36876c94d9ea535bc86bde90ac208a133908e3d9541bdd4dca0559c1109209', '330726********111X', '["*"]', 'enabled', '自硬编码迁移-全局', NOW(), NOW()) +ON CONFLICT DO NOTHING;