Files
dushixiang 787fa6918d feat: WebSocket 消息可靠投递(纯内存事件队列)
- agent 数据写入增加 10s 超时,写失败立即断连重连,消除无超时写卡死期间快照被覆盖的静默丢失
- 事件类消息(SSH登录/防篡改/指令响应/监控检测)改走内存 outbox:seq + 服务端累计 ack + 断线重放,上限 1024 条溢出丢最旧,重启清空(可接受)
- 注册请求携带 BootID 会话标识:探针重启后 seq 从 1 重新分配,服务端据此重置确认位点,避免新消息被旧位点误判为重复而丢弃
- server 读循环与处理解耦(per-agent 有序队列 + 串行 worker),慢处理不再阻塞 TCP 读取,队列满断连由探针重放兜底
- 修复探针快速重连时旧连接误注销新连接的竞态(此前会闪断新连接)
- IsAgentEnabled 加 3s TTL 缓存,禁用立即失效;pong 在线状态写库去抖(首次 + 5 分钟心跳刷新)
- VM 写超时默认 30s -> 10s,快速失败配合重放兜底

新旧版本双向兼容:注册响应 Reliable 字段协商,旧 server 下探针自动降级直发
2026-08-30 22:30:41 +08:00

762 lines
23 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/go-orz/orz"
"github.com/google/uuid"
"github.com/pika-monitor/pika/internal/models"
"github.com/pika-monitor/pika/internal/protocol"
"github.com/pika-monitor/pika/internal/repo"
"go.uber.org/zap"
"gorm.io/gorm"
)
type AgentService struct {
logger *zap.Logger
*orz.Service
AgentRepo *repo.AgentRepo
MonitorRepo *repo.MonitorRepo
TamperEventRepo *repo.TamperEventRepo
SSHLoginEventRepo *repo.SSHLoginEventRepo
apiKeyService *ApiKeyService
metricService *MetricService
geoipService *GeoIPService
// enabledCache IsAgentEnabled 的短 TTL 缓存。该检查位于每条
// WebSocket 消息的处理路径上,直连数据库会在探针数多时拖垮处理。
enabledCache sync.Map // agentID → enabledCacheEntry
}
type enabledCacheEntry struct {
value bool
expiresAt time.Time
}
// enabledCacheTTL 探针启用状态缓存时长:禁用操作最迟该时长后生效
const enabledCacheTTL = 3 * time.Second
// ErrInvalidAgentOrder 表示提交的排序列表无法完整、唯一地对应当前探针列表。
var ErrInvalidAgentOrder = errors.New("无效的探针排序")
func NewAgentService(logger *zap.Logger, db *gorm.DB, apiKeyService *ApiKeyService, metricService *MetricService, geoipService *GeoIPService) *AgentService {
return &AgentService{
logger: logger,
Service: orz.NewService(db),
AgentRepo: repo.NewAgentRepo(db),
MonitorRepo: repo.NewMonitorRepo(db),
TamperEventRepo: repo.NewTamperEventRepo(db),
SSHLoginEventRepo: repo.NewSSHLoginEventRepo(db),
apiKeyService: apiKeyService,
metricService: metricService,
geoipService: geoipService,
}
}
// RegisterAgent 注册探针
func (s *AgentService) RegisterAgent(ctx context.Context, ip string, info *protocol.AgentInfo, apiKey string) (*models.Agent, error) {
// 验证API密钥
if _, err := s.apiKeyService.ValidateApiKey(ctx, apiKey, "agent"); err != nil {
s.logger.Warn("agent registration failed: invalid api key",
zap.String("agentID", info.ID),
zap.String("hostname", info.Hostname),
)
return nil, err
}
// 验证探针 ID
if info.ID == "" {
return nil, fmt.Errorf("agent ID 不能为空")
}
// 使用探针的持久化 ID 来识别同一个探针
// 这样即使主机名变化,也能正确识别
existingAgent, err := s.AgentRepo.FindById(ctx, info.ID)
if err == nil {
// 已禁用的探针仅维持连接,不用注册报文刷新任何主机数据或在线状态。
if !existingAgent.Enabled {
s.logger.Info("disabled agent reconnected; registration data ignored",
zap.String("agentID", existingAgent.ID),
zap.String("hostname", existingAgent.Hostname))
return &existingAgent, nil
}
// 更新现有探针信息(允许主机名、名称等变化)
now := time.Now().UnixMilli()
existingAgent.Hostname = info.Hostname
existingAgent.IP = ip
existingAgent.OS = info.OS
existingAgent.Arch = info.Arch
existingAgent.Version = info.Version
existingAgent.Status = 1
existingAgent.LastSeenAt = now
existingAgent.UpdatedAt = now
if err := s.AgentRepo.UpdateById(ctx, &existingAgent); err != nil {
return nil, err
}
s.logger.Info("agent re-registered",
zap.String("agentID", existingAgent.ID),
zap.String("name", info.Name),
zap.String("hostname", info.Hostname),
zap.String("ip", ip),
zap.String("version", info.Version))
return &existingAgent, nil
}
// 检查短 ID 前缀是否与已有探针冲突(防止短链接歧义)
var shortID string
if len(info.ID) >= 8 {
shortID = info.ID[:8]
} else {
shortID = info.ID
}
_, err = s.AgentRepo.FindByShortID(ctx, shortID)
if !errors.Is(err, gorm.ErrRecordNotFound) {
// 前缀冲突,服务器分配新 UUID
s.logger.Warn("agent ID prefix collision detected, assigning new UUID",
zap.String("originalID", info.ID),
zap.String("conflictPrefix", shortID),
zap.String("hostname", info.Hostname),
)
info.ID = uuid.NewString()
}
// 创建新探针(使用客户端提供的持久化 ID)
now := time.Now().UnixMilli()
agent := &models.Agent{
ID: info.ID, // 使用客户端持久化的 ID
Name: info.Name,
Hostname: info.Hostname,
IP: ip,
OS: info.OS,
Arch: info.Arch,
Version: info.Version,
Enabled: true,
Status: 1,
LastSeenAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.AgentRepo.Create(ctx, agent); err != nil {
return nil, err
}
s.logger.Info("agent registered successfully",
zap.String("agentID", agent.ID),
zap.String("name", info.Name),
zap.String("hostname", info.Hostname),
zap.String("ip", ip),
zap.String("version", info.Version))
return agent, nil
}
// UpdateAgentStatus 更新探针状态
func (s *AgentService) UpdateAgentStatus(ctx context.Context, agentID string, status int) error {
return s.AgentRepo.UpdateStatus(ctx, agentID, status, time.Now().UnixMilli())
}
// IsAgentEnabled 查询探针是否允许接收并处理数据(带短 TTL 缓存,
// 禁用操作会立即失效缓存)。
func (s *AgentService) IsAgentEnabled(ctx context.Context, agentID string) (bool, error) {
if cached, ok := s.enabledCache.Load(agentID); ok {
entry := cached.(enabledCacheEntry)
if time.Now().Before(entry.expiresAt) {
return entry.value, nil
}
}
enabled, err := s.AgentRepo.IsEnabled(ctx, agentID)
if err != nil {
return false, err
}
s.enabledCache.Store(agentID, enabledCacheEntry{
value: enabled,
expiresAt: time.Now().Add(enabledCacheTTL),
})
return enabled, nil
}
// UpdateAgentEnabled 更新探针启用状态。禁用时立即标记离线,后续状态更新由仓储层原子拦截。
func (s *AgentService) UpdateAgentEnabled(ctx context.Context, agentID string, enabled bool) error {
if _, err := s.AgentRepo.FindById(ctx, agentID); err != nil {
return err
}
updates := map[string]interface{}{
"enabled": enabled,
"updated_at": time.Now().UnixMilli(),
}
if !enabled {
updates["status"] = 0
}
if err := s.AgentRepo.UpdateColumnsById(ctx, agentID, updates); err != nil {
return err
}
// 立即失效缓存,让禁用/启用马上对消息处理路径生效
s.enabledCache.Delete(agentID)
return nil
}
// UpdatePublicIP 更新探针的公网 IP 信息
func (s *AgentService) UpdatePublicIP(ctx context.Context, agentID string, ipv4 string, ipv6 string) error {
updates := map[string]interface{}{
"updated_at": time.Now().UnixMilli(),
"ipv4": ipv4,
"ipv6": ipv6,
}
return s.AgentRepo.UpdateColumnsById(ctx, agentID, updates)
}
// GetAgent 获取探针信息
func (s *AgentService) GetAgent(ctx context.Context, agentID string) (*models.Agent, error) {
agent, err := s.AgentRepo.FindById(ctx, agentID)
if err != nil {
return nil, err
}
return &agent, nil
}
// ListAgents 列出所有探针
func (s *AgentService) ListAgents(ctx context.Context) ([]models.Agent, error) {
return s.AgentRepo.FindAll(ctx)
}
// UpdateAgentOrder 按从前到后的 ID 顺序重新分配探针权重。
// 请求必须包含当前全部探针,避免筛选状态下排序时意外覆盖未展示探针的相对顺序。
func (s *AgentService) UpdateAgentOrder(ctx context.Context, agentIDs []string) error {
if len(agentIDs) == 0 {
return fmt.Errorf("%w: 探针ID列表不能为空", ErrInvalidAgentOrder)
}
agents, err := s.AgentRepo.FindAll(ctx)
if err != nil {
return err
}
if len(agentIDs) != len(agents) {
return fmt.Errorf("%w: 排序列表必须包含全部探针", ErrInvalidAgentOrder)
}
existingIDs := make(map[string]struct{}, len(agents))
for _, agent := range agents {
existingIDs[agent.ID] = struct{}{}
}
seenIDs := make(map[string]struct{}, len(agentIDs))
for _, agentID := range agentIDs {
if _, exists := existingIDs[agentID]; !exists {
return fmt.Errorf("%w: 探针 %q 不存在", ErrInvalidAgentOrder, agentID)
}
if _, duplicated := seenIDs[agentID]; duplicated {
return fmt.Errorf("%w: 探针 %q 重复", ErrInvalidAgentOrder, agentID)
}
seenIDs[agentID] = struct{}{}
}
now := time.Now().UnixMilli()
return s.Transaction(ctx, func(ctx context.Context) error {
for index, agentID := range agentIDs {
if err := s.AgentRepo.UpdateColumnsById(ctx, agentID, map[string]interface{}{
"weight": len(agentIDs) - index,
"updated_at": now,
}); err != nil {
return err
}
}
return nil
})
}
// ListOnlineAgents 列出所有在线探针
func (s *AgentService) ListOnlineAgents(ctx context.Context) ([]models.Agent, error) {
return s.AgentRepo.FindOnlineAgents(ctx)
}
// IsAgentByIP 检查指定公网IP是否为探针
func (s *AgentService) IsAgentByIP(ctx context.Context, ip string) (bool, error) {
_, err := s.AgentRepo.FindByIP(ctx, ip)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
// HandleCommandResponse 处理指令响应
func (s *AgentService) HandleCommandResponse(ctx context.Context, agentID string, resp *protocol.CommandResponse) error {
s.logger.Info("command response received",
zap.String("agentID", agentID),
zap.String("cmdID", resp.ID),
zap.String("type", resp.Type),
zap.String("status", resp.Status))
// 根据指令类型处理响应
switch resp.Type {
case "vps_audit":
return s.handleVPSAuditResponse(ctx, agentID, resp)
default:
s.logger.Warn("unknown command type", zap.String("type", resp.Type))
return nil
}
}
// handleVPSAuditResponse 处理VPS审计响应
func (s *AgentService) handleVPSAuditResponse(ctx context.Context, agentID string, resp *protocol.CommandResponse) error {
if resp.Status == "error" {
s.logger.Error("vps audit failed",
zap.String("agentID", agentID),
zap.String("error", resp.Error))
return nil
}
if resp.Status == "running" {
s.logger.Info("vps audit is running", zap.String("agentID", agentID))
return nil
}
if resp.Status == "success" {
// 解析审计结果
var auditResult protocol.VPSAuditResult
if err := json.Unmarshal([]byte(resp.Result), &auditResult); err != nil {
s.logger.Error("failed to parse audit result", zap.Error(err))
return err
}
// 存储审计结果
return s.SaveAuditResult(ctx, agentID, &auditResult)
}
return nil
}
// SaveAuditResult 保存审计结果
func (s *AgentService) SaveAuditResult(ctx context.Context, agentID string, result *protocol.VPSAuditResult) error {
// 为登录记录添加 IP 归属地信息
s.enrichLoginRecordsWithLocation(result)
// 将结果序列化为JSON存储
resultJSON, err := json.Marshal(result)
if err != nil {
return err
}
auditRecord := &models.AuditResult{
AgentID: agentID,
Type: "vps_audit",
Result: string(resultJSON),
StartTime: result.StartTime,
EndTime: result.EndTime,
CreatedAt: time.Now().UnixMilli(),
}
// 保存到数据库
if err := s.AgentRepo.SaveAuditResult(ctx, auditRecord); err != nil {
return err
}
s.logger.Info("审计结果保存成功",
zap.String("agentId", agentID),
zap.Int64("auditId", auditRecord.ID),
)
return nil
}
// enrichLoginRecordsWithLocation 为登录记录添加IP归属地信息
func (s *AgentService) enrichLoginRecordsWithLocation(result *protocol.VPSAuditResult) {
if s.geoipService == nil {
return
}
// 处理成功登录记录
if result.AssetInventory.LoginAssets != nil {
for i := range result.AssetInventory.LoginAssets.SuccessfulLogins {
if result.AssetInventory.LoginAssets.SuccessfulLogins[i].IP != "" {
location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.SuccessfulLogins[i].IP)
result.AssetInventory.LoginAssets.SuccessfulLogins[i].Location = location
}
}
// 处理失败登录记录
for i := range result.AssetInventory.LoginAssets.FailedLogins {
if result.AssetInventory.LoginAssets.FailedLogins[i].IP != "" {
location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.FailedLogins[i].IP)
result.AssetInventory.LoginAssets.FailedLogins[i].Location = location
}
}
// 处理当前登录会话
for i := range result.AssetInventory.LoginAssets.CurrentSessions {
if result.AssetInventory.LoginAssets.CurrentSessions[i].IP != "" {
location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.CurrentSessions[i].IP)
result.AssetInventory.LoginAssets.CurrentSessions[i].Location = location
}
}
}
// 处理用户资产中的当前登录
if result.AssetInventory.UserAssets != nil && result.AssetInventory.UserAssets.CurrentLogins != nil {
for i := range result.AssetInventory.UserAssets.CurrentLogins {
if result.AssetInventory.UserAssets.CurrentLogins[i].IP != "" {
location := s.geoipService.LookupIP(result.AssetInventory.UserAssets.CurrentLogins[i].IP)
result.AssetInventory.UserAssets.CurrentLogins[i].Location = location
}
}
}
}
// GetAuditResult 获取最新的审计结果(原始数据)
func (s *AgentService) GetAuditResult(ctx context.Context, agentID string) (*protocol.VPSAuditResult, error) {
record, err := s.AgentRepo.GetLatestAuditResultByType(ctx, agentID, "vps_audit")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
var result protocol.VPSAuditResult
if err := json.Unmarshal([]byte(record.Result), &result); err != nil {
return nil, err
}
return &result, nil
}
// ListAuditResults 获取审计结果列表
func (s *AgentService) ListAuditResults(ctx context.Context, agentID string) ([]map[string]interface{}, error) {
records, err := s.AgentRepo.ListAuditResults(ctx, agentID)
if err != nil {
return nil, err
}
results := make([]map[string]interface{}, 0, len(records))
for _, record := range records {
var auditResult protocol.VPSAuditResult
if err := json.Unmarshal([]byte(record.Result), &auditResult); err != nil {
s.logger.Error("failed to parse audit result", zap.Error(err))
continue
}
// TODO: 统计安全检查结果应该来自 Server 端分析后的 VPSAuditAnalysis
// Agent 端已经不再产生 SecurityChecks,需要实现 Server 端分析逻辑
results = append(results, map[string]interface{}{
"id": record.ID,
"agentId": record.AgentID,
"type": record.Type,
"startTime": record.StartTime,
"endTime": record.EndTime,
"createdAt": record.CreatedAt,
"systemInfo": auditResult.SystemInfo,
"statistics": auditResult.Statistics,
"collectTime": auditResult.EndTime - auditResult.StartTime,
})
}
return results, nil
}
// GetStatistics 获取探针统计数据
func (s *AgentService) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
total, online, err := s.AgentRepo.GetStatistics(ctx)
if err != nil {
return nil, err
}
offline := total - online
onlineRate := 0.0
if total > 0 {
onlineRate = float64(online) / float64(total) * 100
}
return map[string]interface{}{
"total": total,
"online": online,
"offline": offline,
"onlineRate": onlineRate,
}, nil
}
// DeleteAgent 删除探针及其所有相关数据
func (s *AgentService) DeleteAgent(ctx context.Context, agentID string) error {
// 在事务中执行所有数据库删除操作
err := s.Transaction(ctx, func(ctx context.Context) error {
// 1. 删除探针的审计结果
if err := s.AgentRepo.DeleteAuditResults(ctx, agentID); err != nil {
s.logger.Error("删除探针审计结果失败", zap.String("agentId", agentID), zap.Error(err))
return err
}
// 2. 删除探针的目录保护事件数据
if err := s.TamperEventRepo.DeleteEventsByAgentID(ctx, agentID); err != nil {
s.logger.Error("删除探针目录保护事件失败", zap.String("agentId", agentID), zap.Error(err))
return err
}
// 3. 删除探针的SSH登录事件数据
if err := s.SSHLoginEventRepo.DeleteEventsByAgentID(ctx, agentID); err != nil {
s.logger.Error("删除探针SSH登录事件失败", zap.String("agentId", agentID), zap.Error(err))
return err
}
// 4. 最后删除探针本身
if err := s.AgentRepo.DeleteById(ctx, agentID); err != nil {
s.logger.Error("删除探针失败", zap.String("agentId", agentID), zap.Error(err))
return err
}
return nil
})
if err != nil {
return err
}
// 5. 清理内存缓存中的探针数据(事务外执行)
if s.metricService != nil {
// 清理探针最新指标缓存
s.metricService.DeleteAgentLatestMetricsCache(agentID)
// 清理监控缓存中该探针的数据
s.metricService.CleanAgentFromMonitorCache(agentID)
}
// 6. 清理 VictoriaMetrics 中的指标数据(事务外执行,失败不影响数据库删除结果)
if s.metricService != nil {
if err := s.metricService.CleanAgentMetrics(ctx, agentID); err != nil {
s.logger.Error("清理VictoriaMetrics中的探针指标数据失败", zap.String("agentId", agentID), zap.Error(err))
// 记录错误但不返回,因为数据库删除已成功
}
}
s.logger.Info("探针删除成功", zap.String("agentId", agentID))
return nil
}
// ListByAuth 根据认证状态列出探针(已登录返回全部,未登录返回公开可见)
func (s *AgentService) ListByAuth(ctx context.Context, isAuthenticated bool) ([]models.Agent, error) {
if isAuthenticated {
return s.AgentRepo.FindEnabledAgents(ctx)
}
return s.AgentRepo.FindPublicAgents(ctx)
}
// GetAgentByAuth 根据认证状态获取探针(已登录返回全部,未登录返回公开可见)
// 支持完整UUID和短ID(前缀匹配)
func (s *AgentService) GetAgentByAuth(ctx context.Context, id string, isAuthenticated bool) (*models.Agent, error) {
if isAuthenticated {
agent, err := s.AgentRepo.FindById(ctx, id)
if err == nil {
if !agent.Enabled {
return nil, gorm.ErrRecordNotFound
}
return &agent, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 精确匹配失败,尝试短ID前缀匹配
agent, err = s.AgentRepo.FindByShortID(ctx, id)
if err != nil {
return nil, err
}
if !agent.Enabled {
return nil, gorm.ErrRecordNotFound
}
return &agent, nil
}
agent, err := s.AgentRepo.FindPublicAgentByID(ctx, id)
if err == nil {
return agent, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 精确匹配失败,尝试短ID前缀匹配
return s.AgentRepo.FindPublicAgentByShortID(ctx, id)
}
// GetAllTags 获取所有探针的标签
func (s *AgentService) GetAllTags(ctx context.Context) ([]string, error) {
tags, err := s.AgentRepo.GetAllTags(ctx)
if err != nil {
return nil, err
}
// 排序
sort.Strings(tags)
return tags, nil
}
// BatchUpdateTags 批量更新探针标签
// operation: "add" 添加标签, "remove" 移除标签, "replace" 替换标签
func (s *AgentService) BatchUpdateTags(ctx context.Context, agentIDs []string, tags []string, operation string) error {
if len(agentIDs) == 0 {
return fmt.Errorf("探针ID列表不能为空")
}
if operation == "" {
operation = "replace"
}
agents, err := s.AgentRepo.FindByIdIn(ctx, agentIDs)
if err != nil {
return err
}
// 收集受影响的标签(变更前的标签 + 本次操作涉及的标签),用于事后清理监控缓存
affectedTags := make(map[string]struct{})
for _, tag := range tags {
affectedTags[tag] = struct{}{}
}
for _, agent := range agents {
for _, tag := range agent.Tags {
affectedTags[tag] = struct{}{}
}
}
// 在事务中执行批量更新
if err := s.Transaction(ctx, func(ctx context.Context) error {
for _, agent := range agents {
// 根据操作类型处理标签
var newTags []string
switch operation {
case "add":
// 添加标签(去重)
existingTagsMap := make(map[string]bool)
for _, tag := range agent.Tags {
existingTagsMap[tag] = true
}
newTags = append([]string{}, agent.Tags...)
for _, tag := range tags {
if !existingTagsMap[tag] {
newTags = append(newTags, tag)
}
}
case "remove":
// 移除标签
removeTagsMap := make(map[string]bool)
for _, tag := range tags {
removeTagsMap[tag] = true
}
for _, tag := range agent.Tags {
if !removeTagsMap[tag] {
newTags = append(newTags, tag)
}
}
case "replace":
// 替换标签
newTags = tags
default:
return fmt.Errorf("不支持的操作类型: %s", operation)
}
// 更新探针标签
agent.Tags = newTags
agent.UpdatedAt = time.Now().UnixMilli()
if err := s.AgentRepo.UpdateById(ctx, &agent); err != nil {
s.logger.Error("更新探针标签失败", zap.String("agentId", agent.ID), zap.Error(err))
return err
}
s.logger.Info("探针标签更新成功",
zap.String("agentId", agent.ID),
zap.String("operation", operation),
zap.Strings("tags", newTags))
}
return nil
}); err != nil {
return err
}
// 标签变更后,清理引用了受影响标签的监控任务缓存(避免已移出探针的数据残留)
s.cleanupMonitorCacheForTags(ctx, affectedTags)
return nil
}
// cleanupMonitorCacheForTags 清理引用了指定标签的监控任务缓存(失败仅记录日志,不影响主流程)
func (s *AgentService) cleanupMonitorCacheForTags(ctx context.Context, tags map[string]struct{}) {
if len(tags) == 0 || s.metricService == nil {
return
}
tagList := make([]string, 0, len(tags))
for tag := range tags {
if tag != "" {
tagList = append(tagList, tag)
}
}
monitors, err := s.MonitorRepo.FindByAnyTags(ctx, tagList)
if err != nil {
s.logger.Warn("查询引用标签的监控任务失败", zap.Strings("tags", tagList), zap.Error(err))
return
}
for _, monitor := range monitors {
if err := s.metricService.CleanMonitorCache(ctx, monitor.ID); err != nil {
s.logger.Warn("清理监控缓存失败", zap.String("monitorID", monitor.ID), zap.Error(err))
}
}
}
// BatchUpdateVisibility 批量更新探针可见性
func (s *AgentService) BatchUpdateVisibility(ctx context.Context, agentIDs []string, visibility string) error {
if len(agentIDs) == 0 {
return fmt.Errorf("探针ID列表不能为空")
}
// 验证可见性参数
if visibility != "public" && visibility != "private" {
return fmt.Errorf("可见性参数错误,必须是 public 或 private")
}
agents, err := s.AgentRepo.FindByIdIn(ctx, agentIDs)
if err != nil {
return err
}
// 在事务中执行批量更新
return s.Transaction(ctx, func(ctx context.Context) error {
for _, agent := range agents {
// 更新探针可见性
agent.Visibility = visibility
agent.UpdatedAt = time.Now().UnixMilli()
if err := s.AgentRepo.UpdateById(ctx, &agent); err != nil {
s.logger.Error("更新探针可见性失败", zap.String("agentId", agent.ID), zap.Error(err))
return err
}
s.logger.Info("探针可见性更新成功",
zap.String("agentId", agent.ID),
zap.String("visibility", visibility))
}
return nil
})
}
func (s *AgentService) InitStatus(ctx context.Context) error {
agents, err := s.AgentRepo.FindAll(ctx)
if err != nil {
return err
}
for _, agent := range agents {
if err := s.AgentRepo.UpdateStatus(ctx, agent.ID, 0, 0); err != nil {
return err
}
}
return nil
}