feat: 接入个人中心真实资料
This commit is contained in:
parent
395712e6e5
commit
79650de8b0
20
sql/admin-user-avatar.sql
Normal file
20
sql/admin-user-avatar.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- 为现有 Admin 用户表补充头像字段。
|
||||
-- 只补列,不回写数据,避免覆盖用户已上传头像。
|
||||
|
||||
SET @admin_user_avatar_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'admin_user'
|
||||
AND COLUMN_NAME = 'avatar'
|
||||
);
|
||||
|
||||
SET @admin_user_avatar_sql := IF(
|
||||
@admin_user_avatar_exists = 0,
|
||||
'ALTER TABLE `admin_user` ADD COLUMN `avatar` varchar(1024) NOT NULL DEFAULT '''' AFTER `real_name`',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE admin_user_avatar_stmt FROM @admin_user_avatar_sql;
|
||||
EXECUTE admin_user_avatar_stmt;
|
||||
DEALLOCATE PREPARE admin_user_avatar_stmt;
|
||||
@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `admin_user` (
|
||||
`username` varchar(255) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`real_name` varchar(255) NOT NULL,
|
||||
`avatar` varchar(1024) NOT NULL DEFAULT '',
|
||||
`dept_id` bigint DEFAULT NULL,
|
||||
`home_path` varchar(255) NOT NULL DEFAULT '',
|
||||
`timezone` varchar(255) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
@ -86,6 +87,22 @@ PREPARE admin_user_dept_id_stmt FROM @admin_user_dept_id_sql;
|
||||
EXECUTE admin_user_dept_id_stmt;
|
||||
DEALLOCATE PREPARE admin_user_dept_id_stmt;
|
||||
|
||||
SET @admin_user_avatar_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'admin_user'
|
||||
AND COLUMN_NAME = 'avatar'
|
||||
);
|
||||
SET @admin_user_avatar_sql := IF(
|
||||
@admin_user_avatar_exists = 0,
|
||||
'ALTER TABLE `admin_user` ADD COLUMN `avatar` varchar(1024) NOT NULL DEFAULT '''' AFTER `real_name`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE admin_user_avatar_stmt FROM @admin_user_avatar_sql;
|
||||
EXECUTE admin_user_avatar_stmt;
|
||||
DEALLOCATE PREPARE admin_user_avatar_stmt;
|
||||
|
||||
SET @admin_user_dept_idx_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
@ -229,11 +246,11 @@ ON DUPLICATE KEY UPDATE
|
||||
`status` = VALUES(`status`),
|
||||
`is_deleted` = 0;
|
||||
|
||||
INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `dept_id`, `home_path`, `timezone`, `status`)
|
||||
INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `avatar`, `dept_id`, `home_path`, `timezone`, `status`)
|
||||
VALUES
|
||||
(2041700000000000001, 'vben', '123456', 'Vben', 2041700000000200002, '/workspace', 'Asia/Shanghai', 1),
|
||||
(2041700000000000002, 'admin', '123456', 'Admin', 2041700000000200001, '/workspace', 'Asia/Shanghai', 1),
|
||||
(2041700000000000003, 'jack', '123456', 'Jack', 2041700000000200003, '/analytics', 'Asia/Shanghai', 1)
|
||||
(2041700000000000001, 'vben', '123456', 'Vben', '', 2041700000000200002, '/workspace', 'Asia/Shanghai', 1),
|
||||
(2041700000000000002, 'admin', '123456', 'Admin', '', 2041700000000200001, '/workspace', 'Asia/Shanghai', 1),
|
||||
(2041700000000000003, 'jack', '123456', 'Jack', '', 2041700000000200003, '/analytics', 'Asia/Shanghai', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`password` = VALUES(`password`),
|
||||
`real_name` = VALUES(`real_name`),
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentAdminUser, vbenSuccess } from '@/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
@ -16,4 +22,14 @@ export class AdminUserController {
|
||||
async info(@CurrentAdminUser() user: AdminUser) {
|
||||
return vbenSuccess(this.userService.serializeUser(user));
|
||||
}
|
||||
|
||||
@Put('profile')
|
||||
@ApiOperation({ summary: '更新当前用户基础资料' })
|
||||
async updateProfile(
|
||||
@CurrentAdminUser() user: AdminUser,
|
||||
@Body() body: Record<string, any>,
|
||||
) {
|
||||
const updated = await this.userService.updateCurrentProfile(user.id, body);
|
||||
return vbenSuccess(this.userService.serializeUser(updated));
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,12 @@ export class AdminUser {
|
||||
})
|
||||
realName: string;
|
||||
|
||||
@Column({
|
||||
default: '',
|
||||
length: 1024,
|
||||
})
|
||||
avatar: string;
|
||||
|
||||
@Column({
|
||||
name: 'dept_id',
|
||||
nullable: true,
|
||||
|
||||
@ -142,14 +142,36 @@ export class AdminUserService {
|
||||
return null;
|
||||
}
|
||||
|
||||
async updateCurrentProfile(userId: string, data: AdminUserInput) {
|
||||
const user = await this.findActiveUser(userId);
|
||||
|
||||
if (data.realName !== undefined) {
|
||||
const realName = String(data.realName || '').trim();
|
||||
if (!realName) throwVbenError('姓名不能为空', HttpStatus.BAD_REQUEST);
|
||||
user.realName = realName;
|
||||
}
|
||||
if (data.homePath !== undefined) {
|
||||
user.homePath = String(data.homePath || '').trim() || '/workspace';
|
||||
}
|
||||
if (data.avatar !== undefined) {
|
||||
user.avatar = String(data.avatar || '').trim();
|
||||
}
|
||||
|
||||
await this.userRepository.save(user);
|
||||
return this.findActiveUser(userId);
|
||||
}
|
||||
|
||||
serializeUser(user: AdminUser) {
|
||||
return {
|
||||
avatar: user.avatar || '',
|
||||
homePath: user.homePath,
|
||||
id: user.id,
|
||||
realName: user.realName,
|
||||
roles: (user.roles || [])
|
||||
.filter((role) => !role.isDeleted && role.status === 1)
|
||||
.map((role) => role.roleCode),
|
||||
timezone: user.timezone,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
}
|
||||
@ -168,6 +190,7 @@ export class AdminUserService {
|
||||
deptName: user.dept?.name || '',
|
||||
homePath: user.homePath,
|
||||
id: user.id,
|
||||
avatar: user.avatar,
|
||||
realName: user.realName,
|
||||
roleIds: activeRoles.map((role) => role.id),
|
||||
roleNames: activeRoles.map((role) => role.name),
|
||||
@ -219,6 +242,19 @@ export class AdminUserService {
|
||||
return Array.from(result);
|
||||
}
|
||||
|
||||
private async findActiveUser(id: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
relations: ['roles', 'dept'],
|
||||
where: {
|
||||
id,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throwVbenError('用户不存在', HttpStatus.BAD_REQUEST);
|
||||
return user;
|
||||
}
|
||||
|
||||
private async ensureUsernameAvailable(username: string, ignoreId?: string) {
|
||||
if (!username.trim()) {
|
||||
throwVbenError('用户名不能为空', HttpStatus.BAD_REQUEST);
|
||||
|
||||
@ -768,6 +768,7 @@ function adminUserExample() {
|
||||
id: '1000000000000000001',
|
||||
username: 'admin',
|
||||
realName: '管理员',
|
||||
avatar: '/api/minio/download?objectName=avatars/admin/avatar.jpg',
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
28
test/admin/admin-user-avatar-sql.spec.ts
Normal file
28
test/admin/admin-user-avatar-sql.spec.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('admin user avatar sql', () => {
|
||||
const avatarSql = readFileSync(
|
||||
join(process.cwd(), 'sql/admin-user-avatar.sql'),
|
||||
'utf8',
|
||||
);
|
||||
const initSql = readFileSync(
|
||||
join(process.cwd(), 'sql/vben-admin-init.sql'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
it('adds avatar column for existing databases', () => {
|
||||
expect(avatarSql).toContain("COLUMN_NAME = 'avatar'");
|
||||
expect(avatarSql).toContain('ALTER TABLE `admin_user` ADD COLUMN `avatar`');
|
||||
expect(avatarSql).toContain('varchar(1024)');
|
||||
});
|
||||
|
||||
it('keeps init sql aligned without resetting existing avatars', () => {
|
||||
expect(initSql).toContain('`avatar` varchar(1024) NOT NULL DEFAULT');
|
||||
expect(initSql).toContain("COLUMN_NAME = 'avatar'");
|
||||
expect(initSql).toContain(
|
||||
"INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `avatar`, `dept_id`, `home_path`, `timezone`, `status`)",
|
||||
);
|
||||
expect(initSql).not.toContain('`avatar` = VALUES(`avatar`)');
|
||||
});
|
||||
});
|
||||
86
test/admin/admin-user-profile.service.spec.ts
Normal file
86
test/admin/admin-user-profile.service.spec.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { AdminUserService } from '../../src/admin/user/admin-user.service';
|
||||
|
||||
describe('AdminUserService profile', () => {
|
||||
const userRepository = {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
const roleRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
const deptRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new AdminUserService(
|
||||
userRepository as any,
|
||||
roleRepository as any,
|
||||
deptRepository as any,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('updates current profile fields including uploaded avatar url', async () => {
|
||||
const user = {
|
||||
avatar: '',
|
||||
homePath: '/workspace',
|
||||
id: '2041700000000000001',
|
||||
realName: '旧姓名',
|
||||
roles: [],
|
||||
timezone: 'Asia/Shanghai',
|
||||
username: 'admin',
|
||||
};
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
|
||||
const result = await service.updateCurrentProfile(user.id, {
|
||||
avatar:
|
||||
'/api/minio/download?objectName=avatars%2F2041700000000000001%2Favatar.jpg',
|
||||
homePath: '/profile',
|
||||
realName: ' 新姓名 ',
|
||||
});
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
relations: ['roles', 'dept'],
|
||||
where: {
|
||||
id: user.id,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(userRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
avatar:
|
||||
'/api/minio/download?objectName=avatars%2F2041700000000000001%2Favatar.jpg',
|
||||
homePath: '/profile',
|
||||
realName: '新姓名',
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
|
||||
it('serializes avatar and userId for Admin frontend user store', () => {
|
||||
expect(
|
||||
service.serializeUser({
|
||||
avatar: '/api/minio/download?objectName=avatars%2Favatar.jpg',
|
||||
homePath: '/workspace',
|
||||
id: '2041700000000000001',
|
||||
realName: '管理员',
|
||||
roles: [],
|
||||
timezone: 'Asia/Shanghai',
|
||||
username: 'admin',
|
||||
} as any),
|
||||
).toEqual({
|
||||
avatar: '/api/minio/download?objectName=avatars%2Favatar.jpg',
|
||||
homePath: '/workspace',
|
||||
id: '2041700000000000001',
|
||||
realName: '管理员',
|
||||
roles: [],
|
||||
timezone: 'Asia/Shanghai',
|
||||
userId: '2041700000000000001',
|
||||
username: 'admin',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user