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:
2026-01-28 18:18:09 +08:00
parent 5d5a174dd7
commit 3a4aa9123c
17 changed files with 1309 additions and 22 deletions

View File

@@ -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
*

View File

@@ -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: '头像URLOSS地址' },
},
},
};
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);
/**
* 修改密码
*/

View File

@@ -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 头像URLOSS地址
*/
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 };
}
/**
* 发送验证码
*/