open-im-server/internal/tools/delete_expired_user.go
2026-05-11 15:47:38 +08:00

96 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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