diff --git a/docs/docs.go b/docs/docs.go
index 64fc98e3..bbcdeca1 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -6286,10 +6286,15 @@ const docTemplate = `{
"user.createUserRequest": {
"type": "object",
"required": [
+ "email",
"password",
"username"
],
"properties": {
+ "email": {
+ "type": "string",
+ "maxLength": 255
+ },
"is_active": {
"type": "boolean"
},
diff --git a/docs/swagger.json b/docs/swagger.json
index 6528f81d..bfaffed1 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -6279,10 +6279,15 @@
"user.createUserRequest": {
"type": "object",
"required": [
+ "email",
"password",
"username"
],
"properties": {
+ "email": {
+ "type": "string",
+ "maxLength": 255
+ },
"is_active": {
"type": "boolean"
},
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index a699f518..57454a17 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -987,6 +987,9 @@ definitions:
type: object
user.createUserRequest:
properties:
+ email:
+ maxLength: 255
+ type: string
is_active:
type: boolean
is_admin:
@@ -1003,6 +1006,7 @@ definitions:
minLength: 3
type: string
required:
+ - email
- password
- username
type: object
diff --git a/frontend/components/common/admin/create-user-modal.tsx b/frontend/components/common/admin/create-user-modal.tsx
index 74e5cd4d..71da2a82 100644
--- a/frontend/components/common/admin/create-user-modal.tsx
+++ b/frontend/components/common/admin/create-user-modal.tsx
@@ -13,6 +13,7 @@ const emptyForm: CreateUserRequest = {
username: "",
password: "",
nickname: "",
+ email: "",
is_active: true,
is_admin: false,
}
@@ -46,6 +47,12 @@ export function CreateUserModal({
newErrors.username = "用户名长度不能少于 3 位"
}
+ if (!form.email.trim()) {
+ newErrors.email = "邮箱不能为空"
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
+ newErrors.email = "邮箱格式不正确"
+ }
+
if (!form.password) {
newErrors.password = "密码不能为空"
} else if (form.password.length < 8) {
@@ -66,6 +73,7 @@ export function CreateUserModal({
...form,
username: form.username.trim(),
nickname: form.nickname?.trim() || undefined,
+ email: form.email.trim(),
})
onClose()
} catch {
@@ -109,6 +117,20 @@ export function CreateUserModal({
/>
+
+
+
setForm((prev) => ({ ...prev, email: e.target.value }))}
+ placeholder="请输入邮箱地址"
+ />
+ {errors.email && (
+
{errors.email}
+ )}
+
+
0 {
+ c.JSON(http.StatusBadRequest, util.Err(emailExists))
+ return
+ }
+
newUser := model.User{
ID: idgen.NextUint64ID(),
Username: req.Username,
Nickname: req.Nickname,
+ Email: req.Email,
IsActive: req.IsActive,
IsAdmin: req.IsAdmin,
LastLoginAt: time.Time{},
diff --git a/internal/apps/admin/user/routers_test.go b/internal/apps/admin/user/routers_test.go
index 3590bd06..d499e9f9 100644
--- a/internal/apps/admin/user/routers_test.go
+++ b/internal/apps/admin/user/routers_test.go
@@ -313,6 +313,7 @@ func TestCreateUser(t *testing.T) {
Username: "newuser",
Password: "newpassword123",
Nickname: "New Nickname",
+ Email: "newuser@example.com",
IsActive: true,
IsAdmin: false,
}
@@ -350,6 +351,9 @@ func TestCreateUser(t *testing.T) {
if err := dbConn.Where("username = ?", "newuser").First(&dbUser).Error; err != nil {
t.Fatalf("failed to find user in db: %v", err)
}
+ if dbUser.Email != "newuser@example.com" {
+ t.Errorf("expected email 'newuser@example.com', got '%s'", dbUser.Email)
+ }
if !dbUser.CheckPassword("newpassword123") {
t.Error("password was not hashed correctly")
}
@@ -361,6 +365,7 @@ func TestCreateUser(t *testing.T) {
ID: 2001,
Username: "dupuser",
Nickname: "Dup User",
+ Email: "dupuser@example.com",
}
dbConn.Create(&existing)
@@ -368,6 +373,7 @@ func TestCreateUser(t *testing.T) {
Username: "dupuser",
Password: "password123",
Nickname: "Another Nick",
+ Email: "another@example.com",
IsActive: true,
}
body, _ := json.Marshal(payload)
@@ -387,10 +393,44 @@ func TestCreateUser(t *testing.T) {
}
})
+ t.Run("create user with duplicate email", func(t *testing.T) {
+ existing := model.User{
+ ID: 2002,
+ Username: "existingemail",
+ Nickname: "Existing Email",
+ Email: "dupemail@example.com",
+ }
+ dbConn.Create(&existing)
+
+ payload := createUserRequest{
+ Username: "newuser2",
+ Password: "password123",
+ Nickname: "New User 2",
+ Email: "dupemail@example.com",
+ IsActive: true,
+ }
+ body, _ := json.Marshal(payload)
+ req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
+ }
+
+ var resp util.ResponseAny
+ _ = json.Unmarshal(w.Body.Bytes(), &resp)
+ if resp.ErrorMsg != emailExists {
+ t.Errorf("expected error '%s', got '%s'", emailExists, resp.ErrorMsg)
+ }
+ })
+
t.Run("validation error - password too short", func(t *testing.T) {
payload := createUserRequest{
Username: "shortpass",
Password: "123",
+ Email: "shortpass@example.com",
IsActive: true,
}
body, _ := json.Marshal(payload)
@@ -403,6 +443,24 @@ func TestCreateUser(t *testing.T) {
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
}
})
+
+ t.Run("validation error - invalid email format", func(t *testing.T) {
+ payload := map[string]interface{}{
+ "username": "bademail",
+ "password": "password123",
+ "email": "not-an-email",
+ "is_active": true,
+ }
+ body, _ := json.Marshal(payload)
+ req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
+ }
+ })
}
func TestDeleteUser(t *testing.T) {