admin: add create-user dialog and API
Some checks failed
build quyun / Build (push) Failing after 1m23s
Some checks failed
build quyun / Build (push) Failing after 1m23s
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ export const userService = {
|
||||
}
|
||||
});
|
||||
},
|
||||
createUser({ phone, username } = {}) {
|
||||
return httpClient.post('/users', {
|
||||
phone,
|
||||
username
|
||||
});
|
||||
},
|
||||
searchUser(id) {
|
||||
return httpClient.get(`/users/${id}`);
|
||||
},
|
||||
|
||||
@@ -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 = () => {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="createDialogVisible" modal header="添加用户" :style="{ width: '420px' }">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">手机号(必填)</label>
|
||||
<InputText v-model="createPhoneInput" class="w-full" placeholder="请输入 11 位手机号" inputmode="numeric"
|
||||
maxlength="11" @input="(e) => createPhoneInput = normalizePhone(e.target.value)" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">用户昵称(可选)</label>
|
||||
<InputText v-model="createUsernameInput" class="w-full" placeholder="请输入用户昵称" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button label="取消" text @click="createDialogVisible = false" />
|
||||
<Button label="创建" severity="success" :loading="createSaving" @click="createUser" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-800">用户列表</h1>
|
||||
<Button label="添加用户" icon="pi pi-plus" severity="success" @click="openCreateDialog" />
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user