feat: support set user phone
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
<Dialog v-model:visible="phoneDialogVisible" modal header="设置手机号" :style="{ width: '420px' }">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm text-gray-600" v-if="phoneTargetUser">
|
||||
用户:{{ phoneTargetUser.username }}(ID: {{ phoneTargetUser.id }})
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||||
<InputText v-model="phoneInput" class="w-full" placeholder="请输入 11 位手机号" inputmode="numeric"
|
||||
maxlength="11" @input="(e) => phoneInput = normalizePhone(e.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button label="取消" text @click="phoneDialogVisible = false" />
|
||||
<Button label="保存" severity="success" :loading="phoneSaving" @click="savePhone" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
@@ -164,7 +219,7 @@ onMounted(() => {
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="open_id" header="OpenID" sortable></Column>
|
||||
<Column field="phone" header="Phone" sortable></Column>
|
||||
|
||||
<Column field="status" header="状态" sortable>
|
||||
<template #body="{ data }">
|
||||
@@ -187,6 +242,8 @@ onMounted(() => {
|
||||
<div class="flex justify-center space-x-2">
|
||||
<Button icon="pi pi-credit-card" rounded text severity="success"
|
||||
@click="handleRecharge(data)" label="充值" />
|
||||
<Button icon="pi pi-phone" rounded text severity="info" @click="openPhoneDialog(data)"
|
||||
label="设置手机号" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
Reference in New Issue
Block a user