This commit is contained in:
2026-07-15 21:44:13 +08:00
parent 024b6614ba
commit 4794be896c
22 changed files with 1041 additions and 74 deletions

View File

@@ -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):

View File

@@ -12,8 +12,9 @@ var (
ErrInvalidIP = errors.New("未经授权的IP")
ErrMissingAccessId = errors.New("缺少Access-Id")
ErrInvalidAccessId = errors.New("未经授权的AccessId")
ErrFrozenAccount = errors.New("账户已冻结")
ErrArrears = errors.New("账户余额不足,无法请求")
ErrFrozenAccount = errors.New("账户已冻结")
ErrBlacklistedAccount = errors.New("账号已被列入黑名单")
ErrArrears = errors.New("账户余额不足,无法请求")
ErrInsufficientBalance = errors.New("钱包余额不足")
ErrProductNotFound = errors.New("产品不存在")
ErrProductDisabled = errors.New("产品已停用")
@@ -34,8 +35,9 @@ var ErrorCodeMap = map[error]int{
ErrInvalidIP: 1004,
ErrMissingAccessId: 1005,
ErrInvalidAccessId: 1006,
ErrFrozenAccount: 1007,
ErrArrears: 1007,
ErrFrozenAccount: 1007,
ErrBlacklistedAccount: 1007,
ErrArrears: 1007,
ErrInsufficientBalance: 1007,
ErrProductNotFound: 1008,
ErrProductDisabled: 1008,

View 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:"-"`
}

View File

@@ -8,24 +8,26 @@ type ListUsersQuery struct {
PageSize int `json:"page_size" validate:"min=1,max=100"`
Phone string `json:"phone"`
UserType string `json:"user_type"` // 用户类型: user/admin
IsActive *bool `json:"is_active"` // 是否激活
IsCertified *bool `json:"is_certified"` // 是否已认证
CompanyName string `json:"company_name"` // 企业名称
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
IsActive *bool `json:"is_active"` // 是否激活
IsCertified *bool `json:"is_certified"` // 是否已认证
IsBlacklisted *bool `json:"is_blacklisted"` // 是否黑名单
CompanyName string `json:"company_name"` // 企业名称
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ToDomainQuery 转换为领域查询对象
func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery {
return &queries.ListUsersQuery{
Page: q.Page,
PageSize: q.PageSize,
Phone: q.Phone,
UserType: q.UserType,
IsActive: q.IsActive,
IsCertified: q.IsCertified,
CompanyName: q.CompanyName,
StartDate: q.StartDate,
EndDate: q.EndDate,
Page: q.Page,
PageSize: q.PageSize,
Phone: q.Phone,
UserType: q.UserType,
IsActive: q.IsActive,
IsCertified: q.IsCertified,
IsBlacklisted: q.IsBlacklisted,
CompanyName: q.CompanyName,
StartDate: q.StartDate,
EndDate: q.EndDate,
}
}

View File

@@ -4,22 +4,23 @@ import "time"
// UserListItem 用户列表项
type UserListItem struct {
ID string `json:"id"`
Phone string `json:"phone"`
UserType string `json:"user_type"`
Username string `json:"username"`
IsActive bool `json:"is_active"`
IsCertified bool `json:"is_certified"`
LoginCount int `json:"login_count"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 企业信息
ID string `json:"id"`
Phone string `json:"phone"`
UserType string `json:"user_type"`
Username string `json:"username"`
IsActive bool `json:"is_active"`
IsCertified bool `json:"is_certified"`
IsBlacklisted bool `json:"is_blacklisted"`
BlacklistReason string `json:"blacklist_reason,omitempty"`
BlacklistSource string `json:"blacklist_source,omitempty"`
BlacklistedAt *time.Time `json:"blacklisted_at,omitempty"`
LoginCount int `json:"login_count"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
EnterpriseInfo *EnterpriseInfoItem `json:"enterprise_info,omitempty"`
// 钱包信息
WalletBalance string `json:"wallet_balance,omitempty"`
WalletBalance string `json:"wallet_balance,omitempty"`
}
// EnterpriseInfoItem 企业信息项

View File

@@ -43,8 +43,12 @@ type UserProfileResponse struct {
Phone string `json:"phone" example:"13800138000"`
Username string `json:"username,omitempty" example:"admin"`
UserType string `json:"user_type" example:"user"`
IsActive bool `json:"is_active" example:"true"`
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
IsActive bool `json:"is_active" example:"true"`
IsBlacklisted bool `json:"is_blacklisted" example:"false"`
BlacklistReason string `json:"blacklist_reason,omitempty"`
BlacklistSource string `json:"blacklist_source,omitempty"`
BlacklistedAt *time.Time `json:"blacklisted_at,omitempty"`
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
LoginCount int `json:"login_count" example:"10"`
Permissions []string `json:"permissions,omitempty" example:"['user:read','user:write']"`
EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"`

View File

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

View File

@@ -9,6 +9,7 @@ import (
"hyapi-server/internal/application/user/dto/commands"
"hyapi-server/internal/application/user/dto/queries"
"hyapi-server/internal/application/user/dto/responses"
api_service "hyapi-server/internal/domains/api/services"
finance_service "hyapi-server/internal/domains/finance/services"
"hyapi-server/internal/domains/user/entities"
"hyapi-server/internal/domains/user/events"
@@ -25,6 +26,7 @@ type UserApplicationServiceImpl struct {
smsCodeService *user_service.SMSCodeService
walletService finance_service.WalletAggregateService
contractService user_service.ContractAggregateService
apiUserService api_service.ApiUserAggregateService
eventBus interfaces.EventBus
jwtAuth *middleware.JWTAuthMiddleware
logger *zap.Logger
@@ -37,6 +39,7 @@ func NewUserApplicationService(
smsCodeService *user_service.SMSCodeService,
walletService finance_service.WalletAggregateService,
contractService user_service.ContractAggregateService,
apiUserService api_service.ApiUserAggregateService,
eventBus interfaces.EventBus,
jwtAuth *middleware.JWTAuthMiddleware,
logger *zap.Logger,
@@ -47,6 +50,7 @@ func NewUserApplicationService(
smsCodeService: smsCodeService,
walletService: walletService,
contractService: contractService,
apiUserService: apiUserService,
eventBus: eventBus,
jwtAuth: jwtAuth,
logger: logger,
@@ -316,16 +320,20 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
items := make([]*responses.UserListItem, 0, len(users))
for _, user := range users {
item := &responses.UserListItem{
ID: user.ID,
Phone: user.Phone,
UserType: user.UserType,
Username: user.Username,
IsActive: user.Active,
IsCertified: user.IsCertified,
LoginCount: user.LoginCount,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
ID: user.ID,
Phone: user.Phone,
UserType: user.UserType,
Username: user.Username,
IsActive: user.Active,
IsCertified: user.IsCertified,
IsBlacklisted: user.IsBlacklisted,
BlacklistReason: user.BlacklistReason,
BlacklistSource: user.BlacklistSource,
BlacklistedAt: user.BlacklistedAt,
LoginCount: user.LoginCount,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// 添加企业信息
@@ -388,16 +396,20 @@ func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID s
// 2. 构建响应数据
item := &responses.UserListItem{
ID: user.ID,
Phone: user.Phone,
UserType: user.UserType,
Username: user.Username,
IsActive: user.Active,
IsCertified: user.IsCertified,
LoginCount: user.LoginCount,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
ID: user.ID,
Phone: user.Phone,
UserType: user.UserType,
Username: user.Username,
IsActive: user.Active,
IsCertified: user.IsCertified,
IsBlacklisted: user.IsBlacklisted,
BlacklistReason: user.BlacklistReason,
BlacklistSource: user.BlacklistSource,
BlacklistedAt: user.BlacklistedAt,
LoginCount: user.LoginCount,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// 添加企业信息
@@ -459,3 +471,64 @@ func (s *UserApplicationServiceImpl) GetUserStats(ctx context.Context) (*respons
CertifiedUsers: stats.CertifiedUsers,
}, nil
}
// BlacklistUser 将用户列入黑名单,并同步冻结 API 账号
func (s *UserApplicationServiceImpl) BlacklistUser(ctx context.Context, cmd *commands.BlacklistUserCommand) error {
user, err := s.userAggregateService.GetUserByID(ctx, cmd.UserID)
if err != nil {
return fmt.Errorf("用户不存在")
}
source := cmd.Source
if source == "" {
source = "self"
}
if err := user.AddToBlacklist(cmd.Reason, source, cmd.OperatorUserID); err != nil {
return err
}
if err := s.userAggregateService.SaveUser(ctx, user); err != nil {
return fmt.Errorf("保存黑名单状态失败: %w", err)
}
// 同步冻结 API 访问(无 API 账号时忽略)
if err := s.apiUserService.FreezeApiUser(ctx, cmd.UserID); err != nil {
s.logger.Warn("冻结 API 账号失败(可能尚未开通)",
zap.String("user_id", cmd.UserID),
zap.Error(err))
}
s.logger.Info("用户已列入黑名单",
zap.String("user_id", cmd.UserID),
zap.String("operator", cmd.OperatorUserID),
zap.String("reason", cmd.Reason),
zap.Time("blacklisted_at", *user.BlacklistedAt),
)
return nil
}
// UnblacklistUser 移出黑名单并解冻 API
func (s *UserApplicationServiceImpl) UnblacklistUser(ctx context.Context, cmd *commands.UnblacklistUserCommand) error {
user, err := s.userAggregateService.GetUserByID(ctx, cmd.UserID)
if err != nil {
return fmt.Errorf("用户不存在")
}
if err := user.RemoveFromBlacklist(); err != nil {
return err
}
if err := s.userAggregateService.SaveUser(ctx, user); err != nil {
return fmt.Errorf("移出黑名单失败: %w", err)
}
if err := s.apiUserService.UnfreezeApiUser(ctx, cmd.UserID); err != nil {
s.logger.Warn("解冻 API 账号失败(可能尚未开通)",
zap.String("user_id", cmd.UserID),
zap.Error(err))
}
s.logger.Info("用户已移出黑名单",
zap.String("user_id", cmd.UserID),
zap.String("operator", cmd.OperatorUserID),
)
return nil
}