diff --git a/internal/api/router.go b/internal/api/router.go index bbfed0eba..52e6e4684 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -179,6 +179,10 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co userRouterGroup.POST("/get_user_by_phone", u.GetUserByPhone) // 根据昵称精确查询用户(可多结果,与 getPaginationUsers 模糊搜索不同) userRouterGroup.POST("/get_users_by_nickname", u.GetUsersByNickname) + // 记录用户登录时间,返回登录时间戳及上次登录信息 + userRouterGroup.POST("/login", u.UserLogin) + // 记录用户登出时间,返回登出时间戳(毫秒) + userRouterGroup.POST("/logout", u.UserLogout) // 全局黑名单管理(仅管理员) userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist) diff --git a/internal/api/user.go b/internal/api/user.go index ad4e23fee..925a6a41f 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -370,3 +370,13 @@ func (u *UserApi) GetUserByPhone(c *gin.Context) { func (u *UserApi) GetUsersByNickname(c *gin.Context) { a2r.Call(c, user.UserClient.GetUsersByNickname, u.Client) } + +// UserLogin 记录当前用户的登录时间。 +func (u *UserApi) UserLogin(c *gin.Context) { + a2r.Call(c, user.UserClient.UserLogin, u.Client) +} + +// UserLogout 记录当前用户的登出时间。 +func (u *UserApi) UserLogout(c *gin.Context) { + a2r.Call(c, user.UserClient.UserLogout, u.Client) +} diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index bb263e4db..aefad60c8 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -1064,3 +1064,45 @@ func (s *userServer) SortQuery(ctx context.Context, req *pbuser.SortQueryReq) (* } return &pbuser.SortQueryResp{Users: convert.UsersDB2Pb(users)}, nil } + +// UserLogin 记录当前用户的登录时间(userID 取自上下文 opUserID)。 +func (s *userServer) UserLogin(ctx context.Context, _ *pbuser.UserLoginReq) (*pbuser.UserLoginResp, error) { + userID := mcontext.GetOpUserID(ctx) + if userID == "" { + return nil, errs.ErrArgs.WrapMsg("opUserID is required") + } + if _, err := s.db.FindWithError(ctx, []string{userID}); err != nil { + log.ZError(ctx, "UserLogin: user not found", err, "userID", userID) + return nil, err + } + now := time.Now() + if err := s.db.UpdateByMap(ctx, userID, map[string]any{ + "cur_login_time": now, + }); err != nil { + log.ZError(ctx, "UserLogin: UpdateByMap failed", err, "userID", userID) + return nil, err + } + log.ZDebug(ctx, "UserLogin: recorded login time", "userID", userID, "loginTime", now) + return &pbuser.UserLoginResp{}, nil +} + +// UserLogout 记录当前登录用户的登出时间(userID 取自上下文 opUserID)。 +func (s *userServer) UserLogout(ctx context.Context, _ *pbuser.UserLogoutReq) (*pbuser.UserLogoutResp, error) { + userID := mcontext.GetOpUserID(ctx) + if userID == "" { + return nil, errs.ErrArgs.WrapMsg("opUserID is required") + } + if _, err := s.db.FindWithError(ctx, []string{userID}); err != nil { + log.ZError(ctx, "UserLogout: user not found", err, "userID", userID) + return nil, err + } + now := time.Now() + if err := s.db.UpdateByMap(ctx, userID, map[string]any{ + "last_logout_time": now, + }); err != nil { + log.ZError(ctx, "UserLogout: UpdateByMap failed", err, "userID", userID) + return nil, err + } + log.ZDebug(ctx, "UserLogout: recorded logout time", "userID", userID, "logoutTime", now) + return &pbuser.UserLogoutResp{}, nil +} diff --git a/pkg/common/storage/model/user.go b/pkg/common/storage/model/user.go index e880e1255..5ab5a8cfa 100644 --- a/pkg/common/storage/model/user.go +++ b/pkg/common/storage/model/user.go @@ -59,28 +59,32 @@ const ( ) type User struct { - UserID string `bson:"user_id"` - Nickname string `bson:"nickname"` - FaceURL string `bson:"face_url"` - Ex string `bson:"ex"` - AppMangerLevel int32 `bson:"app_manger_level"` - GlobalRecvMsgOpt int32 `bson:"global_recv_msg_opt"` - CreateTime time.Time `bson:"create_time"` - FirstName string `bson:"first_name"` - LastName string `bson:"last_name"` - FullName string `bson:"full_name"` - Phone string `bson:"phone"` - AreaCode string `bson:"area_code"` - PhoneVisibility int32 `bson:"phone_visibility"` - CallAcceptSetting int32 `bson:"call_accept_setting"` - MsgReceiveSetting int32 `bson:"msg_receive_setting"` - GroupInviteSetting int32 `bson:"group_invite_setting"` + UserID string `bson:"user_id"` + Nickname string `bson:"nickname"` + FaceURL string `bson:"face_url"` + Ex string `bson:"ex"` + AppMangerLevel int32 `bson:"app_manger_level"` + GlobalRecvMsgOpt int32 `bson:"global_recv_msg_opt"` + CreateTime time.Time `bson:"create_time"` + FirstName string `bson:"first_name"` + LastName string `bson:"last_name"` + FullName string `bson:"full_name"` + Phone string `bson:"phone"` + AreaCode string `bson:"area_code"` + PhoneVisibility int32 `bson:"phone_visibility"` + CallAcceptSetting int32 `bson:"call_accept_setting"` + MsgReceiveSetting int32 `bson:"msg_receive_setting"` + GroupInviteSetting int32 `bson:"group_invite_setting"` // CallRingtoneURL 用户自定义来电铃声 URL;对方来电时播放此铃声 CallRingtoneURL string `bson:"call_ringtone_url"` // Status 账号状态:0=正常,1=冻结,2=黑名单 Status int32 `bson:"status"` // MsgBurnDuration 用户全局消息阅后即焚时长(秒);0 表示关闭 MsgBurnDuration int32 `bson:"msg_burn_duration"` + // CurLoginTime 当前(最近一次)登录时间;nil 表示从未登录 + CurLoginTime *time.Time `bson:"cur_login_time"` + // LastLogoutTime 最近一次登出时间;nil 表示从未登出 + LastLogoutTime *time.Time `bson:"last_logout_time"` } func (u *User) GetNickname() string { diff --git a/protocol b/protocol index 9c1ea89b5..4741c40fb 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 9c1ea89b54286e04a376bdd4fa56a96e5b08766f +Subproject commit 4741c40fbd3d369bdf0f9f34c00791ec6483425b