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 }