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,
|
`username` varchar(255) NOT NULL,
|
||||||
`password` varchar(255) NOT NULL,
|
`password` varchar(255) NOT NULL,
|
||||||
`real_name` varchar(255) NOT NULL,
|
`real_name` varchar(255) NOT NULL,
|
||||||
|
`avatar` varchar(1024) NOT NULL DEFAULT '',
|
||||||
`dept_id` bigint DEFAULT NULL,
|
`dept_id` bigint DEFAULT NULL,
|
||||||
`home_path` varchar(255) NOT NULL DEFAULT '',
|
`home_path` varchar(255) NOT NULL DEFAULT '',
|
||||||
`timezone` varchar(255) NOT NULL DEFAULT 'Asia/Shanghai',
|
`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;
|
EXECUTE admin_user_dept_id_stmt;
|
||||||
DEALLOCATE PREPARE 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 := (
|
SET @admin_user_dept_idx_exists := (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
@ -229,11 +246,11 @@ ON DUPLICATE KEY UPDATE
|
|||||||
`status` = VALUES(`status`),
|
`status` = VALUES(`status`),
|
||||||
`is_deleted` = 0;
|
`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
|
VALUES
|
||||||
(2041700000000000001, 'vben', '123456', 'Vben', 2041700000000200002, '/workspace', 'Asia/Shanghai', 1),
|
(2041700000000000001, 'vben', '123456', 'Vben', '', 2041700000000200002, '/workspace', 'Asia/Shanghai', 1),
|
||||||
(2041700000000000002, 'admin', '123456', 'Admin', 2041700000000200001, '/workspace', 'Asia/Shanghai', 1),
|
(2041700000000000002, 'admin', '123456', 'Admin', '', 2041700000000200001, '/workspace', 'Asia/Shanghai', 1),
|
||||||
(2041700000000000003, 'jack', '123456', 'Jack', 2041700000000200003, '/analytics', 'Asia/Shanghai', 1)
|
(2041700000000000003, 'jack', '123456', 'Jack', '', 2041700000000200003, '/analytics', 'Asia/Shanghai', 1)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`password` = VALUES(`password`),
|
`password` = VALUES(`password`),
|
||||||
`real_name` = VALUES(`real_name`),
|
`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 { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { CurrentAdminUser, vbenSuccess } from '@/common';
|
import { CurrentAdminUser, vbenSuccess } from '@/common';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
@ -16,4 +22,14 @@ export class AdminUserController {
|
|||||||
async info(@CurrentAdminUser() user: AdminUser) {
|
async info(@CurrentAdminUser() user: AdminUser) {
|
||||||
return vbenSuccess(this.userService.serializeUser(user));
|
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;
|
realName: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
default: '',
|
||||||
|
length: 1024,
|
||||||
|
})
|
||||||
|
avatar: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
name: 'dept_id',
|
name: 'dept_id',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|||||||
@ -142,14 +142,36 @@ export class AdminUserService {
|
|||||||
return null;
|
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) {
|
serializeUser(user: AdminUser) {
|
||||||
return {
|
return {
|
||||||
|
avatar: user.avatar || '',
|
||||||
homePath: user.homePath,
|
homePath: user.homePath,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
roles: (user.roles || [])
|
roles: (user.roles || [])
|
||||||
.filter((role) => !role.isDeleted && role.status === 1)
|
.filter((role) => !role.isDeleted && role.status === 1)
|
||||||
.map((role) => role.roleCode),
|
.map((role) => role.roleCode),
|
||||||
|
timezone: user.timezone,
|
||||||
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -168,6 +190,7 @@ export class AdminUserService {
|
|||||||
deptName: user.dept?.name || '',
|
deptName: user.dept?.name || '',
|
||||||
homePath: user.homePath,
|
homePath: user.homePath,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
avatar: user.avatar,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
roleIds: activeRoles.map((role) => role.id),
|
roleIds: activeRoles.map((role) => role.id),
|
||||||
roleNames: activeRoles.map((role) => role.name),
|
roleNames: activeRoles.map((role) => role.name),
|
||||||
@ -219,6 +242,19 @@ export class AdminUserService {
|
|||||||
return Array.from(result);
|
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) {
|
private async ensureUsernameAvailable(username: string, ignoreId?: string) {
|
||||||
if (!username.trim()) {
|
if (!username.trim()) {
|
||||||
throwVbenError('用户名不能为空', HttpStatus.BAD_REQUEST);
|
throwVbenError('用户名不能为空', HttpStatus.BAD_REQUEST);
|
||||||
|
|||||||
@ -768,6 +768,7 @@ function adminUserExample() {
|
|||||||
id: '1000000000000000001',
|
id: '1000000000000000001',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
realName: '管理员',
|
realName: '管理员',
|
||||||
|
avatar: '/api/minio/download?objectName=avatars/admin/avatar.jpg',
|
||||||
status: 1,
|
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