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.StatisticsDashboard{},
|
||||||
&statisticsEntities.StatisticsReport{},
|
&statisticsEntities.StatisticsReport{},
|
||||||
&securityEntities.SuspiciousIPRecord{},
|
&securityEntities.SuspiciousIPRecord{},
|
||||||
|
&securityEntities.IPBlacklistEntry{},
|
||||||
|
&securityEntities.BlacklistHitRecord{},
|
||||||
|
|
||||||
// api
|
// api
|
||||||
&apiEntities.ApiUser{},
|
&apiEntities.ApiUser{},
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import (
|
|||||||
product_entities "hyapi-server/internal/domains/product/entities"
|
product_entities "hyapi-server/internal/domains/product/entities"
|
||||||
product_services "hyapi-server/internal/domains/product/services"
|
product_services "hyapi-server/internal/domains/product/services"
|
||||||
user_repositories "hyapi-server/internal/domains/user/repositories"
|
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"
|
task_entities "hyapi-server/internal/infrastructure/task/entities"
|
||||||
"hyapi-server/internal/infrastructure/task/interfaces"
|
"hyapi-server/internal/infrastructure/task/interfaces"
|
||||||
"hyapi-server/internal/shared/crypto"
|
"hyapi-server/internal/shared/crypto"
|
||||||
@@ -83,6 +85,7 @@ type ApiApplicationServiceImpl struct {
|
|||||||
contractInfoService user_repositories.ContractInfoRepository
|
contractInfoService user_repositories.ContractInfoRepository
|
||||||
productManagementService *product_services.ProductManagementService
|
productManagementService *product_services.ProductManagementService
|
||||||
userRepo user_repositories.UserRepository
|
userRepo user_repositories.UserRepository
|
||||||
|
blacklistService *securityServices.BlacklistService
|
||||||
txManager *database.TransactionManager
|
txManager *database.TransactionManager
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
@@ -112,6 +115,7 @@ func NewApiApplicationService(
|
|||||||
subscriptionService *product_services.ProductSubscriptionService,
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
exportManager *export.ExportManager,
|
exportManager *export.ExportManager,
|
||||||
balanceAlertService finance_services.BalanceAlertService,
|
balanceAlertService finance_services.BalanceAlertService,
|
||||||
|
blacklistService *securityServices.BlacklistService,
|
||||||
) ApiApplicationService {
|
) ApiApplicationService {
|
||||||
service := &ApiApplicationServiceImpl{
|
service := &ApiApplicationServiceImpl{
|
||||||
apiCallService: apiCallService,
|
apiCallService: apiCallService,
|
||||||
@@ -121,6 +125,7 @@ func NewApiApplicationService(
|
|||||||
apiCallRepository: apiCallRepository,
|
apiCallRepository: apiCallRepository,
|
||||||
productManagementService: productManagementService,
|
productManagementService: productManagementService,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
|
blacklistService: blacklistService,
|
||||||
txManager: txManager,
|
txManager: txManager,
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -218,6 +223,24 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
|||||||
return nil, ErrFrozenAccount
|
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 {
|
if !product.IsEnabled {
|
||||||
s.logger.Error("产品未启用", zap.String("product_code", product.Code))
|
s.logger.Error("产品未启用", zap.String("product_code", product.Code))
|
||||||
@@ -440,6 +463,9 @@ func (s *ApiApplicationServiceImpl) asyncRecordFailure(ctx context.Context, apiC
|
|||||||
errorMsg = err.Error()
|
errorMsg = err.Error()
|
||||||
case errors.Is(err, ErrFrozenAccount):
|
case errors.Is(err, ErrFrozenAccount):
|
||||||
errorType = entities.ApiCallErrorFrozenAccount
|
errorType = entities.ApiCallErrorFrozenAccount
|
||||||
|
case errors.Is(err, ErrBlacklistedAccount):
|
||||||
|
errorType = entities.ApiCallErrorFrozenAccount
|
||||||
|
errorMsg = err.Error()
|
||||||
case errors.Is(err, ErrInvalidIP):
|
case errors.Is(err, ErrInvalidIP):
|
||||||
errorType = entities.ApiCallErrorInvalidIP
|
errorType = entities.ApiCallErrorInvalidIP
|
||||||
case errors.Is(err, ErrArrears):
|
case errors.Is(err, ErrArrears):
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ var (
|
|||||||
ErrInvalidIP = errors.New("未经授权的IP")
|
ErrInvalidIP = errors.New("未经授权的IP")
|
||||||
ErrMissingAccessId = errors.New("缺少Access-Id")
|
ErrMissingAccessId = errors.New("缺少Access-Id")
|
||||||
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
||||||
ErrFrozenAccount = errors.New("账户已冻结")
|
ErrFrozenAccount = errors.New("账户已冻结")
|
||||||
ErrArrears = errors.New("账户余额不足,无法请求")
|
ErrBlacklistedAccount = errors.New("账号已被列入黑名单")
|
||||||
|
ErrArrears = errors.New("账户余额不足,无法请求")
|
||||||
ErrInsufficientBalance = errors.New("钱包余额不足")
|
ErrInsufficientBalance = errors.New("钱包余额不足")
|
||||||
ErrProductNotFound = errors.New("产品不存在")
|
ErrProductNotFound = errors.New("产品不存在")
|
||||||
ErrProductDisabled = errors.New("产品已停用")
|
ErrProductDisabled = errors.New("产品已停用")
|
||||||
@@ -34,8 +35,9 @@ var ErrorCodeMap = map[error]int{
|
|||||||
ErrInvalidIP: 1004,
|
ErrInvalidIP: 1004,
|
||||||
ErrMissingAccessId: 1005,
|
ErrMissingAccessId: 1005,
|
||||||
ErrInvalidAccessId: 1006,
|
ErrInvalidAccessId: 1006,
|
||||||
ErrFrozenAccount: 1007,
|
ErrFrozenAccount: 1007,
|
||||||
ErrArrears: 1007,
|
ErrBlacklistedAccount: 1007,
|
||||||
|
ErrArrears: 1007,
|
||||||
ErrInsufficientBalance: 1007,
|
ErrInsufficientBalance: 1007,
|
||||||
ErrProductNotFound: 1008,
|
ErrProductNotFound: 1008,
|
||||||
ErrProductDisabled: 1008,
|
ErrProductDisabled: 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:"-"`
|
||||||
|
}
|
||||||
@@ -8,24 +8,26 @@ type ListUsersQuery struct {
|
|||||||
PageSize int `json:"page_size" validate:"min=1,max=100"`
|
PageSize int `json:"page_size" validate:"min=1,max=100"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
UserType string `json:"user_type"` // 用户类型: user/admin
|
UserType string `json:"user_type"` // 用户类型: user/admin
|
||||||
IsActive *bool `json:"is_active"` // 是否激活
|
IsActive *bool `json:"is_active"` // 是否激活
|
||||||
IsCertified *bool `json:"is_certified"` // 是否已认证
|
IsCertified *bool `json:"is_certified"` // 是否已认证
|
||||||
CompanyName string `json:"company_name"` // 企业名称
|
IsBlacklisted *bool `json:"is_blacklisted"` // 是否黑名单
|
||||||
StartDate string `json:"start_date"`
|
CompanyName string `json:"company_name"` // 企业名称
|
||||||
EndDate string `json:"end_date"`
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToDomainQuery 转换为领域查询对象
|
// ToDomainQuery 转换为领域查询对象
|
||||||
func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery {
|
func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery {
|
||||||
return &queries.ListUsersQuery{
|
return &queries.ListUsersQuery{
|
||||||
Page: q.Page,
|
Page: q.Page,
|
||||||
PageSize: q.PageSize,
|
PageSize: q.PageSize,
|
||||||
Phone: q.Phone,
|
Phone: q.Phone,
|
||||||
UserType: q.UserType,
|
UserType: q.UserType,
|
||||||
IsActive: q.IsActive,
|
IsActive: q.IsActive,
|
||||||
IsCertified: q.IsCertified,
|
IsCertified: q.IsCertified,
|
||||||
CompanyName: q.CompanyName,
|
IsBlacklisted: q.IsBlacklisted,
|
||||||
StartDate: q.StartDate,
|
CompanyName: q.CompanyName,
|
||||||
EndDate: q.EndDate,
|
StartDate: q.StartDate,
|
||||||
|
EndDate: q.EndDate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,23 @@ import "time"
|
|||||||
|
|
||||||
// UserListItem 用户列表项
|
// UserListItem 用户列表项
|
||||||
type UserListItem struct {
|
type UserListItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
UserType string `json:"user_type"`
|
UserType string `json:"user_type"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
IsCertified bool `json:"is_certified"`
|
IsCertified bool `json:"is_certified"`
|
||||||
LoginCount int `json:"login_count"`
|
IsBlacklisted bool `json:"is_blacklisted"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
BlacklistReason string `json:"blacklist_reason,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
BlacklistSource string `json:"blacklist_source,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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"`
|
EnterpriseInfo *EnterpriseInfoItem `json:"enterprise_info,omitempty"`
|
||||||
|
WalletBalance string `json:"wallet_balance,omitempty"`
|
||||||
// 钱包信息
|
|
||||||
WalletBalance string `json:"wallet_balance,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnterpriseInfoItem 企业信息项
|
// EnterpriseInfoItem 企业信息项
|
||||||
|
|||||||
@@ -43,8 +43,12 @@ type UserProfileResponse struct {
|
|||||||
Phone string `json:"phone" example:"13800138000"`
|
Phone string `json:"phone" example:"13800138000"`
|
||||||
Username string `json:"username,omitempty" example:"admin"`
|
Username string `json:"username,omitempty" example:"admin"`
|
||||||
UserType string `json:"user_type" example:"user"`
|
UserType string `json:"user_type" example:"user"`
|
||||||
IsActive bool `json:"is_active" example:"true"`
|
IsActive bool `json:"is_active" example:"true"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
|
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"`
|
LoginCount int `json:"login_count" example:"10"`
|
||||||
Permissions []string `json:"permissions,omitempty" example:"['user:read','user:write']"`
|
Permissions []string `json:"permissions,omitempty" example:"['user:read','user:write']"`
|
||||||
EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"`
|
EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"`
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ type UserApplicationService interface {
|
|||||||
ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error)
|
ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error)
|
||||||
GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error)
|
GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error)
|
||||||
GetUserStats(ctx context.Context) (*responses.UserStatsResponse, 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/commands"
|
||||||
"hyapi-server/internal/application/user/dto/queries"
|
"hyapi-server/internal/application/user/dto/queries"
|
||||||
"hyapi-server/internal/application/user/dto/responses"
|
"hyapi-server/internal/application/user/dto/responses"
|
||||||
|
api_service "hyapi-server/internal/domains/api/services"
|
||||||
finance_service "hyapi-server/internal/domains/finance/services"
|
finance_service "hyapi-server/internal/domains/finance/services"
|
||||||
"hyapi-server/internal/domains/user/entities"
|
"hyapi-server/internal/domains/user/entities"
|
||||||
"hyapi-server/internal/domains/user/events"
|
"hyapi-server/internal/domains/user/events"
|
||||||
@@ -25,6 +26,7 @@ type UserApplicationServiceImpl struct {
|
|||||||
smsCodeService *user_service.SMSCodeService
|
smsCodeService *user_service.SMSCodeService
|
||||||
walletService finance_service.WalletAggregateService
|
walletService finance_service.WalletAggregateService
|
||||||
contractService user_service.ContractAggregateService
|
contractService user_service.ContractAggregateService
|
||||||
|
apiUserService api_service.ApiUserAggregateService
|
||||||
eventBus interfaces.EventBus
|
eventBus interfaces.EventBus
|
||||||
jwtAuth *middleware.JWTAuthMiddleware
|
jwtAuth *middleware.JWTAuthMiddleware
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
@@ -37,6 +39,7 @@ func NewUserApplicationService(
|
|||||||
smsCodeService *user_service.SMSCodeService,
|
smsCodeService *user_service.SMSCodeService,
|
||||||
walletService finance_service.WalletAggregateService,
|
walletService finance_service.WalletAggregateService,
|
||||||
contractService user_service.ContractAggregateService,
|
contractService user_service.ContractAggregateService,
|
||||||
|
apiUserService api_service.ApiUserAggregateService,
|
||||||
eventBus interfaces.EventBus,
|
eventBus interfaces.EventBus,
|
||||||
jwtAuth *middleware.JWTAuthMiddleware,
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
@@ -47,6 +50,7 @@ func NewUserApplicationService(
|
|||||||
smsCodeService: smsCodeService,
|
smsCodeService: smsCodeService,
|
||||||
walletService: walletService,
|
walletService: walletService,
|
||||||
contractService: contractService,
|
contractService: contractService,
|
||||||
|
apiUserService: apiUserService,
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
jwtAuth: jwtAuth,
|
jwtAuth: jwtAuth,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -316,16 +320,20 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
|
|||||||
items := make([]*responses.UserListItem, 0, len(users))
|
items := make([]*responses.UserListItem, 0, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
item := &responses.UserListItem{
|
item := &responses.UserListItem{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Phone: user.Phone,
|
Phone: user.Phone,
|
||||||
UserType: user.UserType,
|
UserType: user.UserType,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
IsActive: user.Active,
|
IsActive: user.Active,
|
||||||
IsCertified: user.IsCertified,
|
IsCertified: user.IsCertified,
|
||||||
LoginCount: user.LoginCount,
|
IsBlacklisted: user.IsBlacklisted,
|
||||||
LastLoginAt: user.LastLoginAt,
|
BlacklistReason: user.BlacklistReason,
|
||||||
CreatedAt: user.CreatedAt,
|
BlacklistSource: user.BlacklistSource,
|
||||||
UpdatedAt: user.UpdatedAt,
|
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. 构建响应数据
|
// 2. 构建响应数据
|
||||||
item := &responses.UserListItem{
|
item := &responses.UserListItem{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Phone: user.Phone,
|
Phone: user.Phone,
|
||||||
UserType: user.UserType,
|
UserType: user.UserType,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
IsActive: user.Active,
|
IsActive: user.Active,
|
||||||
IsCertified: user.IsCertified,
|
IsCertified: user.IsCertified,
|
||||||
LoginCount: user.LoginCount,
|
IsBlacklisted: user.IsBlacklisted,
|
||||||
LastLoginAt: user.LastLoginAt,
|
BlacklistReason: user.BlacklistReason,
|
||||||
CreatedAt: user.CreatedAt,
|
BlacklistSource: user.BlacklistSource,
|
||||||
UpdatedAt: user.UpdatedAt,
|
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,
|
CertifiedUsers: stats.CertifiedUsers,
|
||||||
}, nil
|
}, 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"
|
domain_product_repo "hyapi-server/internal/domains/product/repositories"
|
||||||
product_service "hyapi-server/internal/domains/product/services"
|
product_service "hyapi-server/internal/domains/product/services"
|
||||||
statistics_service "hyapi-server/internal/domains/statistics/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"
|
user_service "hyapi-server/internal/domains/user/services"
|
||||||
"hyapi-server/internal/infrastructure/cache"
|
"hyapi-server/internal/infrastructure/cache"
|
||||||
"hyapi-server/internal/infrastructure/database"
|
"hyapi-server/internal/infrastructure/database"
|
||||||
@@ -534,6 +535,7 @@ func NewContainer() *Container {
|
|||||||
middleware.NewOptionalAuthMiddleware,
|
middleware.NewOptionalAuthMiddleware,
|
||||||
middleware.NewAdminAuthMiddleware,
|
middleware.NewAdminAuthMiddleware,
|
||||||
middleware.NewDomainAuthMiddleware,
|
middleware.NewDomainAuthMiddleware,
|
||||||
|
middleware.NewManagedIPBlacklistMiddleware,
|
||||||
middleware.NewTraceIDMiddleware,
|
middleware.NewTraceIDMiddleware,
|
||||||
middleware.NewErrorTrackingMiddleware,
|
middleware.NewErrorTrackingMiddleware,
|
||||||
NewRequestBodyLoggerMiddlewareWrapper,
|
NewRequestBodyLoggerMiddlewareWrapper,
|
||||||
@@ -759,6 +761,7 @@ func NewContainer() *Container {
|
|||||||
|
|
||||||
// 领域服务
|
// 领域服务
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
|
security_service.NewBlacklistService,
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
user_service.NewUserAggregateService,
|
user_service.NewUserAggregateService,
|
||||||
),
|
),
|
||||||
@@ -874,6 +877,7 @@ func NewContainer() *Container {
|
|||||||
subscriptionService *product_services.ProductSubscriptionService,
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
exportManager *export.ExportManager,
|
exportManager *export.ExportManager,
|
||||||
balanceAlertService finance_services.BalanceAlertService,
|
balanceAlertService finance_services.BalanceAlertService,
|
||||||
|
blacklistService *security_service.BlacklistService,
|
||||||
) api_app.ApiApplicationService {
|
) api_app.ApiApplicationService {
|
||||||
return api_app.NewApiApplicationService(
|
return api_app.NewApiApplicationService(
|
||||||
apiCallService,
|
apiCallService,
|
||||||
@@ -892,6 +896,7 @@ func NewContainer() *Container {
|
|||||||
subscriptionService,
|
subscriptionService,
|
||||||
exportManager,
|
exportManager,
|
||||||
balanceAlertService,
|
balanceAlertService,
|
||||||
|
blacklistService,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
fx.As(new(api_app.ApiApplicationService)),
|
fx.As(new(api_app.ApiApplicationService)),
|
||||||
@@ -1335,6 +1340,7 @@ func NewContainer() *Container {
|
|||||||
handlers.NewStatisticsHandler,
|
handlers.NewStatisticsHandler,
|
||||||
// 管理员安全HTTP处理器
|
// 管理员安全HTTP处理器
|
||||||
handlers.NewAdminSecurityHandler,
|
handlers.NewAdminSecurityHandler,
|
||||||
|
handlers.NewAdminBlacklistHandler,
|
||||||
// 文章HTTP处理器
|
// 文章HTTP处理器
|
||||||
func(
|
func(
|
||||||
appService article.ArticleApplicationService,
|
appService article.ArticleApplicationService,
|
||||||
@@ -1428,6 +1434,7 @@ func NewContainer() *Container {
|
|||||||
routes.NewStatisticsRoutes,
|
routes.NewStatisticsRoutes,
|
||||||
// 管理员安全路由
|
// 管理员安全路由
|
||||||
routes.NewAdminSecurityRoutes,
|
routes.NewAdminSecurityRoutes,
|
||||||
|
routes.NewAdminBlacklistRoutes,
|
||||||
// PDFG路由
|
// PDFG路由
|
||||||
routes.NewPDFGRoutes,
|
routes.NewPDFGRoutes,
|
||||||
// 企业报告页面路由
|
// 企业报告页面路由
|
||||||
@@ -1509,6 +1516,7 @@ func RegisterMiddlewares(
|
|||||||
responseTime *middleware.ResponseTimeMiddleware,
|
responseTime *middleware.ResponseTimeMiddleware,
|
||||||
cors *middleware.CORSMiddleware,
|
cors *middleware.CORSMiddleware,
|
||||||
rateLimit *middleware.RateLimitMiddleware,
|
rateLimit *middleware.RateLimitMiddleware,
|
||||||
|
managedIPBlacklist *middleware.ManagedIPBlacklistMiddleware,
|
||||||
dailyRateLimit *middleware.DailyRateLimitMiddleware,
|
dailyRateLimit *middleware.DailyRateLimitMiddleware,
|
||||||
requestLogger *middleware.RequestLoggerMiddleware,
|
requestLogger *middleware.RequestLoggerMiddleware,
|
||||||
traceIDMiddleware *middleware.TraceIDMiddleware,
|
traceIDMiddleware *middleware.TraceIDMiddleware,
|
||||||
@@ -1523,6 +1531,7 @@ func RegisterMiddlewares(
|
|||||||
router.RegisterMiddleware(responseTime)
|
router.RegisterMiddleware(responseTime)
|
||||||
router.RegisterMiddleware(cors)
|
router.RegisterMiddleware(cors)
|
||||||
router.RegisterMiddleware(rateLimit)
|
router.RegisterMiddleware(rateLimit)
|
||||||
|
router.RegisterMiddleware(managedIPBlacklist)
|
||||||
router.RegisterMiddleware(dailyRateLimit)
|
router.RegisterMiddleware(dailyRateLimit)
|
||||||
router.RegisterMiddleware(requestLogger)
|
router.RegisterMiddleware(requestLogger)
|
||||||
router.RegisterMiddleware(traceIDMiddleware)
|
router.RegisterMiddleware(traceIDMiddleware)
|
||||||
@@ -1547,6 +1556,7 @@ func RegisterRoutes(
|
|||||||
apiRoutes *routes.ApiRoutes,
|
apiRoutes *routes.ApiRoutes,
|
||||||
statisticsRoutes *routes.StatisticsRoutes,
|
statisticsRoutes *routes.StatisticsRoutes,
|
||||||
adminSecurityRoutes *routes.AdminSecurityRoutes,
|
adminSecurityRoutes *routes.AdminSecurityRoutes,
|
||||||
|
adminBlacklistRoutes *routes.AdminBlacklistRoutes,
|
||||||
pdfgRoutes *routes.PDFGRoutes,
|
pdfgRoutes *routes.PDFGRoutes,
|
||||||
qyglReportRoutes *routes.QYGLReportRoutes,
|
qyglReportRoutes *routes.QYGLReportRoutes,
|
||||||
jwtAuth *middleware.JWTAuthMiddleware,
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
@@ -1574,6 +1584,7 @@ func RegisterRoutes(
|
|||||||
announcementRoutes.Register(router)
|
announcementRoutes.Register(router)
|
||||||
statisticsRoutes.Register(router)
|
statisticsRoutes.Register(router)
|
||||||
adminSecurityRoutes.Register(router)
|
adminSecurityRoutes.Register(router)
|
||||||
|
adminBlacklistRoutes.Register(router)
|
||||||
pdfgRoutes.Register(router)
|
pdfgRoutes.Register(router)
|
||||||
qyglReportRoutes.Register(router)
|
qyglReportRoutes.Register(router)
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ type QYGL7HBNReq struct {
|
|||||||
type QCXGA8V3Req struct {
|
type QCXGA8V3Req struct {
|
||||||
VinCode string `json:"vin_code" validate:"required"`
|
VinCode string `json:"vin_code" validate:"required"`
|
||||||
}
|
}
|
||||||
|
type QCXGH3R8Req struct {
|
||||||
|
VinCode string `json:"vin_code" validate:"required"`
|
||||||
|
}
|
||||||
|
type QCXGK5M2Req struct {
|
||||||
|
VinCode string `json:"vin_code" validate:"required"`
|
||||||
|
}
|
||||||
type QCXGCP77Req struct {
|
type QCXGCP77Req struct {
|
||||||
VinCode string `json:"vin_code" validate:"required"`
|
VinCode string `json:"vin_code" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -148,8 +154,8 @@ type QCXG1S2LReq struct {
|
|||||||
}
|
}
|
||||||
type YYSYS7Y1Req struct {
|
type YYSYS7Y1Req struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"omitempty,min=1,validName"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JRZQDMLOReq struct {
|
type JRZQDMLOReq struct {
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"QCXGIJY3": qcxg.ProcessQCXGIJY3Request, // 名下车辆诺尔
|
"QCXGIJY3": qcxg.ProcessQCXGIJY3Request, // 名下车辆诺尔
|
||||||
"QCXG6U5G": qcxg.ProcessQCXG6U5GRequest, // 车辆核验
|
"QCXG6U5G": qcxg.ProcessQCXG6U5GRequest, // 车辆核验
|
||||||
"QCXG7K2N": qcxg.ProcessQCXG7K2NRequest, // 人车核验加强版
|
"QCXG7K2N": qcxg.ProcessQCXG7K2NRequest, // 人车核验加强版
|
||||||
|
"QCXGH3R8": qcxg.ProcessQCXGH3R8Request, // 车辆注册日期查询(VIN)
|
||||||
|
"QCXGK5M2": qcxg.ProcessQCXGK5M2Request, // 车辆投保日期与城市查询(VIN)
|
||||||
|
|
||||||
// DWBG系列处理器 - 多维报告
|
// DWBG系列处理器 - 多维报告
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,8 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
|||||||
"FLXGC4CT": &dto.FLXGC4CTReq{}, //汇博-个人涉诉
|
"FLXGC4CT": &dto.FLXGC4CTReq{}, //汇博-个人涉诉
|
||||||
"QYGLLUCM": &dto.QYGLLUCMReq{}, //汇博-企业涉诉
|
"QYGLLUCM": &dto.QYGLLUCMReq{}, //汇博-企业涉诉
|
||||||
"QCXGA8V3": &dto.QCXGA8V3Req{}, //全国车辆配置查验(车五项+使用性质+承保)
|
"QCXGA8V3": &dto.QCXGA8V3Req{}, //全国车辆配置查验(车五项+使用性质+承保)
|
||||||
|
"QCXGH3R8": &dto.QCXGH3R8Req{}, //车辆注册日期查询(VIN)
|
||||||
|
"QCXGK5M2": &dto.QCXGK5M2Req{}, //车辆投保日期与城市查询(VIN)
|
||||||
"QCXGCP77": &dto.QCXGCP77Req{}, //全国车辆配置查验(车辆详情)
|
"QCXGCP77": &dto.QCXGCP77Req{}, //全国车辆配置查验(车辆详情)
|
||||||
"QCXGX2X6": &dto.QCXGX2X6Req{}, //行驶证信息核验V2
|
"QCXGX2X6": &dto.QCXGX2X6Req{}, //行驶证信息核验V2
|
||||||
"QCXG1S2L": &dto.QCXG1S2LReq{}, //汽车车辆五项
|
"QCXG1S2L": &dto.QCXG1S2LReq{}, //汽车车辆五项
|
||||||
|
|||||||
67
internal/domains/api/services/processors/qcxg/a8v3_shared.go
Normal file
67
internal/domains/api/services/processors/qcxg/a8v3_shared.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package qcxg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"hyapi-server/internal/domains/api/dto"
|
||||||
|
"hyapi-server/internal/domains/api/services/processors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a8v3RawResult QCXGA8V3 上游原始字段
|
||||||
|
type a8v3RawResult struct {
|
||||||
|
Usage string
|
||||||
|
RegisterDate string
|
||||||
|
InsuranceMonth string
|
||||||
|
VehicleModel string
|
||||||
|
Brand string
|
||||||
|
PlateNo string
|
||||||
|
EngineNo string
|
||||||
|
Vin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func callA8V3ByVin(ctx context.Context, vinCode string, deps *processors.ProcessorDependencies) (*a8v3RawResult, error) {
|
||||||
|
params, err := json.Marshal(dto.QCXGA8V3Req{VinCode: vinCode})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := ProcessQCXGA8V3Request(ctx, params, deps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseA8V3Result(respBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseA8V3Result(respBytes []byte) (*a8v3RawResult, error) {
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(respBytes, &raw); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &a8v3RawResult{
|
||||||
|
Usage: asString(raw["usage"]),
|
||||||
|
RegisterDate: asString(raw["registerDate"]),
|
||||||
|
InsuranceMonth: asString(raw["insuranceMonth"]),
|
||||||
|
VehicleModel: asString(raw["clxh"]),
|
||||||
|
Brand: asString(raw["brand"]),
|
||||||
|
PlateNo: asString(raw["cph"]),
|
||||||
|
EngineNo: asString(raw["fdjh"]),
|
||||||
|
Vin: asString(raw["vin"]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func asString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
internal/domains/api/services/processors/qcxg/car_plate.go
Normal file
43
internal/domains/api/services/processors/qcxg/car_plate.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package qcxg
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed data/car_plate.json
|
||||||
|
var carPlateJSON []byte
|
||||||
|
|
||||||
|
var carPlateCityMap map[string]string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
carPlateCityMap = make(map[string]string)
|
||||||
|
_ = json.Unmarshal(carPlateJSON, &carPlateCityMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupCityByPlateNo 根据车牌号前缀(省简称+字母)查城市,未匹配返回 "-"
|
||||||
|
func lookupCityByPlateNo(plateNo string) string {
|
||||||
|
prefix := extractPlatePrefix(plateNo)
|
||||||
|
if prefix == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
if city, ok := carPlateCityMap[prefix]; ok && city != "" {
|
||||||
|
return city
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPlatePrefix 取车牌前两个字符作为字典键,如 "鲁A12345" -> "鲁A"
|
||||||
|
func extractPlatePrefix(plateNo string) string {
|
||||||
|
plateNo = strings.TrimSpace(plateNo)
|
||||||
|
if plateNo == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(plateNo) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
runes := []rune(plateNo)
|
||||||
|
return string(runes[:2])
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package qcxg
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLookupCityByPlateNo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
plateNo string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "济南", plateNo: "鲁A12345", want: "济南市"},
|
||||||
|
{name: "青岛增补", plateNo: "鲁U88888", want: "青岛市(增补号段)"},
|
||||||
|
{name: "北京", plateNo: "京A00001", want: "北京市"},
|
||||||
|
{name: "空车牌", plateNo: "", want: "-"},
|
||||||
|
{name: "单字", plateNo: "鲁", want: "-"},
|
||||||
|
{name: "未知前缀", plateNo: "XX12345", want: "-"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := lookupCityByPlateNo(tt.plateNo); got != tt.want {
|
||||||
|
t.Fatalf("lookupCityByPlateNo(%q) = %q, want %q", tt.plateNo, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseA8V3Result(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"fdjh": "UJ91220946",
|
||||||
|
"clxh": "LZW1029PYA",
|
||||||
|
"usage": "非营业",
|
||||||
|
"insuranceMonth": "2025-02",
|
||||||
|
"cph": "鲁A12345",
|
||||||
|
"vin": "LZABCDEF00",
|
||||||
|
"brand": "五菱牌",
|
||||||
|
"registerDate": "2018-12"
|
||||||
|
}`)
|
||||||
|
|
||||||
|
got, err := parseA8V3Result(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseA8V3Result error: %v", err)
|
||||||
|
}
|
||||||
|
if got.RegisterDate != "2018-12" {
|
||||||
|
t.Fatalf("RegisterDate = %q", got.RegisterDate)
|
||||||
|
}
|
||||||
|
if got.InsuranceMonth != "2025-02" {
|
||||||
|
t.Fatalf("InsuranceMonth = %q", got.InsuranceMonth)
|
||||||
|
}
|
||||||
|
if got.PlateNo != "鲁A12345" {
|
||||||
|
t.Fatalf("PlateNo = %q", got.PlateNo)
|
||||||
|
}
|
||||||
|
if got.VehicleModel != "LZW1029PYA" {
|
||||||
|
t.Fatalf("VehicleModel = %q", got.VehicleModel)
|
||||||
|
}
|
||||||
|
if got.EngineNo != "UJ91220946" {
|
||||||
|
t.Fatalf("EngineNo = %q", got.EngineNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
{
|
||||||
|
"京A": "北京市",
|
||||||
|
"京B": "北京市(出租车)",
|
||||||
|
"京C": "北京市",
|
||||||
|
"京E": "北京市",
|
||||||
|
"京F": "北京市",
|
||||||
|
"京G": "北京市(远郊区县)",
|
||||||
|
"京H": "北京市",
|
||||||
|
"京J": "北京市",
|
||||||
|
"京K": "北京市",
|
||||||
|
"京L": "北京市",
|
||||||
|
"京M": "北京市",
|
||||||
|
"京N": "北京市",
|
||||||
|
"京P": "北京市",
|
||||||
|
"京Q": "北京市",
|
||||||
|
"京Y": "北京市(远郊区县)",
|
||||||
|
"津A": "天津市",
|
||||||
|
"津B": "天津市",
|
||||||
|
"津C": "天津市",
|
||||||
|
"津D": "天津市",
|
||||||
|
"津E": "天津市(出租车)",
|
||||||
|
"津F": "天津市",
|
||||||
|
"津G": "天津市",
|
||||||
|
"津H": "天津市",
|
||||||
|
"津J": "天津市",
|
||||||
|
"津K": "天津市",
|
||||||
|
"津L": "天津市",
|
||||||
|
"津M": "天津市",
|
||||||
|
"津N": "天津市",
|
||||||
|
"津P": "天津市",
|
||||||
|
"津Q": "天津市",
|
||||||
|
"津R": "天津市",
|
||||||
|
"沪A": "上海市",
|
||||||
|
"沪B": "上海市",
|
||||||
|
"沪C": "上海市(远郊区县,禁入外环内)",
|
||||||
|
"沪D": "上海市",
|
||||||
|
"沪E": "上海市",
|
||||||
|
"沪F": "上海市",
|
||||||
|
"沪G": "上海市",
|
||||||
|
"沪H": "上海市",
|
||||||
|
"沪J": "上海市",
|
||||||
|
"沪K": "上海市",
|
||||||
|
"沪L": "上海市",
|
||||||
|
"沪M": "上海市",
|
||||||
|
"沪N": "上海市",
|
||||||
|
"沪R": "上海市(崇明、长兴、横沙)",
|
||||||
|
"渝A": "重庆市(主城九区)",
|
||||||
|
"渝B": "重庆市(主城区增补)",
|
||||||
|
"渝C": "重庆市(永川区、江津区、合川区、潼南区、铜梁区、璧山区、大足区、綦江区、荣昌区)",
|
||||||
|
"渝D": "重庆市(主城区增补)",
|
||||||
|
"渝F": "重庆市(万州区、开州区、梁平区、城口县、巫山县、巫溪县、忠县、云阳县、奉节县)",
|
||||||
|
"渝G": "重庆市(涪陵区、南川区、垫江县、丰都县、武隆区)",
|
||||||
|
"渝H": "重庆市(黔江区、石柱土家族自治县、秀山土家族苗族自治县、酉阳土家族苗族自治县、彭水苗族土家族自治县)",
|
||||||
|
"冀A": "石家庄市",
|
||||||
|
"冀B": "唐山市",
|
||||||
|
"冀C": "秦皇岛市",
|
||||||
|
"冀D": "邯郸市",
|
||||||
|
"冀E": "邢台市",
|
||||||
|
"冀F": "保定市",
|
||||||
|
"冀G": "张家口市",
|
||||||
|
"冀H": "承德市",
|
||||||
|
"冀J": "沧州市",
|
||||||
|
"冀R": "廊坊市",
|
||||||
|
"冀T": "衡水市",
|
||||||
|
"晋A": "太原市",
|
||||||
|
"晋B": "大同市",
|
||||||
|
"晋C": "阳泉市",
|
||||||
|
"晋D": "长治市",
|
||||||
|
"晋E": "晋城市",
|
||||||
|
"晋F": "朔州市",
|
||||||
|
"晋H": "忻州市",
|
||||||
|
"晋J": "吕梁市",
|
||||||
|
"晋K": "晋中市",
|
||||||
|
"晋L": "临汾市",
|
||||||
|
"晋M": "运城市",
|
||||||
|
"蒙A": "呼和浩特市",
|
||||||
|
"蒙B": "包头市",
|
||||||
|
"蒙C": "乌海市",
|
||||||
|
"蒙D": "赤峰市",
|
||||||
|
"蒙E": "呼伦贝尔市",
|
||||||
|
"蒙F": "兴安盟",
|
||||||
|
"蒙G": "通辽市",
|
||||||
|
"蒙H": "锡林郭勒盟",
|
||||||
|
"蒙J": "乌兰察布市",
|
||||||
|
"蒙K": "鄂尔多斯市",
|
||||||
|
"蒙L": "巴彦淖尔市",
|
||||||
|
"蒙M": "阿拉善盟",
|
||||||
|
"辽A": "沈阳市",
|
||||||
|
"辽B": "大连市",
|
||||||
|
"辽C": "鞍山市",
|
||||||
|
"辽D": "抚顺市",
|
||||||
|
"辽E": "本溪市",
|
||||||
|
"辽F": "丹东市",
|
||||||
|
"辽G": "锦州市",
|
||||||
|
"辽H": "营口市",
|
||||||
|
"辽J": "阜新市",
|
||||||
|
"辽K": "辽阳市",
|
||||||
|
"辽L": "盘锦市",
|
||||||
|
"辽M": "铁岭市",
|
||||||
|
"辽N": "朝阳市",
|
||||||
|
"辽P": "葫芦岛市",
|
||||||
|
"吉A": "长春市",
|
||||||
|
"吉B": "吉林市",
|
||||||
|
"吉C": "四平市",
|
||||||
|
"吉D": "辽源市",
|
||||||
|
"吉E": "通化市",
|
||||||
|
"吉F": "白山市",
|
||||||
|
"吉G": "白城市",
|
||||||
|
"吉H": "延边朝鲜族自治州",
|
||||||
|
"吉J": "松原市",
|
||||||
|
"吉K": "长白山保护开发区",
|
||||||
|
"黑A": "哈尔滨市",
|
||||||
|
"黑B": "齐齐哈尔市",
|
||||||
|
"黑C": "牡丹江市",
|
||||||
|
"黑D": "佳木斯市",
|
||||||
|
"黑E": "大庆市",
|
||||||
|
"黑F": "伊春市",
|
||||||
|
"黑G": "鸡西市",
|
||||||
|
"黑H": "鹤岗市",
|
||||||
|
"黑J": "双鸭山市",
|
||||||
|
"黑K": "七台河市",
|
||||||
|
"黑L": "哈尔滨市(原松花江地区)",
|
||||||
|
"黑M": "绥化市",
|
||||||
|
"黑N": "黑河市",
|
||||||
|
"黑P": "大兴安岭地区",
|
||||||
|
"黑R": "黑龙江省农垦系统",
|
||||||
|
"苏A": "南京市",
|
||||||
|
"苏B": "无锡市",
|
||||||
|
"苏C": "徐州市",
|
||||||
|
"苏D": "常州市",
|
||||||
|
"苏E": "苏州市",
|
||||||
|
"苏U": "苏州市(增补号段)",
|
||||||
|
"苏F": "南通市",
|
||||||
|
"苏G": "连云港市",
|
||||||
|
"苏H": "淮安市",
|
||||||
|
"苏J": "盐城市",
|
||||||
|
"苏K": "扬州市",
|
||||||
|
"苏L": "镇江市",
|
||||||
|
"苏M": "泰州市",
|
||||||
|
"苏N": "宿迁市",
|
||||||
|
"浙A": "杭州市",
|
||||||
|
"浙M": "杭州市(增补号段)",
|
||||||
|
"浙B": "宁波市",
|
||||||
|
"浙C": "温州市",
|
||||||
|
"浙D": "绍兴市",
|
||||||
|
"浙E": "湖州市",
|
||||||
|
"浙F": "嘉兴市",
|
||||||
|
"浙G": "金华市",
|
||||||
|
"浙H": "衢州市",
|
||||||
|
"浙J": "台州市",
|
||||||
|
"浙K": "丽水市",
|
||||||
|
"浙L": "舟山市",
|
||||||
|
"皖A": "合肥市",
|
||||||
|
"皖B": "芜湖市",
|
||||||
|
"皖C": "蚌埠市",
|
||||||
|
"皖D": "淮南市",
|
||||||
|
"皖E": "马鞍山市",
|
||||||
|
"皖F": "淮北市",
|
||||||
|
"皖G": "铜陵市",
|
||||||
|
"皖H": "安庆市",
|
||||||
|
"皖J": "黄山市",
|
||||||
|
"皖K": "阜阳市",
|
||||||
|
"皖L": "宿州市",
|
||||||
|
"皖M": "滁州市",
|
||||||
|
"皖N": "六安市",
|
||||||
|
"皖P": "宣城市",
|
||||||
|
"皖Q": "巢湖市(已撤销,存量继续使用)",
|
||||||
|
"皖R": "池州市",
|
||||||
|
"皖S": "亳州市",
|
||||||
|
"闽A": "福州市",
|
||||||
|
"闽B": "莆田市",
|
||||||
|
"闽C": "泉州市",
|
||||||
|
"闽D": "厦门市",
|
||||||
|
"闽E": "漳州市",
|
||||||
|
"闽F": "龙岩市",
|
||||||
|
"闽G": "三明市",
|
||||||
|
"闽H": "南平市",
|
||||||
|
"闽J": "宁德市",
|
||||||
|
"闽K": "平潭综合实验区",
|
||||||
|
"赣A": "南昌市",
|
||||||
|
"赣M": "南昌市(省直系统)",
|
||||||
|
"赣B": "赣州市",
|
||||||
|
"赣C": "宜春市",
|
||||||
|
"赣D": "吉安市",
|
||||||
|
"赣E": "上饶市",
|
||||||
|
"赣F": "抚州市",
|
||||||
|
"赣G": "九江市",
|
||||||
|
"赣H": "景德镇市",
|
||||||
|
"赣J": "萍乡市",
|
||||||
|
"赣K": "新余市",
|
||||||
|
"赣L": "鹰潭市",
|
||||||
|
"鲁A": "济南市",
|
||||||
|
"鲁S": "济南市(原莱芜市,存量继续使用)",
|
||||||
|
"鲁B": "青岛市",
|
||||||
|
"鲁U": "青岛市(增补号段)",
|
||||||
|
"鲁C": "淄博市",
|
||||||
|
"鲁D": "枣庄市",
|
||||||
|
"鲁E": "东营市",
|
||||||
|
"鲁F": "烟台市",
|
||||||
|
"鲁Y": "烟台市(增补号段)",
|
||||||
|
"鲁G": "潍坊市",
|
||||||
|
"鲁V": "潍坊市(增补号段)",
|
||||||
|
"鲁H": "济宁市",
|
||||||
|
"鲁J": "泰安市",
|
||||||
|
"鲁K": "威海市",
|
||||||
|
"鲁L": "日照市",
|
||||||
|
"鲁M": "滨州市",
|
||||||
|
"鲁N": "德州市",
|
||||||
|
"鲁P": "聊城市",
|
||||||
|
"鲁Q": "临沂市",
|
||||||
|
"鲁R": "菏泽市",
|
||||||
|
"豫A": "郑州市",
|
||||||
|
"豫V": "郑州市(增补号段)",
|
||||||
|
"豫B": "开封市",
|
||||||
|
"豫C": "洛阳市",
|
||||||
|
"豫D": "平顶山市",
|
||||||
|
"豫E": "安阳市",
|
||||||
|
"豫F": "鹤壁市",
|
||||||
|
"豫G": "新乡市",
|
||||||
|
"豫H": "焦作市",
|
||||||
|
"豫J": "濮阳市",
|
||||||
|
"豫K": "许昌市",
|
||||||
|
"豫L": "漯河市",
|
||||||
|
"豫M": "三门峡市",
|
||||||
|
"豫N": "商丘市",
|
||||||
|
"豫P": "周口市",
|
||||||
|
"豫Q": "驻马店市",
|
||||||
|
"豫R": "南阳市",
|
||||||
|
"豫S": "信阳市",
|
||||||
|
"豫U": "济源市",
|
||||||
|
"鄂A": "武汉市",
|
||||||
|
"鄂W": "武汉市(增补号段)",
|
||||||
|
"鄂B": "黄石市",
|
||||||
|
"鄂C": "十堰市",
|
||||||
|
"鄂D": "荆州市",
|
||||||
|
"鄂E": "宜昌市",
|
||||||
|
"鄂F": "襄阳市",
|
||||||
|
"鄂G": "鄂州市",
|
||||||
|
"鄂H": "荆门市",
|
||||||
|
"鄂J": "黄冈市",
|
||||||
|
"鄂K": "孝感市",
|
||||||
|
"鄂L": "咸宁市",
|
||||||
|
"鄂M": "仙桃市",
|
||||||
|
"鄂N": "潜江市",
|
||||||
|
"鄂P": "神农架林区",
|
||||||
|
"鄂Q": "恩施土家族苗族自治州",
|
||||||
|
"鄂R": "天门市",
|
||||||
|
"鄂S": "随州市",
|
||||||
|
"湘A": "长沙市",
|
||||||
|
"湘B": "株洲市",
|
||||||
|
"湘C": "湘潭市",
|
||||||
|
"湘D": "衡阳市",
|
||||||
|
"湘E": "邵阳市",
|
||||||
|
"湘F": "岳阳市",
|
||||||
|
"湘G": "张家界市",
|
||||||
|
"湘H": "益阳市",
|
||||||
|
"湘J": "常德市",
|
||||||
|
"湘K": "娄底市",
|
||||||
|
"湘L": "郴州市",
|
||||||
|
"湘M": "永州市",
|
||||||
|
"湘N": "怀化市",
|
||||||
|
"湘U": "湘西土家族苗族自治州",
|
||||||
|
"粤A": "广州市",
|
||||||
|
"粤B": "深圳市",
|
||||||
|
"粤C": "珠海市",
|
||||||
|
"粤D": "汕头市",
|
||||||
|
"粤E": "佛山市",
|
||||||
|
"粤X": "佛山市(顺德区,存量继续使用)",
|
||||||
|
"粤Y": "佛山市(南海区,存量继续使用)",
|
||||||
|
"粤F": "韶关市",
|
||||||
|
"粤G": "湛江市",
|
||||||
|
"粤H": "肇庆市",
|
||||||
|
"粤J": "江门市",
|
||||||
|
"粤K": "茂名市",
|
||||||
|
"粤L": "惠州市",
|
||||||
|
"粤M": "梅州市",
|
||||||
|
"粤N": "汕尾市",
|
||||||
|
"粤P": "河源市",
|
||||||
|
"粤Q": "阳江市",
|
||||||
|
"粤R": "清远市",
|
||||||
|
"粤S": "东莞市",
|
||||||
|
"粤T": "中山市",
|
||||||
|
"粤U": "潮州市",
|
||||||
|
"粤V": "揭阳市",
|
||||||
|
"粤W": "云浮市",
|
||||||
|
"桂A": "南宁市",
|
||||||
|
"桂B": "柳州市",
|
||||||
|
"桂C": "桂林市",
|
||||||
|
"桂H": "桂林市(原桂林地区)",
|
||||||
|
"桂D": "梧州市",
|
||||||
|
"桂E": "北海市",
|
||||||
|
"桂F": "崇左市",
|
||||||
|
"桂G": "来宾市",
|
||||||
|
"桂J": "贺州市",
|
||||||
|
"桂K": "玉林市",
|
||||||
|
"桂L": "百色市",
|
||||||
|
"桂M": "河池市",
|
||||||
|
"桂N": "钦州市",
|
||||||
|
"桂P": "防城港市",
|
||||||
|
"桂R": "贵港市",
|
||||||
|
"琼A": "海口市",
|
||||||
|
"琼B": "三亚市",
|
||||||
|
"琼C": "琼北地区(文昌市、琼海市、万宁市、定安县、屯昌县、澄迈县、临高县)",
|
||||||
|
"琼D": "琼南地区(五指山市、东方市、白沙黎族自治县、昌江黎族自治县、乐东黎族自治县、陵水黎族自治县、保亭黎族苗族自治县、琼中黎族苗族自治县)",
|
||||||
|
"琼E": "洋浦经济开发区",
|
||||||
|
"琼F": "儋州市",
|
||||||
|
"川A": "成都市",
|
||||||
|
"川G": "成都市(增补号段)",
|
||||||
|
"川B": "绵阳市",
|
||||||
|
"川C": "自贡市",
|
||||||
|
"川D": "攀枝花市",
|
||||||
|
"川E": "泸州市",
|
||||||
|
"川F": "德阳市",
|
||||||
|
"川H": "广元市",
|
||||||
|
"川J": "遂宁市",
|
||||||
|
"川K": "内江市",
|
||||||
|
"川L": "乐山市",
|
||||||
|
"川M": "资阳市",
|
||||||
|
"川Q": "宜宾市",
|
||||||
|
"川R": "南充市",
|
||||||
|
"川S": "达州市",
|
||||||
|
"川T": "雅安市",
|
||||||
|
"川U": "阿坝藏族羌族自治州",
|
||||||
|
"川V": "甘孜藏族自治州",
|
||||||
|
"川W": "凉山彝族自治州",
|
||||||
|
"川X": "广安市",
|
||||||
|
"川Y": "巴中市",
|
||||||
|
"川Z": "眉山市",
|
||||||
|
"贵A": "贵阳市",
|
||||||
|
"贵B": "六盘水市",
|
||||||
|
"贵C": "遵义市",
|
||||||
|
"贵D": "铜仁市",
|
||||||
|
"贵E": "黔西南布依族苗族自治州",
|
||||||
|
"贵F": "毕节市",
|
||||||
|
"贵G": "安顺市",
|
||||||
|
"贵H": "黔东南苗族侗族自治州",
|
||||||
|
"贵J": "黔南布依族苗族自治州",
|
||||||
|
"云A": "昆明市",
|
||||||
|
"云B": "东川市(已撤销,存量继续使用)",
|
||||||
|
"云C": "昭通市",
|
||||||
|
"云D": "曲靖市",
|
||||||
|
"云E": "楚雄彝族自治州",
|
||||||
|
"云F": "玉溪市",
|
||||||
|
"云G": "红河哈尼族彝族自治州",
|
||||||
|
"云H": "文山壮族苗族自治州",
|
||||||
|
"云J": "普洱市",
|
||||||
|
"云K": "西双版纳傣族自治州",
|
||||||
|
"云L": "大理白族自治州",
|
||||||
|
"云M": "保山市",
|
||||||
|
"云N": "德宏傣族景颇族自治州",
|
||||||
|
"云P": "丽江市",
|
||||||
|
"云Q": "怒江傈僳族自治州",
|
||||||
|
"云R": "迪庆藏族自治州",
|
||||||
|
"云S": "临沧市",
|
||||||
|
"藏A": "拉萨市",
|
||||||
|
"藏B": "昌都市",
|
||||||
|
"藏C": "山南市",
|
||||||
|
"藏D": "日喀则市",
|
||||||
|
"藏E": "那曲市",
|
||||||
|
"藏F": "阿里地区",
|
||||||
|
"藏G": "林芝市",
|
||||||
|
"藏H": "驻四川省天全县车辆管理所",
|
||||||
|
"藏J": "驻青海省格尔木市车辆管理所",
|
||||||
|
"陕A": "西安市",
|
||||||
|
"陕U": "西安市(增补号段)",
|
||||||
|
"陕B": "铜川市",
|
||||||
|
"陕C": "宝鸡市",
|
||||||
|
"陕D": "咸阳市",
|
||||||
|
"陕E": "渭南市",
|
||||||
|
"陕F": "汉中市",
|
||||||
|
"陕G": "安康市",
|
||||||
|
"陕H": "商洛市",
|
||||||
|
"陕J": "延安市",
|
||||||
|
"陕K": "榆林市",
|
||||||
|
"陕V": "杨凌农业高新技术产业示范区",
|
||||||
|
"甘A": "兰州市",
|
||||||
|
"甘B": "嘉峪关市",
|
||||||
|
"甘C": "金昌市",
|
||||||
|
"甘D": "白银市",
|
||||||
|
"甘E": "天水市",
|
||||||
|
"甘F": "酒泉市",
|
||||||
|
"甘G": "张掖市",
|
||||||
|
"甘H": "武威市",
|
||||||
|
"甘J": "定西市",
|
||||||
|
"甘K": "陇南市",
|
||||||
|
"甘L": "平凉市",
|
||||||
|
"甘M": "庆阳市",
|
||||||
|
"甘N": "临夏回族自治州",
|
||||||
|
"甘P": "甘南藏族自治州",
|
||||||
|
"青A": "西宁市",
|
||||||
|
"青B": "海东市",
|
||||||
|
"青C": "海北藏族自治州",
|
||||||
|
"青D": "黄南藏族自治州",
|
||||||
|
"青E": "海南藏族自治州",
|
||||||
|
"青F": "果洛藏族自治州",
|
||||||
|
"青G": "玉树藏族自治州",
|
||||||
|
"青H": "海西蒙古族藏族自治州",
|
||||||
|
"宁A": "银川市",
|
||||||
|
"宁B": "石嘴山市",
|
||||||
|
"宁C": "吴忠市",
|
||||||
|
"宁D": "固原市",
|
||||||
|
"宁E": "中卫市",
|
||||||
|
"新A": "乌鲁木齐市",
|
||||||
|
"新B": "昌吉回族自治州",
|
||||||
|
"新C": "石河子市",
|
||||||
|
"新D": "奎屯市",
|
||||||
|
"新E": "博尔塔拉蒙古自治州",
|
||||||
|
"新F": "伊犁哈萨克自治州",
|
||||||
|
"新G": "塔城地区",
|
||||||
|
"新H": "阿勒泰地区",
|
||||||
|
"新J": "克拉玛依市",
|
||||||
|
"新K": "吐鲁番市",
|
||||||
|
"新L": "哈密市",
|
||||||
|
"新M": "巴音郭楞蒙古自治州",
|
||||||
|
"新N": "阿克苏地区",
|
||||||
|
"新P": "克孜勒苏柯尔克孜自治州",
|
||||||
|
"新Q": "喀什地区",
|
||||||
|
"新R": "和田地区",
|
||||||
|
"新S": "昆玉市"
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package qcxg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"hyapi-server/internal/domains/api/dto"
|
||||||
|
"hyapi-server/internal/domains/api/services/processors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessQCXGH3R8Request QCXGH3R8 车辆注册日期查询(VIN)
|
||||||
|
// 复用 QCXGA8V3,仅返回 registerDate。
|
||||||
|
func ProcessQCXGH3R8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
var paramsDto dto.QCXGH3R8Req
|
||||||
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := callA8V3ByVin(ctx, paramsDto.VinCode, deps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(map[string]string{
|
||||||
|
"registerDate": result.RegisterDate,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
return respBytes, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package qcxg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"hyapi-server/internal/domains/api/dto"
|
||||||
|
"hyapi-server/internal/domains/api/services/processors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessQCXGK5M2Request QCXGK5M2 车辆投保日期与城市查询(VIN)
|
||||||
|
// 复用 QCXGA8V3,返回 insuranceMonth,并根据车牌前缀字典解析 city。
|
||||||
|
func ProcessQCXGK5M2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
var paramsDto dto.QCXGK5M2Req
|
||||||
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := callA8V3ByVin(ctx, paramsDto.VinCode, deps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(map[string]string{
|
||||||
|
"insuranceMonth": result.InsuranceMonth,
|
||||||
|
"city": lookupCityByPlateNo(result.PlateNo),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
return respBytes, nil
|
||||||
|
}
|
||||||
@@ -21,12 +21,16 @@ func ProcessYYSYS7Y1Request(ctx context.Context, params []byte, deps *processors
|
|||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建数据宝入参:phone、name、idCard 需传 MD5 密文
|
// 构建数据宝入参:phone、name、idCard 需传 MD5 密文(name、idCard 选填)
|
||||||
reqParams := map[string]interface{}{
|
reqParams := map[string]interface{}{
|
||||||
"key": "40130990be30f9fea6422168d138ff6c",
|
"key": "40130990be30f9fea6422168d138ff6c",
|
||||||
"phone": shujubao.MD5Encrypt(paramsDto.MobileNo),
|
"phone": shujubao.MD5Encrypt(paramsDto.MobileNo),
|
||||||
"name": shujubao.MD5Encrypt(paramsDto.Name),
|
}
|
||||||
"idCard": shujubao.MD5Encrypt(paramsDto.IDCard),
|
if paramsDto.Name != "" {
|
||||||
|
reqParams["name"] = shujubao.MD5Encrypt(paramsDto.Name)
|
||||||
|
}
|
||||||
|
if paramsDto.IDCard != "" {
|
||||||
|
reqParams["idCard"] = shujubao.MD5Encrypt(paramsDto.IDCard)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||||
|
|||||||
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:"登录次数统计"`
|
LoginCount int `gorm:"default:0" json:"login_count" comment:"登录次数统计"`
|
||||||
Permissions string `gorm:"type:text" json:"permissions" comment:"权限列表(JSON格式存储)"`
|
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:"创建时间"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||||
@@ -457,7 +464,7 @@ func (u *User) ResetPassword(newPassword, confirmPassword string) error {
|
|||||||
// CanLogin 检查用户是否可以登录
|
// CanLogin 检查用户是否可以登录
|
||||||
func (u *User) CanLogin() bool {
|
func (u *User) CanLogin() bool {
|
||||||
// 检查用户是否被删除
|
// 检查用户是否被删除
|
||||||
if !u.DeletedAt.Time.IsZero() {
|
if u.IsDeleted() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,14 +473,62 @@ func (u *User) CanLogin() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是管理员,检查是否激活
|
// 黑名单用户禁止登录
|
||||||
if u.IsAdmin() && !u.Active {
|
if u.IsBlacklisted {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未激活禁止登录(普通用户与管理员一致)
|
||||||
|
if !u.Active {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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 检查用户是否处于活跃状态
|
// IsActive 检查用户是否处于活跃状态
|
||||||
func (u *User) IsActive() bool {
|
func (u *User) IsActive() bool {
|
||||||
return u.DeletedAt.Time.IsZero()
|
return u.DeletedAt.Time.IsZero()
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ type ListUsersQuery struct {
|
|||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
UserType string `json:"user_type"` // 用户类型: user/admin
|
UserType string `json:"user_type"` // 用户类型: user/admin
|
||||||
IsActive *bool `json:"is_active"` // 是否激活
|
IsActive *bool `json:"is_active"` // 是否激活
|
||||||
IsCertified *bool `json:"is_certified"` // 是否已认证
|
IsCertified *bool `json:"is_certified"` // 是否已认证
|
||||||
CompanyName string `json:"company_name"` // 企业名称
|
IsBlacklisted *bool `json:"is_blacklisted"` // 是否黑名单
|
||||||
StartDate string `json:"start_date"`
|
CompanyName string `json:"company_name"` // 企业名称
|
||||||
EndDate string `json:"end_date"`
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSMSCodesQuery 短信验证码列表查询参数
|
// ListSMSCodesQuery 短信验证码列表查询参数
|
||||||
|
|||||||
@@ -8,23 +8,27 @@ import (
|
|||||||
|
|
||||||
"hyapi-server/internal/domains/user/entities"
|
"hyapi-server/internal/domains/user/entities"
|
||||||
"hyapi-server/internal/domains/user/repositories"
|
"hyapi-server/internal/domains/user/repositories"
|
||||||
|
securityEntities "hyapi-server/internal/domains/security/entities"
|
||||||
|
securityServices "hyapi-server/internal/domains/security/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserAuthService 用户认证领域服务
|
// UserAuthService 用户认证领域服务
|
||||||
// 负责用户认证相关的业务逻辑,包括密码验证、登录状态管理等
|
|
||||||
type UserAuthService struct {
|
type UserAuthService struct {
|
||||||
userRepo repositories.UserRepository
|
userRepo repositories.UserRepository
|
||||||
logger *zap.Logger
|
blacklist *securityServices.BlacklistService
|
||||||
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserAuthService 创建用户认证领域服务
|
// NewUserAuthService 创建用户认证领域服务
|
||||||
func NewUserAuthService(
|
func NewUserAuthService(
|
||||||
userRepo repositories.UserRepository,
|
userRepo repositories.UserRepository,
|
||||||
|
blacklist *securityServices.BlacklistService,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) *UserAuthService {
|
) *UserAuthService {
|
||||||
return &UserAuthService{
|
return &UserAuthService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
logger: logger,
|
blacklist: blacklist,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +39,17 @@ func (s *UserAuthService) ValidatePassword(ctx context.Context, phone, password
|
|||||||
return nil, fmt.Errorf("用户名或密码错误")
|
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() {
|
if !user.CanLogin() {
|
||||||
return nil, fmt.Errorf("用户状态异常,无法登录")
|
return nil, fmt.Errorf("用户状态异常,无法登录")
|
||||||
}
|
}
|
||||||
@@ -54,6 +69,17 @@ func (s *UserAuthService) ValidateUserLogin(ctx context.Context, phone string) (
|
|||||||
return nil, fmt.Errorf("用户不存在")
|
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() {
|
if !user.CanLogin() {
|
||||||
return nil, fmt.Errorf("用户状态异常,无法登录")
|
return nil, fmt.Errorf("用户状态异常,无法登录")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ func (r *GormUserRepository) ListUsers(ctx context.Context, query *queries.ListU
|
|||||||
if query.IsCertified != nil {
|
if query.IsCertified != nil {
|
||||||
db = db.Where("users.is_certified = ?", *query.IsCertified)
|
db = db.Where("users.is_certified = ?", *query.IsCertified)
|
||||||
}
|
}
|
||||||
|
if query.IsBlacklisted != nil {
|
||||||
|
db = db.Where("users.is_blacklisted = ?", *query.IsBlacklisted)
|
||||||
|
}
|
||||||
if query.CompanyName != "" {
|
if query.CompanyName != "" {
|
||||||
db = db.Joins("LEFT JOIN enterprise_infos ON users.id = enterprise_infos.user_id").
|
db = db.Joins("LEFT JOIN enterprise_infos ON users.id = enterprise_infos.user_id").
|
||||||
Where("enterprise_infos.company_name LIKE ?", "%"+query.CompanyName+"%")
|
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)
|
resp, err := h.appService.ListUsers(c.Request.Context(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -520,6 +526,71 @@ func (h *UserHandler) GetUserStats(c *gin.Context) {
|
|||||||
h.response.Success(c, resp, "获取用户统计信息成功")
|
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
|
// getCurrentUserID 获取当前用户ID
|
||||||
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||||||
if userID, exists := c.Get("user_id"); exists {
|
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.Use(r.adminAuthMiddleware.Handle())
|
||||||
{
|
{
|
||||||
adminGroup.GET("/list", r.handler.ListUsers) // 管理员查看用户列表
|
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("/:user_id", r.handler.GetUserDetail) // 管理员获取用户详情
|
||||||
adminGroup.GET("/stats", r.handler.GetUserStats) // 管理员获取用户统计信息
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hyapi-server/internal/config"
|
"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/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -14,15 +17,24 @@ import (
|
|||||||
|
|
||||||
// JWTAuthMiddleware JWT认证中间件
|
// JWTAuthMiddleware JWT认证中间件
|
||||||
type JWTAuthMiddleware struct {
|
type JWTAuthMiddleware struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
userRepo user_repositories.UserRepository
|
||||||
|
blacklist *securityServices.BlacklistService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJWTAuthMiddleware 创建JWT认证中间件
|
// 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{
|
return &JWTAuthMiddleware{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
userRepo: userRepo,
|
||||||
|
blacklist: blacklist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +87,10 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !m.ensureUserAllowed(c, claims.UserID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 将用户信息添加到上下文
|
// 将用户信息添加到上下文
|
||||||
c.Set("user_id", claims.UserID)
|
c.Set("user_id", claims.UserID)
|
||||||
c.Set("username", claims.Username)
|
c.Set("username", claims.Username)
|
||||||
@@ -124,6 +140,52 @@ func (m *JWTAuthMiddleware) validateToken(tokenString string) (*JWTClaims, error
|
|||||||
return claims, nil
|
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 返回未授权响应
|
// respondUnauthorized 返回未授权响应
|
||||||
func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) {
|
func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
@@ -331,6 +393,10 @@ func (m *AdminAuthMiddleware) Handle() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !m.jwtAuth.ensureUserAllowed(c, claims.UserID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查用户类型是否为管理员
|
// 检查用户类型是否为管理员
|
||||||
if claims.UserType != "admin" {
|
if claims.UserType != "admin" {
|
||||||
m.respondForbidden(c, "需要管理员权限")
|
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