feat(用户管理): 添加最后登录时间和会员状态功能
Build docker and publish / build (20.15.1) (push) Successful in 4m57s

- 新增数据库迁移文件添加last_login_time字段
- 在登录逻辑中更新最后登录时间
- 添加FindActiveSubscribesByUserIds方法查询用户订阅状态
- 在用户列表接口中聚合最后登录时间和会员状态信息
- 更新相关API定义和模型结构
- 修复迁移文件版本号冲突问题
- 移除omitempty标签确保字段始终返回
This commit is contained in:
2026-01-05 01:46:39 -08:00
parent 5598181a48
commit 657c2930b1
13 changed files with 279 additions and 76 deletions
+85 -76
View File
@@ -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)
+10
View File
@@ -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)