mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-06-16 12:28:16 +08:00
优化通话记录
This commit is contained in:
parent
7abd7b5299
commit
a11f49299c
@ -386,6 +386,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
||||
rtcGroup.POST("/signal_send_custom_signal", rc.SignalSendCustomSignal)
|
||||
rtcGroup.POST("/get_signal_invitation_records", rc.GetSignalInvitationRecords)
|
||||
rtcGroup.POST("/delete_signal_records", rc.DeleteSignalRecords)
|
||||
rtcGroup.POST("/get_call_records", rc.GetCallRecords)
|
||||
}
|
||||
|
||||
// Crypto / E2EE
|
||||
|
||||
@ -63,3 +63,7 @@ func (o *RtcApi) GetSignalInvitationRecords(c *gin.Context) {
|
||||
func (o *RtcApi) DeleteSignalRecords(c *gin.Context) {
|
||||
a2r.Call(c, rtc.RtcServiceClient.DeleteSignalRecords, o.Client)
|
||||
}
|
||||
|
||||
func (o *RtcApi) GetCallRecords(c *gin.Context) {
|
||||
a2r.Call(c, rtc.RtcServiceClient.GetCallRecords, o.Client)
|
||||
}
|
||||
|
||||
@ -61,6 +61,11 @@ func Start(ctx context.Context, cfg *Config, client discovery.SvcDiscoveryRegist
|
||||
return err
|
||||
}
|
||||
|
||||
callRecordDB, err := mgo.NewCallRecordMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgConn, err := client.GetConn(ctx, cfg.Share.RpcRegisterName.Msg)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -91,7 +96,7 @@ func Start(ctx context.Context, cfg *Config, client discovery.SvcDiscoveryRegist
|
||||
|
||||
s := &rtcServer{
|
||||
config: cfg,
|
||||
db: controller.NewRtcDatabase(signalDB),
|
||||
db: controller.NewRtcDatabase(signalDB, callRecordDB),
|
||||
roomClient: roomClient,
|
||||
msgClient: rpcli.NewMsgClient(msgConn),
|
||||
userClient: rpcli.NewUserClient(userConn),
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -77,6 +78,10 @@ func (s *rtcServer) SignalMessageAssemble(ctx context.Context, req *rtc.SignalMe
|
||||
r, err := s.handleGetTokenByRoomID(ctx, payload.GetTokenByRoomID)
|
||||
resp.Payload = &rtc.SignalResp_GetTokenByRoomID{GetTokenByRoomID: r}
|
||||
respErr = err
|
||||
case *rtc.SignalReq_Timeout:
|
||||
r, err := s.handleTimeout(ctx, payload.Timeout, req.SignalReq)
|
||||
resp.Payload = &rtc.SignalResp_Timeout{Timeout: r}
|
||||
respErr = err
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("unknown signal payload type")
|
||||
}
|
||||
@ -403,6 +408,11 @@ func (s *rtcServer) handleAccept(ctx context.Context, req *rtc.SignalAcceptReq,
|
||||
log.ZWarn(ctx, "sendSignalingNotification accept to inviter failed", err, "inviterID", dbInv.InviterUserID)
|
||||
}
|
||||
|
||||
// Record the exact moment the callee accepted; used later to split dial vs. call duration.
|
||||
if err := s.db.SetConnectTime(ctx, dbInv.RoomID, time.Now().UnixMilli()); err != nil {
|
||||
log.ZWarn(ctx, "SetConnectTime failed", err, "roomID", dbInv.RoomID)
|
||||
}
|
||||
|
||||
// 接受邀请后不删除 invitation:通话仍在进行,双方应被标记为忙线(BusyLineUserIDList)。
|
||||
// invitation 的清理由以下路径负责:
|
||||
// - 主动挂断:handleHungUp → DeleteInvitation
|
||||
@ -451,6 +461,8 @@ func (s *rtcServer) handleReject(ctx context.Context, req *rtc.SignalRejectReq,
|
||||
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||
}
|
||||
// For 1v1 calls, rejection means the call was never answered.
|
||||
go s.writeCallRecord(context.WithoutCancel(ctx), dbInv, model.CallStatusNotConnected, time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
return &rtc.SignalRejectResp{}, nil
|
||||
@ -488,9 +500,55 @@ func (s *rtcServer) handleCancel(ctx context.Context, req *rtc.SignalCancelReq,
|
||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||
}
|
||||
|
||||
go s.writeCallRecord(context.WithoutCancel(ctx), dbInv, model.CallStatusNotConnected, time.Now().UnixMilli())
|
||||
|
||||
return &rtc.SignalCancelResp{}, nil
|
||||
}
|
||||
|
||||
// handleTimeout processes a call timeout: the inviter's ring timer fired without any invitee answering.
|
||||
// Semantics are similar to cancel, but the payload type is Timeout so clients can show "missed call" UI.
|
||||
func (s *rtcServer) handleTimeout(ctx context.Context, req *rtc.SignalTimeoutReq, signalReq *rtc.SignalReq) (*rtc.SignalTimeoutResp, error) {
|
||||
if req.Invitation == nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||
}
|
||||
|
||||
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.Invitation.RoomID)
|
||||
if err != nil {
|
||||
// Invitation may have been cleaned up by TTL already; treat as no-op.
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
log.ZWarn(ctx, "handleTimeout: invitation already expired or not found", nil, "roomID", req.Invitation.RoomID)
|
||||
return &rtc.SignalTimeoutResp{}, nil
|
||||
}
|
||||
return nil, errs.WrapMsg(err, "get invitation failed", "roomID", req.Invitation.RoomID)
|
||||
}
|
||||
if req.UserID != dbInv.InviterUserID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("only the inviter can trigger timeout", "userID", req.UserID, "inviterUserID", dbInv.InviterUserID)
|
||||
}
|
||||
|
||||
sessionType := int32(constant.SingleChatType)
|
||||
if dbInv.GroupID != "" {
|
||||
sessionType = int32(constant.ReadGroupChatType)
|
||||
}
|
||||
content, err := marshalSignalReq(signalReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Notify each invitee so they can dismiss the incoming-call UI and show "missed call".
|
||||
for _, inviteeID := range dbInv.InviteeUserIDList {
|
||||
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, sessionType, dbInv.GroupID, req.OfflinePushInfo, content); err != nil {
|
||||
log.ZWarn(ctx, "handleTimeout: sendSignalingNotification to invitee failed", err, "inviteeID", inviteeID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||
log.ZWarn(ctx, "handleTimeout: DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||
}
|
||||
|
||||
go s.writeCallRecord(context.WithoutCancel(ctx), dbInv, model.CallStatusNotConnected, time.Now().UnixMilli())
|
||||
|
||||
return &rtc.SignalTimeoutResp{}, nil
|
||||
}
|
||||
|
||||
// handleHungUp processes a call hang-up.
|
||||
func (s *rtcServer) handleHungUp(ctx context.Context, req *rtc.SignalHungUpReq, signalReq *rtc.SignalReq) (*rtc.SignalHungUpResp, error) {
|
||||
if req.Invitation == nil {
|
||||
@ -529,6 +587,8 @@ func (s *rtcServer) handleHungUp(ctx context.Context, req *rtc.SignalHungUpReq,
|
||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||
}
|
||||
|
||||
go s.writeCallRecord(context.WithoutCancel(ctx), dbInv, model.CallStatusAnswered, time.Now().UnixMilli())
|
||||
|
||||
return &rtc.SignalHungUpResp{}, nil
|
||||
}
|
||||
|
||||
@ -765,6 +825,95 @@ func (s *rtcServer) DeleteSignalRecords(ctx context.Context, req *rtc.DeleteSign
|
||||
return &rtc.DeleteSignalRecordsResp{}, nil
|
||||
}
|
||||
|
||||
// GetCallRecords returns paginated call records for a user.
|
||||
// status=0 returns all records; status=1 returns answered calls; status=2 returns not-connected calls.
|
||||
// For 1v1 calls, InviterUserNickname is resolved per-viewer with priority: remark > firstName+lastName > nickname.
|
||||
func (s *rtcServer) GetCallRecords(ctx context.Context, req *rtc.GetCallRecordsReq) (*rtc.GetCallRecordsResp, error) {
|
||||
if req.UserID == "" {
|
||||
req.UserID = mcontext.GetOpUserID(ctx)
|
||||
}
|
||||
total, records, err := s.db.SearchCallRecords(ctx, req.UserID, req.Status, req.StartTime, req.EndTime, req.Keyword, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For 1v1 calls, resolve InviterUserNickname from the querying user's perspective:
|
||||
// remark (if friend) > firstName + lastName > nickname.
|
||||
// Collect unique inviter IDs that appear in 1v1 records.
|
||||
inviterIDSet := make(map[string]struct{})
|
||||
for _, r := range records {
|
||||
if r.GroupID == "" && r.InviterUserID != "" {
|
||||
inviterIDSet[r.InviterUserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
userInfoMap := make(map[string]*sdkws.UserInfo)
|
||||
remarkMap := make(map[string]string) // inviterUserID → remark
|
||||
if len(inviterIDSet) > 0 {
|
||||
inviterIDs := make([]string, 0, len(inviterIDSet))
|
||||
for id := range inviterIDSet {
|
||||
inviterIDs = append(inviterIDs, id)
|
||||
}
|
||||
if infoMap, e := s.userClient.GetUsersInfoMap(ctx, inviterIDs); e == nil {
|
||||
userInfoMap = infoMap
|
||||
} else {
|
||||
log.ZWarn(ctx, "GetCallRecords: GetUsersInfoMap failed", e)
|
||||
}
|
||||
if friendInfos, e := s.relationClient.GetFriendsInfo(ctx, req.UserID, inviterIDs); e == nil {
|
||||
for _, f := range friendInfos {
|
||||
if f.GetRemark() != "" {
|
||||
remarkMap[f.GetFriendUserID()] = f.GetRemark()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.ZWarn(ctx, "GetCallRecords: GetFriendsInfo failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]*rtc.CallRecordItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
direction := model.CallDirectionIncoming
|
||||
if r.InviterUserID == req.UserID {
|
||||
direction = model.CallDirectionOutgoing
|
||||
}
|
||||
|
||||
inviterNickname := r.InviterUserNickname
|
||||
if r.GroupID == "" && r.InviterUserID != "" {
|
||||
if remark, ok := remarkMap[r.InviterUserID]; ok {
|
||||
inviterNickname = remark
|
||||
} else if ui, ok := userInfoMap[r.InviterUserID]; ok {
|
||||
if name := strings.TrimSpace(ui.FirstName + " " + ui.LastName); name != "" {
|
||||
inviterNickname = name
|
||||
} else {
|
||||
inviterNickname = ui.Nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, &rtc.CallRecordItem{
|
||||
Sid: r.SID,
|
||||
RoomID: r.RoomID,
|
||||
Status: r.Status,
|
||||
Duration: r.Duration,
|
||||
DialDuration: r.DialDuration,
|
||||
CallDuration: r.CallDuration,
|
||||
CreateTime: r.CreateTime,
|
||||
MediaType: r.MediaType,
|
||||
SessionType: r.SessionType,
|
||||
InviterUserID: r.InviterUserID,
|
||||
InviterUserNickname: inviterNickname,
|
||||
InviterUserFaceURL: r.InviterUserFaceURL,
|
||||
InviteeUserIDList: r.InviteeUserIDList,
|
||||
GroupID: r.GroupID,
|
||||
GroupName: r.GroupName,
|
||||
Direction: direction,
|
||||
})
|
||||
}
|
||||
return &rtc.GetCallRecordsResp{
|
||||
Total: int32(total),
|
||||
Records: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
// genToken generates a LiveKit access token for the given room and identity.
|
||||
@ -918,6 +1067,75 @@ func modelToInvitationInfo(m *model.SignalInvitation) *rtc.InvitationInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// writeCallRecord creates a call record entry after a call ends (best-effort, logs on failure).
|
||||
// status: model.CallStatusAnswered or model.CallStatusNotConnected.
|
||||
// endTimeMs: Unix ms timestamp when the call ended (used to compute duration for answered calls).
|
||||
func (s *rtcServer) writeCallRecord(ctx context.Context, inv *model.SignalInvitation, status int32, endTimeMs int64) {
|
||||
sid := fmt.Sprintf("call-%s", uuid.New().String())
|
||||
|
||||
// totalDuration: kept for backward compatibility (initiate → end).
|
||||
var totalDuration, dialDuration, callDuration int64
|
||||
if inv.InitiateTime > 0 {
|
||||
if status == model.CallStatusAnswered && inv.ConnectTime > 0 {
|
||||
// 拨打时长 = 振铃到接听
|
||||
dialDuration = (inv.ConnectTime - inv.InitiateTime) / 1000
|
||||
// 通话时长 = 接听到挂断
|
||||
callDuration = (endTimeMs - inv.ConnectTime) / 1000
|
||||
totalDuration = dialDuration + callDuration
|
||||
} else {
|
||||
// 未接通:全程视为拨打时长
|
||||
dialDuration = (endTimeMs - inv.InitiateTime) / 1000
|
||||
}
|
||||
if dialDuration < 0 {
|
||||
dialDuration = 0
|
||||
}
|
||||
if callDuration < 0 {
|
||||
callDuration = 0
|
||||
}
|
||||
if totalDuration < 0 {
|
||||
totalDuration = 0
|
||||
}
|
||||
}
|
||||
|
||||
record := &model.CallRecord{
|
||||
SID: sid,
|
||||
RoomID: inv.RoomID,
|
||||
Status: status,
|
||||
Duration: totalDuration,
|
||||
DialDuration: dialDuration,
|
||||
CallDuration: callDuration,
|
||||
CreateTime: inv.InitiateTime,
|
||||
MediaType: inv.MediaType,
|
||||
SessionType: inv.SessionType,
|
||||
InviterUserID: inv.InviterUserID,
|
||||
InviteeUserIDList: inv.InviteeUserIDList,
|
||||
GroupID: inv.GroupID,
|
||||
}
|
||||
|
||||
// Fetch inviter's nickname and face URL.
|
||||
if inv.InviterUserID != "" {
|
||||
if userInfo, err := s.userClient.GetUserInfo(ctx, inv.InviterUserID); err == nil {
|
||||
record.InviterUserNickname = userInfo.Nickname
|
||||
record.InviterUserFaceURL = userInfo.FaceURL
|
||||
} else {
|
||||
log.ZWarn(ctx, "writeCallRecord: GetUserInfo failed", err, "inviterUserID", inv.InviterUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch group name if this is a group call.
|
||||
if inv.GroupID != "" {
|
||||
if groupInfo, err := s.groupClient.GetGroupInfo(ctx, inv.GroupID); err == nil {
|
||||
record.GroupName = groupInfo.GroupName
|
||||
} else {
|
||||
log.ZWarn(ctx, "writeCallRecord: GetGroupInfo failed", err, "groupID", inv.GroupID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.CreateCallRecord(ctx, record); err != nil {
|
||||
log.ZWarn(ctx, "writeCallRecord: CreateCallRecord failed", err, "roomID", inv.RoomID, "status", status)
|
||||
}
|
||||
}
|
||||
|
||||
// hungUpPeerIDsFromDB returns IDs that should receive hang-up notification, based on authoritative DB data.
|
||||
func hungUpPeerIDsFromDB(inv *model.SignalInvitation, callerID string) []string {
|
||||
if callerID == inv.InviterUserID {
|
||||
|
||||
@ -29,6 +29,7 @@ type RtcDatabase interface {
|
||||
GetInvitationByInviteeUserID(ctx context.Context, userID string) (*model.SignalInvitation, error)
|
||||
DeleteInvitation(ctx context.Context, roomID string) error
|
||||
RemoveInvitee(ctx context.Context, roomID string, userID string) error
|
||||
SetConnectTime(ctx context.Context, roomID string, connectTimeMs int64) error
|
||||
GetInvitationByGroupID(ctx context.Context, groupID string) (*model.SignalInvitation, error)
|
||||
GetInvitationsByRoomIDs(ctx context.Context, roomIDs []string) ([]*model.SignalInvitation, error)
|
||||
// GetBusyUserIDs returns the subset of userIDs that are currently in an active call.
|
||||
@ -37,14 +38,20 @@ type RtcDatabase interface {
|
||||
CreateRecord(ctx context.Context, record *model.SignalRecord) error
|
||||
SearchRecords(ctx context.Context, sendID, recvID string, sessionType int32, startTime, endTime int64, pagination pagination.Pagination) (int64, []*model.SignalRecord, error)
|
||||
DeleteRecords(ctx context.Context, sIDs []string) error
|
||||
|
||||
// Call record operations (通话记录).
|
||||
CreateCallRecord(ctx context.Context, record *model.CallRecord) error
|
||||
SearchCallRecords(ctx context.Context, userID string, status int32, startTime, endTime int64, keyword string, pg pagination.Pagination) (int64, []*model.CallRecord, error)
|
||||
DeleteCallRecords(ctx context.Context, sids []string) error
|
||||
}
|
||||
|
||||
type rtcDatabase struct {
|
||||
db database.SignalDatabase
|
||||
db database.SignalDatabase
|
||||
callRecord database.CallRecordDatabase
|
||||
}
|
||||
|
||||
func NewRtcDatabase(db database.SignalDatabase) RtcDatabase {
|
||||
return &rtcDatabase{db: db}
|
||||
func NewRtcDatabase(db database.SignalDatabase, callRecord database.CallRecordDatabase) RtcDatabase {
|
||||
return &rtcDatabase{db: db, callRecord: callRecord}
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) CreateInvitation(ctx context.Context, inv *model.SignalInvitation) error {
|
||||
@ -67,6 +74,10 @@ func (r *rtcDatabase) RemoveInvitee(ctx context.Context, roomID string, userID s
|
||||
return r.db.RemoveInvitee(ctx, roomID, userID)
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) SetConnectTime(ctx context.Context, roomID string, connectTimeMs int64) error {
|
||||
return r.db.SetConnectTime(ctx, roomID, connectTimeMs)
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) GetInvitationByGroupID(ctx context.Context, groupID string) (*model.SignalInvitation, error) {
|
||||
return r.db.GetInvitationByGroupID(ctx, groupID)
|
||||
}
|
||||
@ -90,3 +101,15 @@ func (r *rtcDatabase) SearchRecords(ctx context.Context, sendID, recvID string,
|
||||
func (r *rtcDatabase) DeleteRecords(ctx context.Context, sIDs []string) error {
|
||||
return r.db.DeleteRecords(ctx, sIDs)
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) CreateCallRecord(ctx context.Context, record *model.CallRecord) error {
|
||||
return r.callRecord.CreateCallRecord(ctx, record)
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) SearchCallRecords(ctx context.Context, userID string, status int32, startTime, endTime int64, keyword string, pg pagination.Pagination) (int64, []*model.CallRecord, error) {
|
||||
return r.callRecord.SearchCallRecords(ctx, userID, status, startTime, endTime, keyword, pg)
|
||||
}
|
||||
|
||||
func (r *rtcDatabase) DeleteCallRecords(ctx context.Context, sids []string) error {
|
||||
return r.callRecord.DeleteCallRecords(ctx, sids)
|
||||
}
|
||||
|
||||
33
pkg/common/storage/database/call_record.go
Normal file
33
pkg/common/storage/database/call_record.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright © 2024 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
)
|
||||
|
||||
// CallRecordDatabase defines storage operations for the call record table.
|
||||
type CallRecordDatabase interface {
|
||||
// CreateCallRecord writes a new call record entry.
|
||||
CreateCallRecord(ctx context.Context, record *model.CallRecord) error
|
||||
// SearchCallRecords returns paginated call records involving userID,
|
||||
// optionally filtered by status, time range and a keyword (fuzzy match on InviterUserNickname).
|
||||
SearchCallRecords(ctx context.Context, userID string, status int32, startTime, endTime int64, keyword string, pg pagination.Pagination) (int64, []*model.CallRecord, error)
|
||||
// DeleteCallRecords removes call records by their SIDs.
|
||||
DeleteCallRecords(ctx context.Context, sids []string) error
|
||||
}
|
||||
92
pkg/common/storage/database/mgo/call_record.go
Normal file
92
pkg/common/storage/database/mgo/call_record.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright © 2024 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewCallRecordMongo(db *mongo.Database) (database.CallRecordDatabase, error) {
|
||||
coll := db.Collection(database.CallRecordName)
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "sid", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "inviter_user_id", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "invitee_user_id_list", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "status", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &callRecordMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type callRecordMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (c *callRecordMgo) CreateCallRecord(ctx context.Context, record *model.CallRecord) error {
|
||||
return mongoutil.InsertMany(ctx, c.coll, []*model.CallRecord{record})
|
||||
}
|
||||
|
||||
func (c *callRecordMgo) SearchCallRecords(ctx context.Context, userID string, status int32, startTime, endTime int64, keyword string, pg pagination.Pagination) (int64, []*model.CallRecord, error) {
|
||||
filter := bson.M{}
|
||||
if userID != "" {
|
||||
filter["$or"] = bson.A{
|
||||
bson.M{"inviter_user_id": userID},
|
||||
bson.M{"invitee_user_id_list": userID},
|
||||
}
|
||||
}
|
||||
if status != 0 {
|
||||
filter["status"] = status
|
||||
}
|
||||
if startTime > 0 || endTime > 0 {
|
||||
timeFilter := bson.M{}
|
||||
if startTime > 0 {
|
||||
timeFilter["$gte"] = startTime
|
||||
}
|
||||
if endTime > 0 {
|
||||
timeFilter["$lte"] = endTime
|
||||
}
|
||||
filter["create_time"] = timeFilter
|
||||
}
|
||||
if keyword != "" {
|
||||
filter["inviter_user_nickname"] = bson.M{"$regex": keyword, "$options": "i"}
|
||||
}
|
||||
return mongoutil.FindPage[*model.CallRecord](ctx, c.coll, filter, pg, options.Find().SetSort(bson.M{"create_time": -1}))
|
||||
}
|
||||
|
||||
func (c *callRecordMgo) DeleteCallRecords(ctx context.Context, sids []string) error {
|
||||
return mongoutil.DeleteMany(ctx, c.coll, bson.M{"sid": bson.M{"$in": sids}})
|
||||
}
|
||||
@ -108,6 +108,14 @@ func (s *signalMgo) RemoveInvitee(ctx context.Context, roomID string, userID str
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *signalMgo) SetConnectTime(ctx context.Context, roomID string, connectTimeMs int64) error {
|
||||
_, err := s.invColl.UpdateOne(ctx,
|
||||
bson.M{"room_id": roomID},
|
||||
bson.M{"$set": bson.M{"connect_time": connectTimeMs}},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *signalMgo) GetInvitationByGroupID(ctx context.Context, groupID string) (*model.SignalInvitation, error) {
|
||||
opts := options.FindOne().SetSort(bson.M{"create_time": -1})
|
||||
return mongoutil.FindOne[*model.SignalInvitation](ctx, s.invColl, bson.M{"group_id": groupID}, opts)
|
||||
|
||||
@ -23,6 +23,7 @@ const (
|
||||
PhoneSNInfoName = "phone_sn_info"
|
||||
SignalInvitationName = "signal_invitation"
|
||||
SignalRecordName = "signal_record"
|
||||
CallRecordName = "call_record"
|
||||
SpamReportName = "spam_report"
|
||||
MsgBurnDeadlineName = "msg_burn_deadline"
|
||||
UserOfflineRecordName = "user_offline_record"
|
||||
|
||||
@ -34,6 +34,8 @@ type SignalDatabase interface {
|
||||
// RemoveInvitee removes a single user from the invitee list via $pull;
|
||||
// if the list becomes empty the document is deleted automatically.
|
||||
RemoveInvitee(ctx context.Context, roomID string, userID string) error
|
||||
// SetConnectTime records the Unix ms timestamp when a callee first accepted the call.
|
||||
SetConnectTime(ctx context.Context, roomID string, connectTimeMs int64) error
|
||||
// GetInvitationByGroupID retrieves the active invitation for a group.
|
||||
GetInvitationByGroupID(ctx context.Context, groupID string) (*model.SignalInvitation, error)
|
||||
// GetInvitationsByRoomIDs retrieves invitations for the given room IDs.
|
||||
|
||||
@ -16,6 +16,18 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Call record status values for CallRecord.Status.
|
||||
const (
|
||||
CallStatusAnswered int32 = 1 // 已接听
|
||||
CallStatusNotConnected int32 = 2 // 未接通
|
||||
)
|
||||
|
||||
// Call record direction values for the querying user's perspective.
|
||||
const (
|
||||
CallDirectionOutgoing int32 = 1 // 主叫(发起方)
|
||||
CallDirectionIncoming int32 = 2 // 被叫(接收方)
|
||||
)
|
||||
|
||||
// SignalInvitation stores an ongoing or pending signal invitation, keyed by roomID.
|
||||
// It is created when a call is initiated and can be queried when the callee starts the app.
|
||||
type SignalInvitation struct {
|
||||
@ -29,6 +41,8 @@ type SignalInvitation struct {
|
||||
PlatformID int32 `bson:"platform_id"`
|
||||
SessionType int32 `bson:"session_type"`
|
||||
InitiateTime int64 `bson:"initiate_time"`
|
||||
// ConnectTime is the Unix ms timestamp when a callee accepted the call (0 until answered).
|
||||
ConnectTime int64 `bson:"connect_time"`
|
||||
BusyLineUserIDList []string `bson:"busy_line_user_id_list"`
|
||||
OfflinePushTitle string `bson:"offline_push_title"`
|
||||
OfflinePushDesc string `bson:"offline_push_desc"`
|
||||
@ -39,6 +53,25 @@ type SignalInvitation struct {
|
||||
ExpireAt time.Time `bson:"expire_at"`
|
||||
}
|
||||
|
||||
// CallRecord stores a completed call event (answered or not connected) for call history.
|
||||
type CallRecord struct {
|
||||
SID string `bson:"sid"`
|
||||
RoomID string `bson:"room_id"`
|
||||
Status int32 `bson:"status"` // CallStatusAnswered / CallStatusNotConnected
|
||||
Duration int64 `bson:"duration"` // total duration in seconds (initiate→end); kept for backward compat
|
||||
DialDuration int64 `bson:"dial_duration"` // 拨打时长: initiate→connect (answered) or initiate→end (not connected), seconds
|
||||
CallDuration int64 `bson:"call_duration"` // 通话时长: connect→end for answered calls; 0 if not connected, seconds
|
||||
CreateTime int64 `bson:"create_time"` // Unix ms, when the call was initiated
|
||||
MediaType string `bson:"media_type"` // "audio" or "video"
|
||||
SessionType int32 `bson:"session_type"`
|
||||
InviterUserID string `bson:"inviter_user_id"`
|
||||
InviterUserNickname string `bson:"inviter_user_nickname"`
|
||||
InviterUserFaceURL string `bson:"inviter_user_face_url"`
|
||||
InviteeUserIDList []string `bson:"invitee_user_id_list"` // all invitees (for search by participant)
|
||||
GroupID string `bson:"group_id"`
|
||||
GroupName string `bson:"group_name"`
|
||||
}
|
||||
|
||||
// SignalRecord stores a completed call record used for history queries.
|
||||
type SignalRecord struct {
|
||||
SID string `bson:"sid"`
|
||||
|
||||
2
protocol
2
protocol
@ -1 +1 @@
|
||||
Subproject commit bf66521b27c2302dcae38ff441b035cd443edf20
|
||||
Subproject commit d52af007aa8c2024f53f1c376be8501b73eea1b7
|
||||
Loading…
x
Reference in New Issue
Block a user