From 0185e51396f73f591b5a6f4d1c26ea58b8ef18d1 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 20 Dec 2025 14:27:29 +0800 Subject: [PATCH] feat: support set user phone --- backend_v1/app/http/admin/routes.gen.go | 9 ++++ backend_v1/app/http/admin/users.go | 27 ++++++++++- backend_v1/app/services/users.go | 39 +++++++++++++++- frontend/admin/src/api/userService.js | 7 ++- frontend/admin/src/pages/UserPage.vue | 59 ++++++++++++++++++++++++- 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index 227ec69..60da0a2 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -193,6 +193,15 @@ func (r *Routes) Register(router fiber.Router) { }, Body[UserBalance]("balance"), )) + r.log.Debugf("Registering route: Post /admin/v1/users/:id/phone -> users.SetPhone") + router.Post("/admin/v1/users/:id/phone"[len(r.Path()):], Func2( + r.users.SetPhone, + func(ctx fiber.Ctx) (*models.User, error) { + v := fiber.Params[int](ctx, "id") + return models.UserQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("id = ?", v)).First() + }, + Body[UserPhoneForm]("form"), + )) r.log.Info("Successfully registered all routes") } diff --git a/backend_v1/app/http/admin/users.go b/backend_v1/app/http/admin/users.go index 665d65a..345518c 100644 --- a/backend_v1/app/http/admin/users.go +++ b/backend_v1/app/http/admin/users.go @@ -29,8 +29,11 @@ type users struct{} // @Bind pagination query // @Bind query query func (ctl *users) List(ctx fiber.Ctx, pagination *requests.Pagination, query *UserListQuery) (*requests.Pager, error) { - conds := []gen.Condition{ - models.UserQuery.Username.Like(database.WrapLike(*query.Keyword)), + conds := []gen.Condition{} + if query.Keyword != nil && *query.Keyword != "" { + conds = append(conds, + models.UserQuery.Username.Like(database.WrapLike(*query.Keyword)), + ) } return services.Users.List(ctx, pagination, conds...) } @@ -67,6 +70,10 @@ type UserBalance struct { Balance int64 `json:"balance"` } +type UserPhoneForm struct { + Phone string `json:"phone"` // 用户手机号(11 位数字) +} + // Balance // // @Summary 调整用户余额 @@ -82,3 +89,19 @@ type UserBalance struct { func (ctl *users) Balance(ctx fiber.Ctx, user *models.User, balance *UserBalance) error { return services.Users.AddBalance(ctx, user.ID, balance.Balance) } + +// SetPhone +// +// @Summary 设置用户手机号 +// @Tags Admin Users +// @Accept json +// @Produce json +// @Param id path int64 true "用户 ID" +// @Param form body UserPhoneForm true "请求体" +// @Success 200 {object} any "成功" +// @Router /admin/v1/users/:id/phone [post] +// @Bind user path key(id) model(id) +// @Bind form body +func (ctl *users) SetPhone(ctx fiber.Ctx, user *models.User, form *UserPhoneForm) error { + return services.Users.SetPhone(ctx, user.ID, form.Phone) +} diff --git a/backend_v1/app/services/users.go b/backend_v1/app/services/users.go index ad673b6..d854230 100644 --- a/backend_v1/app/services/users.go +++ b/backend_v1/app/services/users.go @@ -42,9 +42,12 @@ func (m *users) List( conds ...gen.Condition, ) (*requests.Pager, error) { pagination.Format() - _, query := models.UserQuery.QueryContext(ctx) + tbl, query := models.UserQuery.QueryContext(ctx) - items, cnt, err := query.Where(conds...).FindByPage(int(pagination.Offset()), int(pagination.Limit)) + items, cnt, err := query. + Where(conds...). + Order(tbl.ID.Desc()). + FindByPage(int(pagination.Offset()), int(pagination.Limit)) if err != nil { return nil, errors.Wrap(err, "query users error") } @@ -361,3 +364,35 @@ func (m *users) ValidatePhoneCode(ctx context.Context, phone, code string) (*mod return user, nil } + +// SetPhone 管理端设置用户手机号。 +func (m *users) SetPhone(ctx context.Context, userID int64, phone string) error { + phone = strings.TrimSpace(phone) + if phone == "" { + return errors.New("手机号不能为空") + } + if len(phone) != 11 { + return errors.New("手机号必须为 11 位数字") + } + for _, r := range phone { + if r < '0' || r > '9' { + return errors.New("手机号必须为 11 位数字") + } + } + + // 业务约束:手机号建议全局唯一(至少在本系统内),避免登录/验证身份混淆。 + tbl, query := models.UserQuery.QueryContext(ctx) + _, err := query.Where(tbl.Phone.Eq(phone), tbl.ID.Neq(userID)).First() + if err == nil { + return errors.New("手机号已被其他用户占用") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "failed to check phone uniqueness") + } + + // 仅更新 phone 字段,避免覆盖其它字段。 + if _, err := query.Where(tbl.ID.Eq(userID)).Update(tbl.Phone, phone); err != nil { + return errors.Wrap(err, "failed to update user phone") + } + return nil +} diff --git a/frontend/admin/src/api/userService.js b/frontend/admin/src/api/userService.js index 8f84817..157962c 100644 --- a/frontend/admin/src/api/userService.js +++ b/frontend/admin/src/api/userService.js @@ -18,6 +18,11 @@ export const userService = { balance }); }, + setPhone(id, phone) { + return httpClient.post(`/users/${id}/phone`, { + phone + }); + }, getUser(id) { return httpClient.get(`/users/${id}`); }, @@ -35,4 +40,4 @@ export const userService = { } }); } -} \ No newline at end of file +} diff --git a/frontend/admin/src/pages/UserPage.vue b/frontend/admin/src/pages/UserPage.vue index cd325d6..4dd0216 100644 --- a/frontend/admin/src/pages/UserPage.vue +++ b/frontend/admin/src/pages/UserPage.vue @@ -6,6 +6,7 @@ import Button from 'primevue/button'; import Column from 'primevue/column'; import ConfirmDialog from 'primevue/confirmdialog'; import DataTable from 'primevue/datatable'; +import Dialog from 'primevue/dialog'; import InputText from 'primevue/inputtext'; import ProgressSpinner from 'primevue/progressspinner'; import Toast from 'primevue/toast'; @@ -36,6 +37,11 @@ const users = ref({ const first = ref(0); const rows = ref(10); +const phoneDialogVisible = ref(false); +const phoneSaving = ref(false); +const phoneTargetUser = ref(null); +const phoneInput = ref(''); + const fetchUsers = async () => { loading.value = true; try { @@ -100,6 +106,36 @@ const handleRecharge = (user) => { }); }; +const openPhoneDialog = (user) => { + phoneTargetUser.value = user; + phoneInput.value = (user?.phone || '').toString(); + phoneDialogVisible.value = true; +}; + +const normalizePhone = (v) => v.toString().replace(/\D/g, '').slice(0, 11); + +const savePhone = async () => { + if (!phoneTargetUser.value) return; + const phone = normalizePhone(phoneInput.value); + if (phone.length !== 11) { + toast.add({ severity: 'error', summary: '错误', detail: '手机号必须为 11 位数字', life: 3000 }); + return; + } + + phoneSaving.value = true; + try { + await userService.setPhone(phoneTargetUser.value.id, phone); + toast.add({ severity: 'success', summary: '成功', detail: '手机号已更新', life: 3000 }); + phoneDialogVisible.value = false; + await fetchUsers(); + } catch (error) { + console.error('Failed to set phone:', error); + toast.add({ severity: 'error', summary: '错误', detail: error?.response?.data?.message || '设置手机号失败', life: 3000 }); + } finally { + phoneSaving.value = false; + } +}; + onMounted(() => { fetchUsers(); }); @@ -108,6 +144,25 @@ onMounted(() => { - +