From 06b51c92ebd814317d1a5031d0a46d35fd3720d9 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 6 Jul 2026 11:56:47 +0800 Subject: [PATCH 1/2] f --- internal/domains/api/dto/api_request_dto.go | 5 +++++ .../domains/api/services/api_request_service.go | 1 + .../domains/api/services/form_config_service.go | 1 + .../services/processors/yysy/yysyc2v7_processor.go | 14 ++++++++++++++ 4 files changed, 21 insertions(+) create mode 100644 internal/domains/api/services/processors/yysy/yysyc2v7_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index ea16585..8322758 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -1086,6 +1086,11 @@ type YYSY7R4VReq struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` } +// YYSYC2V7Req 运营商近3个月平均账单 +type YYSYC2V7Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + type QYGL5S1IReq struct { EntCode string `json:"ent_code" validate:"omitempty,validUSCI"` EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 30b12c1..8e40c1f 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -221,6 +221,7 @@ func registerAllProcessors(combService *comb.CombService) { "YYSYP7PL": yysy.ProcessYYSYP7PLRequest, //手机二次放号检测查询 "YYSY2M8Q": yysy.ProcessYYSY2M8QRequest, //手机消费区间验证 "YYSY7R4V": yysy.ProcessYYSY7R4VRequest, //手机停机验证 + "YYSYC2V7": yysy.ProcessYYSYC2V7Request, //运营商近3个月平均账单 "YYSYS7Y1": yysy.ProcessYYSYS7Y1Request, //移动手机号易诉分 // IVYZ系列处理器 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index ce7eb57..f455cc8 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -254,6 +254,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "YYSYGLSF": &dto.YYSYGLSFReq{}, //全网手机三要素验证1979周更新版 "YYSY2M8Q": &dto.YYSY2M8QReq{}, //手机消费区间验证 "YYSY7R4V": &dto.YYSY7R4VReq{}, //手机停机验证 + "YYSYC2V7": &dto.YYSYC2V7Req{}, //运营商近3个月平均账单 "QCXG3M7Z": &dto.QCXG3M7ZReq{}, //人车关系核验(ETC)10093 月更 "JRZQ1P5G": &dto.JRZQ1P5GReq{}, //全国自然人借贷压力指数查询(2) "IVYZ9OHN": &dto.IVYZ9OHNReq{}, //身份证OCR diff --git a/internal/domains/api/services/processors/yysy/yysyc2v7_processor.go b/internal/domains/api/services/processors/yysy/yysyc2v7_processor.go new file mode 100644 index 0000000..83e6eaf --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyc2v7_processor.go @@ -0,0 +1,14 @@ +package yysy + +import ( + "context" + + "hyapi-server/internal/domains/api/dto" + "hyapi-server/internal/domains/api/services/processors" +) + +// ProcessYYSYC2V7Request YYSYP72D 运营商近3个月平均账单(天远中转) +func ProcessYYSYC2V7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYC2V7Req + return processors.ValidateParamsAndCallTianyuan(ctx, deps, "YYSYP72D", params, ¶msDto) +} From 4794be896c70f702ebddd82abd139ac65937a252 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 15 Jul 2026 21:44:13 +0800 Subject: [PATCH 2/2] f --- internal/app/app.go | 2 + .../api/api_application_service.go | 26 ++ internal/application/api/errors.go | 10 +- .../user/dto/commands/blacklist_commands.go | 15 ++ .../user/dto/queries/list_users_query.go | 30 +-- .../user/dto/responses/user_list_response.go | 31 +-- .../user/dto/responses/user_responses.go | 8 +- .../user/user_application_service.go | 2 + .../user/user_application_service_impl.go | 113 +++++++-- internal/container/container.go | 11 + .../domains/security/entities/blacklist.go | 46 ++++ .../security/services/blacklist_service.go | 228 ++++++++++++++++++ internal/domains/user/entities/user.go | 61 ++++- .../user/repositories/queries/user_queries.go | 11 +- .../user/services/user_auth_service.go | 36 ++- .../repositories/user/gorm_user_repository.go | 3 + .../http/handlers/admin_blacklist_handler.go | 223 +++++++++++++++++ .../http/handlers/user_handler.go | 71 ++++++ .../http/routes/admin_blacklist_routes.go | 42 ++++ .../infrastructure/http/routes/user_routes.go | 4 +- internal/shared/middleware/auth.go | 76 +++++- .../shared/middleware/managed_ip_blacklist.go | 66 +++++ 22 files changed, 1041 insertions(+), 74 deletions(-) create mode 100644 internal/application/user/dto/commands/blacklist_commands.go create mode 100644 internal/domains/security/entities/blacklist.go create mode 100644 internal/domains/security/services/blacklist_service.go create mode 100644 internal/infrastructure/http/handlers/admin_blacklist_handler.go create mode 100644 internal/infrastructure/http/routes/admin_blacklist_routes.go create mode 100644 internal/shared/middleware/managed_ip_blacklist.go diff --git a/internal/app/app.go b/internal/app/app.go index 556f3c5..3364d4b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -258,6 +258,8 @@ func (a *Application) autoMigrate(db *gorm.DB) error { &statisticsEntities.StatisticsDashboard{}, &statisticsEntities.StatisticsReport{}, &securityEntities.SuspiciousIPRecord{}, + &securityEntities.IPBlacklistEntry{}, + &securityEntities.BlacklistHitRecord{}, // api &apiEntities.ApiUser{}, diff --git a/internal/application/api/api_application_service.go b/internal/application/api/api_application_service.go index 521135d..aa3603a 100644 --- a/internal/application/api/api_application_service.go +++ b/internal/application/api/api_application_service.go @@ -21,6 +21,8 @@ import ( product_entities "hyapi-server/internal/domains/product/entities" product_services "hyapi-server/internal/domains/product/services" user_repositories "hyapi-server/internal/domains/user/repositories" + securityEntities "hyapi-server/internal/domains/security/entities" + securityServices "hyapi-server/internal/domains/security/services" task_entities "hyapi-server/internal/infrastructure/task/entities" "hyapi-server/internal/infrastructure/task/interfaces" "hyapi-server/internal/shared/crypto" @@ -83,6 +85,7 @@ type ApiApplicationServiceImpl struct { contractInfoService user_repositories.ContractInfoRepository productManagementService *product_services.ProductManagementService userRepo user_repositories.UserRepository + blacklistService *securityServices.BlacklistService txManager *database.TransactionManager config *config.Config logger *zap.Logger @@ -112,6 +115,7 @@ func NewApiApplicationService( subscriptionService *product_services.ProductSubscriptionService, exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, + blacklistService *securityServices.BlacklistService, ) ApiApplicationService { service := &ApiApplicationServiceImpl{ apiCallService: apiCallService, @@ -121,6 +125,7 @@ func NewApiApplicationService( apiCallRepository: apiCallRepository, productManagementService: productManagementService, userRepo: userRepo, + blacklistService: blacklistService, txManager: txManager, config: config, logger: logger, @@ -218,6 +223,24 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co return nil, ErrFrozenAccount } + if platformUser, err := s.userRepo.GetByID(ctx, apiUser.UserId); err == nil { + if platformUser.IsOnBlacklist() { + s.logger.Error("账号已列入黑名单", zap.String("userId", apiUser.UserId)) + if s.blacklistService != nil { + s.blacklistService.RecordHit(&securityEntities.BlacklistHitRecord{ + HitType: securityEntities.HitTypeUserAPI, + UserID: apiUser.UserId, + Phone: platformUser.Phone, + IP: cmd.ClientIP, + Path: cmd.ApiName, + Method: "POST", + Reason: platformUser.BlacklistReason, + }) + } + return nil, ErrBlacklistedAccount + } + } + // 验证产品是否启用 if !product.IsEnabled { s.logger.Error("产品未启用", zap.String("product_code", product.Code)) @@ -440,6 +463,9 @@ func (s *ApiApplicationServiceImpl) asyncRecordFailure(ctx context.Context, apiC errorMsg = err.Error() case errors.Is(err, ErrFrozenAccount): errorType = entities.ApiCallErrorFrozenAccount + case errors.Is(err, ErrBlacklistedAccount): + errorType = entities.ApiCallErrorFrozenAccount + errorMsg = err.Error() case errors.Is(err, ErrInvalidIP): errorType = entities.ApiCallErrorInvalidIP case errors.Is(err, ErrArrears): diff --git a/internal/application/api/errors.go b/internal/application/api/errors.go index 629634e..92a7f3a 100644 --- a/internal/application/api/errors.go +++ b/internal/application/api/errors.go @@ -12,8 +12,9 @@ var ( ErrInvalidIP = errors.New("未经授权的IP") ErrMissingAccessId = errors.New("缺少Access-Id") ErrInvalidAccessId = errors.New("未经授权的AccessId") - ErrFrozenAccount = errors.New("账户已冻结") - ErrArrears = errors.New("账户余额不足,无法请求") + ErrFrozenAccount = errors.New("账户已冻结") + ErrBlacklistedAccount = errors.New("账号已被列入黑名单") + ErrArrears = errors.New("账户余额不足,无法请求") ErrInsufficientBalance = errors.New("钱包余额不足") ErrProductNotFound = errors.New("产品不存在") ErrProductDisabled = errors.New("产品已停用") @@ -34,8 +35,9 @@ var ErrorCodeMap = map[error]int{ ErrInvalidIP: 1004, ErrMissingAccessId: 1005, ErrInvalidAccessId: 1006, - ErrFrozenAccount: 1007, - ErrArrears: 1007, + ErrFrozenAccount: 1007, + ErrBlacklistedAccount: 1007, + ErrArrears: 1007, ErrInsufficientBalance: 1007, ErrProductNotFound: 1008, ErrProductDisabled: 1008, diff --git a/internal/application/user/dto/commands/blacklist_commands.go b/internal/application/user/dto/commands/blacklist_commands.go new file mode 100644 index 0000000..acbb95a --- /dev/null +++ b/internal/application/user/dto/commands/blacklist_commands.go @@ -0,0 +1,15 @@ +package commands + +// BlacklistUserCommand 管理员将用户列入黑名单 +type BlacklistUserCommand struct { + UserID string `json:"-"` + OperatorUserID string `json:"-"` + Reason string `json:"reason" binding:"required"` + Source string `json:"source"` // self / police / test,默认 self +} + +// UnblacklistUserCommand 管理员将用户移出黑名单 +type UnblacklistUserCommand struct { + UserID string `json:"-"` + OperatorUserID string `json:"-"` +} diff --git a/internal/application/user/dto/queries/list_users_query.go b/internal/application/user/dto/queries/list_users_query.go index c994a68..578c717 100644 --- a/internal/application/user/dto/queries/list_users_query.go +++ b/internal/application/user/dto/queries/list_users_query.go @@ -8,24 +8,26 @@ type ListUsersQuery struct { PageSize int `json:"page_size" validate:"min=1,max=100"` Phone string `json:"phone"` UserType string `json:"user_type"` // 用户类型: user/admin - IsActive *bool `json:"is_active"` // 是否激活 - IsCertified *bool `json:"is_certified"` // 是否已认证 - CompanyName string `json:"company_name"` // 企业名称 - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` + IsActive *bool `json:"is_active"` // 是否激活 + IsCertified *bool `json:"is_certified"` // 是否已认证 + IsBlacklisted *bool `json:"is_blacklisted"` // 是否黑名单 + CompanyName string `json:"company_name"` // 企业名称 + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` } // ToDomainQuery 转换为领域查询对象 func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery { return &queries.ListUsersQuery{ - Page: q.Page, - PageSize: q.PageSize, - Phone: q.Phone, - UserType: q.UserType, - IsActive: q.IsActive, - IsCertified: q.IsCertified, - CompanyName: q.CompanyName, - StartDate: q.StartDate, - EndDate: q.EndDate, + Page: q.Page, + PageSize: q.PageSize, + Phone: q.Phone, + UserType: q.UserType, + IsActive: q.IsActive, + IsCertified: q.IsCertified, + IsBlacklisted: q.IsBlacklisted, + CompanyName: q.CompanyName, + StartDate: q.StartDate, + EndDate: q.EndDate, } } diff --git a/internal/application/user/dto/responses/user_list_response.go b/internal/application/user/dto/responses/user_list_response.go index 43ab67d..43ec8a8 100644 --- a/internal/application/user/dto/responses/user_list_response.go +++ b/internal/application/user/dto/responses/user_list_response.go @@ -4,22 +4,23 @@ import "time" // UserListItem 用户列表项 type UserListItem struct { - ID string `json:"id"` - Phone string `json:"phone"` - UserType string `json:"user_type"` - Username string `json:"username"` - IsActive bool `json:"is_active"` - IsCertified bool `json:"is_certified"` - LoginCount int `json:"login_count"` - LastLoginAt *time.Time `json:"last_login_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - // 企业信息 + ID string `json:"id"` + Phone string `json:"phone"` + UserType string `json:"user_type"` + Username string `json:"username"` + IsActive bool `json:"is_active"` + IsCertified bool `json:"is_certified"` + IsBlacklisted bool `json:"is_blacklisted"` + BlacklistReason string `json:"blacklist_reason,omitempty"` + BlacklistSource string `json:"blacklist_source,omitempty"` + BlacklistedAt *time.Time `json:"blacklisted_at,omitempty"` + LoginCount int `json:"login_count"` + LastLoginAt *time.Time `json:"last_login_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + EnterpriseInfo *EnterpriseInfoItem `json:"enterprise_info,omitempty"` - - // 钱包信息 - WalletBalance string `json:"wallet_balance,omitempty"` + WalletBalance string `json:"wallet_balance,omitempty"` } // EnterpriseInfoItem 企业信息项 diff --git a/internal/application/user/dto/responses/user_responses.go b/internal/application/user/dto/responses/user_responses.go index e6f62ee..807256e 100644 --- a/internal/application/user/dto/responses/user_responses.go +++ b/internal/application/user/dto/responses/user_responses.go @@ -43,8 +43,12 @@ type UserProfileResponse struct { Phone string `json:"phone" example:"13800138000"` Username string `json:"username,omitempty" example:"admin"` UserType string `json:"user_type" example:"user"` - IsActive bool `json:"is_active" example:"true"` - LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"` + IsActive bool `json:"is_active" example:"true"` + IsBlacklisted bool `json:"is_blacklisted" example:"false"` + BlacklistReason string `json:"blacklist_reason,omitempty"` + BlacklistSource string `json:"blacklist_source,omitempty"` + BlacklistedAt *time.Time `json:"blacklisted_at,omitempty"` + LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"` LoginCount int `json:"login_count" example:"10"` Permissions []string `json:"permissions,omitempty" example:"['user:read','user:write']"` EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"` diff --git a/internal/application/user/user_application_service.go b/internal/application/user/user_application_service.go index df95495..b298d95 100644 --- a/internal/application/user/user_application_service.go +++ b/internal/application/user/user_application_service.go @@ -22,4 +22,6 @@ type UserApplicationService interface { ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error) GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error) GetUserStats(ctx context.Context) (*responses.UserStatsResponse, error) + BlacklistUser(ctx context.Context, cmd *commands.BlacklistUserCommand) error + UnblacklistUser(ctx context.Context, cmd *commands.UnblacklistUserCommand) error } diff --git a/internal/application/user/user_application_service_impl.go b/internal/application/user/user_application_service_impl.go index 3d5f2b9..99cedaf 100644 --- a/internal/application/user/user_application_service_impl.go +++ b/internal/application/user/user_application_service_impl.go @@ -9,6 +9,7 @@ import ( "hyapi-server/internal/application/user/dto/commands" "hyapi-server/internal/application/user/dto/queries" "hyapi-server/internal/application/user/dto/responses" + api_service "hyapi-server/internal/domains/api/services" finance_service "hyapi-server/internal/domains/finance/services" "hyapi-server/internal/domains/user/entities" "hyapi-server/internal/domains/user/events" @@ -25,6 +26,7 @@ type UserApplicationServiceImpl struct { smsCodeService *user_service.SMSCodeService walletService finance_service.WalletAggregateService contractService user_service.ContractAggregateService + apiUserService api_service.ApiUserAggregateService eventBus interfaces.EventBus jwtAuth *middleware.JWTAuthMiddleware logger *zap.Logger @@ -37,6 +39,7 @@ func NewUserApplicationService( smsCodeService *user_service.SMSCodeService, walletService finance_service.WalletAggregateService, contractService user_service.ContractAggregateService, + apiUserService api_service.ApiUserAggregateService, eventBus interfaces.EventBus, jwtAuth *middleware.JWTAuthMiddleware, logger *zap.Logger, @@ -47,6 +50,7 @@ func NewUserApplicationService( smsCodeService: smsCodeService, walletService: walletService, contractService: contractService, + apiUserService: apiUserService, eventBus: eventBus, jwtAuth: jwtAuth, logger: logger, @@ -316,16 +320,20 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri items := make([]*responses.UserListItem, 0, len(users)) for _, user := range users { item := &responses.UserListItem{ - ID: user.ID, - Phone: user.Phone, - UserType: user.UserType, - Username: user.Username, - IsActive: user.Active, - IsCertified: user.IsCertified, - LoginCount: user.LoginCount, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + ID: user.ID, + Phone: user.Phone, + UserType: user.UserType, + Username: user.Username, + IsActive: user.Active, + IsCertified: user.IsCertified, + IsBlacklisted: user.IsBlacklisted, + BlacklistReason: user.BlacklistReason, + BlacklistSource: user.BlacklistSource, + BlacklistedAt: user.BlacklistedAt, + LoginCount: user.LoginCount, + LastLoginAt: user.LastLoginAt, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } // 添加企业信息 @@ -388,16 +396,20 @@ func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID s // 2. 构建响应数据 item := &responses.UserListItem{ - ID: user.ID, - Phone: user.Phone, - UserType: user.UserType, - Username: user.Username, - IsActive: user.Active, - IsCertified: user.IsCertified, - LoginCount: user.LoginCount, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + ID: user.ID, + Phone: user.Phone, + UserType: user.UserType, + Username: user.Username, + IsActive: user.Active, + IsCertified: user.IsCertified, + IsBlacklisted: user.IsBlacklisted, + BlacklistReason: user.BlacklistReason, + BlacklistSource: user.BlacklistSource, + BlacklistedAt: user.BlacklistedAt, + LoginCount: user.LoginCount, + LastLoginAt: user.LastLoginAt, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } // 添加企业信息 @@ -459,3 +471,64 @@ func (s *UserApplicationServiceImpl) GetUserStats(ctx context.Context) (*respons CertifiedUsers: stats.CertifiedUsers, }, nil } + +// BlacklistUser 将用户列入黑名单,并同步冻结 API 账号 +func (s *UserApplicationServiceImpl) BlacklistUser(ctx context.Context, cmd *commands.BlacklistUserCommand) error { + user, err := s.userAggregateService.GetUserByID(ctx, cmd.UserID) + if err != nil { + return fmt.Errorf("用户不存在") + } + + source := cmd.Source + if source == "" { + source = "self" + } + if err := user.AddToBlacklist(cmd.Reason, source, cmd.OperatorUserID); err != nil { + return err + } + if err := s.userAggregateService.SaveUser(ctx, user); err != nil { + return fmt.Errorf("保存黑名单状态失败: %w", err) + } + + // 同步冻结 API 访问(无 API 账号时忽略) + if err := s.apiUserService.FreezeApiUser(ctx, cmd.UserID); err != nil { + s.logger.Warn("冻结 API 账号失败(可能尚未开通)", + zap.String("user_id", cmd.UserID), + zap.Error(err)) + } + + s.logger.Info("用户已列入黑名单", + zap.String("user_id", cmd.UserID), + zap.String("operator", cmd.OperatorUserID), + zap.String("reason", cmd.Reason), + zap.Time("blacklisted_at", *user.BlacklistedAt), + ) + return nil +} + +// UnblacklistUser 移出黑名单并解冻 API +func (s *UserApplicationServiceImpl) UnblacklistUser(ctx context.Context, cmd *commands.UnblacklistUserCommand) error { + user, err := s.userAggregateService.GetUserByID(ctx, cmd.UserID) + if err != nil { + return fmt.Errorf("用户不存在") + } + + if err := user.RemoveFromBlacklist(); err != nil { + return err + } + if err := s.userAggregateService.SaveUser(ctx, user); err != nil { + return fmt.Errorf("移出黑名单失败: %w", err) + } + + if err := s.apiUserService.UnfreezeApiUser(ctx, cmd.UserID); err != nil { + s.logger.Warn("解冻 API 账号失败(可能尚未开通)", + zap.String("user_id", cmd.UserID), + zap.Error(err)) + } + + s.logger.Info("用户已移出黑名单", + zap.String("user_id", cmd.UserID), + zap.String("operator", cmd.OperatorUserID), + ) + return nil +} diff --git a/internal/container/container.go b/internal/container/container.go index e87d534..df895dc 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -28,6 +28,7 @@ import ( domain_product_repo "hyapi-server/internal/domains/product/repositories" product_service "hyapi-server/internal/domains/product/services" statistics_service "hyapi-server/internal/domains/statistics/services" + security_service "hyapi-server/internal/domains/security/services" user_service "hyapi-server/internal/domains/user/services" "hyapi-server/internal/infrastructure/cache" "hyapi-server/internal/infrastructure/database" @@ -481,6 +482,7 @@ func NewContainer() *Container { middleware.NewOptionalAuthMiddleware, middleware.NewAdminAuthMiddleware, middleware.NewDomainAuthMiddleware, + middleware.NewManagedIPBlacklistMiddleware, middleware.NewTraceIDMiddleware, middleware.NewErrorTrackingMiddleware, NewRequestBodyLoggerMiddlewareWrapper, @@ -706,6 +708,7 @@ func NewContainer() *Container { // 领域服务 fx.Provide( + security_service.NewBlacklistService, fx.Annotate( user_service.NewUserAggregateService, ), @@ -821,6 +824,7 @@ func NewContainer() *Container { subscriptionService *product_services.ProductSubscriptionService, exportManager *export.ExportManager, balanceAlertService finance_services.BalanceAlertService, + blacklistService *security_service.BlacklistService, ) api_app.ApiApplicationService { return api_app.NewApiApplicationService( apiCallService, @@ -839,6 +843,7 @@ func NewContainer() *Container { subscriptionService, exportManager, balanceAlertService, + blacklistService, ) }, fx.As(new(api_app.ApiApplicationService)), @@ -1280,6 +1285,7 @@ func NewContainer() *Container { handlers.NewStatisticsHandler, // 管理员安全HTTP处理器 handlers.NewAdminSecurityHandler, + handlers.NewAdminBlacklistHandler, // 文章HTTP处理器 func( appService article.ArticleApplicationService, @@ -1373,6 +1379,7 @@ func NewContainer() *Container { routes.NewStatisticsRoutes, // 管理员安全路由 routes.NewAdminSecurityRoutes, + routes.NewAdminBlacklistRoutes, // PDFG路由 routes.NewPDFGRoutes, // 企业报告页面路由 @@ -1454,6 +1461,7 @@ func RegisterMiddlewares( responseTime *middleware.ResponseTimeMiddleware, cors *middleware.CORSMiddleware, rateLimit *middleware.RateLimitMiddleware, + managedIPBlacklist *middleware.ManagedIPBlacklistMiddleware, dailyRateLimit *middleware.DailyRateLimitMiddleware, requestLogger *middleware.RequestLoggerMiddleware, traceIDMiddleware *middleware.TraceIDMiddleware, @@ -1468,6 +1476,7 @@ func RegisterMiddlewares( router.RegisterMiddleware(responseTime) router.RegisterMiddleware(cors) router.RegisterMiddleware(rateLimit) + router.RegisterMiddleware(managedIPBlacklist) router.RegisterMiddleware(dailyRateLimit) router.RegisterMiddleware(requestLogger) router.RegisterMiddleware(traceIDMiddleware) @@ -1492,6 +1501,7 @@ func RegisterRoutes( apiRoutes *routes.ApiRoutes, statisticsRoutes *routes.StatisticsRoutes, adminSecurityRoutes *routes.AdminSecurityRoutes, + adminBlacklistRoutes *routes.AdminBlacklistRoutes, pdfgRoutes *routes.PDFGRoutes, qyglReportRoutes *routes.QYGLReportRoutes, jwtAuth *middleware.JWTAuthMiddleware, @@ -1519,6 +1529,7 @@ func RegisterRoutes( announcementRoutes.Register(router) statisticsRoutes.Register(router) adminSecurityRoutes.Register(router) + adminBlacklistRoutes.Register(router) pdfgRoutes.Register(router) qyglReportRoutes.Register(router) diff --git a/internal/domains/security/entities/blacklist.go b/internal/domains/security/entities/blacklist.go new file mode 100644 index 0000000..5afa2a8 --- /dev/null +++ b/internal/domains/security/entities/blacklist.go @@ -0,0 +1,46 @@ +package entities + +import "time" + +// IPBlacklistEntry 管理端维护的 IP 黑名单 +type IPBlacklistEntry struct { + ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"` + IP string `gorm:"type:varchar(64);not null;uniqueIndex" json:"ip"` + Reason string `gorm:"type:varchar(500);not null;default:''" json:"reason"` + Source string `gorm:"type:varchar(50);not null;default:'self'" json:"source"` + CreatedBy string `gorm:"type:varchar(36);not null;default:''" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime;index" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"-"` // 软删用手动字段,便于简单实现 + IsActive bool `gorm:"not null;default:true;index" json:"is_active"` +} + +func (IPBlacklistEntry) TableName() string { + return "ip_blacklist_entries" +} + +// HitType 拦截类型 +const ( + HitTypeUserLogin = "user_login" + HitTypeUserJWT = "user_jwt" + HitTypeUserAPI = "user_api" + HitTypeIP = "ip" +) + +// BlacklistHitRecord 黑名单拦截记录 +type BlacklistHitRecord struct { + ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"` + HitType string `gorm:"type:varchar(32);not null;index:idx_hit_type_created,priority:1" json:"hit_type"` + UserID string `gorm:"type:varchar(36);index" json:"user_id,omitempty"` + Phone string `gorm:"type:varchar(20);index" json:"phone,omitempty"` + IP string `gorm:"type:varchar(64);index" json:"ip,omitempty"` + Path string `gorm:"type:varchar(255);not null;default:''" json:"path"` + Method string `gorm:"type:varchar(16);not null;default:''" json:"method"` + Reason string `gorm:"type:varchar(500);not null;default:''" json:"reason"` + UserAgent string `gorm:"type:varchar(512);not null;default:''" json:"user_agent"` + CreatedAt time.Time `gorm:"autoCreateTime;index:idx_hit_type_created,priority:2;index" json:"created_at"` +} + +func (BlacklistHitRecord) TableName() string { + return "blacklist_hit_records" +} diff --git a/internal/domains/security/services/blacklist_service.go b/internal/domains/security/services/blacklist_service.go new file mode 100644 index 0000000..f6c4f57 --- /dev/null +++ b/internal/domains/security/services/blacklist_service.go @@ -0,0 +1,228 @@ +package services + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "time" + + "hyapi-server/internal/domains/security/entities" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +// BlacklistService 黑名单管理与拦截记录 +type BlacklistService struct { + db *gorm.DB + logger *zap.Logger + + mu sync.RWMutex + ipCache map[string]entities.IPBlacklistEntry + cacheAt time.Time + cacheTTL time.Duration +} + +func NewBlacklistService(db *gorm.DB, logger *zap.Logger) *BlacklistService { + return &BlacklistService{ + db: db, + logger: logger, + ipCache: map[string]entities.IPBlacklistEntry{}, + cacheTTL: 30 * time.Second, + } +} + +func (s *BlacklistService) InvalidateIPCache() { + s.mu.Lock() + defer s.mu.Unlock() + s.ipCache = map[string]entities.IPBlacklistEntry{} + s.cacheAt = time.Time{} +} + +func (s *BlacklistService) refreshIPCache(ctx context.Context) error { + s.mu.RLock() + fresh := time.Since(s.cacheAt) < s.cacheTTL && s.cacheAt.Unix() > 0 + s.mu.RUnlock() + if fresh { + return nil + } + + var list []entities.IPBlacklistEntry + if err := s.db.WithContext(ctx).Where("is_active = ?", true).Find(&list).Error; err != nil { + return err + } + m := make(map[string]entities.IPBlacklistEntry, len(list)) + for _, e := range list { + m[strings.TrimSpace(e.IP)] = e + } + s.mu.Lock() + s.ipCache = m + s.cacheAt = time.Now() + s.mu.Unlock() + return nil +} + +// IsIPBlocked 检查 IP 是否在管理端黑名单(支持精确 IP) +func (s *BlacklistService) IsIPBlocked(ctx context.Context, ip string) (bool, *entities.IPBlacklistEntry) { + ip = strings.TrimSpace(ip) + if ip == "" { + return false, nil + } + _ = s.refreshIPCache(ctx) + s.mu.RLock() + defer s.mu.RUnlock() + if e, ok := s.ipCache[ip]; ok { + cp := e + return true, &cp + } + // 简易通配:缓存里带 * 的模式 + for pattern, e := range s.ipCache { + if matchIPPattern(ip, pattern) { + cp := e + return true, &cp + } + } + return false, nil +} + +func matchIPPattern(ip, pattern string) bool { + if !strings.Contains(pattern, "*") { + return ip == pattern + } + parts := strings.Split(pattern, ".") + clientParts := strings.Split(ip, ".") + if len(parts) != 4 || len(clientParts) != 4 { + return false + } + for i := range parts { + if parts[i] != "*" && parts[i] != clientParts[i] { + return false + } + } + return true +} + +// RecordHit 异步写入拦截记录 +func (s *BlacklistService) RecordHit(hit *entities.BlacklistHitRecord) { + if hit == nil { + return + } + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := s.db.WithContext(ctx).Create(hit).Error; err != nil { + s.logger.Warn("写入黑名单拦截记录失败", zap.Error(err)) + } + }() +} + +func (s *BlacklistService) ListIP(ctx context.Context, page, pageSize int, keyword string) ([]entities.IPBlacklistEntry, int64, error) { + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + q := s.db.WithContext(ctx).Model(&entities.IPBlacklistEntry{}).Where("is_active = ?", true) + if keyword != "" { + q = q.Where("ip LIKE ? OR reason LIKE ?", "%"+keyword+"%", "%"+keyword+"%") + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + var list []entities.IPBlacklistEntry + err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error + return list, total, err +} + +func (s *BlacklistService) AddIP(ctx context.Context, ip, reason, source, createdBy string) (*entities.IPBlacklistEntry, error) { + ip = strings.TrimSpace(ip) + if ip == "" { + return nil, fmt.Errorf("IP不能为空") + } + if strings.Contains(ip, "*") { + // 允许简单通配,跳过 net.ParseIP + } else if net.ParseIP(ip) == nil { + return nil, fmt.Errorf("IP格式无效") + } + if reason == "" { + return nil, fmt.Errorf("原因不能为空") + } + if source == "" { + source = "self" + } + + var existing entities.IPBlacklistEntry + err := s.db.WithContext(ctx).Where("ip = ?", ip).First(&existing).Error + if err == nil { + if existing.IsActive { + return nil, fmt.Errorf("该IP已在黑名单中") + } + existing.IsActive = true + existing.Reason = reason + existing.Source = source + existing.CreatedBy = createdBy + if err := s.db.WithContext(ctx).Save(&existing).Error; err != nil { + return nil, err + } + s.InvalidateIPCache() + return &existing, nil + } + if err != gorm.ErrRecordNotFound { + return nil, err + } + + entry := &entities.IPBlacklistEntry{ + IP: ip, + Reason: reason, + Source: source, + CreatedBy: createdBy, + IsActive: true, + } + if err := s.db.WithContext(ctx).Create(entry).Error; err != nil { + return nil, err + } + s.InvalidateIPCache() + return entry, nil +} + +func (s *BlacklistService) RemoveIP(ctx context.Context, id uint64) error { + res := s.db.WithContext(ctx).Model(&entities.IPBlacklistEntry{}). + Where("id = ? AND is_active = ?", id, true). + Updates(map[string]interface{}{"is_active": false}) + if res.Error != nil { + return res.Error + } + if res.RowsAffected == 0 { + return fmt.Errorf("记录不存在或已移除") + } + s.InvalidateIPCache() + return nil +} + +func (s *BlacklistService) ListHits(ctx context.Context, page, pageSize int, hitType, keyword string) ([]entities.BlacklistHitRecord, int64, error) { + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + q := s.db.WithContext(ctx).Model(&entities.BlacklistHitRecord{}) + if hitType != "" { + q = q.Where("hit_type = ?", hitType) + } + if keyword != "" { + like := "%" + keyword + "%" + q = q.Where("phone LIKE ? OR ip LIKE ? OR user_id LIKE ? OR path LIKE ? OR reason LIKE ?", like, like, like, like, like) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + var list []entities.BlacklistHitRecord + err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error + return list, total, err +} diff --git a/internal/domains/user/entities/user.go b/internal/domains/user/entities/user.go index c99bd45..30c2c7a 100644 --- a/internal/domains/user/entities/user.go +++ b/internal/domains/user/entities/user.go @@ -39,6 +39,13 @@ type User struct { LoginCount int `gorm:"default:0" json:"login_count" comment:"登录次数统计"` Permissions string `gorm:"type:text" json:"permissions" comment:"权限列表(JSON格式存储)"` + // 用户黑名单(网安合规:纳入后拦截登录与 API) + IsBlacklisted bool `gorm:"column:is_blacklisted;default:false;index" json:"is_blacklisted" comment:"是否已列入黑名单"` + BlacklistReason string `gorm:"column:blacklist_reason;type:varchar(500)" json:"blacklist_reason,omitempty" comment:"纳入黑名单原因"` + BlacklistSource string `gorm:"column:blacklist_source;type:varchar(50)" json:"blacklist_source,omitempty" comment:"来源:self_发现/police_公安通报/test"` + BlacklistedAt *time.Time `gorm:"column:blacklisted_at" json:"blacklisted_at,omitempty" comment:"黑名单生效时间"` + BlacklistedBy string `gorm:"column:blacklisted_by;type:varchar(36)" json:"blacklisted_by,omitempty" comment:"操作人用户ID"` + // 时间戳字段 CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` @@ -457,7 +464,7 @@ func (u *User) ResetPassword(newPassword, confirmPassword string) error { // CanLogin 检查用户是否可以登录 func (u *User) CanLogin() bool { // 检查用户是否被删除 - if !u.DeletedAt.Time.IsZero() { + if u.IsDeleted() { return false } @@ -466,14 +473,62 @@ func (u *User) CanLogin() bool { return false } - // 如果是管理员,检查是否激活 - if u.IsAdmin() && !u.Active { + // 黑名单用户禁止登录 + if u.IsBlacklisted { + return false + } + + // 未激活禁止登录(普通用户与管理员一致) + if !u.Active { return false } return true } +// IsOnBlacklist 是否在黑名单中 +func (u *User) IsOnBlacklist() bool { + return u.IsBlacklisted +} + +// AddToBlacklist 纳入黑名单(立即生效) +func (u *User) AddToBlacklist(reason, source, operatorUserID string) error { + if u.IsAdmin() { + return fmt.Errorf("不能将管理员列入黑名单") + } + if u.IsBlacklisted { + return fmt.Errorf("用户已在黑名单中") + } + if reason == "" { + return NewValidationError("纳入原因不能为空") + } + if source == "" { + source = "self" + } + now := time.Now() + u.IsBlacklisted = true + u.BlacklistReason = reason + u.BlacklistSource = source + u.BlacklistedAt = &now + u.BlacklistedBy = operatorUserID + u.Active = false + return nil +} + +// RemoveFromBlacklist 移出黑名单并恢复激活 +func (u *User) RemoveFromBlacklist() error { + if !u.IsBlacklisted { + return fmt.Errorf("用户不在黑名单中") + } + u.IsBlacklisted = false + u.BlacklistReason = "" + u.BlacklistSource = "" + u.BlacklistedAt = nil + u.BlacklistedBy = "" + u.Active = true + return nil +} + // IsActive 检查用户是否处于活跃状态 func (u *User) IsActive() bool { return u.DeletedAt.Time.IsZero() diff --git a/internal/domains/user/repositories/queries/user_queries.go b/internal/domains/user/repositories/queries/user_queries.go index 7c75578..e9b08f2 100644 --- a/internal/domains/user/repositories/queries/user_queries.go +++ b/internal/domains/user/repositories/queries/user_queries.go @@ -6,11 +6,12 @@ type ListUsersQuery struct { PageSize int `json:"page_size"` Phone string `json:"phone"` UserType string `json:"user_type"` // 用户类型: user/admin - IsActive *bool `json:"is_active"` // 是否激活 - IsCertified *bool `json:"is_certified"` // 是否已认证 - CompanyName string `json:"company_name"` // 企业名称 - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` + IsActive *bool `json:"is_active"` // 是否激活 + IsCertified *bool `json:"is_certified"` // 是否已认证 + IsBlacklisted *bool `json:"is_blacklisted"` // 是否黑名单 + CompanyName string `json:"company_name"` // 企业名称 + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` } // ListSMSCodesQuery 短信验证码列表查询参数 diff --git a/internal/domains/user/services/user_auth_service.go b/internal/domains/user/services/user_auth_service.go index 42276ef..2dab9e4 100644 --- a/internal/domains/user/services/user_auth_service.go +++ b/internal/domains/user/services/user_auth_service.go @@ -8,23 +8,27 @@ import ( "hyapi-server/internal/domains/user/entities" "hyapi-server/internal/domains/user/repositories" + securityEntities "hyapi-server/internal/domains/security/entities" + securityServices "hyapi-server/internal/domains/security/services" ) // UserAuthService 用户认证领域服务 -// 负责用户认证相关的业务逻辑,包括密码验证、登录状态管理等 type UserAuthService struct { - userRepo repositories.UserRepository - logger *zap.Logger + userRepo repositories.UserRepository + blacklist *securityServices.BlacklistService + logger *zap.Logger } // NewUserAuthService 创建用户认证领域服务 func NewUserAuthService( userRepo repositories.UserRepository, + blacklist *securityServices.BlacklistService, logger *zap.Logger, ) *UserAuthService { return &UserAuthService{ - userRepo: userRepo, - logger: logger, + userRepo: userRepo, + blacklist: blacklist, + logger: logger, } } @@ -35,6 +39,17 @@ func (s *UserAuthService) ValidatePassword(ctx context.Context, phone, password return nil, fmt.Errorf("用户名或密码错误") } + if user.IsOnBlacklist() { + if s.blacklist != nil { + s.blacklist.RecordHit(&securityEntities.BlacklistHitRecord{ + HitType: securityEntities.HitTypeUserLogin, + UserID: user.ID, + Phone: user.Phone, + Reason: user.BlacklistReason, + }) + } + return nil, fmt.Errorf("账号已被列入黑名单,无法登录") + } if !user.CanLogin() { return nil, fmt.Errorf("用户状态异常,无法登录") } @@ -54,6 +69,17 @@ func (s *UserAuthService) ValidateUserLogin(ctx context.Context, phone string) ( return nil, fmt.Errorf("用户不存在") } + if user.IsOnBlacklist() { + if s.blacklist != nil { + s.blacklist.RecordHit(&securityEntities.BlacklistHitRecord{ + HitType: securityEntities.HitTypeUserLogin, + UserID: user.ID, + Phone: user.Phone, + Reason: user.BlacklistReason, + }) + } + return nil, fmt.Errorf("账号已被列入黑名单,无法登录") + } if !user.CanLogin() { return nil, fmt.Errorf("用户状态异常,无法登录") } diff --git a/internal/infrastructure/database/repositories/user/gorm_user_repository.go b/internal/infrastructure/database/repositories/user/gorm_user_repository.go index 7cd154c..08a3f8a 100644 --- a/internal/infrastructure/database/repositories/user/gorm_user_repository.go +++ b/internal/infrastructure/database/repositories/user/gorm_user_repository.go @@ -213,6 +213,9 @@ func (r *GormUserRepository) ListUsers(ctx context.Context, query *queries.ListU if query.IsCertified != nil { db = db.Where("users.is_certified = ?", *query.IsCertified) } + if query.IsBlacklisted != nil { + db = db.Where("users.is_blacklisted = ?", *query.IsBlacklisted) + } if query.CompanyName != "" { db = db.Joins("LEFT JOIN enterprise_infos ON users.id = enterprise_infos.user_id"). Where("enterprise_infos.company_name LIKE ?", "%"+query.CompanyName+"%") diff --git a/internal/infrastructure/http/handlers/admin_blacklist_handler.go b/internal/infrastructure/http/handlers/admin_blacklist_handler.go new file mode 100644 index 0000000..73aef95 --- /dev/null +++ b/internal/infrastructure/http/handlers/admin_blacklist_handler.go @@ -0,0 +1,223 @@ +package handlers + +import ( + "strconv" + "strings" + + "hyapi-server/internal/application/user" + "hyapi-server/internal/application/user/dto/commands" + "hyapi-server/internal/application/user/dto/queries" + securityServices "hyapi-server/internal/domains/security/services" + "hyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// AdminBlacklistHandler 管理端黑名单 +type AdminBlacklistHandler struct { + userApp user.UserApplicationService + blacklist *securityServices.BlacklistService + response interfaces.ResponseBuilder + logger *zap.Logger +} + +func NewAdminBlacklistHandler( + userApp user.UserApplicationService, + blacklist *securityServices.BlacklistService, + response interfaces.ResponseBuilder, + logger *zap.Logger, +) *AdminBlacklistHandler { + return &AdminBlacklistHandler{ + userApp: userApp, + blacklist: blacklist, + response: response, + logger: logger, + } +} + +func (h *AdminBlacklistHandler) operatorID(c *gin.Context) string { + if v, ok := c.Get("user_id"); ok { + if id, ok := v.(string); ok { + return id + } + } + return "" +} + +func (h *AdminBlacklistHandler) pageParams(c *gin.Context) (int, int) { + page, pageSize := 1, 20 + if v := c.Query("page"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + page = n + } + } + if v := c.Query("page_size"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 { + pageSize = n + } + } + return page, pageSize +} + +// ListUsers 用户黑名单列表 +func (h *AdminBlacklistHandler) ListUsers(c *gin.Context) { + page, pageSize := h.pageParams(c) + flag := true + q := &queries.ListUsersQuery{ + Page: page, + PageSize: pageSize, + Phone: c.Query("phone"), + IsBlacklisted: &flag, + } + resp, err := h.userApp.ListUsers(c.Request.Context(), q) + if err != nil { + h.logger.Error("查询用户黑名单失败", zap.Error(err)) + h.response.BadRequest(c, "查询失败") + return + } + h.response.Success(c, resp, "ok") +} + +// AddUser 按手机号或用户ID加入黑名单 +func (h *AdminBlacklistHandler) AddUser(c *gin.Context) { + op := h.operatorID(c) + if op == "" { + h.response.Unauthorized(c, "未登录") + return + } + var body struct { + UserID string `json:"user_id"` + Phone string `json:"phone"` + Reason string `json:"reason" binding:"required"` + Source string `json:"source"` + } + if err := c.ShouldBindJSON(&body); err != nil { + h.response.BadRequest(c, "请填写纳入原因") + return + } + + userID := strings.TrimSpace(body.UserID) + if userID == "" { + phone := strings.TrimSpace(body.Phone) + if phone == "" { + h.response.BadRequest(c, "请提供用户ID或手机号") + return + } + list, err := h.userApp.ListUsers(c.Request.Context(), &queries.ListUsersQuery{ + Page: 1, + PageSize: 50, + Phone: phone, + }) + if err != nil || list == nil || len(list.Items) == 0 { + h.response.BadRequest(c, "未找到该手机号用户") + return + } + for _, item := range list.Items { + if item.Phone == phone { + userID = item.ID + break + } + } + if userID == "" { + userID = list.Items[0].ID + } + } + + cmd := &commands.BlacklistUserCommand{ + UserID: userID, + OperatorUserID: op, + Reason: body.Reason, + Source: body.Source, + } + if err := h.userApp.BlacklistUser(c.Request.Context(), cmd); err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, gin.H{"user_id": userID}, "已列入用户黑名单") +} + +// RemoveUser 移出用户黑名单 +func (h *AdminBlacklistHandler) RemoveUser(c *gin.Context) { + op := h.operatorID(c) + userID := c.Param("user_id") + if userID == "" { + h.response.BadRequest(c, "用户ID不能为空") + return + } + if err := h.userApp.UnblacklistUser(c.Request.Context(), &commands.UnblacklistUserCommand{ + UserID: userID, + OperatorUserID: op, + }); err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, nil, "已移出用户黑名单") +} + +// ListIPs IP 黑名单 +func (h *AdminBlacklistHandler) ListIPs(c *gin.Context) { + page, pageSize := h.pageParams(c) + list, total, err := h.blacklist.ListIP(c.Request.Context(), page, pageSize, c.Query("keyword")) + if err != nil { + h.response.BadRequest(c, "查询失败") + return + } + h.response.Success(c, gin.H{ + "items": list, + "total": total, + "page": page, + "size": pageSize, + }, "ok") +} + +// AddIP 添加 IP 黑名单 +func (h *AdminBlacklistHandler) AddIP(c *gin.Context) { + op := h.operatorID(c) + var body struct { + IP string `json:"ip" binding:"required"` + Reason string `json:"reason" binding:"required"` + Source string `json:"source"` + } + if err := c.ShouldBindJSON(&body); err != nil { + h.response.BadRequest(c, "请填写IP和原因") + return + } + entry, err := h.blacklist.AddIP(c.Request.Context(), body.IP, body.Reason, body.Source, op) + if err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, entry, "已加入IP黑名单") +} + +// RemoveIP 移除 IP 黑名单 +func (h *AdminBlacklistHandler) RemoveIP(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + h.response.BadRequest(c, "无效ID") + return + } + if err := h.blacklist.RemoveIP(c.Request.Context(), id); err != nil { + h.response.BadRequest(c, err.Error()) + return + } + h.response.Success(c, nil, "已移除IP黑名单") +} + +// ListHits 拦截记录 +func (h *AdminBlacklistHandler) ListHits(c *gin.Context) { + page, pageSize := h.pageParams(c) + list, total, err := h.blacklist.ListHits(c.Request.Context(), page, pageSize, c.Query("hit_type"), c.Query("keyword")) + if err != nil { + h.response.BadRequest(c, "查询失败") + return + } + h.response.Success(c, gin.H{ + "items": list, + "total": total, + "page": page, + "size": pageSize, + }, "ok") +} diff --git a/internal/infrastructure/http/handlers/user_handler.go b/internal/infrastructure/http/handlers/user_handler.go index 34815e0..3667e2a 100644 --- a/internal/infrastructure/http/handlers/user_handler.go +++ b/internal/infrastructure/http/handlers/user_handler.go @@ -445,6 +445,12 @@ func (h *UserHandler) ListUsers(c *gin.Context) { } } + if isBlacklisted := c.Query("is_blacklisted"); isBlacklisted != "" { + if blacklisted, err := strconv.ParseBool(isBlacklisted); err == nil { + query.IsBlacklisted = &blacklisted + } + } + // 调用应用服务 resp, err := h.appService.ListUsers(c.Request.Context(), query) if err != nil { @@ -520,6 +526,71 @@ func (h *UserHandler) GetUserStats(c *gin.Context) { h.response.Success(c, resp, "获取用户统计信息成功") } +// BlacklistUser 管理员将用户列入黑名单 +func (h *UserHandler) BlacklistUser(c *gin.Context) { + operatorID := h.getCurrentUserID(c) + if operatorID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + targetUserID := c.Param("user_id") + if targetUserID == "" { + h.response.BadRequest(c, "用户ID不能为空") + return + } + + var body struct { + Reason string `json:"reason" binding:"required"` + Source string `json:"source"` + } + if err := c.ShouldBindJSON(&body); err != nil { + h.response.BadRequest(c, "请填写纳入原因") + return + } + + cmd := &commands.BlacklistUserCommand{ + UserID: targetUserID, + OperatorUserID: operatorID, + Reason: body.Reason, + Source: body.Source, + } + if err := h.appService.BlacklistUser(c.Request.Context(), cmd); err != nil { + h.logger.Error("列入黑名单失败", zap.Error(err), zap.String("user_id", targetUserID)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, nil, "已列入黑名单") +} + +// UnblacklistUser 管理员将用户移出黑名单 +func (h *UserHandler) UnblacklistUser(c *gin.Context) { + operatorID := h.getCurrentUserID(c) + if operatorID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + targetUserID := c.Param("user_id") + if targetUserID == "" { + h.response.BadRequest(c, "用户ID不能为空") + return + } + + cmd := &commands.UnblacklistUserCommand{ + UserID: targetUserID, + OperatorUserID: operatorID, + } + if err := h.appService.UnblacklistUser(c.Request.Context(), cmd); err != nil { + h.logger.Error("移出黑名单失败", zap.Error(err), zap.String("user_id", targetUserID)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, nil, "已移出黑名单") +} + // getCurrentUserID 获取当前用户ID func (h *UserHandler) getCurrentUserID(c *gin.Context) string { if userID, exists := c.Get("user_id"); exists { diff --git a/internal/infrastructure/http/routes/admin_blacklist_routes.go b/internal/infrastructure/http/routes/admin_blacklist_routes.go new file mode 100644 index 0000000..a5dd60d --- /dev/null +++ b/internal/infrastructure/http/routes/admin_blacklist_routes.go @@ -0,0 +1,42 @@ +package routes + +import ( + "hyapi-server/internal/infrastructure/http/handlers" + sharedhttp "hyapi-server/internal/shared/http" + "hyapi-server/internal/shared/middleware" + + "go.uber.org/zap" +) + +// AdminBlacklistRoutes 管理端黑名单路由 +type AdminBlacklistRoutes struct { + handler *handlers.AdminBlacklistHandler + admin *middleware.AdminAuthMiddleware + logger *zap.Logger +} + +func NewAdminBlacklistRoutes( + handler *handlers.AdminBlacklistHandler, + admin *middleware.AdminAuthMiddleware, + logger *zap.Logger, +) *AdminBlacklistRoutes { + return &AdminBlacklistRoutes{handler: handler, admin: admin, logger: logger} +} + +func (r *AdminBlacklistRoutes) Register(router *sharedhttp.GinRouter) { + engine := router.GetEngine() + g := engine.Group("/api/v1/admin/blacklist") + g.Use(r.admin.Handle()) + { + g.GET("/users", r.handler.ListUsers) + g.POST("/users", r.handler.AddUser) + g.DELETE("/users/:user_id", r.handler.RemoveUser) + + g.GET("/ips", r.handler.ListIPs) + g.POST("/ips", r.handler.AddIP) + g.DELETE("/ips/:id", r.handler.RemoveIP) + + g.GET("/hits", r.handler.ListHits) + } + r.logger.Info("管理员黑名单路由注册完成") +} diff --git a/internal/infrastructure/http/routes/user_routes.go b/internal/infrastructure/http/routes/user_routes.go index d986b26..f83d1a8 100644 --- a/internal/infrastructure/http/routes/user_routes.go +++ b/internal/infrastructure/http/routes/user_routes.go @@ -57,8 +57,10 @@ func (r *UserRoutes) Register(router *sharedhttp.GinRouter) { adminGroup.Use(r.adminAuthMiddleware.Handle()) { adminGroup.GET("/list", r.handler.ListUsers) // 管理员查看用户列表 + adminGroup.GET("/stats", r.handler.GetUserStats) // 管理员获取用户统计信息(须在 :user_id 前) + adminGroup.POST("/:user_id/blacklist", r.handler.BlacklistUser) + adminGroup.DELETE("/:user_id/blacklist", r.handler.UnblacklistUser) adminGroup.GET("/:user_id", r.handler.GetUserDetail) // 管理员获取用户详情 - adminGroup.GET("/stats", r.handler.GetUserStats) // 管理员获取用户统计信息 } } diff --git a/internal/shared/middleware/auth.go b/internal/shared/middleware/auth.go index 9176afa..20b0f6d 100644 --- a/internal/shared/middleware/auth.go +++ b/internal/shared/middleware/auth.go @@ -6,6 +6,9 @@ import ( "time" "hyapi-server/internal/config" + securityEntities "hyapi-server/internal/domains/security/entities" + securityServices "hyapi-server/internal/domains/security/services" + user_repositories "hyapi-server/internal/domains/user/repositories" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -14,15 +17,24 @@ import ( // JWTAuthMiddleware JWT认证中间件 type JWTAuthMiddleware struct { - config *config.Config - logger *zap.Logger + config *config.Config + logger *zap.Logger + userRepo user_repositories.UserRepository + blacklist *securityServices.BlacklistService } // NewJWTAuthMiddleware 创建JWT认证中间件 -func NewJWTAuthMiddleware(cfg *config.Config, logger *zap.Logger) *JWTAuthMiddleware { +func NewJWTAuthMiddleware( + cfg *config.Config, + logger *zap.Logger, + userRepo user_repositories.UserRepository, + blacklist *securityServices.BlacklistService, +) *JWTAuthMiddleware { return &JWTAuthMiddleware{ - config: cfg, - logger: logger, + config: cfg, + logger: logger, + userRepo: userRepo, + blacklist: blacklist, } } @@ -75,6 +87,10 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc { return } + if !m.ensureUserAllowed(c, claims.UserID) { + return + } + // 将用户信息添加到上下文 c.Set("user_id", claims.UserID) c.Set("username", claims.Username) @@ -124,6 +140,52 @@ func (m *JWTAuthMiddleware) validateToken(tokenString string) (*JWTClaims, error return claims, nil } +// ensureUserAllowed 复查用户是否仍可访问(黑名单即时生效) +func (m *JWTAuthMiddleware) ensureUserAllowed(c *gin.Context, userID string) bool { + if m.userRepo == nil || userID == "" { + return true + } + user, err := m.userRepo.GetByID(c.Request.Context(), userID) + if err != nil { + m.logger.Warn("查询用户状态失败", zap.String("user_id", userID), zap.Error(err)) + m.respondUnauthorized(c, "用户状态无效") + return false + } + if user.IsOnBlacklist() { + if m.blacklist != nil { + m.blacklist.RecordHit(&securityEntities.BlacklistHitRecord{ + HitType: securityEntities.HitTypeUserJWT, + UserID: userID, + Phone: user.Phone, + IP: c.ClientIP(), + Path: c.Request.URL.Path, + Method: c.Request.Method, + Reason: user.BlacklistReason, + UserAgent: c.GetHeader("User-Agent"), + }) + } + m.respondForbidden(c, "账号已被列入黑名单") + return false + } + if !user.CanLogin() { + m.respondForbidden(c, "账号不可用") + return false + } + return true +} + +// respondForbidden 禁止访问 +func (m *JWTAuthMiddleware) respondForbidden(c *gin.Context, message string) { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "访问被拒绝", + "error": message, + "request_id": c.GetString("request_id"), + "timestamp": time.Now().Unix(), + }) + c.Abort() +} + // respondUnauthorized 返回未授权响应 func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) { c.JSON(http.StatusUnauthorized, gin.H{ @@ -331,6 +393,10 @@ func (m *AdminAuthMiddleware) Handle() gin.HandlerFunc { return } + if !m.jwtAuth.ensureUserAllowed(c, claims.UserID) { + return + } + // 检查用户类型是否为管理员 if claims.UserType != "admin" { m.respondForbidden(c, "需要管理员权限") diff --git a/internal/shared/middleware/managed_ip_blacklist.go b/internal/shared/middleware/managed_ip_blacklist.go new file mode 100644 index 0000000..7fe8e11 --- /dev/null +++ b/internal/shared/middleware/managed_ip_blacklist.go @@ -0,0 +1,66 @@ +package middleware + +import ( + "net/http" + "time" + + securityEntities "hyapi-server/internal/domains/security/entities" + securityServices "hyapi-server/internal/domains/security/services" + "hyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// ManagedIPBlacklistMiddleware 管理端维护的 IP 黑名单拦截 +type ManagedIPBlacklistMiddleware struct { + blacklist *securityServices.BlacklistService + response interfaces.ResponseBuilder + logger *zap.Logger +} + +func NewManagedIPBlacklistMiddleware( + blacklist *securityServices.BlacklistService, + response interfaces.ResponseBuilder, + logger *zap.Logger, +) *ManagedIPBlacklistMiddleware { + return &ManagedIPBlacklistMiddleware{ + blacklist: blacklist, + response: response, + logger: logger, + } +} + +func (m *ManagedIPBlacklistMiddleware) GetName() string { return "managed_ip_blacklist" } +func (m *ManagedIPBlacklistMiddleware) GetPriority() int { return 25 } +func (m *ManagedIPBlacklistMiddleware) IsGlobal() bool { return true } + +func (m *ManagedIPBlacklistMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + blocked, entry := m.blacklist.IsIPBlocked(c.Request.Context(), ip) + if !blocked { + c.Next() + return + } + reason := "IP在黑名单中" + if entry != nil && entry.Reason != "" { + reason = entry.Reason + } + m.blacklist.RecordHit(&securityEntities.BlacklistHitRecord{ + HitType: securityEntities.HitTypeIP, + IP: ip, + Path: c.Request.URL.Path, + Method: c.Request.Method, + Reason: reason, + UserAgent: c.GetHeader("User-Agent"), + }) + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "访问被拒绝", + "error": "该IP已被列入黑名单", + "request_id": c.GetString("request_id"), + "timestamp": time.Now().Unix(), + }) + } +}