package tools import ( "bytes" "context" "encoding/json" "fmt" "net/http" "os" "time" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" ) const deleteExpiredUserBatchLimit = 100 // chatHTTPClient 带超时,防止 chat 服务无响应时 cron worker 永久挂起。 var chatHTTPClient = &http.Client{Timeout: 3 * time.Second} // deleteExpiredOfflineUsers 是 cron "@hourly" 触发的入口。 // 批量查询离线时长超过 delete_account_interval 的用户并依次调用 chat /account/del 删除。 func (c *cronServer) deleteExpiredOfflineUsers() { now := time.Now() operationID := fmt.Sprintf("cron_del_expired_user_%d_%d", os.Getpid(), now.UnixMilli()) ctx := mcontext.SetOperationID(c.ctx, operationID) log.ZInfo(ctx, "deleteExpiredOfflineUsers: start", "time", now) users, err := c.userOfflineRecordDB.FindExpiredUsers(ctx, now, deleteExpiredUserBatchLimit) if err != nil { log.ZError(ctx, "deleteExpiredOfflineUsers: FindExpiredUsers failed", err) return } if len(users) == 0 { log.ZDebug(ctx, "deleteExpiredOfflineUsers: no expired users found") return } log.ZInfo(ctx, "deleteExpiredOfflineUsers: found expired users", "count", len(users)) adminToken, err := c.fetchChatAdminToken(ctx) if err != nil { log.ZError(ctx, "deleteExpiredOfflineUsers: fetchChatAdminToken failed", err) return } for i, u := range users { subCtx := mcontext.SetOperationID(c.ctx, fmt.Sprintf("%s_%d", operationID, i)) c.deleteExpiredUser(subCtx, adminToken, u.UserID) } log.ZInfo(ctx, "deleteExpiredOfflineUsers: done", "count", len(users), "elapsed", time.Since(now)) } // deleteExpiredUser 通过 chat HTTP API POST /account/del 删除单个过期用户。 // chat 服务端会处理:强制登出、删除好友/群组关系、清理 chat 账号数据等。 // adminToken 为当次批次开始时通过 admin-api /account/login 获取的管理员 token。 func (c *cronServer) deleteExpiredUser(ctx context.Context, adminToken, userID string) { log.ZInfo(ctx, "deleteExpiredUser: start", "userID", userID) operationID := mcontext.GetOperationID(ctx) body, _ := json.Marshal(map[string]any{"userIDs": []string{userID}}) url := c.chatAPIAddress + "/account/del" req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { log.ZError(ctx, "deleteExpiredUser: build request failed", err, "userID", userID) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("token", adminToken) req.Header.Set("operationID", operationID) resp, err := chatHTTPClient.Do(req) if err != nil { log.ZError(ctx, "deleteExpiredUser: HTTP call failed", err, "userID", userID, "url", url) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var result map[string]any _ = json.NewDecoder(resp.Body).Decode(&result) log.ZError(ctx, "deleteExpiredUser: chat API returned error", fmt.Errorf("status %d", resp.StatusCode), "userID", userID, "response", result) return } // chat /account/del 已处理好友/群组/IM用户删除;仅清理 user_offline_record 防止重复触发 if err := c.userOfflineRecordDB.Delete(ctx, userID); err != nil { log.ZWarn(ctx, "deleteExpiredUser: Delete offline record failed", err, "userID", userID) } log.ZInfo(ctx, "deleteExpiredUser: done", "userID", userID) }