feat: Add Personal Center module and UI improvements
- Add ProfilePage with avatar upload, password change, and module status display - Update logo and favicon for login page and browser tab - Redirect Data Cleaning module default route to Tool C - Hide Settings button from top navigation for MVP - Add avatar display in top navigation bar with refresh sync - Add Prompt knowledge base integration development plan docs
This commit is contained in:
@@ -197,6 +197,142 @@ export async function changePassword(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新头像(URL方式)
|
||||
*
|
||||
* PUT /api/v1/auth/me/avatar
|
||||
*/
|
||||
export async function updateAvatar(
|
||||
request: FastifyRequest<{ Body: { avatarUrl: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
const { avatarUrl } = request.body;
|
||||
|
||||
if (!avatarUrl) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message: '请提供头像URL',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.updateAvatar(request.user.userId, avatarUrl);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '更新头像失败';
|
||||
logger.warn('更新头像失败', { error: message, userId: request.user?.userId });
|
||||
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像(文件上传方式)
|
||||
*
|
||||
* POST /api/v1/auth/me/avatar/upload
|
||||
*
|
||||
* 接收 multipart/form-data,上传到 staticStorage(公开访问)
|
||||
*/
|
||||
export async function uploadAvatar(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
const data = await request.file();
|
||||
|
||||
if (!data) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message: '请上传头像文件',
|
||||
});
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedMimeTypes.includes(data.mimetype)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message: '仅支持 JPG、PNG、GIF、WebP 格式的图片',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文件内容
|
||||
const buffer = await data.toBuffer();
|
||||
|
||||
// 检查文件大小(最大 5MB)
|
||||
if (buffer.length > 5 * 1024 * 1024) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message: '头像文件大小不能超过 5MB',
|
||||
});
|
||||
}
|
||||
|
||||
// 生成存储 Key
|
||||
const { randomUUID } = await import('crypto');
|
||||
const path = await import('path');
|
||||
const uuid = randomUUID().replace(/-/g, '').substring(0, 16);
|
||||
const ext = path.extname(data.filename).toLowerCase() || '.jpg';
|
||||
const storageKey = `avatars/${request.user.userId}/${uuid}${ext}`;
|
||||
|
||||
// 上传到 staticStorage(公开访问)
|
||||
const { staticStorage } = await import('../storage/index.js');
|
||||
const avatarUrl = await staticStorage.upload(storageKey, buffer);
|
||||
|
||||
// 更新数据库
|
||||
const result = await authService.updateAvatar(request.user.userId, avatarUrl);
|
||||
|
||||
logger.info('头像上传成功', {
|
||||
userId: request.user.userId,
|
||||
storageKey,
|
||||
avatarUrl,
|
||||
fileSize: buffer.length
|
||||
});
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '头像上传失败';
|
||||
logger.error('头像上传失败', { error: message, userId: request.user?.userId });
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: 'InternalServerError',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
getCurrentUser,
|
||||
getUserModules,
|
||||
changePassword,
|
||||
updateAvatar,
|
||||
uploadAvatar,
|
||||
refreshToken,
|
||||
logout,
|
||||
} from './auth.controller.js';
|
||||
@@ -65,6 +67,16 @@ const changePasswordSchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const updateAvatarSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['avatarUrl'],
|
||||
properties: {
|
||||
avatarUrl: { type: 'string', description: '头像URL(OSS地址)' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const refreshTokenSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -128,6 +140,22 @@ export async function authRoutes(
|
||||
preHandler: [authenticate],
|
||||
}, getUserModules);
|
||||
|
||||
/**
|
||||
* 更新头像(URL方式)
|
||||
*/
|
||||
fastify.put('/me/avatar', {
|
||||
preHandler: [authenticate],
|
||||
schema: updateAvatarSchema,
|
||||
}, updateAvatar as any);
|
||||
|
||||
/**
|
||||
* 上传头像(文件上传方式)
|
||||
* 接收 multipart/form-data
|
||||
*/
|
||||
fastify.post('/me/avatar/upload', {
|
||||
preHandler: [authenticate],
|
||||
}, uploadAvatar as any);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,13 @@ export interface UserInfoResponse {
|
||||
isDefaultPassword: boolean;
|
||||
permissions: string[];
|
||||
modules: string[]; // 用户可访问的模块代码列表
|
||||
// 2026-01-28: 个人中心扩展字段
|
||||
avatarUrl?: string | null;
|
||||
status?: string;
|
||||
kbQuota?: number;
|
||||
kbUsed?: number;
|
||||
isTrial?: boolean;
|
||||
trialEndsAt?: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +286,13 @@ export class AuthService {
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
modules, // 新增:返回模块列表
|
||||
// 2026-01-28: 个人中心扩展字段
|
||||
avatarUrl: user.avatarUrl,
|
||||
status: user.status,
|
||||
kbQuota: user.kbQuota,
|
||||
kbUsed: user.kbUsed,
|
||||
isTrial: user.isTrial,
|
||||
trialEndsAt: user.trialEndsAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,6 +344,30 @@ export class AuthService {
|
||||
logger.info('用户修改密码成功', { userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param avatarUrl 头像URL(OSS地址)
|
||||
*/
|
||||
async updateAvatar(userId: string, avatarUrl: string): Promise<{ avatarUrl: string }> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { avatarUrl },
|
||||
});
|
||||
|
||||
logger.info('用户更新头像成功', { userId, avatarUrl });
|
||||
return { avatarUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user