fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation

This commit is contained in:
2026-07-23 14:15:52 +08:00
parent f39136d9ce
commit 3ac99b808e
11 changed files with 282 additions and 142 deletions

View File

@@ -312,8 +312,11 @@ interface MatchModalProps {
}
const JinshujuMatchModal: React.FC<MatchModalProps> = ({ open, onClose, onApplied }) => {
const { hasPermission } = usePermission();
const { hasPermission, hasAllPermissions, permissionsReady } = usePermission();
const canReadSync = hasPermission('sync:read');
const canTriggerSync = hasPermission('sync:trigger');
const canEnterModal = permissionsReady && hasAllPermissions('sync:read', 'sync:trigger');
const canWriteRules = permissionsReady && canTriggerSync;
const [step, setStep] = useState<'connection' | 'rule' | 'match' | 'applying'>('connection');
const [loading, setLoading] = useState(false);
const [selectedRuleId, setSelectedRuleId] = useState<number | undefined>();
@@ -334,8 +337,22 @@ const JinshujuMatchModal: React.FC<MatchModalProps> = ({ open, onClose, onApplie
// Load rules on open
useEffect(() => {
if (open) loadRules();
}, [open]);
if (open && canEnterModal) loadRules();
}, [open, canEnterModal]);
// Close and reset when permission is lost
const enteredRef = useRef(false);
useEffect(() => {
if (canEnterModal) {
enteredRef.current = true;
return;
}
if (enteredRef.current) {
enteredRef.current = false;
reset();
onClose();
}
}, [canEnterModal, onClose]);
const loadRules = async () => {
try {
@@ -347,6 +364,7 @@ const JinshujuMatchModal: React.FC<MatchModalProps> = ({ open, onClose, onApplie
};
const handleConnectionNext = async () => {
if (!canTriggerSync) return;
try {
const values = await credForm.validateFields();
setLoading(true);
@@ -366,6 +384,7 @@ const JinshujuMatchModal: React.FC<MatchModalProps> = ({ open, onClose, onApplie
};
const handlePreview = async () => {
if (!canTriggerSync) return;
try {
const values = await credForm.validateFields();
setLoading(true);
@@ -689,7 +708,7 @@ const JinshujuMatchModal: React.FC<MatchModalProps> = ({ open, onClose, onApplie
return (
<Modal
title="同步金数据"
open={open}
open={open && canEnterModal}
onCancel={handleClose}
width={step === 'match' || step === 'applying' ? 900 : 640}
maskClosable={false}

View File

@@ -1434,9 +1434,9 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
return;
}
api
.get('/organizations', { params: { includeArchived: 'false' } })
.get('/organizations/options')
.then((res: unknown) => {
setOrganizations(res as Array<{ id: number; name: string }>);
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
})
.catch(() => {});
}, [canLoadOrganizations]);
@@ -1454,7 +1454,11 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
}
}, [studentId]);
const handleViewSensitive = useViewSensitive(studentId, '学生档案');
const handleViewSensitive = useViewSensitive(
studentId,
'学生档案',
hasPermission('log:create'),
);
const tabItems = useMemo(() => {
if (!aggregateData) return [];

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Modal } from 'antd';
import api from '../api';
import { message } from '../ui/app-message';
@@ -9,16 +9,35 @@ import { message } from '../ui/app-message';
*
* @param studentId - The student whose data is being viewed
* @param module - Audit module label (e.g. '学生管理', '学生档案')
* @param canLog - Whether the current user has log:create; when false any
* already-open confirm modal is destroyed.
*/
export function useViewSensitive(studentId: number, module: string) {
export function useViewSensitive(studentId: number, module: string, canLog: boolean) {
const canLogRef = useRef(canLog);
const modalRef = useRef<ReturnType<typeof Modal.confirm> | null>(null);
canLogRef.current = canLog;
useEffect(() => {
if (!canLogRef.current && modalRef.current) {
modalRef.current.destroy();
modalRef.current = null;
}
return () => {
modalRef.current?.destroy();
modalRef.current = null;
};
}, []);
return useCallback(
(field: string, value: string) => {
Modal.confirm({
if (!canLogRef.current) return;
modalRef.current = Modal.confirm({
title: '查看敏感信息',
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
okText: '确认查看',
cancelText: '取消',
onOk: async () => {
if (!canLogRef.current) return;
try {
await api.post('/operation-logs/audit', {
module,
@@ -37,6 +56,9 @@ export function useViewSensitive(studentId: number, module: string) {
okText: '关闭',
});
},
afterClose: () => {
modalRef.current = null;
},
});
},
[studentId, module],

View File

@@ -27,7 +27,7 @@ interface ExamDetail extends ExamItem {
}
const PhoneCell: React.FC<{ row: ScoreRow }> = ({ row }) => {
const reveal = useViewSensitive(row.studentId, '考试管理');
const reveal = useViewSensitive(row.studentId, '考试管理', hasPermission('log:create'));
const { hasPermission } = usePermission();
if (!row.phone) return <>-</>;
return (

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
import {
Table,
Button,
@@ -38,8 +38,11 @@ import { usePermission } from '../../hooks/usePermission';
const { RangePicker } = DatePicker;
const OccupanciesPage: React.FC = () => {
const { hasPermission } = usePermission();
const canCheckIn = hasPermission('occupancy:checkin');
const { hasPermission, permissionsReady } = usePermission();
const canCheckIn = permissionsReady && hasPermission('occupancy:checkin');
const canCheckOut = permissionsReady && hasPermission('occupancy:checkout');
const canTransfer = permissionsReady && hasPermission('occupancy:transfer');
const canDelete = permissionsReady && hasPermission('occupancy:delete');
const [data, setData] = useState<any[]>([]);
const [students, setStudents] = useState<any[]>([]);
const [rooms, setRooms] = useState<any[]>([]);
@@ -69,6 +72,12 @@ const OccupanciesPage: React.FC = () => {
const selectedCheckInRoomId = Form.useWatch('roomId', checkInForm);
const selectedTransferRoomId = Form.useWatch('newRoomId', transferForm);
// Close modals when the user loses the required permission
useEffect(() => { if (!canCheckIn) { setCheckInModal(false); checkInForm.resetFields(); } }, [canCheckIn, checkInForm]);
useEffect(() => { if (!canCheckOut && checkOutModal) { setCheckOutModal(null); checkOutForm.resetFields(); } }, [canCheckOut, checkOutModal, checkOutForm]);
useEffect(() => { if (!canCheckOut) { setBatchCheckOutModal(false); batchCheckOutForm.resetFields(); } }, [canCheckOut, batchCheckOutForm]);
useEffect(() => { if (!canTransfer && transferModal) { setTransferModal(null); transferForm.resetFields(); } }, [canTransfer, transferModal, transferForm]);
const activeOccupancyByStudentId = useMemo(() => {
const map = new Map<number, any>();
data.forEach((item) => {
@@ -372,27 +381,28 @@ const OccupanciesPage: React.FC = () => {
) : (
<Space>
<Tag>退宿</Tag>
<Popconfirm
title="确定归档此记录?"
onConfirm={async () => {
try {
await api.delete(`/occupancies/${record.id}`);
message.success('归档成功');
fetchData();
} catch (e: any) {
message.error(e?.message || '归档失败');
}
}}
>
<PermissionButton
permission="occupancy:delete"
size="small"
danger
icon={<InboxOutlined />}
{canDelete ? (
<Popconfirm
title="确定归档此记录?"
onConfirm={async () => {
try {
await api.delete(`/occupancies/${record.id}`);
message.success('归档成功');
fetchData();
} catch (e: any) {
message.error(e?.message || '归档失败');
}
}}
>
</PermissionButton>
</Popconfirm>
<Button
size="small"
danger
icon={<InboxOutlined />}
>
</Button>
</Popconfirm>
) : null}
</Space>
),
},
@@ -584,15 +594,15 @@ const OccupanciesPage: React.FC = () => {
>
退宿
</PermissionButton>
) : (
</>)}
{canDelete ? (
<Popconfirm
title={`确定归档选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`}
onConfirm={handleBatchDelete}
okText="归档"
cancelText="取消"
>
<PermissionButton
permission="occupancy:delete"
<Button
danger
size="small"
icon={<InboxOutlined />}
@@ -600,8 +610,9 @@ const OccupanciesPage: React.FC = () => {
loading={batchLoading}
>
</PermissionButton>
</Button>
</Popconfirm>
) : null}
)}
<Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}>
取消选择
@@ -629,8 +640,8 @@ const OccupanciesPage: React.FC = () => {
/>
<Modal
title="入住登记"
open={checkInModal}
onOk={handleCheckIn}
open={checkInModal && canCheckIn}
onOk={canCheckIn ? handleCheckIn : undefined}
onCancel={() => {
setCheckInModal(false);
setAvailableBeds([]);
@@ -804,8 +815,8 @@ const OccupanciesPage: React.FC = () => {
{/* 退宿弹窗 */}
<Modal
title={`退宿 - ${checkOutModal?.student?.name}`}
open={!!checkOutModal}
onOk={handleCheckOut}
open={!!checkOutModal && canCheckOut}
onOk={canCheckOut ? handleCheckOut : undefined}
onCancel={() => setCheckOutModal(null)}
okText="确认退宿"
confirmLoading={saving}
@@ -861,8 +872,8 @@ const OccupanciesPage: React.FC = () => {
{/* 批量退宿弹窗 */}
<Modal
title={`批量退宿(${selectedRowKeys.length} 人)`}
open={batchCheckOutModal}
onOk={handleBatchCheckOut}
open={batchCheckOutModal && canCheckOut}
onOk={canCheckOut ? handleBatchCheckOut : undefined}
onCancel={() => setBatchCheckOutModal(false)}
okText="确认批量退宿"
width={500}
@@ -938,7 +949,7 @@ const OccupanciesPage: React.FC = () => {
{/* 换房弹窗 */}
<Modal
title={`换房 - ${transferModal?.student?.name}`}
open={!!transferModal}
open={!!transferModal && canTransfer}
onOk={handleTransfer}
onCancel={() => {
setTransferModal(null);

View File

@@ -85,12 +85,15 @@ function parseRoomNumber(input: string) {
}
const RoomsPage: React.FC = () => {
const { hasPermission } = usePermission();
const canEditRooms = hasPermission('room:edit');
const { hasPermission, permissionsReady } = usePermission();
const canEditRooms = permissionsReady && hasPermission('room:edit');
const canCreateRooms = permissionsReady && hasPermission('room:create');
const canDeleteRooms = permissionsReady && hasPermission('room:delete');
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const canSaveRoom = editing ? canEditRooms : canCreateRooms;
const [showArchived, setShowArchived] = useState(false);
const [archivedCount, setArchivedCount] = useState(0);
const [searchText, setSearchText] = useState('');
@@ -114,6 +117,9 @@ const RoomsPage: React.FC = () => {
const [savingLocker, setSavingLocker] = useState(false);
const [batchLoading, setBatchLoading] = useState(false);
// Close modals when the required permission is lost
useEffect(() => { if (!canSaveRoom && modalOpen) { setModalOpen(false); setEditing(null); form.resetFields(); } }, [canSaveRoom, modalOpen, form]);
const handleBatchDelete = async () => {
setBatchLoading(true);
try {
@@ -522,16 +528,17 @@ const RoomsPage: React.FC = () => {
return (
<Space>
{r.status === 'archived' ? (
<Popconfirm title="确定恢复此宿舍?" onConfirm={() => handleRestore(r.id)}>
<PermissionButton
permission="room:edit"
size="small"
icon={<UndoOutlined />}
type="link"
>
</PermissionButton>
</Popconfirm>
canEditRooms ? (
<Popconfirm title="确定恢复此宿舍?" onConfirm={() => handleRestore(r.id)}>
<Button
size="small"
icon={<UndoOutlined />}
type="link"
>
</Button>
</Popconfirm>
) : null
) : (
<>
<PermissionButton
@@ -559,15 +566,16 @@ const RoomsPage: React.FC = () => {
>
</PermissionButton>
<Popconfirm title="确定归档?" onConfirm={() => handleArchive(r.id)}>
<PermissionButton
permission="room:delete"
size="small"
icon={<InboxOutlined />}
>
</PermissionButton>
</Popconfirm>
{canDeleteRooms ? (
<Popconfirm title="确定归档?" onConfirm={() => handleArchive(r.id)}>
<Button
size="small"
icon={<InboxOutlined />}
>
</Button>
</Popconfirm>
) : null}
</>
)}
</Space>
@@ -638,23 +646,24 @@ const RoomsPage: React.FC = () => {
</Button>
</Space>
<Space wrap className="responsive-toolbar__group">
<Popconfirm
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
onConfirm={handleBatchDelete}
okText="归档"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
>
<PermissionButton
permission="room:delete"
danger
icon={<InboxOutlined />}
{canDeleteRooms ? (
<Popconfirm
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
onConfirm={handleBatchDelete}
okText="归档"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
loading={batchLoading}
>
</PermissionButton>
</Popconfirm>
<Button
danger
icon={<InboxOutlined />}
disabled={selectedRowKeys.length === 0}
loading={batchLoading}
>
</Button>
</Popconfirm>
) : null}
<PermissionButton
permission="room:create"
type="primary"
@@ -732,8 +741,8 @@ const RoomsPage: React.FC = () => {
<Modal
title={editing ? '编辑宿舍' : '添加宿舍'}
open={modalOpen}
onOk={handleSave}
open={modalOpen && canSaveRoom}
onOk={canSaveRoom ? handleSave : undefined}
onCancel={() => {
setModalOpen(false);
setEditing(null);
@@ -994,20 +1003,19 @@ const RoomsPage: React.FC = () => {
>
</PermissionButton>
{r.status !== 'occupied' && (
{r.status !== 'occupied' && canEditRooms && (
<Popconfirm
title="确定归档?"
onConfirm={() => handleDeleteBed(r.id)}
>
<PermissionButton
permission="room:edit"
<Button
size="small"
type="link"
danger
disabled={drawerRoom?.status === 'archived'}
>
</PermissionButton>
</Button>
</Popconfirm>
)}
</Space>
@@ -1147,20 +1155,19 @@ const RoomsPage: React.FC = () => {
>
</PermissionButton>
{r.status !== 'occupied' && (
{r.status !== 'occupied' && canEditRooms && (
<Popconfirm
title="确定归档?"
onConfirm={() => handleDeleteLocker(r.id)}
>
<PermissionButton
permission="room:edit"
<Button
size="small"
type="link"
danger
disabled={drawerRoom?.status === 'archived'}
>
</PermissionButton>
</Button>
</Popconfirm>
)}
</Space>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Alert,
App,
@@ -93,12 +93,16 @@ const StudentsPage: React.FC = () => {
'student:edit',
);
const canChooseOrganization = hasAnyPermission('student:create', 'student:edit');
const canCreateStudent = hasPermission('student:create');
const canEditStudent = hasPermission('student:edit');
const canDeleteStudent = hasPermission('student:delete');
const canSyncJinshuju = hasAllPermissions('sync:read', 'sync:trigger');
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [organizations, setOrganizations] = useState<any[]>([]);
const [editing, setEditing] = useState<any>(null);
const canSaveStudent = editing ? canEditStudent : canCreateStudent;
const [searchName, setSearchName] = useState('');
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
const [filterOrganizationId, setFilterOrganizationId] = useState<number | undefined>(undefined);
@@ -123,13 +127,40 @@ const StudentsPage: React.FC = () => {
const [jinshujuOpen, setJinshujuOpen] = useState(false);
// Sensitive info modal — command-style; destroy when log:create is lost or comp unmounts.
// Close the student form modal when the user loses the required permission.
useEffect(() => {
if (!canSaveStudent && modalOpen) {
setModalOpen(false);
setEditing(null);
form.resetFields();
}
}, [canSaveStudent, modalOpen, form]);
// Close sensitive modal when log:create is lost (imperative ref already set above).
const logCreateRef = React.useRef(hasPermission('log:create'));
const sensitiveModalRef = React.useRef<ReturnType<typeof modal.confirm> | null>(null);
logCreateRef.current = hasPermission('log:create');
useEffect(() => {
if (!logCreateRef.current && sensitiveModalRef.current) {
sensitiveModalRef.current.destroy();
sensitiveModalRef.current = null;
}
return () => {
sensitiveModalRef.current?.destroy();
sensitiveModalRef.current = null;
};
}, []);
const handleViewSensitive = (studentId: number, field: string, value: string) => {
modal.confirm({
if (!logCreateRef.current) return;
sensitiveModalRef.current = modal.confirm({
title: '查看敏感信息',
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
okText: '确认查看',
cancelText: '取消',
onOk: async () => {
if (!logCreateRef.current) return;
try {
await api.post('/operation-logs/audit', {
module: '学生管理',
@@ -147,6 +178,9 @@ const StudentsPage: React.FC = () => {
message.error('审计日志记录失败,请稍后重试');
}
},
afterClose: () => {
sensitiveModalRef.current = null;
},
});
};
@@ -199,16 +233,25 @@ const StudentsPage: React.FC = () => {
}, [fetchData]);
useEffect(() => {
if (canLoadOrganizations) {
if (!canLoadOrganizations) {
setOrganizations([]);
setFilterOrganizationId(undefined);
return;
}
if (canViewOrganizations) {
api
.get('/organizations', { params: { includeArchived: 'false' } })
.then((res: unknown) => {
setOrganizations(res as Array<{ id: number; name: string }>);
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
})
.catch(() => {});
} else {
setOrganizations([]);
setFilterOrganizationId(undefined);
api
.get('/organizations/options')
.then((res: unknown) => {
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
})
.catch(() => {});
}
api
.get<StudentFilterLookups>('/students/filter-lookups')
@@ -619,21 +662,18 @@ const StudentsPage: React.FC = () => {
render: (_: any, record: any) => (
<Space>
{record.status === 'archived' ? (
<Popconfirm
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
onConfirm={() => handleRestore(record.id)}
okText="恢复"
cancelText="取消"
>
<PermissionButton
permission="student:edit"
size="small"
icon={<UndoOutlined />}
type="link"
canEditStudent ? (
<Popconfirm
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
onConfirm={() => handleRestore(record.id)}
okText="恢复"
cancelText="取消"
>
</PermissionButton>
</Popconfirm>
<Button size="small" icon={<UndoOutlined />} type="link">
</Button>
</Popconfirm>
) : null
) : (
<>
<PermissionButton
@@ -655,20 +695,21 @@ const StudentsPage: React.FC = () => {
>
</PermissionButton>
<Popconfirm
title="归档后不会删除数据,可随时恢复。确定归档?"
onConfirm={() => handleArchive(record.id)}
okText="归档"
cancelText="取消"
>
<PermissionButton
permission="student:delete"
size="small"
{canDeleteStudent ? (
<Popconfirm
title="归档后不会删除数据,可随时恢复。确定归档?"
onConfirm={() => handleArchive(record.id)}
okText="归档"
cancelText="取消"
>
<Button
size="small"
icon={<InboxOutlined />}
>
</PermissionButton>
</Button>
</Popconfirm>
) : null}
</>
)}
</Space>
@@ -770,23 +811,24 @@ const StudentsPage: React.FC = () => {
</Button>
</Space>
<Space wrap className="responsive-toolbar__group">
<Popconfirm
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
onConfirm={handleBatchDelete}
okText="归档"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
>
<PermissionButton
permission="student:delete"
danger
icon={<InboxOutlined />}
{canDeleteStudent ? (
<Popconfirm
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
onConfirm={handleBatchDelete}
okText="归档"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
loading={batchLoading}
>
</PermissionButton>
</Popconfirm>
<Button
danger
icon={<InboxOutlined />}
disabled={selectedRowKeys.length === 0}
loading={batchLoading}
>
</Button>
</Popconfirm>
) : null}
<PermissionButton
permission="student:create"
type="primary"
@@ -932,8 +974,8 @@ const StudentsPage: React.FC = () => {
title={editing ? '编辑学生' : '添加学生'}
className="student-form-modal"
width={720}
open={modalOpen}
onOk={handleSave}
open={modalOpen && canSaveStudent}
onOk={canSaveStudent ? handleSave : undefined}
onCancel={() => {
setModalOpen(false);
setEditing(null);

View File

@@ -3,11 +3,15 @@ import { PERMISSION_KEY } from '../auth/decorators/permission.decorator';
import { OrganizationsController } from './organizations.controller';
describe('OrganizationsController permissions', () => {
it('allows student editors to list organization options', () => {
it('allows student editors to use the options endpoint without full entity exposure', () => {
expect(
Reflect.getMetadata(PERMISSION_KEY, OrganizationsController.prototype.findOptions),
).toEqual(['organization:view', 'student:create', 'student:edit']);
});
it('keeps the full entity list restricted to organization viewers only', () => {
expect(Reflect.getMetadata(PERMISSION_KEY, OrganizationsController.prototype.findAll)).toEqual([
'organization:view',
'student:create',
'student:edit',
]);
});

View File

@@ -25,8 +25,14 @@ export class OrganizationsController {
private logService: OperationLogsService,
) {}
@Get()
@Get('options')
@RequirePermission('organization:view', 'student:create', 'student:edit')
findOptions() {
return this.service.findOptions();
}
@Get()
@RequirePermission('organization:view')
findAll(
@Query('includeArchived') includeArchived?: string,
@Query('scope') scope?: 'all' | 'host' | 'external',

View File

@@ -27,4 +27,21 @@ describe('OrganizationsService — host organization rules', () => {
await expect(service.remove(1)).rejects.toBeInstanceOf(BadRequestException);
expect(repo.update).not.toHaveBeenCalled();
});
it('findOptions returns only id, name, isHost for active organizations', async () => {
const orgs = [
{ id: 1, name: '本机构', isHost: true },
{ id: 2, name: '分校', isHost: false },
];
repo.find.mockResolvedValue(orgs as Organization[]);
const result = await service.findOptions();
expect(repo.find).toHaveBeenCalledWith({
select: ['id', 'name', 'isHost'],
where: { status: 'active' },
order: { isHost: 'DESC', name: 'ASC' },
});
expect(result).toEqual(orgs);
});
});

View File

@@ -30,6 +30,14 @@ export class OrganizationsService {
return this.repo.find({ where, order: { isHost: 'DESC', name: 'ASC' } });
}
async findOptions() {
return this.repo.find({
select: ['id', 'name', 'isHost'] as const,
where: { status: 'active' },
order: { isHost: 'DESC' as const, name: 'ASC' as const },
});
}
async findOne(id: number) {
const organization = await this.repo.findOne({ where: { id } });
if (!organization) throw new NotFoundException('机构不存在');