- 新增数据库迁移文件添加last_login_time字段 - 在登录逻辑中更新最后登录时间 - 添加FindActiveSubscribesByUserIds方法查询用户订阅状态 - 在用户列表接口中聚合最后登录时间和会员状态信息 - 更新相关API定义和模型结构 - 修复迁移文件版本号冲突问题 - 移除omitempty标签确保字段始终返回
This commit is contained in:
@@ -39,12 +39,40 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserListLogic failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// Batch fetch active subscriptions
|
||||
userIds := make([]int64, 0, len(list))
|
||||
for _, u := range list {
|
||||
userIds = append(userIds, u.Id)
|
||||
}
|
||||
activeSubs, err := l.svcCtx.UserModel.FindActiveSubscribesByUserIds(l.ctx, userIds)
|
||||
if err != nil {
|
||||
// Log error but continue
|
||||
l.Logger.Error("FindActiveSubscribesByUserIds failed", logger.Field("error", err.Error()))
|
||||
}
|
||||
|
||||
userRespList := make([]types.User, 0, len(list))
|
||||
|
||||
for _, item := range list {
|
||||
var u types.User
|
||||
tool.DeepCopy(&u, item)
|
||||
|
||||
// Set LastLoginTime
|
||||
if item.LastLoginTime != nil {
|
||||
u.LastLoginTime = item.LastLoginTime.Unix()
|
||||
}
|
||||
|
||||
// Set MemberStatus and update LastLoginTime from traffic
|
||||
if info, ok := activeSubs[item.Id]; ok {
|
||||
u.MemberStatus = info.MemberStatus
|
||||
|
||||
if info.LastTrafficAt != nil {
|
||||
trafficTime := info.LastTrafficAt.Unix()
|
||||
if trafficTime > u.LastLoginTime {
|
||||
u.LastLoginTime = trafficTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 AuthMethods
|
||||
authMethods := make([]types.UserAuthMethod, len(u.AuthMethods)) // 直接创建目标 slice
|
||||
for i, method := range u.AuthMethods {
|
||||
|
||||
@@ -44,46 +44,45 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
|
||||
// Verify Code
|
||||
// Using "Security" type or "Register"? Since it can be used for both, we need to know what the frontend requested.
|
||||
// But usually, the "Get Code" interface requires a "type".
|
||||
// If the user doesn't exist, they probably requested "Register" code or "Login" code?
|
||||
// Let's assume the frontend requests a "Security" code or a specific "Login" code.
|
||||
// However, looking at resetPasswordLogic, it uses `constant.Security`.
|
||||
// Looking at userRegisterLogic, it uses `constant.Register`.
|
||||
// Since this is a "Login" interface, but implicitly registers, we might need to check which code was sent.
|
||||
// Or, more robustly, we check both? Or we decide on one.
|
||||
// Usually "Login" implies "Security" or "Login" type.
|
||||
// If we assume the user calls `/verify/email` with type "login" (if it exists) or "register".
|
||||
// For simplicity, let's assume `constant.Security` (Common for login) or we need to support `constant.Register` if it's a new user flow?
|
||||
// User flow:
|
||||
// 1. Enter Email -> Click "Get Code". The type sent to "Get Code" determines the Redis key.
|
||||
// DOES the frontend know if the user exists? Probably not (Privacy).
|
||||
// So the frontend probably sends type="login" (or similar).
|
||||
// Let's check `constant` package for available types? I don't see it.
|
||||
// Assuming `constant.Security` for generic verification.
|
||||
scenes := []string{constant.Security.String(), constant.Register.String()}
|
||||
var verified bool
|
||||
var cacheKeyUsed string
|
||||
var payload common.CacheKeyPayload
|
||||
for _, scene := range scenes {
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email)
|
||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
if err != nil || value == "" {
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
||||
continue
|
||||
}
|
||||
if payload.Code == req.Code {
|
||||
verified = true
|
||||
cacheKeyUsed = cacheKey
|
||||
break
|
||||
}
|
||||
}
|
||||
if !verified {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verification code error or expired")
|
||||
}
|
||||
l.svcCtx.Redis.Del(l.ctx, cacheKeyUsed)
|
||||
|
||||
// But usually, the "Get Code" interface requires a "type".
|
||||
// If the user doesn't exist, they probably requested "Register" code or "Login" code?
|
||||
// Let's assume the frontend requests a "Security" code or a specific "Login" code.
|
||||
// However, looking at resetPasswordLogic, it uses `constant.Security`.
|
||||
// Looking at userRegisterLogic, it uses `constant.Register`.
|
||||
// Since this is a "Login" interface, but implicitly registers, we might need to check which code was sent.
|
||||
// Or, more robustly, we check both? Or we decide on one.
|
||||
// Usually "Login" implies "Security" or "Login" type.
|
||||
// If we assume the user calls `/verify/email` with type "login" (if it exists) or "register".
|
||||
// For simplicity, let's assume `constant.Security` (Common for login) or we need to support `constant.Register` if it's a new user flow?
|
||||
// User flow:
|
||||
// 1. Enter Email -> Click "Get Code". The type sent to "Get Code" determines the Redis key.
|
||||
// DOES the frontend know if the user exists? Probably not (Privacy).
|
||||
// So the frontend probably sends type="login" (or similar).
|
||||
// Let's check `constant` package for available types? I don't see it.
|
||||
// Assuming `constant.Security` for generic verification.
|
||||
scenes := []string{constant.Security.String(), constant.Register.String()}
|
||||
var verified bool
|
||||
var cacheKeyUsed string
|
||||
var payload common.CacheKeyPayload
|
||||
for _, scene := range scenes {
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email)
|
||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
if err != nil || value == "" {
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
||||
continue
|
||||
}
|
||||
if payload.Code == req.Code {
|
||||
verified = true
|
||||
cacheKeyUsed = cacheKey
|
||||
break
|
||||
}
|
||||
}
|
||||
if !verified {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verification code error or expired")
|
||||
}
|
||||
l.svcCtx.Redis.Del(l.ctx, cacheKeyUsed)
|
||||
|
||||
// Check User
|
||||
userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
|
||||
@@ -113,9 +112,9 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
|
||||
// Create User
|
||||
// Use a random password for email login user? Or empty?
|
||||
// User model usually requires password? `userRegisterLogic` encodes it.
|
||||
// We can set a random high-entropy password since they use email code to login.
|
||||
pwd := tool.EncodePassWord(uuidx.NewUUID().String())
|
||||
// User model usually requires password? `userRegisterLogic` encodes it.
|
||||
// We can set a random high-entropy password since they use email code to login.
|
||||
pwd := tool.EncodePassWord(uuidx.NewUUID().String())
|
||||
userInfo = &user.User{
|
||||
Password: pwd,
|
||||
Algo: "default",
|
||||
@@ -156,41 +155,51 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
|
||||
// Record login status
|
||||
defer func() {
|
||||
if userInfo.Id != 0 {
|
||||
loginLog := log.Login{
|
||||
Method: "email_code",
|
||||
LoginIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Success: loginStatus,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := loginLog.Marshal()
|
||||
l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeLogin.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: userInfo.Id,
|
||||
Content: string(content),
|
||||
})
|
||||
|
||||
if isNewUser {
|
||||
registerLog := log.Register{
|
||||
AuthMethod: "email_code",
|
||||
Identifier: req.Email,
|
||||
RegisterIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
regContent, _ := registerLog.Marshal()
|
||||
l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeRegister.Uint8(),
|
||||
ObjectID: userInfo.Id,
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
Content: string(regContent),
|
||||
})
|
||||
}
|
||||
}
|
||||
if userInfo.Id != 0 {
|
||||
loginLog := log.Login{
|
||||
Method: "email_code",
|
||||
LoginIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Success: loginStatus,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := loginLog.Marshal()
|
||||
l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeLogin.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: userInfo.Id,
|
||||
Content: string(content),
|
||||
})
|
||||
|
||||
if isNewUser {
|
||||
registerLog := log.Register{
|
||||
AuthMethod: "email_code",
|
||||
Identifier: req.Email,
|
||||
RegisterIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
regContent, _ := registerLog.Marshal()
|
||||
l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeRegister.Uint8(),
|
||||
ObjectID: userInfo.Id,
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
Content: string(regContent),
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Update last login time
|
||||
now := time.Now()
|
||||
userInfo.LastLoginTime = &now
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil {
|
||||
l.Errorw("failed to update last login time",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
// Login (Generate Token)
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
|
||||
@@ -81,6 +81,16 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
now := time.Now()
|
||||
userInfo.LastLoginTime = &now
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil {
|
||||
l.Errorw("failed to update last login time",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
|
||||
Reference in New Issue
Block a user