Merge pull request '移除删除账号功能' (#15)

This commit is contained in:
2026-07-16 02:55:22 +00:00
7 changed files with 66 additions and 87 deletions

View File

@@ -154,16 +154,6 @@ const ClassesPage: React.FC = () => {
} }
}; };
const handleDelete = async (id: number) => {
try {
await api.delete(`/classes/${id}`);
message.success('已删除');
fetchData();
} catch (e: any) {
message.error(e?.message || '删除失败');
}
};
const columns: ColumnsType<ClassItem> = useMemo(() => [ const columns: ColumnsType<ClassItem> = useMemo(() => [
{ {
title: '班级名称', dataIndex: 'name', width: 120, title: '班级名称', dataIndex: 'name', width: 120,
@@ -208,11 +198,6 @@ const ClassesPage: React.FC = () => {
<PermissionButton permission="class:edit" size="small"></PermissionButton> <PermissionButton permission="class:edit" size="small"></PermissionButton>
</Popconfirm> </Popconfirm>
)} )}
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(r.id)}>
<PermissionButton permission="class:delete" size="small" danger>
</PermissionButton>
</Popconfirm>
</Space> </Space>
), ),
}, },

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Alert, Empty, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag } from 'antd'; import { Alert, Empty, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag } from 'antd';
import { BankOutlined, InboxOutlined, PlusOutlined } from '@ant-design/icons'; import { BankOutlined, InboxOutlined, PlusOutlined, UndoOutlined } from '@ant-design/icons';
import api from '../../api'; import api from '../../api';
import PermissionButton from '../../components/PermissionButton'; import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message'; import { message } from '../../ui/app-message';
@@ -148,35 +148,61 @@ const OrganizationsPage: React.FC = () => {
width: 160, width: 160,
render: (_: unknown, record: OrganizationItem) => ( render: (_: unknown, record: OrganizationItem) => (
<Space> <Space>
<PermissionButton {record.status === 'archived' ? (
permission="organization:edit"
size="small"
onClick={() => openEditor(record)}
>
</PermissionButton>
{!record.isHost && record.status === 'active' ? (
<Popconfirm <Popconfirm
title="归档后仍保留历史学生、入住和租赁记录" title="确定恢复此机构?"
onConfirm={async () => { onConfirm={async () => {
try { try {
await api.delete(`/organizations/${record.id}`); await api.put(`/organizations/${record.id}`, { status: 'active' });
message.success('机构已归档'); message.success('机构已恢复');
await fetchData(); await fetchData();
} catch (error: any) { } catch (error: any) {
message.error(error?.message || '归档失败'); message.error(error?.message || '恢复失败');
} }
}} }}
> >
<PermissionButton <PermissionButton
permission="organization:delete" permission="organization:edit"
size="small" size="small"
icon={<InboxOutlined />} type="link"
icon={<UndoOutlined />}
> >
</PermissionButton> </PermissionButton>
</Popconfirm> </Popconfirm>
) : null} ) : (
<>
<PermissionButton
permission="organization:edit"
size="small"
onClick={() => openEditor(record)}
>
</PermissionButton>
{!record.isHost ? (
<Popconfirm
title="归档后仍保留历史学生、入住和租赁记录"
onConfirm={async () => {
try {
await api.delete(`/organizations/${record.id}`);
message.success('机构已归档');
await fetchData();
} catch (error: any) {
message.error(error?.message || '归档失败');
}
}}
>
<PermissionButton
permission="organization:delete"
size="small"
icon={<InboxOutlined />}
>
</PermissionButton>
</Popconfirm>
) : null}
</>
)}
</Space> </Space>
), ),
}, },

View File

@@ -10,7 +10,7 @@ import {
Tag, Tag,
Popconfirm, Popconfirm,
} from 'antd'; } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, IdcardOutlined, InboxOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, KeyOutlined, IdcardOutlined, InboxOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import api from '../../api'; import api from '../../api';
import PermissionButton from '../../components/PermissionButton'; import PermissionButton from '../../components/PermissionButton';
@@ -141,18 +141,6 @@ const UsersPage: React.FC = () => {
} }
}; };
const handleDelete = async (id: number) => {
try {
await api.delete(`/rbac/users/${id}`);
message.success('已删除');
fetchData();
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '删除失败');
}
};
const handleResetPwd = (record: any) => { const handleResetPwd = (record: any) => {
setResetTarget(record); setResetTarget(record);
pwdForm.resetFields(); pwdForm.resetFields();
@@ -253,13 +241,6 @@ const UsersPage: React.FC = () => {
<PermissionButton permission="user:edit" type="link" size="small"></PermissionButton> <PermissionButton permission="user:edit" type="link" size="small"></PermissionButton>
</Popconfirm> </Popconfirm>
)} )}
{record.username !== 'admin' && (
<Popconfirm title="确认删除?需先归档" onConfirm={() => handleDelete(record.id)}>
<PermissionButton permission="user:delete" type="link" size="small" danger icon={<DeleteOutlined />}>
</PermissionButton>
</Popconfirm>
)}
</Space> </Space>
), ),
}, },

View File

@@ -220,6 +220,6 @@ export const PERMISSION_NODES = [
'report:generate', 'report:generate',
'log:view', 'log:view',
'role:view', 'role:add', 'role:update', 'role:delete', 'role:view', 'role:add', 'role:update', 'role:delete',
'user:view', 'user:add', 'user:update', 'user:delete', 'user:view', 'user:add', 'user:update',
'dashboard:view', 'dashboard:view',
] as const; ] as const;

View File

@@ -198,28 +198,6 @@ export class RbacController {
} }
} }
@Delete('users/:id')
@RequirePermission('user:delete')
async deleteUser(@Param('id') id: string, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
try {
const result = await this.rbacService.deleteUser(+id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账号',
action: '删除账号',
targetId: +id,
targetType: 'user',
ipAddress,
userAgent,
});
return result;
} catch (e: any) {
throw new BadRequestException(e.message);
}
}
@Put('users/:id/archive') @Put('users/:id/archive')
@RequirePermission('user:edit') @RequirePermission('user:edit')
async archiveUser(@Param('id') id: string) { async archiveUser(@Param('id') id: string) {

View File

@@ -70,7 +70,6 @@ const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> =
{ code: 'user:view', name: '查看用户', group: 'user' }, { code: 'user:view', name: '查看用户', group: 'user' },
{ code: 'user:create', name: '创建用户', group: 'user' }, { code: 'user:create', name: '创建用户', group: 'user' },
{ code: 'user:edit', name: '编辑用户', group: 'user' }, { code: 'user:edit', name: '编辑用户', group: 'user' },
{ code: 'user:delete', name: '删除用户', group: 'user' },
{ code: 'user:reset-password', name: '重置密码', group: 'user' }, { code: 'user:reset-password', name: '重置密码', group: 'user' },
{ code: 'role:view', name: '查看角色', group: 'role' }, { code: 'role:view', name: '查看角色', group: 'role' },
{ code: 'role:create', name: '创建角色', group: 'role' }, { code: 'role:create', name: '创建角色', group: 'role' },
@@ -250,7 +249,12 @@ export class RbacService {
await this.permRepo.save(this.permRepo.create(p)); await this.permRepo.save(this.permRepo.create(p));
} }
} }
const allPerms = await this.permRepo.find(); const deprecatedUserDeletePermission = await this.permRepo.findOne({
where: { code: 'user:delete' },
});
const allPerms = (await this.permRepo.find()).filter(
(permission) => permission.code !== 'user:delete',
);
// Step 2: 幂等插入预置角色 // Step 2: 幂等插入预置角色
for (const r of PRESET_ROLES) { for (const r of PRESET_ROLES) {
@@ -271,6 +275,19 @@ export class RbacService {
} }
const allRoles = await this.roleRepo.find({ relations: ['permissions', 'users'] }); const allRoles = await this.roleRepo.find({ relations: ['permissions', 'users'] });
if (deprecatedUserDeletePermission) {
for (const role of allRoles) {
const permissions = role.permissions ?? [];
if (permissions.some((permission) => permission.id === deprecatedUserDeletePermission.id)) {
role.permissions = permissions.filter(
(permission) => permission.id !== deprecatedUserDeletePermission.id,
);
await this.roleRepo.save(role);
}
}
await this.permRepo.remove(deprecatedUserDeletePermission);
}
// Step 3: 合并旧角色并构建新的职责权限矩阵 // Step 3: 合并旧角色并构建新的职责权限矩阵
for (const preset of PRESET_ROLES) { for (const preset of PRESET_ROLES) {
const matchesPreset = (role: Role) => const matchesPreset = (role: Role) =>
@@ -564,15 +581,6 @@ export class RbacService {
return { message: '已恢复为学员' }; return { message: '已恢复为学员' };
} }
async deleteUser(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
if (user.username === 'admin') throw new Error('不能删除默认管理员');
if (!user.isArchived) throw new Error('请先归档再删除');
await this.userRepo.remove(user);
return { message: '用户已删除' };
}
// ---- 用户资料 ---- // ---- 用户资料 ----
async getUserProfile(id: number) { async getUserProfile(id: number) {

View File

@@ -169,7 +169,8 @@
| GET | /api/auth/users | 用户列表 | | GET | /api/auth/users | 用户列表 |
| PUT | /api/auth/users/:id | 编辑用户 | | PUT | /api/auth/users/:id | 编辑用户 |
| PUT | /api/auth/users/:id/password | 重置密码 | | PUT | /api/auth/users/:id/password | 重置密码 |
| DELETE | /api/auth/users/:id | 删除用户 | | PUT | /api/rbac/users/:id/archive | 归档用户 |
| PUT | /api/rbac/users/:id/restore | 恢复用户 |
### 业务模块 ### 业务模块
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |