完成admin-user-update

This commit is contained in:
2025-05-12 11:40:21 +08:00
parent 2dd088fdf3
commit 4f782e4cea
9 changed files with 155 additions and 57 deletions

View File

@@ -1,23 +1,23 @@
import { IsOptional, IsString } from "class-validator"; import { IsString, Length, ValidateIf } from "class-validator";
export class CreateDto { export class CreateDto {
@IsOptional() @ValidateIf(o => o.username !== null)
@IsString() @IsString({ message: '用户名不得为空' })
username?: string; @Length(6, 32, { message: '用户名长度只能为6~32' })
username: string | null;
@IsOptional() @ValidateIf(o => o.username !== null)
@IsString() @IsString({ message: '昵称不得为空' })
nickname?: string; @Length(6, 30, { message: '昵称长度只能为6~30' })
nickname: string | null;
@IsOptional() @ValidateIf(o => o.username !== null)
@IsString() @IsString({ message: '邮箱不得为空' })
email?: string; @Length(6, 254, { message: '邮箱长度只能为6~254' })
email: string | null;
@IsOptional() @ValidateIf(o => o.username !== null)
@IsString() @IsString({ message: '手机号不得为空' })
phone?: string; @Length(11, 11, { message: '手机号长度只能为11' })
phone: string | null;
@IsOptional()
@IsString()
avatar?: string;
} }

View File

@@ -1,23 +1,29 @@
import { IsOptional, IsString } from "class-validator"; import { IsEmail, IsOptional, IsString, Length, Matches, ValidateIf } from "class-validator";
export class UpdateDto { export class UpdateDto {
@IsOptional() @IsString({ message: '用户名不得为空' })
@IsString() @Length(6, 32, { message: '用户名长度只能为6~32' })
username?: string; username: string;
@IsString({ message: '昵称不得为空' })
@Length(6, 30, { message: '昵称长度只能为6~30' })
nickname: string;
@IsOptional() @IsOptional()
@IsString() @IsEmail({}, { message: '请输入有效的邮箱地址', always: false })
nickname?: string; @Length(6, 254, {
message: '邮箱长度只能为6~254',
@IsOptional() // 仅在值不为 null 或 undefined 时验证
@IsString() always: false
})
email?: string; email?: string;
@IsOptional() @IsOptional() // 标记字段为可选
@IsString() @IsString({ message: '手机号不得为空', always: false })
@Matches(/^1[3456789]\d{9}$/, {
message: '请输入有效的手机号码',
// 仅在值不为 null 或 undefined 时验证
always: false
})
phone?: string; phone?: string;
@IsOptional()
@IsString()
avatar?: string;
} }

View File

@@ -2,16 +2,18 @@ import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@Entity() @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 { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column('uuid', { unique: true, default: () => 'gen_random_uuid()' }) @Column('uuid', { default: () => 'gen_random_uuid()' })
@Index({ unique: true })
userId: string; userId: string;
@Column({ length: 32 }) @Column({ length: 32 })
@Index({ unique: true })
username: string; username: string;
@Column({ length: 30 }) @Column({ length: 30 })
@@ -33,15 +35,33 @@ export class User {
@Column({ nullable: true, type: 'char', length: 64 }) @Column({ nullable: true, type: 'char', length: 64 })
password_hash: string; password_hash: string;
@Column({ nullable: true, length: 254 })// RFC 5321 @Column({
@Index({ unique: true }) nullable: true,
email: string; 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 @Column({
@Index({ unique: true }) nullable: true,
phone: string; 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; avatar: string;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })

View File

@@ -1,8 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity'; import { User } from './entities/user.entity';
import { Repository } from 'typeorm'; import { QueryFailedError, Repository } from 'typeorm';
import { createHash } from 'crypto'; import { createHash, ECDH } from 'crypto';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>; type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>;
@@ -31,8 +31,14 @@ export class UserService {
if (!existingUser) { if (!existingUser) {
throw new BadRequestException('User not found'); throw new BadRequestException('User not found');
} }
Object.assign(existingUser, user); try {
return this.userRepository.save(existingUser); 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<void> { async delete(userId: string): Promise<void> {
@@ -61,4 +67,18 @@ export class UserService {
user.salt = salt; user.salt = salt;
return this.userRepository.save(user); 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 '数据已存在,请检查输入';
}
} }

View File

@@ -17,15 +17,34 @@ import { Label } from "@/components/ui/label"
import { useUser } from "@/hooks/admin/user/use-user"; import { useUser } from "@/hooks/admin/user/use-user";
import { User } from "@/lib/types/user"; import { User } from "@/lib/types/user";
import { Skeleton } from "@/components/ui/skeleton"; 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({ export function UserInfoEditor({
onClose, onClose,
onUserUpdate,
userId, userId,
}: { }: {
onClose: () => void, onClose: () => void,
onUserUpdate: (user: User) => void,
userId: string userId: string
}) { }) {
const { user, isLoading, error } = userId ? useUser(userId) : {}; 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 ( return (
<Drawer open={!!userId} onClose={onClose} > <Drawer open={!!userId} onClose={onClose} >
@@ -36,7 +55,14 @@ export function UserInfoEditor({
</DrawerHeader> </DrawerHeader>
{user && <ProfileForm className="px-4" user={user} onSubmit={(e) => { {user && <ProfileForm className="px-4" user={user} onSubmit={(e) => {
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 && {isLoading &&
@@ -59,24 +85,24 @@ function ProfileForm({ className, user, ...props }: React.ComponentProps<"form">
return ( return (
<form className={cn("grid items-start gap-4", className)} {...props}> <form className={cn("grid items-start gap-4", className)} {...props}>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email">UserId</Label> <Label htmlFor="userId">UserId</Label>
<Input id="email" defaultValue={user.userId} disabled /> <Input id="userId" name="userId" defaultValue={user.userId} disabled />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="username"></Label> <Label htmlFor="username"></Label>
<Input id="username" defaultValue={user.username} /> <Input id="username" name="username" defaultValue={user.username} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="nickname"></Label> <Label htmlFor="nickname"></Label>
<Input id="nickname" defaultValue={user.nickname} /> <Input id="nickname" name="nickname" defaultValue={user.nickname} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email"></Label> <Label htmlFor="email"></Label>
<Input id="email" defaultValue={user.email} /> <Input id="email" name="email" defaultValue={user.email} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="phone"></Label> <Label htmlFor="phone"></Label>
<Input id="phone" defaultValue={user.phone} /> <Input id="phone" name="phone" defaultValue={user.phone} />
</div> </div>
<Button type="submit"></Button> <Button type="submit"></Button>
</form> </form>

View File

@@ -67,7 +67,12 @@ export default function Page() {
</TableBody> </TableBody>
</Table> </Table>
<UserInfoEditor onClose={() => setEditorUserId('')} userId={editorUserId} /> <UserInfoEditor onClose={() => setEditorUserId('')} userId={editorUserId} onUserUpdate={(user) => {
const index = users.findIndex((u) => u.userId === user.userId);
if (index !== -1) {
users[index] = user;
}
}} />
</> </>
) )
} }

View File

@@ -0,0 +1,3 @@
export function create() {
}

View File

@@ -1,2 +1,4 @@
export * from './list'; export * from './list';
export * from './get'; export * from './get';
export * from './create';
export * from './update';

View File

@@ -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<User>(`/api/admin/user/${userId}`, {
body: JSON.stringify(user),
method: "PUT",
});
}