Files
open-im-server-deploy/internal/api/statistics.go
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

556 lines
16 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 api
import (
"context"
"errors"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
rediscache "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/gin-gonic/gin"
"github.com/openimsdk/tools/a2r"
"github.com/openimsdk/tools/apiresp"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/redis/go-redis/v9"
)
type StatisticsApi struct {
rdb redis.UniversalClient
msgDatabase database.Msg
userClient *rpcli.UserClient
groupClient *rpcli.GroupClient
}
func NewStatisticsApi(rdb redis.UniversalClient, msgDatabase database.Msg, userClient *rpcli.UserClient, groupClient *rpcli.GroupClient) *StatisticsApi {
return &StatisticsApi{
rdb: rdb,
msgDatabase: msgDatabase,
userClient: userClient,
groupClient: groupClient,
}
}
const (
trendIntervalMinutes15 = 15
trendIntervalMinutes30 = 30
trendIntervalMinutes60 = 60
trendChatTypeSingle = 1
trendChatTypeGroup = 2
defaultTrendDuration = 24 * time.Hour
)
// refreshOnlineUserCountAndHistory 刷新在线人数并写入历史采样
func refreshOnlineUserCountAndHistory(ctx context.Context, rdb redis.UniversalClient) {
count, err := rediscache.RefreshOnlineUserCount(ctx, rdb)
if err != nil {
log.ZWarn(ctx, "refresh online user count failed", err)
return
}
if err := rediscache.AppendOnlineUserCountHistory(ctx, rdb, time.Now().UnixMilli(), count); err != nil {
log.ZWarn(ctx, "append online user count history failed", err)
}
}
// startOnlineCountRefresher 定时刷新在线人数缓存
func startOnlineCountRefresher(ctx context.Context, cfg *Config, rdb redis.UniversalClient) {
if cfg == nil || rdb == nil {
return
}
refreshCfg := cfg.API.OnlineCountRefresh
if !refreshCfg.Enable || refreshCfg.Interval <= 0 {
return
}
log.ZInfo(ctx, "online user count refresh enabled", "interval", refreshCfg.Interval)
go func() {
refreshOnlineUserCountAndHistory(ctx, rdb)
ticker := time.NewTicker(refreshCfg.Interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
refreshOnlineUserCountAndHistory(ctx, rdb)
}
}
}()
}
// OnlineUserCount 在线人数统计接口
func (s *StatisticsApi) OnlineUserCount(c *gin.Context) {
if err := authverify.CheckAdmin(c); err != nil {
apiresp.GinError(c, err)
return
}
if s.rdb == nil {
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("redis client is nil"))
return
}
count, err := rediscache.GetOnlineUserCount(c, s.rdb)
if err != nil {
if errors.Is(err, redis.Nil) {
count, err = rediscache.RefreshOnlineUserCount(c, s.rdb)
if err == nil {
if appendErr := rediscache.AppendOnlineUserCountHistory(c, s.rdb, time.Now().UnixMilli(), count); appendErr != nil {
log.ZWarn(c, "append online user count history failed", appendErr)
}
}
}
}
if err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, &apistruct.OnlineUserCountResp{OnlineCount: count})
}
// OnlineUserCountTrend 在线人数走势统计接口
func (s *StatisticsApi) OnlineUserCountTrend(c *gin.Context) {
if err := authverify.CheckAdmin(c); err != nil {
apiresp.GinError(c, err)
return
}
req, err := a2r.ParseRequest[apistruct.OnlineUserCountTrendReq](c)
if err != nil {
apiresp.GinError(c, err)
return
}
if s.rdb == nil {
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("redis client is nil"))
return
}
intervalMillis, err := parseTrendIntervalMillis(req.IntervalMinutes)
if err != nil {
apiresp.GinError(c, err)
return
}
startTime, endTime, err := normalizeTrendTimeRange(req.StartTime, req.EndTime)
if err != nil {
apiresp.GinError(c, err)
return
}
bucketStart, bucketEnd := alignTrendRange(startTime, endTime, intervalMillis)
// 使用对齐后的时间范围获取历史数据,确保数据范围与构建数据点的范围一致
samples, err := rediscache.GetOnlineUserCountHistory(c, s.rdb, bucketStart, bucketEnd)
if err != nil {
apiresp.GinError(c, err)
return
}
// 将当前在线人数作为最新采样,确保最后一个时间段展示该段内的最大在线人数
now := time.Now().UnixMilli()
currentBucket := now - (now % intervalMillis)
if now < 0 && now%intervalMillis != 0 {
currentBucket = now - ((now % intervalMillis) + intervalMillis)
}
if currentBucket >= bucketStart && currentBucket <= bucketEnd {
if currentCount, err := rediscache.GetOnlineUserCount(c, s.rdb); err == nil {
samples = append(samples, rediscache.OnlineUserCountSample{
Timestamp: now,
Count: currentCount,
})
}
}
points := buildOnlineUserCountTrendPoints(samples, bucketStart, bucketEnd, intervalMillis)
apiresp.GinSuccess(c, &apistruct.OnlineUserCountTrendResp{
IntervalMinutes: req.IntervalMinutes,
Points: points,
})
}
// UserSendMsgCount 用户发送消息总数统计
func (s *StatisticsApi) UserSendMsgCount(c *gin.Context) {
if err := authverify.CheckAdmin(c); err != nil {
apiresp.GinError(c, err)
return
}
_, err := a2r.ParseRequest[apistruct.UserSendMsgCountReq](c)
if err != nil {
apiresp.GinError(c, err)
return
}
if s.msgDatabase == nil {
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("msg database is nil"))
return
}
now := time.Now()
endTime := now.UnixMilli()
start24h := now.Add(-24 * time.Hour).UnixMilli()
start7d := now.Add(-7 * 24 * time.Hour).UnixMilli()
start30d := now.Add(-30 * 24 * time.Hour).UnixMilli()
count24h, err := s.msgDatabase.CountUserSendMessages(c, "", start24h, endTime, "")
if err != nil {
apiresp.GinError(c, err)
return
}
count7d, err := s.msgDatabase.CountUserSendMessages(c, "", start7d, endTime, "")
if err != nil {
apiresp.GinError(c, err)
return
}
count30d, err := s.msgDatabase.CountUserSendMessages(c, "", start30d, endTime, "")
if err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, &apistruct.UserSendMsgCountResp{
Count24h: count24h,
Count7d: count7d,
Count30d: count30d,
})
}
// UserSendMsgCountTrend 用户发送消息走势统计
func (s *StatisticsApi) UserSendMsgCountTrend(c *gin.Context) {
if err := authverify.CheckAdmin(c); err != nil {
apiresp.GinError(c, err)
return
}
req, err := a2r.ParseRequest[apistruct.UserSendMsgCountTrendReq](c)
if err != nil {
apiresp.GinError(c, err)
return
}
if s.msgDatabase == nil {
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("msg database is nil"))
return
}
intervalMillis, err := parseTrendIntervalMillis(req.IntervalMinutes)
if err != nil {
apiresp.GinError(c, err)
return
}
startTime, endTime, err := normalizeTrendTimeRange(req.StartTime, req.EndTime)
if err != nil {
apiresp.GinError(c, err)
return
}
sessionTypes, err := mapTrendChatType(req.ChatType)
if err != nil {
apiresp.GinError(c, err)
return
}
bucketStart, bucketEnd := alignTrendRange(startTime, endTime, intervalMillis)
countMap, err := s.msgDatabase.CountUserSendMessagesTrend(c, req.UserID, sessionTypes, startTime, endTime, intervalMillis)
if err != nil {
apiresp.GinError(c, err)
return
}
points := buildUserSendMsgCountTrendPoints(countMap, bucketStart, bucketEnd, intervalMillis)
apiresp.GinSuccess(c, &apistruct.UserSendMsgCountTrendResp{
UserID: req.UserID,
ChatType: req.ChatType,
IntervalMinutes: req.IntervalMinutes,
Points: points,
})
}
// UserSendMsgQuery 用户发送消息查询
func (s *StatisticsApi) UserSendMsgQuery(c *gin.Context) {
if err := authverify.CheckAdmin(c); err != nil {
apiresp.GinError(c, err)
return
}
req, err := a2r.ParseRequest[apistruct.UserSendMsgQueryReq](c)
if err != nil {
apiresp.GinError(c, err)
return
}
if req.StartTime > 0 && req.EndTime > 0 && req.EndTime <= req.StartTime {
apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid time range"))
return
}
if s.msgDatabase == nil {
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("msg database is nil"))
return
}
pageNumber := req.PageNumber
if pageNumber <= 0 {
pageNumber = 1
}
showNumber := req.ShowNumber
if showNumber <= 0 {
showNumber = 50
}
const maxShowNumber int32 = 200
if showNumber > maxShowNumber {
showNumber = maxShowNumber
}
total, msgs, err := s.msgDatabase.SearchUserMessages(c, req.UserID, req.StartTime, req.EndTime, req.Content, pageNumber, showNumber)
if err != nil {
apiresp.GinError(c, err)
return
}
sendIDs := make([]string, 0, len(msgs))
recvIDs := make([]string, 0, len(msgs))
groupIDs := make([]string, 0, len(msgs))
for _, item := range msgs {
if item == nil || item.Msg == nil {
continue
}
msg := item.Msg
if msg.SendID != "" {
sendIDs = append(sendIDs, msg.SendID)
}
switch msg.SessionType {
case constant.ReadGroupChatType, constant.WriteGroupChatType:
if msg.GroupID != "" {
groupIDs = append(groupIDs, msg.GroupID)
}
default:
if msg.RecvID != "" {
recvIDs = append(recvIDs, msg.RecvID)
}
}
}
sendIDs = datautil.Distinct(sendIDs)
recvIDs = datautil.Distinct(recvIDs)
groupIDs = datautil.Distinct(groupIDs)
sendMap, recvMap, groupMap := map[string]*sdkws.UserInfo{}, map[string]*sdkws.UserInfo{}, map[string]*sdkws.GroupInfo{}
if s.userClient != nil {
if len(sendIDs) > 0 {
if users, err := s.userClient.GetUsersInfo(c, sendIDs); err == nil {
sendMap = datautil.SliceToMap(users, (*sdkws.UserInfo).GetUserID)
}
}
if len(recvIDs) > 0 {
if users, err := s.userClient.GetUsersInfo(c, recvIDs); err == nil {
recvMap = datautil.SliceToMap(users, (*sdkws.UserInfo).GetUserID)
}
}
}
if s.groupClient != nil && len(groupIDs) > 0 {
if groups, err := s.groupClient.GetGroupsInfo(c, groupIDs); err == nil {
groupMap = datautil.SliceToMap(groups, (*sdkws.GroupInfo).GetGroupID)
}
}
records := make([]*apistruct.UserSendMsgQueryRecord, 0, len(msgs))
for _, item := range msgs {
if item == nil || item.Msg == nil {
continue
}
msg := item.Msg
msgID := msg.ServerMsgID
if msgID == "" {
msgID = msg.ClientMsgID
}
senderName := msg.SenderNickname
if senderName == "" {
if u := sendMap[msg.SendID]; u != nil {
senderName = u.Nickname
} else {
senderName = msg.SendID
}
}
recvID := msg.RecvID
recvName := ""
if msg.SessionType == constant.ReadGroupChatType || msg.SessionType == constant.WriteGroupChatType {
if msg.GroupID != "" {
recvID = msg.GroupID
}
if g := groupMap[recvID]; g != nil {
recvName = g.GroupName
} else if recvID != "" {
recvName = recvID
}
} else {
if u := recvMap[msg.RecvID]; u != nil {
recvName = u.Nickname
} else if msg.RecvID != "" {
recvName = msg.RecvID
}
}
records = append(records, &apistruct.UserSendMsgQueryRecord{
MsgID: msgID,
SendID: msg.SendID,
SenderName: senderName,
RecvID: recvID,
RecvName: recvName,
ContentType: msg.ContentType,
ContentTypeName: contentTypeName(msg.ContentType),
SessionType: msg.SessionType,
ChatTypeName: chatTypeName(msg.SessionType),
Content: msg.Content,
SendTime: msg.SendTime,
})
}
apiresp.GinSuccess(c, &apistruct.UserSendMsgQueryResp{
Count: total,
PageNumber: pageNumber,
ShowNumber: showNumber,
Records: records,
})
}
// parseTrendIntervalMillis 解析走势统计间隔并转换为毫秒
func parseTrendIntervalMillis(intervalMinutes int32) (int64, error) {
switch intervalMinutes {
case trendIntervalMinutes15, trendIntervalMinutes30, trendIntervalMinutes60:
return int64(intervalMinutes) * int64(time.Minute/time.Millisecond), nil
default:
return 0, errs.ErrArgs.WrapMsg("invalid intervalMinutes")
}
}
// normalizeTrendTimeRange 标准化走势统计时间区间
func normalizeTrendTimeRange(startTime int64, endTime int64) (int64, int64, error) {
now := time.Now().UnixMilli()
if endTime <= 0 {
endTime = now
}
if startTime <= 0 {
startTime = endTime - int64(defaultTrendDuration/time.Millisecond)
}
if startTime < 0 {
startTime = 0
}
if endTime <= startTime {
return 0, 0, errs.ErrArgs.WrapMsg("invalid time range")
}
return startTime, endTime, nil
}
// alignTrendRange 对齐走势统计区间到间隔边界
func alignTrendRange(startTime int64, endTime int64, intervalMillis int64) (int64, int64) {
if intervalMillis <= 0 {
return startTime, endTime
}
// 开始时间向下对齐到间隔边界
bucketStart := startTime - (startTime % intervalMillis)
if startTime < 0 {
bucketStart = startTime - ((startTime % intervalMillis) + intervalMillis)
}
// 结束时间向下对齐到所在间隔的起始(只包含已发生的间隔)
bucketEnd := endTime - (endTime % intervalMillis)
if endTime < 0 && endTime%intervalMillis != 0 {
bucketEnd = endTime - ((endTime % intervalMillis) + intervalMillis)
}
// 确保至少覆盖一个间隔
if bucketEnd < bucketStart {
bucketEnd = bucketStart
}
return bucketStart, bucketEnd
}
// buildOnlineUserCountTrendPoints 构建在线人数走势数据点
func buildOnlineUserCountTrendPoints(samples []rediscache.OnlineUserCountSample, startTime int64, endTime int64, intervalMillis int64) []*apistruct.OnlineUserCountTrendItem {
points := make([]*apistruct.OnlineUserCountTrendItem, 0)
if intervalMillis <= 0 || endTime <= startTime {
return points
}
maxMap := make(map[int64]int64)
for _, sample := range samples {
// 将采样时间戳对齐到间隔边界
bucket := sample.Timestamp - (sample.Timestamp % intervalMillis)
// 处理负数时间戳的情况(虽然通常不会发生)
if sample.Timestamp < 0 && sample.Timestamp%intervalMillis != 0 {
bucket = sample.Timestamp - ((sample.Timestamp % intervalMillis) + intervalMillis)
}
if sample.Count > maxMap[bucket] {
maxMap[bucket] = sample.Count
}
}
// 计算需要生成的数据点数量
// endTime是对齐后的最后一个bucket的起始时间所以需要包含它
estimated := int((endTime-startTime)/intervalMillis) + 1
if estimated > 0 {
points = make([]*apistruct.OnlineUserCountTrendItem, 0, estimated)
}
// 生成从startTime到endTime包含endTime的所有时间点
// endTime已经是对齐后的最后一个bucket的起始时间
for ts := startTime; ts <= endTime; ts += intervalMillis {
maxVal := maxMap[ts]
points = append(points, &apistruct.OnlineUserCountTrendItem{
Timestamp: ts,
OnlineCount: maxVal,
})
}
return points
}
// buildUserSendMsgCountTrendPoints 构建用户发送消息走势数据点
func buildUserSendMsgCountTrendPoints(countMap map[int64]int64, startTime int64, endTime int64, intervalMillis int64) []*apistruct.UserSendMsgCountTrendItem {
points := make([]*apistruct.UserSendMsgCountTrendItem, 0)
if intervalMillis <= 0 || endTime <= startTime {
return points
}
estimated := int((endTime - startTime) / intervalMillis)
if estimated > 0 {
points = make([]*apistruct.UserSendMsgCountTrendItem, 0, estimated)
}
for ts := startTime; ts < endTime; ts += intervalMillis {
points = append(points, &apistruct.UserSendMsgCountTrendItem{
Timestamp: ts,
Count: countMap[ts],
})
}
return points
}
// mapTrendChatType 走势统计聊天类型转为 sessionType 列表
func mapTrendChatType(chatType int32) ([]int32, error) {
switch chatType {
case trendChatTypeSingle:
return []int32{constant.SingleChatType}, nil
case trendChatTypeGroup:
return []int32{constant.ReadGroupChatType, constant.WriteGroupChatType}, nil
default:
return nil, errs.ErrArgs.WrapMsg("invalid chatType")
}
}
// contentTypeName 消息类型名称转换
func contentTypeName(contentType int32) string {
switch contentType {
case constant.Text:
return "文本消息"
case constant.Picture:
return "图片消息"
case constant.Voice:
return "语音消息"
case constant.Video:
return "视频消息"
case constant.File:
return "文件消息"
case constant.AtText:
return "艾特消息"
case constant.Merger:
return "合并消息"
case constant.Card:
return "名片消息"
case constant.Location:
return "位置消息"
case constant.Custom:
return "自定义消息"
case constant.Revoke:
return "撤回消息"
case constant.MarkdownText:
return "Markdown消息"
default:
return "未知消息"
}
}
// chatTypeName 聊天类型名称转换
func chatTypeName(sessionType int32) string {
switch sessionType {
case constant.SingleChatType:
return "单聊"
case constant.ReadGroupChatType, constant.WriteGroupChatType:
return "群聊"
case constant.NotificationChatType:
return "通知"
default:
return "未知"
}
}