Merge branch 'main' of http://1.117.67.95:3000/team/hyapi-server
This commit is contained in:
@@ -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{},
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
ErrMissingAccessId = errors.New("缺少Access-Id")
|
||||
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
||||
ErrFrozenAccount = errors.New("账户已冻结")
|
||||
ErrBlacklistedAccount = errors.New("账号已被列入黑名单")
|
||||
ErrArrears = errors.New("账户余额不足,无法请求")
|
||||
ErrInsufficientBalance = errors.New("钱包余额不足")
|
||||
ErrProductNotFound = errors.New("产品不存在")
|
||||
@@ -35,6 +36,7 @@ var ErrorCodeMap = map[error]int{
|
||||
ErrMissingAccessId: 1005,
|
||||
ErrInvalidAccessId: 1006,
|
||||
ErrFrozenAccount: 1007,
|
||||
ErrBlacklistedAccount: 1007,
|
||||
ErrArrears: 1007,
|
||||
ErrInsufficientBalance: 1007,
|
||||
ErrProductNotFound: 1008,
|
||||
|
||||
15
internal/application/user/dto/commands/blacklist_commands.go
Normal file
15
internal/application/user/dto/commands/blacklist_commands.go
Normal file
@@ -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:"-"`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ type ListUsersQuery struct {
|
||||
UserType string `json:"user_type"` // 用户类型: user/admin
|
||||
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"`
|
||||
@@ -24,6 +25,7 @@ func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery {
|
||||
UserType: q.UserType,
|
||||
IsActive: q.IsActive,
|
||||
IsCertified: q.IsCertified,
|
||||
IsBlacklisted: q.IsBlacklisted,
|
||||
CompanyName: q.CompanyName,
|
||||
StartDate: q.StartDate,
|
||||
EndDate: q.EndDate,
|
||||
|
||||
@@ -10,15 +10,16 @@ type UserListItem struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ type UserProfileResponse struct {
|
||||
Username string `json:"username,omitempty" example:"admin"`
|
||||
UserType string `json:"user_type" example:"user"`
|
||||
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']"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -322,6 +326,10 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
|
||||
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,
|
||||
@@ -394,6 +402,10 @@ func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID s
|
||||
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,
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1091,6 +1091,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"`
|
||||
|
||||
@@ -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系列处理器
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
46
internal/domains/security/entities/blacklist.go
Normal file
46
internal/domains/security/entities/blacklist.go
Normal file
@@ -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"
|
||||
}
|
||||
228
internal/domains/security/services/blacklist_service.go
Normal file
228
internal/domains/security/services/blacklist_service.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -8,6 +8,7 @@ type ListUsersQuery struct {
|
||||
UserType string `json:"user_type"` // 用户类型: user/admin
|
||||
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"`
|
||||
|
||||
@@ -8,22 +8,26 @@ 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
|
||||
blacklist *securityServices.BlacklistService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserAuthService 创建用户认证领域服务
|
||||
func NewUserAuthService(
|
||||
userRepo repositories.UserRepository,
|
||||
blacklist *securityServices.BlacklistService,
|
||||
logger *zap.Logger,
|
||||
) *UserAuthService {
|
||||
return &UserAuthService{
|
||||
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("用户状态异常,无法登录")
|
||||
}
|
||||
|
||||
@@ -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+"%")
|
||||
|
||||
223
internal/infrastructure/http/handlers/admin_blacklist_handler.go
Normal file
223
internal/infrastructure/http/handlers/admin_blacklist_handler.go
Normal file
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("管理员黑名单路由注册完成")
|
||||
}
|
||||
@@ -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) // 管理员获取用户统计信息
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -16,13 +19,22 @@ import (
|
||||
type JWTAuthMiddleware struct {
|
||||
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,
|
||||
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, "需要管理员权限")
|
||||
|
||||
66
internal/shared/middleware/managed_ip_blacklist.go
Normal file
66
internal/shared/middleware/managed_ip_blacklist.go
Normal file
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user