feat(admin): 用户管理增强 - CSV 批量导入用户、批量状态/组操作、登录审计日志
Build and Deploy / build-and-deploy (push) Canceled after 0s

This commit is contained in:
2026-09-13 12:09:55 +08:00
parent 32959c7330
commit 62e2250202
10 changed files with 884 additions and 0 deletions
+59
View File
@@ -664,3 +664,62 @@ func AdminBatchDeleteOAuthClient(c *gin.Context) {
}
c.JSON(200, serializer.Response{})
}
// AdminImportUsersCSV 批量导入 CSV 用户(字段:用户名,密码)
func AdminImportUsersCSV(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(200, serializer.Err(c, serializer.NewError(serializer.CodeParamErr, "未找到上传文件 field=file", err)))
return
}
res, err := admin.ImportUsers(c, file)
if err != nil {
c.JSON(200, serializer.Err(c, err))
return
}
c.JSON(200, serializer.Response{Data: res})
}
// AdminBatchSetUserStatus 批量设置用户状态
func AdminBatchSetUserStatus(c *gin.Context) {
service := ParametersFromContext[*admin.BatchUserStatusService](c, admin.BatchUserStatusParamCtx{})
err := service.SetStatus(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
return
}
c.JSON(200, serializer.Response{})
}
// AdminBatchMoveUserGroup 批量移动用户到指定用户组
func AdminBatchMoveUserGroup(c *gin.Context) {
service := ParametersFromContext[*admin.BatchUserMoveGroupService](c, admin.BatchUserMoveGroupParamCtx{})
err := service.MoveGroup(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
return
}
c.JSON(200, serializer.Response{})
}
// AdminListLoginAudits 登录审计列表
func AdminListLoginAudits(c *gin.Context) {
service := ParametersFromContext[*admin.LoginAuditListService](c, admin.LoginAuditListParamCtx{})
res, err := service.List(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
return
}
c.JSON(200, serializer.Response{Data: res})
}
// AdminGetLoginAuditDailyStats 登录审计每日统计
func AdminGetLoginAuditDailyStats(c *gin.Context) {
service := ParametersFromContext[*admin.LoginAuditDailyStatsService](c, admin.LoginAuditDailyStatsParamCtx{})
res, err := service.DailyStats(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
return
}
c.JSON(200, serializer.Response{Data: res})
}
+47
View File
@@ -1,6 +1,8 @@
package controllers
import (
"time"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
@@ -13,6 +15,37 @@ import (
"github.com/samber/lo"
)
// recordLoginAuditAsync 异步写入登录审计,失败不影响主流程
func recordLoginAuditAsync(c *gin.Context, userID *int, success bool, failReason, usernameTried *string) {
defer func() { _ = recover() }()
dep := dependency.FromContext(c)
client := dep.LoginAuditClient()
ip := c.ClientIP()
ua := c.GetHeader("User-Agent")
ctx := c.Request.Context()
go func() {
defer func() { _ = recover() }()
_, _ = client.RecordLogin(ctx, userID, ip, ua, success, failReason, usernameTried)
}()
}
// markLogoutAsync 异步标记登出,失败不影响主流程
func markLogoutAsync(c *gin.Context, uid int) {
defer func() { _ = recover() }()
dep := dependency.FromContext(c)
client := dep.LoginAuditClient()
ip := c.ClientIP()
ctx := c.Request.Context()
now := time.Now()
go func() {
defer func() { _ = recover() }()
_ = client.MarkLogout(ctx, uid, ip, now)
}()
}
// ptrString 返回字符串指针
func ptrString(s string) *string { return &s }
// StartLoginAuthn 开始注册WebAuthn登录
func StartLoginAuthn(c *gin.Context) {
res, err := user.PreparePasskeyLogin(c)
@@ -29,6 +62,7 @@ func FinishLoginAuthn(c *gin.Context) {
service := ParametersFromContext[*user.FinishPasskeyLoginService](c, user.FinishPasskeyLoginParameterCtx{})
u, err := service.FinishPasskeyLogin(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), nil)
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -77,6 +111,7 @@ func UserLoginValidation(c *gin.Context) {
service := ParametersFromContext[*user.UserLoginService](c, user.LoginParameterCtx{})
expectedUser, twoFaSession, err := service.Login(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), ptrString(service.UserName))
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -98,6 +133,7 @@ func UserLogin2FAValidation(c *gin.Context) {
service := ParametersFromContext[*user.OtpValidationService](c, user.OtpValidationParameterCtx{})
expectedUser, err := service.Verify2FA(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), nil)
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -116,6 +152,12 @@ func UserIssueToken(c *gin.Context) {
return
}
// 登录成功:写审计记录
if u, ok := c.Value(inventory.UserCtx{}).(*ent.User); ok {
uid := u.ID
recordLoginAuditAsync(c, &uid, true, nil, ptrString(u.Email))
}
c.JSON(200, serializer.Response{
Data: resp,
})
@@ -180,6 +222,11 @@ func UserSignOut(c *gin.Context) {
return
}
// 标记登出时间
if current := inventory.UserFromContext(c); current != nil && !inventory.IsAnonymousUser(current) {
markLogoutAsync(c, current.ID)
}
c.JSON(200, serializer.Response{
Data: res,
})