From d8fd52d73e45b2070bc1ad015ec6336e41659b41 Mon Sep 17 00:00:00 2001 From: tone <3341154833@qq.com> Date: Mon, 12 May 2025 11:40:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90admin-user-update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/admin/dto/admin-user/create.dto.ts | 34 +++++++-------- .../src/admin/dto/admin-user/update.dto.ts | 36 +++++++++------- .../src/user/entities/user.entity.ts | 40 +++++++++++++----- tone-page-server/src/user/user.service.ts | 30 ++++++++++--- .../user/components/user-info-editor.tsx | 42 +++++++++++++++---- .../app/console/(with-menu)/user/page.tsx | 7 +++- tone-page-web/lib/api/admin/user/create.ts | 3 ++ tone-page-web/lib/api/admin/user/index.ts | 4 +- tone-page-web/lib/api/admin/user/update.ts | 16 +++++++ 9 files changed, 155 insertions(+), 57 deletions(-) create mode 100644 tone-page-web/lib/api/admin/user/create.ts create mode 100644 tone-page-web/lib/api/admin/user/update.ts diff --git a/tone-page-server/src/admin/dto/admin-user/create.dto.ts b/tone-page-server/src/admin/dto/admin-user/create.dto.ts index 94a0b10..30f0e87 100644 --- a/tone-page-server/src/admin/dto/admin-user/create.dto.ts +++ b/tone-page-server/src/admin/dto/admin-user/create.dto.ts @@ -1,23 +1,23 @@ -import { IsOptional, IsString } from "class-validator"; +import { IsString, Length, ValidateIf } from "class-validator"; export class CreateDto { - @IsOptional() - @IsString() - username?: string; + @ValidateIf(o => o.username !== null) + @IsString({ message: '用户名不得为空' }) + @Length(6, 32, { message: '用户名长度只能为6~32' }) + username: string | null; - @IsOptional() - @IsString() - nickname?: string; + @ValidateIf(o => o.username !== null) + @IsString({ message: '昵称不得为空' }) + @Length(6, 30, { message: '昵称长度只能为6~30' }) + nickname: string | null; - @IsOptional() - @IsString() - email?: string; + @ValidateIf(o => o.username !== null) + @IsString({ message: '邮箱不得为空' }) + @Length(6, 254, { message: '邮箱长度只能为6~254' }) + email: string | null; - @IsOptional() - @IsString() - phone?: string; - - @IsOptional() - @IsString() - avatar?: string; + @ValidateIf(o => o.username !== null) + @IsString({ message: '手机号不得为空' }) + @Length(11, 11, { message: '手机号长度只能为11' }) + phone: string | null; } \ No newline at end of file diff --git a/tone-page-server/src/admin/dto/admin-user/update.dto.ts b/tone-page-server/src/admin/dto/admin-user/update.dto.ts index 849ceaa..994b9f4 100644 --- a/tone-page-server/src/admin/dto/admin-user/update.dto.ts +++ b/tone-page-server/src/admin/dto/admin-user/update.dto.ts @@ -1,23 +1,29 @@ -import { IsOptional, IsString } from "class-validator"; +import { IsEmail, IsOptional, IsString, Length, Matches, ValidateIf } from "class-validator"; export class UpdateDto { - @IsOptional() - @IsString() - username?: string; + @IsString({ message: '用户名不得为空' }) + @Length(6, 32, { message: '用户名长度只能为6~32' }) + username: string; + + @IsString({ message: '昵称不得为空' }) + @Length(6, 30, { message: '昵称长度只能为6~30' }) + nickname: string; @IsOptional() - @IsString() - nickname?: string; - - @IsOptional() - @IsString() + @IsEmail({}, { message: '请输入有效的邮箱地址', always: false }) + @Length(6, 254, { + message: '邮箱长度只能为6~254', + // 仅在值不为 null 或 undefined 时验证 + always: false + }) email?: string; - @IsOptional() - @IsString() + @IsOptional() // 标记字段为可选 + @IsString({ message: '手机号不得为空', always: false }) + @Matches(/^1[3456789]\d{9}$/, { + message: '请输入有效的手机号码', + // 仅在值不为 null 或 undefined 时验证 + always: false + }) phone?: string; - - @IsOptional() - @IsString() - avatar?: string; } \ No newline at end of file diff --git a/tone-page-server/src/user/entities/user.entity.ts b/tone-page-server/src/user/entities/user.entity.ts index 0cd0b92..f7eebe2 100644 --- a/tone-page-server/src/user/entities/user.entity.ts +++ b/tone-page-server/src/user/entities/user.entity.ts @@ -2,16 +2,18 @@ import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index import { v4 as uuidv4 } from 'uuid'; @Entity() +@Index("IDX_user_userid", ["userId"], { unique: true }) +@Index("IDX_user_username", ["username"], { unique: true }) +@Index("IDX_user_email", ["email"], { unique: true, where: "email IS NOT NULL" }) +@Index("IDX_user_phone", ["phone"], { unique: true, where: "phone IS NOT NULL" }) export class User { @PrimaryGeneratedColumn() id: number; - @Column('uuid', { unique: true, default: () => 'gen_random_uuid()' }) - @Index({ unique: true }) + @Column('uuid', { default: () => 'gen_random_uuid()' }) userId: string; @Column({ length: 32 }) - @Index({ unique: true }) username: string; @Column({ length: 30 }) @@ -33,15 +35,33 @@ export class User { @Column({ nullable: true, type: 'char', length: 64 }) password_hash: string; - @Column({ nullable: true, length: 254 })// RFC 5321 - @Index({ unique: true }) - email: string; + @Column({ + nullable: true, + length: 254, + transformer: { + to: (value: string | null) => value?.trim() || null, + from: (value: string | null) => value, + } + })// RFC 5321 + email: string | null; - @Column({ nullable: true, length: 20 })// China Mainland - @Index({ unique: true }) - phone: string; + @Column({ + nullable: true, + length: 20, + transformer: { + to: (value: string | null) => value?.trim() || null, + from: (value: string | null) => value, + } + })// China Mainland + phone: string | null; - @Column({ nullable: true }) + @Column({ + nullable: true, + transformer: { + to: (value: string | null) => value?.trim() || null, + from: (value: string | null) => value, + } + }) avatar: string; @CreateDateColumn({ precision: 3 }) diff --git a/tone-page-server/src/user/user.service.ts b/tone-page-server/src/user/user.service.ts index 0bf6358..15ee244 100644 --- a/tone-page-server/src/user/user.service.ts +++ b/tone-page-server/src/user/user.service.ts @@ -1,8 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; -import { Repository } from 'typeorm'; -import { createHash } from 'crypto'; +import { QueryFailedError, Repository } from 'typeorm'; +import { createHash, ECDH } from 'crypto'; import { v4 as uuid } from 'uuid'; type UserFindOptions = Partial>; @@ -31,8 +31,14 @@ export class UserService { if (!existingUser) { throw new BadRequestException('User not found'); } - Object.assign(existingUser, user); - return this.userRepository.save(existingUser); + try { + Object.assign(existingUser, user); + return await this.userRepository.save(existingUser); + } catch (error) { + if (error instanceof QueryFailedError) { + throw new ConflictException(this.getDuplicateErrorMessage(error)); + } + } } async delete(userId: string): Promise { @@ -61,4 +67,18 @@ export class UserService { user.salt = salt; return this.userRepository.save(user); } + + private getDuplicateErrorMessage(error: QueryFailedError): string { + // 根据具体的错误信息返回友好的提示 + if (error.message.includes('IDX_user_username')) { + return '用户名已被使用'; + } + if (error.message.includes('IDX_user_email')) { + return '邮箱已被使用'; + } + if (error.message.includes('IDX_user_phone')) { + return '手机号已被使用'; + } + return '数据已存在,请检查输入'; + } } diff --git a/tone-page-web/app/console/(with-menu)/user/components/user-info-editor.tsx b/tone-page-web/app/console/(with-menu)/user/components/user-info-editor.tsx index 46de5fb..112c3e4 100644 --- a/tone-page-web/app/console/(with-menu)/user/components/user-info-editor.tsx +++ b/tone-page-web/app/console/(with-menu)/user/components/user-info-editor.tsx @@ -17,15 +17,34 @@ import { Label } from "@/components/ui/label" import { useUser } from "@/hooks/admin/user/use-user"; import { User } from "@/lib/types/user"; import { Skeleton } from "@/components/ui/skeleton"; +import { updateUser } from "@/lib/api/admin/user"; +import { AdminApi } from "@/lib/api"; +import { toast } from "sonner"; export function UserInfoEditor({ onClose, + onUserUpdate, userId, }: { onClose: () => void, + onUserUpdate: (user: User) => void, userId: string }) { const { user, isLoading, error } = userId ? useUser(userId) : {}; + const handleSave = async (user: updateUser) => { + try { + const res = await AdminApi.user.update(userId, user); + if (res) { + toast.success("保存成功"); + onUserUpdate(res); + onClose(); + } else { + throw new Error(); + } + } catch (error) { + toast.error((error as Error).message || "保存失败"); + } + } return ( @@ -36,7 +55,14 @@ export function UserInfoEditor({ {user && { - e.preventDefault(); + e.preventDefault() + const formData = new FormData(e.currentTarget); + handleSave({ + username: formData.get("username")?.toString()!, + nickname: formData.get("nickname")?.toString()!, + email: formData.get("email")?.toString() || null, + phone: formData.get("phone")?.toString() || null, + }) }} />} {isLoading && @@ -44,7 +70,7 @@ export function UserInfoEditor({ )) } - + @@ -59,24 +85,24 @@ function ProfileForm({ className, user, ...props }: React.ComponentProps<"form"> return (
- - + +
- +
- +
- +
- +
diff --git a/tone-page-web/app/console/(with-menu)/user/page.tsx b/tone-page-web/app/console/(with-menu)/user/page.tsx index cc5f114..d901b55 100644 --- a/tone-page-web/app/console/(with-menu)/user/page.tsx +++ b/tone-page-web/app/console/(with-menu)/user/page.tsx @@ -67,7 +67,12 @@ export default function Page() { - setEditorUserId('')} userId={editorUserId} /> + setEditorUserId('')} userId={editorUserId} onUserUpdate={(user) => { + const index = users.findIndex((u) => u.userId === user.userId); + if (index !== -1) { + users[index] = user; + } + }} /> ) } \ No newline at end of file diff --git a/tone-page-web/lib/api/admin/user/create.ts b/tone-page-web/lib/api/admin/user/create.ts new file mode 100644 index 0000000..07e5411 --- /dev/null +++ b/tone-page-web/lib/api/admin/user/create.ts @@ -0,0 +1,3 @@ +export function create() { + +} \ No newline at end of file diff --git a/tone-page-web/lib/api/admin/user/index.ts b/tone-page-web/lib/api/admin/user/index.ts index 03ea753..97ae510 100644 --- a/tone-page-web/lib/api/admin/user/index.ts +++ b/tone-page-web/lib/api/admin/user/index.ts @@ -1,2 +1,4 @@ export * from './list'; -export * from './get'; \ No newline at end of file +export * from './get'; +export * from './create'; +export * from './update'; \ No newline at end of file diff --git a/tone-page-web/lib/api/admin/user/update.ts b/tone-page-web/lib/api/admin/user/update.ts new file mode 100644 index 0000000..51ade8f --- /dev/null +++ b/tone-page-web/lib/api/admin/user/update.ts @@ -0,0 +1,16 @@ +import { User } from "@/lib/types/user"; +import fetcher from "../../fetcher"; + +export type updateUser = { + username: string; + nickname: string; + email: string | null; + phone: string | null; +} + +export async function update(userId: string, user: updateUser) { + return fetcher(`/api/admin/user/${userId}`, { + body: JSON.stringify(user), + method: "PUT", + }); +} \ No newline at end of file