diff --git a/src/tasks/task.service.ts b/src/tasks/task.service.ts
index d5de0ec..010a12e 100644
--- a/src/tasks/task.service.ts
+++ b/src/tasks/task.service.ts
@@ -84,7 +84,7 @@ export class TaskService {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const skip = (page - 1) * pageSize;
- const where = this.buildTaskRecordWhere(query, hospitalId);
+ const where = this.buildTaskRecordWhere(actor, query, hospitalId);
const [total, items] = await Promise.all([
this.prisma.taskItem.count({ where }),
@@ -608,16 +608,23 @@ export class TaskService {
* 构造调压记录查询条件。
*/
private buildTaskRecordWhere(
+ actor: ActorContext,
query: TaskRecordQueryDto,
hospitalId?: number,
): Prisma.TaskItemWhereInput {
const keyword = query.keyword?.trim();
+ const patientScope = this.buildTaskPatientScope(actor);
const where: Prisma.TaskItemWhereInput = {
task: {
hospitalId,
status: query.status,
},
+ device: patientScope
+ ? {
+ patient: patientScope,
+ }
+ : undefined,
};
if (!keyword) {
@@ -676,6 +683,42 @@ export class TaskService {
return where;
}
+ /**
+ * 按角色构造任务列表中的患者可见范围,保持与患者列表一致。
+ */
+ private buildTaskPatientScope(actor: ActorContext): Prisma.PatientWhereInput | null {
+ switch (actor.role) {
+ case Role.SYSTEM_ADMIN:
+ case Role.HOSPITAL_ADMIN:
+ case Role.ENGINEER:
+ return null;
+ case Role.DOCTOR:
+ return {
+ doctorId: actor.id,
+ };
+ case Role.LEADER:
+ if (!actor.groupId) {
+ throw new BadRequestException(MESSAGES.PATIENT.GROUP_REQUIRED);
+ }
+ return {
+ doctor: {
+ groupId: actor.groupId,
+ },
+ };
+ case Role.DIRECTOR:
+ if (!actor.departmentId) {
+ throw new BadRequestException(MESSAGES.PATIENT.DEPARTMENT_REQUIRED);
+ }
+ return {
+ doctor: {
+ departmentId: actor.departmentId,
+ },
+ };
+ default:
+ throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
+ }
+ }
+
/**
* 调压目标挡位标准化。
*/
diff --git a/test/e2e/specs/tasks.e2e-spec.ts b/test/e2e/specs/tasks.e2e-spec.ts
index 08aba70..9f9c9ce 100644
--- a/test/e2e/specs/tasks.e2e-spec.ts
+++ b/test/e2e/specs/tasks.e2e-spec.ts
@@ -26,6 +26,7 @@ describe('BTasksController (e2e)', () => {
let ctx: E2EContext;
let samplePngBuffer: Buffer;
let doctorBToken = '';
+ let doctorA2Token = '';
beforeAll(async () => {
ctx = await createE2EContext();
@@ -44,6 +45,11 @@ describe('BTasksController (e2e)', () => {
Role.DOCTOR,
ctx.fixtures.hospitalBId,
);
+ doctorA2Token = await loginByUser(
+ ctx.fixtures.users.doctorA2Id,
+ Role.DOCTOR,
+ ctx.fixtures.hospitalAId,
+ );
});
afterAll(async () => {
@@ -293,21 +299,53 @@ describe('BTasksController (e2e)', () => {
);
});
- it('成功:DOCTOR 仅可查看本院调压记录', async () => {
+ it('成功:DOCTOR 仅可查看本人患者调压记录', async () => {
+ const keywordPrefix = uniqueSeedValue('scope-doctor');
+ const [selfDevice] = await createAdjustableDevices({
+ actorToken: ctx.tokens[Role.DOCTOR],
+ doctorId: ctx.fixtures.users.doctorAId,
+ patientName: `${keywordPrefix}-self`,
+ });
+ const [peerDevice] = await createAdjustableDevices({
+ actorToken: doctorA2Token,
+ doctorId: ctx.fixtures.users.doctorA2Id,
+ patientName: `${keywordPrefix}-peer`,
+ });
+
+ await publishPendingTask(
+ selfDevice.id,
+ '1.5',
+ ctx.tokens[Role.SYSTEM_ADMIN],
+ );
+ await publishPendingTask(
+ peerDevice.id,
+ '1.5',
+ ctx.tokens[Role.SYSTEM_ADMIN],
+ );
+
const response = await request(ctx.app.getHttpServer())
.get('/b/tasks')
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
- .query({ page: 1, pageSize: 20 });
+ .query({ page: 1, pageSize: 50, keyword: keywordPrefix });
expectSuccessEnvelope(response, 200);
expect(Array.isArray(response.body.data.list)).toBe(true);
expect(response.body.data.total).toBeGreaterThan(0);
expect(
response.body.data.list.every(
- (item: { hospital?: { id?: number } }) =>
- item.hospital?.id === ctx.fixtures.hospitalAId,
+ (item: {
+ hospital?: { id?: number };
+ patient?: { name?: string };
+ }) =>
+ item.hospital?.id === ctx.fixtures.hospitalAId &&
+ item.patient?.name?.includes(`${keywordPrefix}-self`),
),
).toBe(true);
+ expect(
+ response.body.data.list.some((item: { patient?: { name?: string } }) =>
+ item.patient?.name?.includes(`${keywordPrefix}-peer`),
+ ),
+ ).toBe(false);
});
it('失败:hospitalId 非法返回 400', async () => {
diff --git a/tyt-admin/src/views/patients/Patients.vue b/tyt-admin/src/views/patients/Patients.vue
index ffb4c25..dbdfebb 100644
--- a/tyt-admin/src/views/patients/Patients.vue
+++ b/tyt-admin/src/views/patients/Patients.vue
@@ -78,7 +78,7 @@
-
+
{{ row.hospital?.name || '-' }}
@@ -421,7 +421,7 @@
{{ detailPatient.creator?.name || '-' }}
-
+
{{ detailPatient.hospital?.name || '-' }}
@@ -716,7 +716,7 @@
{{ row.surgery?.surgeryName || '-' }}
-
+
{{ row.hospital?.name || '-' }}
@@ -791,6 +791,9 @@ const doctorTreeProps = {
};
const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN');
+const canViewHospitalInfo = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
+);
const canPublishAdjustTask = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
userStore.role,
diff --git a/tyt-admin/src/views/tasks/Tasks.vue b/tyt-admin/src/views/tasks/Tasks.vue
index 15f3ae2..629acc2 100644
--- a/tyt-admin/src/views/tasks/Tasks.vue
+++ b/tyt-admin/src/views/tasks/Tasks.vue
@@ -193,13 +193,7 @@
-
-
+
- 取消接收
+ {{ getCancelButtonText(row) }}
userStore.role === 'SYSTEM_ADMIN');
const isEngineer = computed(() => userStore.role === 'ENGINEER');
+const canCreatorCancel = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
+ userStore.role,
+ ),
+);
const currentUserId = computed(() => userStore.userInfo?.id || null);
const uploadHospitalId = computed(() => userStore.userInfo?.hospitalId || null);
const pageAlertTitle = computed(() =>
@@ -454,11 +453,27 @@ function canComplete(row) {
}
function canCancel(row) {
- return (
+ const canEngineerRelease =
isEngineer.value &&
row?.status === 'ACCEPTED' &&
- row?.engineer?.id === currentUserId.value
- );
+ row?.engineer?.id === currentUserId.value;
+
+ const canCreatorCancelTask =
+ canCreatorCancel.value &&
+ row?.status &&
+ ['PENDING', 'ACCEPTED'].includes(row.status) &&
+ row?.creator?.id === currentUserId.value;
+
+ return canEngineerRelease || canCreatorCancelTask;
+}
+
+function getCancelButtonText(row) {
+ const isEngineerRelease =
+ isEngineer.value &&
+ row?.status === 'ACCEPTED' &&
+ row?.engineer?.id === currentUserId.value;
+
+ return isEngineerRelease ? '取消接收' : '取消任务';
}
function isImageMaterial(material) {
@@ -580,13 +595,22 @@ function removeCompletionMaterial(index) {
}
async function handleCancel(row) {
+ const isEngineerRelease =
+ isEngineer.value && row?.status === 'ACCEPTED' && row?.engineer?.id === currentUserId.value;
+ const confirmTitle = isEngineerRelease ? '取消接收' : '取消任务';
+ const confirmText = isEngineerRelease
+ ? '取消接收后,任务会退回待接收状态,其他同院工程师可重新接收,是否继续?'
+ : '取消后任务将变更为已取消状态,是否继续?';
+ const confirmButtonText = isEngineerRelease ? '确认取消接收' : '确认取消任务';
+ const successText = isEngineerRelease ? '任务已退回待接收' : '任务已取消';
+
try {
await ElMessageBox.confirm(
- '取消接收后,任务会退回待接收状态,其他同院工程师可重新接收,是否继续?',
- '取消接收',
+ confirmText,
+ confirmTitle,
{
type: 'warning',
- confirmButtonText: '确认取消接收',
+ confirmButtonText,
cancelButtonText: '返回',
},
);
@@ -598,7 +622,7 @@ async function handleCancel(row) {
actionType.value = 'cancel';
try {
await cancelTask({ taskId: row.taskId });
- ElMessage.success('任务已退回待接收');
+ ElMessage.success(successText);
await fetchData();
} finally {
actionTaskId.value = null;
diff --git a/tyt-admin/src/views/users/Users.vue b/tyt-admin/src/views/users/Users.vue
index b392b44..610ee20 100644
--- a/tyt-admin/src/views/users/Users.vue
+++ b/tyt-admin/src/views/users/Users.vue
@@ -58,7 +58,7 @@
-
+
{{ resolveHospitalName(row.hospitalId) }}