This commit is contained in:
2026-06-18 21:16:02 +08:00
parent 9685d34187
commit 3a5a0d0028
36 changed files with 1566 additions and 66 deletions

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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",
}