From 859d628cd62e82a01883f45b6bfb279e02a8ada2 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 22 Dec 2025 15:57:32 +0800 Subject: [PATCH] admin: add create-user dialog and API --- backend_v1/app/http/admin/routes.gen.go | 5 +++ backend_v1/app/http/admin/users.go | 19 ++++++++ backend_v1/app/services/users.go | 50 +++++++++++++++++++++ frontend/admin/src/api/userService.js | 6 +++ frontend/admin/src/pages/UserPage.vue | 59 +++++++++++++++++++++++++ 5 files changed, 139 insertions(+) diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index da83f38..1266da8 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -173,6 +173,11 @@ func (r *Routes) Register(router fiber.Router) { r.users.List, Query[dto.UserListQuery]("query"), )) + r.log.Debugf("Registering route: Post /admin/v1/users -> users.Create") + router.Post("/admin/v1/users"[len(r.Path()):], DataFunc1( + r.users.Create, + Body[UserCreateForm]("form"), + )) r.log.Debugf("Registering route: Get /admin/v1/users/:id -> users.Show") router.Get("/admin/v1/users/:id"[len(r.Path()):], DataFunc1( r.users.Show, diff --git a/backend_v1/app/http/admin/users.go b/backend_v1/app/http/admin/users.go index cca5eed..e8db568 100644 --- a/backend_v1/app/http/admin/users.go +++ b/backend_v1/app/http/admin/users.go @@ -92,6 +92,11 @@ type UserPhoneForm struct { Phone string `json:"phone"` // 用户手机号(11 位数字) } +type UserCreateForm struct { + Phone string `json:"phone"` // 用户手机号(必填,11 位数字) + Username string `json:"username"` // 用户昵称(可选) +} + // Balance // // @Summary 调整用户余额 @@ -123,3 +128,17 @@ func (ctl *users) Balance(ctx fiber.Ctx, user *models.User, balance *UserBalance func (ctl *users) SetPhone(ctx fiber.Ctx, user *models.User, form *UserPhoneForm) error { return services.Users.SetPhone(ctx, user.ID, form.Phone) } + +// Create user +// +// @Summary 创建用户 +// @Tags Admin Users +// @Accept json +// @Produce json +// @Param form body UserCreateForm true "请求体" +// @Success 200 {object} models.User "成功" +// @Router /admin/v1/users [post] +// @Bind form body +func (ctl *users) Create(ctx fiber.Ctx, form *UserCreateForm) (*models.User, error) { + return services.Users.CreateByPhone(ctx, form.Phone, form.Username) +} diff --git a/backend_v1/app/services/users.go b/backend_v1/app/services/users.go index 0cbff36..c8f0faa 100644 --- a/backend_v1/app/services/users.go +++ b/backend_v1/app/services/users.go @@ -477,3 +477,53 @@ func (m *users) SetPhone(ctx context.Context, userID int64, phone string) error } return nil } + +// CreateByPhone 管理端通过手机号创建新用户(手机号必填,昵称可选)。 +func (m *users) CreateByPhone(ctx context.Context, phone, username string) (*models.User, error) { + phone = strings.TrimSpace(phone) + if phone == "" { + return nil, errors.New("手机号不能为空") + } + if len(phone) != 11 { + return nil, errors.New("手机号必须为 11 位数字") + } + for _, r := range phone { + if r < '0' || r > '9' { + return nil, errors.New("手机号必须为 11 位数字") + } + } + + _, err := m.FindByPhone(ctx, phone) + if err == nil { + return nil, errors.New("手机号已被其他用户占用") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrap(err, "failed to check phone uniqueness") + } + + openID := "phone:" + phone + tbl, query := models.UserQuery.QueryContext(ctx) + if _, err := query.Where(tbl.OpenID.Eq(openID)).First(); err == nil { + return nil, errors.New("用户已存在") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrap(err, "failed to check open_id uniqueness") + } + + username = strings.TrimSpace(username) + if username == "" { + username = "用户" + phone[len(phone)-4:] + } + + user := &models.User{ + OpenID: openID, + Username: username, + Phone: phone, + Balance: 0, + Avatar: "", + } + + if err := _db.WithContext(ctx).Omit("metas", "auth_token").Create(user).Error; err != nil { + return nil, errors.Wrap(err, "failed to create user") + } + return user, nil +} diff --git a/frontend/admin/src/api/userService.js b/frontend/admin/src/api/userService.js index e65af2c..37e95bf 100644 --- a/frontend/admin/src/api/userService.js +++ b/frontend/admin/src/api/userService.js @@ -11,6 +11,12 @@ export const userService = { } }); }, + createUser({ phone, username } = {}) { + return httpClient.post('/users', { + phone, + username + }); + }, searchUser(id) { return httpClient.get(`/users/${id}`); }, diff --git a/frontend/admin/src/pages/UserPage.vue b/frontend/admin/src/pages/UserPage.vue index c84cd55..1a4ceda 100644 --- a/frontend/admin/src/pages/UserPage.vue +++ b/frontend/admin/src/pages/UserPage.vue @@ -49,6 +49,11 @@ const phoneSaving = ref(false); const phoneTargetUser = ref(null); const phoneInput = ref(''); +const createDialogVisible = ref(false); +const createSaving = ref(false); +const createPhoneInput = ref(''); +const createUsernameInput = ref(''); + const articlesDialogVisible = ref(false); const articlesLoading = ref(false); const articlesUser = ref(null); @@ -130,6 +135,12 @@ const openPhoneDialog = (user) => { const normalizePhone = (v) => v.toString().replace(/\D/g, '').slice(0, 11); +const openCreateDialog = () => { + createPhoneInput.value = ''; + createUsernameInput.value = ''; + createDialogVisible.value = true; +}; + const formatMoney = (cents) => `¥${(cents / 100).toFixed(2)}`; const formatBoughtPrice = (priceCents) => { @@ -193,6 +204,32 @@ const savePhone = async () => { } }; +const createUser = async () => { + const phone = normalizePhone(createPhoneInput.value); + if (phone.length !== 11) { + toast.add({ severity: 'error', summary: '错误', detail: '手机号必须为 11 位数字', life: 3000 }); + return; + } + + createSaving.value = true; + try { + const username = createUsernameInput.value.trim(); + await userService.createUser({ + phone, + ...(username ? { username } : {}) + }); + toast.add({ severity: 'success', summary: '成功', detail: '用户已创建', life: 3000 }); + createDialogVisible.value = false; + first.value = 0; + await fetchUsers(); + } catch (error) { + console.error('Failed to create user:', error); + toast.add({ severity: 'error', summary: '错误', detail: error?.response?.data?.message || '创建用户失败', life: 3000 }); + } finally { + createSaving.value = false; + } +}; + onMounted(() => { fetchUsers(); }); @@ -270,9 +307,31 @@ const onOnlyBoughtChange = () => { + +
+
+ + +
+
+ + +
+
+ + +
+

用户列表

+