From ed43f6f398fc5ea1b48655b88f4df10be1f1daf9 Mon Sep 17 00:00:00 2001 From: logan Date: Thu, 28 May 2026 16:26:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5(LDAP+=E6=9C=AC=E5=9C=B0+CS?= =?UTF-8?q?V=E6=A8=A1=E6=9D=BF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BatchImportLdapUsers: 从LDAP导入用户到本地DB 解析 dn/cn/uid/mail 字段,默认密码 admin123,角色 developer 重复用户自动跳过 - BatchImportUsers: 本地CSV批量导入 支持 username/display_name/email/role/password skip_existing 选项控制重复行为 - ImportTemplate: 真实CSV模板下载(UTF-8 BOM) - 3个endpoint从placeholder改为真实实现 --- backend/internal/handler/workflow.go | 163 ++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/backend/internal/handler/workflow.go b/backend/internal/handler/workflow.go index b2627eb..16aa716 100644 --- a/backend/internal/handler/workflow.go +++ b/backend/internal/handler/workflow.go @@ -181,17 +181,174 @@ func ResetPassword(c *gin.Context) { // POST /api/v1/users/batch-import func BatchImportUsers(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "Batch import (placeholder)", "count": 0}) + var input struct { + Users []struct { + Username string `json:"username"` + DisplayName string `json:"display_name"` + Email string `json:"email"` + Role string `json:"role"` + Password string `json:"password"` + } `json:"users"` + ImportType string `json:"import_type"` + SkipExisting bool `json:"skip_existing"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + successCount := 0 + var errors []string + + for _, u := range input.Users { + if u.Username == "" { + errors = append(errors, "skip: empty username") + continue + } + + // Check if already exists + var existing int64 + model.DB.Model(&model.User{}).Where("username = ?", u.Username).Count(&existing) + if existing > 0 { + if input.SkipExisting { + errors = append(errors, "skip: "+u.Username+" already exists") + continue + } + errors = append(errors, "fail: "+u.Username+" already exists") + continue + } + + password := u.Password + if password == "" { + password = "admin123" + } + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + errors = append(errors, "fail: "+u.Username+" hash error") + continue + } + + role := u.Role + if role == "" { + role = "developer" + } + displayName := u.DisplayName + if displayName == "" { + displayName = u.Username + } + + emailPtr := &u.Email + if u.Email == "" { + emailPtr = nil + } + + user := model.User{ + Username: u.Username, + DisplayName: displayName, + Email: emailPtr, + HashedPassword: string(hash), + Role: role, + } + if err := model.DB.Create(&user).Error; err != nil { + errors = append(errors, "fail: "+u.Username+" - "+err.Error()) + continue + } + successCount++ + } + + c.JSON(http.StatusOK, gin.H{ + "success_count": successCount, + "fail_count": len(input.Users) - successCount, + "errors": errors, + }) } // POST /api/v1/users/batch-import-ldap func BatchImportLdapUsers(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "LDAP import (placeholder)", "count": 0}) + var input struct { + Users []struct { + DN string `json:"dn"` + CN string `json:"cn"` + Nickname string `json:"nickname"` + UID string `json:"uid"` + Username string `json:"username"` + Mail string `json:"mail"` + } `json:"users"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + successCount := 0 + var errors []string + defaultPassword := "admin123" + hash, _ := bcrypt.GenerateFromPassword([]byte(defaultPassword), bcrypt.DefaultCost) + + for _, u := range input.Users { + // Derive username: uid > username > cn + username := u.UID + if username == "" { + username = u.Username + } + if username == "" { + username = u.CN + } + if username == "" { + errors = append(errors, "skip: empty username for DN="+u.DN) + continue + } + + // Check if already exists + var existing int64 + model.DB.Model(&model.User{}).Where("username = ?", username).Count(&existing) + if existing > 0 { + errors = append(errors, "skip: "+username+" already exists") + continue + } + + // Derive display name: cn > nickname > username + displayName := u.CN + if displayName == "" { + displayName = u.Nickname + } + if displayName == "" { + displayName = username + } + + email := u.Mail + emailPtr := &email + if email == "" { + emailPtr = nil + } + + user := model.User{ + Username: username, + DisplayName: displayName, + Email: emailPtr, + HashedPassword: string(hash), + Role: "developer", + } + if err := model.DB.Create(&user).Error; err != nil { + errors = append(errors, "fail: "+username+" - "+err.Error()) + continue + } + successCount++ + } + + c.JSON(http.StatusOK, gin.H{ + "success_count": successCount, + "fail_count": len(input.Users) - successCount, + "errors": errors, + }) } // GET /api/v1/users/import-template func ImportTemplate(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "Template download (placeholder)"}) + csv := "username,display_name,email,role,password\nzhangsan,张三,zhangsan@test.com,developer,\nlisi,李四,lisi@test.com,dba,pass123\n" + c.Header("Content-Type", "text/csv; charset=utf-8") + c.Header("Content-Disposition", "attachment; filename=users_import_template.csv") + c.Writer.Write([]byte("\xEF\xBB\xBF" + csv)) } // POST /api/v1/users/:userId/reset-password