新增主任范围校验:仅可操作同医院同科室的 DOCTOR 账号
限制主任变更:禁止将医生改为其他角色,禁止跨科室调整归属 新增 DIRECTOR_SCOPE_FORBIDDEN 统一错误文案 前端权限同步:主任可进入用户页,页面文案调整为“医生管理” 前端交互同步:主任创建/编辑时角色固定为医生,医院与科室范围锁定 仪表盘统计按角色收敛,主任视角展示本科室医生相关统计 补充 e2e 场景覆盖与接口文档说明"
This commit is contained in:
parent
6ec2d0b0e0
commit
64d1ad7896
@ -29,8 +29,10 @@
|
|||||||
- 患者列表权限:
|
- 患者列表权限:
|
||||||
- `SYSTEM_ADMIN` 查询时必须传 `hospitalId`
|
- `SYSTEM_ADMIN` 查询时必须传 `hospitalId`
|
||||||
- 用户管理接口:
|
- 用户管理接口:
|
||||||
- `SYSTEM_ADMIN/HOSPITAL_ADMIN` 可访问列表与创建
|
- `SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR` 可访问列表与创建
|
||||||
- 删除和工程师绑定医院仅 `SYSTEM_ADMIN`
|
- `DIRECTOR` 页面语义调整为“医生管理”,仅管理本科室医生
|
||||||
|
- 工程师绑定医院仅 `SYSTEM_ADMIN`
|
||||||
|
- 删除:`SYSTEM_ADMIN` 可删除任意无关联用户;`DIRECTOR` 可删除本科室无关联医生
|
||||||
|
|
||||||
## 3.1 结构图页面交互调整
|
## 3.1 结构图页面交互调整
|
||||||
|
|
||||||
@ -40,9 +42,11 @@
|
|||||||
## 3.2 后台页面路由权限(与后端 RBAC 对齐)
|
## 3.2 后台页面路由权限(与后端 RBAC 对齐)
|
||||||
|
|
||||||
- `organization/tree`、`organization/departments`、`organization/groups`、`users`
|
- `organization/tree`、`organization/departments`、`organization/groups`、`users`
|
||||||
- `organization/tree`、`organization/departments`、`organization/groups`:
|
- `organization/tree`、`organization/groups`:
|
||||||
`SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问
|
`SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问
|
||||||
- `users`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问
|
- `organization/departments`:
|
||||||
|
仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问
|
||||||
|
- `users`:`SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR` 可访问
|
||||||
- `devices`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问
|
- `devices`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问
|
||||||
- `organization/hospitals`
|
- `organization/hospitals`
|
||||||
- 仅 `SYSTEM_ADMIN` 可访问
|
- 仅 `SYSTEM_ADMIN` 可访问
|
||||||
@ -56,12 +60,14 @@
|
|||||||
## 3.3 主任/组长组织管理范围
|
## 3.3 主任/组长组织管理范围
|
||||||
|
|
||||||
- `DIRECTOR`
|
- `DIRECTOR`
|
||||||
- 可查看组织架构、科室列表、小组列表(限定本科室范围)
|
- 可查看组织架构、小组列表(限定本科室范围)
|
||||||
- 可编辑本科室名称、创建/编辑/删除本科室下小组
|
- 可创建/编辑/删除本科室下小组
|
||||||
|
- 可进入“医生管理”页,创建/维护本科室医生
|
||||||
- `LEADER`
|
- `LEADER`
|
||||||
- 可查看组织架构、科室列表、小组列表(限定本科室/本小组范围)
|
- 可查看组织架构、小组列表(限定本科室/本小组范围)
|
||||||
- 可编辑本科室名称与本小组名称
|
- 可编辑本小组名称
|
||||||
- 负责人设置(设主任/设组长)与人员管理入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。
|
- 主任/组长不再显示独立“科室管理”页面。
|
||||||
|
- 负责人设置(设主任/设组长)入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。
|
||||||
|
|
||||||
## 4. 本地运行
|
## 4. 本地运行
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
- 医院内数据按 `hospitalId` 强隔离。
|
- 医院内数据按 `hospitalId` 强隔离。
|
||||||
- 仅 `SYSTEM_ADMIN` 可执行工程师绑定医院。
|
- 仅 `SYSTEM_ADMIN` 可执行工程师绑定医院。
|
||||||
- `DIRECTOR/LEADER` 可读取用户列表,但仅返回当前科室可见用户。
|
- `DIRECTOR/LEADER` 可读取用户列表,但仅返回当前科室可见用户。
|
||||||
|
- `DIRECTOR` 可创建、查看、编辑、删除本科室医生,但不能跨科室操作,也不能把医生改成其他角色。
|
||||||
- 用户组织字段校验:
|
- 用户组织字段校验:
|
||||||
- 院管/医生/工程师等需有医院归属;
|
- 院管/医生/工程师等需有医院归属;
|
||||||
- 主任/组长需有科室/小组等必要归属;
|
- 主任/组长需有科室/小组等必要归属;
|
||||||
@ -31,6 +32,13 @@
|
|||||||
- `GET /users`、`GET /users/:id`、`PATCH /users/:id`、`DELETE /users/:id`
|
- `GET /users`、`GET /users/:id`、`PATCH /users/:id`、`DELETE /users/:id`
|
||||||
- `POST /b/users/:id/assign-engineer-hospital`
|
- `POST /b/users/:id/assign-engineer-hospital`
|
||||||
|
|
||||||
|
其中主任侧的常用链路为:
|
||||||
|
|
||||||
|
- `POST /users`:创建本科室医生
|
||||||
|
- `GET /users/:id`:查看本科室医生详情
|
||||||
|
- `PATCH /users/:id`:修改本科室医生信息
|
||||||
|
- `DELETE /users/:id`:删除无关联数据的本科室医生
|
||||||
|
|
||||||
## 5. 开发改造建议
|
## 5. 开发改造建议
|
||||||
|
|
||||||
- 若增加角色,请同步修改:
|
- 若增加角色,请同步修改:
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export const MESSAGES = {
|
|||||||
'检测到多个同手机号账号,请传 hospitalId 指定登录医院',
|
'检测到多个同手机号账号,请传 hospitalId 指定登录医院',
|
||||||
CREATE_FORBIDDEN: '当前角色无权限创建该用户',
|
CREATE_FORBIDDEN: '当前角色无权限创建该用户',
|
||||||
HOSPITAL_ADMIN_SCOPE_FORBIDDEN: '医院管理员仅可操作本院非管理员账号',
|
HOSPITAL_ADMIN_SCOPE_FORBIDDEN: '医院管理员仅可操作本院非管理员账号',
|
||||||
|
DIRECTOR_SCOPE_FORBIDDEN: '科室主任仅可操作本科室医生账号',
|
||||||
},
|
},
|
||||||
|
|
||||||
TASK: {
|
TASK: {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class UsersController {
|
|||||||
* 创建用户。
|
* 创建用户。
|
||||||
*/
|
*/
|
||||||
@Post()
|
@Post()
|
||||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR)
|
||||||
@ApiOperation({ summary: '创建用户' })
|
@ApiOperation({ summary: '创建用户' })
|
||||||
create(
|
create(
|
||||||
@CurrentActor() actor: ActorContext,
|
@CurrentActor() actor: ActorContext,
|
||||||
@ -61,7 +61,7 @@ export class UsersController {
|
|||||||
* 查询用户详情。
|
* 查询用户详情。
|
||||||
*/
|
*/
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR)
|
||||||
@ApiOperation({ summary: '查询用户详情' })
|
@ApiOperation({ summary: '查询用户详情' })
|
||||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||||
findOne(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
findOne(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
||||||
@ -72,7 +72,7 @@ export class UsersController {
|
|||||||
* 更新用户。
|
* 更新用户。
|
||||||
*/
|
*/
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR)
|
||||||
@ApiOperation({ summary: '更新用户' })
|
@ApiOperation({ summary: '更新用户' })
|
||||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||||
update(
|
update(
|
||||||
@ -87,7 +87,7 @@ export class UsersController {
|
|||||||
* 删除用户。
|
* 删除用户。
|
||||||
*/
|
*/
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@Roles(Role.SYSTEM_ADMIN)
|
@Roles(Role.SYSTEM_ADMIN, Role.DIRECTOR)
|
||||||
@ApiOperation({ summary: '删除用户' })
|
@ApiOperation({ summary: '删除用户' })
|
||||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||||
remove(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
remove(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
||||||
|
|||||||
@ -278,6 +278,7 @@ export class UsersService {
|
|||||||
|
|
||||||
this.assertUpdateTargetRoleAllowed(actor, nextRole);
|
this.assertUpdateTargetRoleAllowed(actor, nextRole);
|
||||||
this.assertUpdateHospitalScopeAllowed(actor, nextHospitalId);
|
this.assertUpdateHospitalScopeAllowed(actor, nextHospitalId);
|
||||||
|
this.assertUpdateDepartmentScopeAllowed(actor, nextDepartmentId);
|
||||||
|
|
||||||
const assigningDepartmentOrGroup =
|
const assigningDepartmentOrGroup =
|
||||||
(updateUserDto.departmentId !== undefined && nextDepartmentId != null) ||
|
(updateUserDto.departmentId !== undefined && nextDepartmentId != null) ||
|
||||||
@ -681,9 +682,38 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||||
|
if (actor.role !== Role.DIRECTOR) {
|
||||||
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 科室主任仅允许创建本科室医生。
|
||||||
|
if (targetRole !== Role.DOCTOR) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorHospitalId = this.requireActorScopeInt(
|
||||||
|
actor.hospitalId,
|
||||||
|
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||||
|
);
|
||||||
|
const actorDepartmentId = this.requireActorScopeInt(
|
||||||
|
actor.departmentId,
|
||||||
|
MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hospitalId != null && hospitalId !== actorHospitalId) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (departmentId != null && departmentId !== actorDepartmentId) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hospitalId: actorHospitalId,
|
||||||
|
departmentId: actorDepartmentId,
|
||||||
|
groupId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
targetRole === Role.SYSTEM_ADMIN ||
|
targetRole === Role.SYSTEM_ADMIN ||
|
||||||
targetRole === Role.HOSPITAL_ADMIN
|
targetRole === Role.HOSPITAL_ADMIN
|
||||||
@ -710,7 +740,7 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取权限:系统管理员可读全量;其余仅可读自己与本院(医院管理员)。
|
* 读取权限:系统管理员可读全量;院管可读本院;主任可读本科室医生。
|
||||||
*/
|
*/
|
||||||
private assertUserReadable(
|
private assertUserReadable(
|
||||||
actor: ActorContext,
|
actor: ActorContext,
|
||||||
@ -718,6 +748,7 @@ export class UsersService {
|
|||||||
id: number;
|
id: number;
|
||||||
role: Role;
|
role: Role;
|
||||||
hospitalId: number | null;
|
hospitalId: number | null;
|
||||||
|
departmentId: number | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||||
@ -737,11 +768,31 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor.role === Role.DIRECTOR) {
|
||||||
|
const actorHospitalId = this.requireActorScopeInt(
|
||||||
|
actor.hospitalId,
|
||||||
|
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||||
|
);
|
||||||
|
const actorDepartmentId = this.requireActorScopeInt(
|
||||||
|
actor.departmentId,
|
||||||
|
MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
target.role === Role.DOCTOR &&
|
||||||
|
target.hospitalId === actorHospitalId &&
|
||||||
|
target.departmentId === actorDepartmentId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 写权限:医院管理员仅可写本院非管理员账号。
|
* 写权限:院管可写本院非管理员账号;主任仅可写本科室医生。
|
||||||
*/
|
*/
|
||||||
private assertUserWritable(
|
private assertUserWritable(
|
||||||
actor: ActorContext,
|
actor: ActorContext,
|
||||||
@ -749,12 +800,32 @@ export class UsersService {
|
|||||||
id: number;
|
id: number;
|
||||||
role: Role;
|
role: Role;
|
||||||
hospitalId: number | null;
|
hospitalId: number | null;
|
||||||
|
departmentId: number | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor.role === Role.DIRECTOR) {
|
||||||
|
const actorHospitalId = this.requireActorScopeInt(
|
||||||
|
actor.hospitalId,
|
||||||
|
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||||
|
);
|
||||||
|
const actorDepartmentId = this.requireActorScopeInt(
|
||||||
|
actor.departmentId,
|
||||||
|
MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
target.role !== Role.DOCTOR ||
|
||||||
|
target.hospitalId !== actorHospitalId ||
|
||||||
|
target.departmentId !== actorDepartmentId
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||||
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
||||||
}
|
}
|
||||||
@ -786,6 +857,10 @@ export class UsersService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor.role === Role.DIRECTOR && nextRole !== Role.DOCTOR) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
actor.role === Role.HOSPITAL_ADMIN &&
|
actor.role === Role.HOSPITAL_ADMIN &&
|
||||||
(nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN)
|
(nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN)
|
||||||
@ -803,6 +878,17 @@ export class UsersService {
|
|||||||
actor: ActorContext,
|
actor: ActorContext,
|
||||||
hospitalId: number | null,
|
hospitalId: number | null,
|
||||||
) {
|
) {
|
||||||
|
if (actor.role === Role.DIRECTOR) {
|
||||||
|
const actorHospitalId = this.requireActorScopeInt(
|
||||||
|
actor.hospitalId,
|
||||||
|
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||||
|
);
|
||||||
|
if (hospitalId !== actorHospitalId) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -818,6 +904,26 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入时科室范围校验,避免主任跨科调整医生归属。
|
||||||
|
*/
|
||||||
|
private assertUpdateDepartmentScopeAllowed(
|
||||||
|
actor: ActorContext,
|
||||||
|
departmentId: number | null,
|
||||||
|
) {
|
||||||
|
if (actor.role !== Role.DIRECTOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorDepartmentId = this.requireActorScopeInt(
|
||||||
|
actor.departmentId,
|
||||||
|
MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
|
||||||
|
);
|
||||||
|
if (departmentId !== actorDepartmentId) {
|
||||||
|
throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签发访问令牌。
|
* 签发访问令牌。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -65,6 +65,10 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]);
|
await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('成功:DIRECTOR 可创建本科室医生', async () => {
|
||||||
|
await createDoctorUser(ctx.tokens[Role.DIRECTOR]);
|
||||||
|
});
|
||||||
|
|
||||||
it('失败:参数校验失败返回 400', async () => {
|
it('失败:参数校验失败返回 400', async () => {
|
||||||
const response = await request(ctx.app.getHttpServer())
|
const response = await request(ctx.app.getHttpServer())
|
||||||
.post('/users')
|
.post('/users')
|
||||||
@ -82,14 +86,31 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expectErrorEnvelope(response, 400, 'phone 必须是合法手机号');
|
expectErrorEnvelope(response, 400, 'phone 必须是合法手机号');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
it('失败:DIRECTOR 创建非医生角色返回 403', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.post('/users')
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
|
||||||
|
.send({
|
||||||
|
name: uniqueSeedValue('主任创建组长'),
|
||||||
|
phone: uniquePhone(),
|
||||||
|
password: 'Seed@1234',
|
||||||
|
role: Role.LEADER,
|
||||||
|
hospitalId: ctx.fixtures.hospitalAId,
|
||||||
|
departmentId: ctx.fixtures.departmentA1Id,
|
||||||
|
groupId: ctx.fixtures.groupA1Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectErrorEnvelope(response, 403, '当前角色无权限创建该用户');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => {
|
||||||
await assertRoleMatrix({
|
await assertRoleMatrix({
|
||||||
name: 'POST /users role matrix',
|
name: 'POST /users role matrix',
|
||||||
tokens: ctx.tokens,
|
tokens: ctx.tokens,
|
||||||
expectedStatusByRole: {
|
expectedStatusByRole: {
|
||||||
[Role.SYSTEM_ADMIN]: 400,
|
[Role.SYSTEM_ADMIN]: 400,
|
||||||
[Role.HOSPITAL_ADMIN]: 400,
|
[Role.HOSPITAL_ADMIN]: 400,
|
||||||
[Role.DIRECTOR]: 403,
|
[Role.DIRECTOR]: 400,
|
||||||
[Role.LEADER]: 403,
|
[Role.LEADER]: 403,
|
||||||
[Role.DOCTOR]: 403,
|
[Role.DOCTOR]: 403,
|
||||||
[Role.ENGINEER]: 403,
|
[Role.ENGINEER]: 403,
|
||||||
@ -152,6 +173,15 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId);
|
expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('成功:DIRECTOR 可查询本科室医生详情', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.get(`/users/${ctx.fixtures.users.doctorAId}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||||||
|
|
||||||
|
expectSuccessEnvelope(response, 200);
|
||||||
|
expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId);
|
||||||
|
});
|
||||||
|
|
||||||
it('失败:查询不存在用户返回 404', async () => {
|
it('失败:查询不存在用户返回 404', async () => {
|
||||||
const response = await request(ctx.app.getHttpServer())
|
const response = await request(ctx.app.getHttpServer())
|
||||||
.get('/users/99999999')
|
.get('/users/99999999')
|
||||||
@ -160,14 +190,22 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expectErrorEnvelope(response, 404, '用户不存在');
|
expectErrorEnvelope(response, 404, '用户不存在');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
it('失败:DIRECTOR 查询非本科室医生返回 403', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.get(`/users/${ctx.fixtures.users.doctorA3Id}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||||||
|
|
||||||
|
expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可访问,其他角色 403,未登录 401', async () => {
|
||||||
await assertRoleMatrix({
|
await assertRoleMatrix({
|
||||||
name: 'GET /users/:id role matrix',
|
name: 'GET /users/:id role matrix',
|
||||||
tokens: ctx.tokens,
|
tokens: ctx.tokens,
|
||||||
expectedStatusByRole: {
|
expectedStatusByRole: {
|
||||||
[Role.SYSTEM_ADMIN]: 200,
|
[Role.SYSTEM_ADMIN]: 200,
|
||||||
[Role.HOSPITAL_ADMIN]: 200,
|
[Role.HOSPITAL_ADMIN]: 200,
|
||||||
[Role.DIRECTOR]: 403,
|
[Role.DIRECTOR]: 200,
|
||||||
[Role.LEADER]: 403,
|
[Role.LEADER]: 403,
|
||||||
[Role.DOCTOR]: 403,
|
[Role.DOCTOR]: 403,
|
||||||
[Role.ENGINEER]: 403,
|
[Role.ENGINEER]: 403,
|
||||||
@ -198,6 +236,19 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expect(response.body.data.name).toBe(nextName);
|
expect(response.body.data.name).toBe(nextName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('成功:DIRECTOR 可更新本科室医生姓名', async () => {
|
||||||
|
const created = await createDoctorUser(ctx.tokens[Role.DIRECTOR]);
|
||||||
|
const nextName = uniqueSeedValue('主任更新医生名');
|
||||||
|
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.patch(`/users/${created.id}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
|
||||||
|
.send({ name: nextName });
|
||||||
|
|
||||||
|
expectSuccessEnvelope(response, 200);
|
||||||
|
expect(response.body.data.name).toBe(nextName);
|
||||||
|
});
|
||||||
|
|
||||||
it('失败:非医生调整科室/小组返回 400', async () => {
|
it('失败:非医生调整科室/小组返回 400', async () => {
|
||||||
const response = await request(ctx.app.getHttpServer())
|
const response = await request(ctx.app.getHttpServer())
|
||||||
.patch(`/users/${ctx.fixtures.users.engineerAId}`)
|
.patch(`/users/${ctx.fixtures.users.engineerAId}`)
|
||||||
@ -214,14 +265,32 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
it('失败:DIRECTOR 不能把医生改成其他角色', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.patch(`/users/${ctx.fixtures.users.doctorAId}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
|
||||||
|
.send({ role: Role.LEADER });
|
||||||
|
|
||||||
|
expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('失败:DIRECTOR 不能把医生调整到其他科室', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.patch(`/users/${ctx.fixtures.users.doctorAId}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
|
||||||
|
.send({ departmentId: ctx.fixtures.departmentA2Id });
|
||||||
|
|
||||||
|
expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => {
|
||||||
await assertRoleMatrix({
|
await assertRoleMatrix({
|
||||||
name: 'PATCH /users/:id role matrix',
|
name: 'PATCH /users/:id role matrix',
|
||||||
tokens: ctx.tokens,
|
tokens: ctx.tokens,
|
||||||
expectedStatusByRole: {
|
expectedStatusByRole: {
|
||||||
[Role.SYSTEM_ADMIN]: 404,
|
[Role.SYSTEM_ADMIN]: 404,
|
||||||
[Role.HOSPITAL_ADMIN]: 404,
|
[Role.HOSPITAL_ADMIN]: 404,
|
||||||
[Role.DIRECTOR]: 403,
|
[Role.DIRECTOR]: 404,
|
||||||
[Role.LEADER]: 403,
|
[Role.LEADER]: 403,
|
||||||
[Role.DOCTOR]: 403,
|
[Role.DOCTOR]: 403,
|
||||||
[Role.ENGINEER]: 403,
|
[Role.ENGINEER]: 403,
|
||||||
@ -250,6 +319,16 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expect(response.body.data.id).toBe(created.id);
|
expect(response.body.data.id).toBe(created.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('成功:DIRECTOR 可删除本科室医生', async () => {
|
||||||
|
const created = await createDoctorUser(ctx.tokens[Role.DIRECTOR]);
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.delete(`/users/${created.id}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||||||
|
|
||||||
|
expectSuccessEnvelope(response, 200);
|
||||||
|
expect(response.body.data.id).toBe(created.id);
|
||||||
|
});
|
||||||
|
|
||||||
it('失败:存在关联患者/任务时返回 409', async () => {
|
it('失败:存在关联患者/任务时返回 409', async () => {
|
||||||
const response = await request(ctx.app.getHttpServer())
|
const response = await request(ctx.app.getHttpServer())
|
||||||
.delete(`/users/${ctx.fixtures.users.doctorAId}`)
|
.delete(`/users/${ctx.fixtures.users.doctorAId}`)
|
||||||
@ -258,6 +337,14 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expectErrorEnvelope(response, 409, '用户存在关联患者或任务,无法删除');
|
expectErrorEnvelope(response, 409, '用户存在关联患者或任务,无法删除');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('失败:DIRECTOR 删除非本科室医生返回 403', async () => {
|
||||||
|
const response = await request(ctx.app.getHttpServer())
|
||||||
|
.delete(`/users/${ctx.fixtures.users.doctorA3Id}`)
|
||||||
|
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||||||
|
|
||||||
|
expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号');
|
||||||
|
});
|
||||||
|
|
||||||
it('失败:HOSPITAL_ADMIN 无法删除返回 403', async () => {
|
it('失败:HOSPITAL_ADMIN 无法删除返回 403', async () => {
|
||||||
const response = await request(ctx.app.getHttpServer())
|
const response = await request(ctx.app.getHttpServer())
|
||||||
.delete(`/users/${ctx.fixtures.users.doctorAId}`)
|
.delete(`/users/${ctx.fixtures.users.doctorAId}`)
|
||||||
@ -266,14 +353,14 @@ describe('UsersController + BUsersController (e2e)', () => {
|
|||||||
expectErrorEnvelope(response, 403, '无权限执行当前操作');
|
expectErrorEnvelope(response, 403, '无权限执行当前操作');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
it('角色矩阵:SYSTEM_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => {
|
||||||
await assertRoleMatrix({
|
await assertRoleMatrix({
|
||||||
name: 'DELETE /users/:id role matrix',
|
name: 'DELETE /users/:id role matrix',
|
||||||
tokens: ctx.tokens,
|
tokens: ctx.tokens,
|
||||||
expectedStatusByRole: {
|
expectedStatusByRole: {
|
||||||
[Role.SYSTEM_ADMIN]: 404,
|
[Role.SYSTEM_ADMIN]: 404,
|
||||||
[Role.HOSPITAL_ADMIN]: 403,
|
[Role.HOSPITAL_ADMIN]: 403,
|
||||||
[Role.DIRECTOR]: 403,
|
[Role.DIRECTOR]: 404,
|
||||||
[Role.LEADER]: 403,
|
[Role.LEADER]: 403,
|
||||||
[Role.DOCTOR]: 403,
|
[Role.DOCTOR]: 403,
|
||||||
[Role.ENGINEER]: 403,
|
[Role.ENGINEER]: 403,
|
||||||
|
|||||||
@ -8,6 +8,11 @@ const ORG_MANAGER_ROLES = Object.freeze([
|
|||||||
'DIRECTOR',
|
'DIRECTOR',
|
||||||
'LEADER',
|
'LEADER',
|
||||||
]);
|
]);
|
||||||
|
const USER_MANAGER_ROLES = Object.freeze([
|
||||||
|
'SYSTEM_ADMIN',
|
||||||
|
'HOSPITAL_ADMIN',
|
||||||
|
'DIRECTOR',
|
||||||
|
]);
|
||||||
const TASK_ROLES = Object.freeze(['DOCTOR', 'DIRECTOR', 'LEADER', 'ENGINEER']);
|
const TASK_ROLES = Object.freeze(['DOCTOR', 'DIRECTOR', 'LEADER', 'ENGINEER']);
|
||||||
const PATIENT_ROLES = Object.freeze([
|
const PATIENT_ROLES = Object.freeze([
|
||||||
'SYSTEM_ADMIN',
|
'SYSTEM_ADMIN',
|
||||||
@ -21,9 +26,10 @@ const PATIENT_ROLES = Object.freeze([
|
|||||||
export const ROLE_PERMISSIONS = Object.freeze({
|
export const ROLE_PERMISSIONS = Object.freeze({
|
||||||
ORG_TREE: ORG_MANAGER_ROLES,
|
ORG_TREE: ORG_MANAGER_ROLES,
|
||||||
ORG_HOSPITALS: Object.freeze(['SYSTEM_ADMIN']),
|
ORG_HOSPITALS: Object.freeze(['SYSTEM_ADMIN']),
|
||||||
ORG_DEPARTMENTS: ORG_MANAGER_ROLES,
|
// 主任/组长仍可通过接口读取科室信息,但不再开放独立“科室管理”页面。
|
||||||
|
ORG_DEPARTMENTS: ADMIN_ROLES,
|
||||||
ORG_GROUPS: ORG_MANAGER_ROLES,
|
ORG_GROUPS: ORG_MANAGER_ROLES,
|
||||||
USERS: ADMIN_ROLES,
|
USERS: USER_MANAGER_ROLES,
|
||||||
DEVICES: ADMIN_ROLES,
|
DEVICES: ADMIN_ROLES,
|
||||||
TASKS: TASK_ROLES,
|
TASKS: TASK_ROLES,
|
||||||
PATIENTS: PATIENT_ROLES,
|
PATIENTS: PATIENT_ROLES,
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
<el-icon><Share /></el-icon>
|
<el-icon><Share /></el-icon>
|
||||||
<span>组织架构图</span>
|
<span>组织架构图</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/organization/departments">
|
<el-menu-item v-if="canAccessDepartments" index="/organization/departments">
|
||||||
<el-icon><OfficeBuilding /></el-icon>
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
<span>科室管理</span>
|
<span>科室管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<el-menu-item v-if="canAccessUsers" index="/users">
|
<el-menu-item v-if="canAccessUsers" index="/users">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>用户管理</span>
|
<span>{{ usersMenuLabel }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item v-if="canAccessDevices" index="/devices">
|
<el-menu-item v-if="canAccessDevices" index="/devices">
|
||||||
@ -129,6 +129,7 @@ const activeMenu = computed(() => {
|
|||||||
return route.path;
|
return route.path;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDirector = computed(() => userStore.role === 'DIRECTOR');
|
||||||
const canAccessUsers = computed(() =>
|
const canAccessUsers = computed(() =>
|
||||||
hasRolePermission(userStore.role, ROLE_PERMISSIONS.USERS),
|
hasRolePermission(userStore.role, ROLE_PERMISSIONS.USERS),
|
||||||
);
|
);
|
||||||
@ -138,12 +139,18 @@ const canAccessDevices = computed(() =>
|
|||||||
const canAccessOrgTree = computed(() =>
|
const canAccessOrgTree = computed(() =>
|
||||||
hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_TREE),
|
hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_TREE),
|
||||||
);
|
);
|
||||||
|
const canAccessDepartments = computed(() =>
|
||||||
|
hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_DEPARTMENTS),
|
||||||
|
);
|
||||||
const canAccessTasks = computed(() =>
|
const canAccessTasks = computed(() =>
|
||||||
hasRolePermission(userStore.role, ROLE_PERMISSIONS.TASKS),
|
hasRolePermission(userStore.role, ROLE_PERMISSIONS.TASKS),
|
||||||
);
|
);
|
||||||
const canAccessPatients = computed(() =>
|
const canAccessPatients = computed(() =>
|
||||||
hasRolePermission(userStore.role, ROLE_PERMISSIONS.PATIENTS),
|
hasRolePermission(userStore.role, ROLE_PERMISSIONS.PATIENTS),
|
||||||
);
|
);
|
||||||
|
const usersMenuLabel = computed(() =>
|
||||||
|
isDirector.value ? '医生管理' : '用户管理',
|
||||||
|
);
|
||||||
|
|
||||||
const handleCommand = (command) => {
|
const handleCommand = (command) => {
|
||||||
if (command === 'logout') {
|
if (command === 'logout') {
|
||||||
|
|||||||
@ -5,17 +5,13 @@
|
|||||||
<p>当前角色:{{ userStore.role || '未登录' }}</p>
|
<p>当前角色:{{ userStore.role || '未登录' }}</p>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card
|
<el-card v-if="isSystemAdmin" shadow="never" class="filter-card">
|
||||||
v-if="isSystemAdmin"
|
|
||||||
shadow="never"
|
|
||||||
class="filter-card"
|
|
||||||
>
|
|
||||||
<el-form inline>
|
<el-form inline>
|
||||||
<el-form-item label="患者统计医院">
|
<el-form-item label="患者统计医院">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedHospitalId"
|
v-model="selectedHospitalId"
|
||||||
placeholder="请选择医院"
|
placeholder="请选择医院"
|
||||||
style="width: 280px;"
|
style="width: 280px"
|
||||||
@change="fetchDashboardData"
|
@change="fetchDashboardData"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@ -30,7 +26,13 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-row :gutter="16" v-loading="loading">
|
<el-row :gutter="16" v-loading="loading">
|
||||||
<el-col :xs="24" :sm="12" :lg="6" v-for="item in statCards" :key="item.key">
|
<el-col
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:lg="6"
|
||||||
|
v-for="item in statCards"
|
||||||
|
:key="item.key"
|
||||||
|
>
|
||||||
<el-card shadow="hover" class="stat-card">
|
<el-card shadow="hover" class="stat-card">
|
||||||
<div class="stat-title">{{ item.title }}</div>
|
<div class="stat-title">{{ item.title }}</div>
|
||||||
<div class="stat-value">{{ item.value }}</div>
|
<div class="stat-value">{{ item.value }}</div>
|
||||||
@ -75,19 +77,44 @@ const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN');
|
|||||||
const canViewOrg = computed(() =>
|
const canViewOrg = computed(() =>
|
||||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
|
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
|
||||||
);
|
);
|
||||||
|
const canViewUsers = computed(() =>
|
||||||
|
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role),
|
||||||
|
);
|
||||||
const canViewPatients = computed(() =>
|
const canViewPatients = computed(() =>
|
||||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER', 'DOCTOR'].includes(
|
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER', 'DOCTOR'].includes(
|
||||||
userStore.role,
|
userStore.role,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const statCards = computed(() => [
|
const statCards = computed(() => {
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
if (canViewOrg.value) {
|
||||||
|
cards.push(
|
||||||
{ key: 'hospitals', title: '医院总数', value: stats.value.hospitals },
|
{ key: 'hospitals', title: '医院总数', value: stats.value.hospitals },
|
||||||
{ key: 'departments', title: '科室总数', value: stats.value.departments },
|
{ key: 'departments', title: '科室总数', value: stats.value.departments },
|
||||||
{ key: 'groups', title: '小组总数', value: stats.value.groups },
|
{ key: 'groups', title: '小组总数', value: stats.value.groups },
|
||||||
{ key: 'users', title: '用户总数', value: stats.value.users },
|
);
|
||||||
{ key: 'patients', title: '可见患者数', value: stats.value.patients },
|
}
|
||||||
]);
|
|
||||||
|
if (canViewUsers.value) {
|
||||||
|
cards.push({
|
||||||
|
key: 'users',
|
||||||
|
title: userStore.role === 'DIRECTOR' ? '本科室医生数' : '用户总数',
|
||||||
|
value: stats.value.users,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canViewPatients.value) {
|
||||||
|
cards.push({
|
||||||
|
key: 'patients',
|
||||||
|
title: '可见患者数',
|
||||||
|
value: stats.value.patients,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
|
|
||||||
const fetchHospitalsForFilter = async () => {
|
const fetchHospitalsForFilter = async () => {
|
||||||
if (!isSystemAdmin.value) {
|
if (!isSystemAdmin.value) {
|
||||||
@ -104,16 +131,23 @@ const fetchDashboardData = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
if (canViewOrg.value) {
|
if (canViewOrg.value) {
|
||||||
const [hospitalRes, departmentRes, groupRes, usersRes] = await Promise.all([
|
const [hospitalRes, departmentRes, groupRes] = await Promise.all([
|
||||||
getHospitals({ page: 1, pageSize: 1 }),
|
getHospitals({ page: 1, pageSize: 1 }),
|
||||||
getDepartments({ page: 1, pageSize: 1 }),
|
getDepartments({ page: 1, pageSize: 1 }),
|
||||||
getGroups({ page: 1, pageSize: 1 }),
|
getGroups({ page: 1, pageSize: 1 }),
|
||||||
getUsers({ page: 1, pageSize: 1 }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stats.value.hospitals = hospitalRes.total ?? 0;
|
stats.value.hospitals = hospitalRes.total ?? 0;
|
||||||
stats.value.departments = departmentRes.total ?? 0;
|
stats.value.departments = departmentRes.total ?? 0;
|
||||||
stats.value.groups = groupRes.total ?? 0;
|
stats.value.groups = groupRes.total ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canViewUsers.value) {
|
||||||
|
const usersRes = await getUsers({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
role: userStore.role === 'DIRECTOR' ? 'DOCTOR' : undefined,
|
||||||
|
});
|
||||||
stats.value.users = usersRes.total ?? 0;
|
stats.value.users = usersRes.total ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +160,9 @@ const fetchDashboardData = async () => {
|
|||||||
stats.value.patients = 0;
|
stats.value.patients = 0;
|
||||||
} else {
|
} else {
|
||||||
const patientRes = await getPatients(params);
|
const patientRes = await getPatients(params);
|
||||||
stats.value.patients = Array.isArray(patientRes) ? patientRes.length : 0;
|
stats.value.patients = Array.isArray(patientRes)
|
||||||
|
? patientRes.length
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -10,18 +10,18 @@
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色">
|
<el-form-item v-if="!isDirector" label="角色">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="searchForm.role"
|
v-model="searchForm.role"
|
||||||
placeholder="请选择角色"
|
placeholder="请选择角色"
|
||||||
clearable
|
clearable
|
||||||
>
|
>
|
||||||
<el-option label="系统管理员" value="SYSTEM_ADMIN" />
|
<el-option
|
||||||
<el-option label="医院管理员" value="HOSPITAL_ADMIN" />
|
v-for="option in roleOptions"
|
||||||
<el-option label="科室主任" value="DIRECTOR" />
|
:key="option.value"
|
||||||
<el-option label="小组组长" value="LEADER" />
|
:label="option.label"
|
||||||
<el-option label="医生" value="DOCTOR" />
|
:value="option.value"
|
||||||
<el-option label="工程师" value="ENGINEER" />
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||||
<el-button type="success" @click="openCreateDialog" icon="Plus">
|
<el-button type="success" @click="openCreateDialog" icon="Plus">
|
||||||
新增用户
|
{{ createButtonText }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -84,7 +84,7 @@
|
|||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userStore.role === 'SYSTEM_ADMIN'"
|
v-if="canDeleteUser(row)"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDelete(row)"
|
@click="handleDelete(row)"
|
||||||
@ -110,7 +110,7 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="isEdit ? '编辑用户' : '新增用户'"
|
:title="dialogTitle"
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
width="620px"
|
width="620px"
|
||||||
@close="resetForm"
|
@close="resetForm"
|
||||||
@ -131,17 +131,23 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色" prop="role">
|
<el-form-item label="角色" prop="role">
|
||||||
|
<el-input
|
||||||
|
v-if="isDirector"
|
||||||
|
:model-value="getRoleName('DOCTOR')"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
|
v-else
|
||||||
v-model="form.role"
|
v-model="form.role"
|
||||||
placeholder="请选择角色"
|
placeholder="请选择角色"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option label="系统管理员" value="SYSTEM_ADMIN" />
|
<el-option
|
||||||
<el-option label="医院管理员" value="HOSPITAL_ADMIN" />
|
v-for="option in roleOptions"
|
||||||
<el-option label="科室主任" value="DIRECTOR" />
|
:key="option.value"
|
||||||
<el-option label="小组组长" value="LEADER" />
|
:label="option.label"
|
||||||
<el-option label="医生" value="DOCTOR" />
|
:value="option.value"
|
||||||
<el-option label="工程师" value="ENGINEER" />
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@ -150,6 +156,7 @@
|
|||||||
v-model="form.hospitalId"
|
v-model="form.hospitalId"
|
||||||
placeholder="请选择医院"
|
placeholder="请选择医院"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:disabled="lockHospital"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="hospital in hospitals"
|
v-for="hospital in hospitals"
|
||||||
@ -169,6 +176,7 @@
|
|||||||
v-model="form.departmentId"
|
v-model="form.departmentId"
|
||||||
placeholder="请选择科室"
|
placeholder="请选择科室"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:disabled="lockDepartment"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="department in formDepartments"
|
v-for="department in formDepartments"
|
||||||
@ -266,6 +274,15 @@ import {
|
|||||||
} from '../../api/organization';
|
} from '../../api/organization';
|
||||||
import { useUserStore } from '../../store/user';
|
import { useUserStore } from '../../store/user';
|
||||||
|
|
||||||
|
const roleOptions = [
|
||||||
|
{ label: '系统管理员', value: 'SYSTEM_ADMIN' },
|
||||||
|
{ label: '医院管理员', value: 'HOSPITAL_ADMIN' },
|
||||||
|
{ label: '科室主任', value: 'DIRECTOR' },
|
||||||
|
{ label: '小组组长', value: 'LEADER' },
|
||||||
|
{ label: '医生', value: 'DOCTOR' },
|
||||||
|
{ label: '工程师', value: 'ENGINEER' },
|
||||||
|
];
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@ -279,9 +296,10 @@ const hospitals = ref([]);
|
|||||||
const departments = ref([]);
|
const departments = ref([]);
|
||||||
const groups = ref([]);
|
const groups = ref([]);
|
||||||
|
|
||||||
|
const isDirector = computed(() => userStore.role === 'DIRECTOR');
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
role: '',
|
role: isDirector.value ? 'DOCTOR' : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
@ -306,11 +324,24 @@ const assignDialogVisible = ref(false);
|
|||||||
const currentAssignUser = ref(null);
|
const currentAssignUser = ref(null);
|
||||||
const assignHospitalId = ref(null);
|
const assignHospitalId = ref(null);
|
||||||
|
|
||||||
|
const createButtonText = computed(() =>
|
||||||
|
isDirector.value ? '新增医生' : '新增用户',
|
||||||
|
);
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (isDirector.value) {
|
||||||
|
return isEdit.value ? '编辑医生' : '新增医生';
|
||||||
|
}
|
||||||
|
return isEdit.value ? '编辑用户' : '新增用户';
|
||||||
|
});
|
||||||
const needHospital = computed(() => form.role && form.role !== 'SYSTEM_ADMIN');
|
const needHospital = computed(() => form.role && form.role !== 'SYSTEM_ADMIN');
|
||||||
const needDepartment = computed(() =>
|
const needDepartment = computed(() =>
|
||||||
['DIRECTOR', 'LEADER', 'DOCTOR'].includes(form.role),
|
['DIRECTOR', 'LEADER', 'DOCTOR'].includes(form.role),
|
||||||
);
|
);
|
||||||
const needGroup = computed(() => ['LEADER', 'DOCTOR'].includes(form.role));
|
const needGroup = computed(() => ['LEADER', 'DOCTOR'].includes(form.role));
|
||||||
|
const lockHospital = computed(() =>
|
||||||
|
['HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role),
|
||||||
|
);
|
||||||
|
const lockDepartment = computed(() => isDirector.value);
|
||||||
|
|
||||||
const rules = computed(() => ({
|
const rules = computed(() => ({
|
||||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||||
@ -379,6 +410,16 @@ const resolveGroupName = (id) => {
|
|||||||
return groupMap.value.get(id) || `#${id}`;
|
return groupMap.value.get(id) || `#${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveDirectorScope = () => {
|
||||||
|
const hospitalId = userStore.userInfo?.hospitalId || null;
|
||||||
|
const departmentId = userStore.userInfo?.departmentId || null;
|
||||||
|
if (!hospitalId || !departmentId) {
|
||||||
|
ElMessage.error('当前主任账号缺少医院或科室归属');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { hospitalId, departmentId };
|
||||||
|
};
|
||||||
|
|
||||||
const fetchCommonData = async () => {
|
const fetchCommonData = async () => {
|
||||||
const [hospitalRes, departmentRes, groupRes] = await Promise.all([
|
const [hospitalRes, departmentRes, groupRes] = await Promise.all([
|
||||||
getHospitals({ page: 1, pageSize: 100 }),
|
getHospitals({ page: 1, pageSize: 100 }),
|
||||||
@ -427,7 +468,7 @@ const fetchData = async () => {
|
|||||||
const res = await getUsers({
|
const res = await getUsers({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
role: searchForm.role || undefined,
|
role: (isDirector.value ? 'DOCTOR' : searchForm.role) || undefined,
|
||||||
keyword: searchForm.keyword || undefined,
|
keyword: searchForm.keyword || undefined,
|
||||||
});
|
});
|
||||||
tableData.value = res.list || [];
|
tableData.value = res.list || [];
|
||||||
@ -444,15 +485,29 @@ const handleSearch = () => {
|
|||||||
|
|
||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
searchForm.keyword = '';
|
searchForm.keyword = '';
|
||||||
searchForm.role = '';
|
searchForm.role = isDirector.value ? 'DOCTOR' : '';
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreateDialog = async () => {
|
const openCreateDialog = async () => {
|
||||||
|
resetForm();
|
||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
currentId.value = null;
|
currentId.value = null;
|
||||||
|
|
||||||
|
if (isDirector.value) {
|
||||||
|
const directorScope = resolveDirectorScope();
|
||||||
|
if (!directorScope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.role = 'DOCTOR';
|
||||||
|
form.hospitalId = directorScope.hospitalId;
|
||||||
|
await fetchDepartmentsForForm(form.hospitalId);
|
||||||
|
form.departmentId = directorScope.departmentId;
|
||||||
|
await fetchGroupsForForm(form.departmentId);
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (userStore.role === 'HOSPITAL_ADMIN') {
|
if (userStore.role === 'HOSPITAL_ADMIN') {
|
||||||
form.hospitalId = userStore.userInfo?.hospitalId || null;
|
form.hospitalId = userStore.userInfo?.hospitalId || null;
|
||||||
@ -460,9 +515,16 @@ const openCreateDialog = async () => {
|
|||||||
await fetchDepartmentsForForm(form.hospitalId);
|
await fetchDepartmentsForForm(form.hospitalId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditDialog = async (row) => {
|
const openEditDialog = async (row) => {
|
||||||
|
if (isDirector.value && row.role !== 'DOCTOR') {
|
||||||
|
ElMessage.warning('主任仅可编辑本科室医生');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isEdit.value = true;
|
isEdit.value = true;
|
||||||
currentId.value = row.id;
|
currentId.value = row.id;
|
||||||
|
|
||||||
@ -505,6 +567,31 @@ const resetForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildSubmitPayload = () => {
|
const buildSubmitPayload = () => {
|
||||||
|
if (isDirector.value) {
|
||||||
|
const directorScope = resolveDirectorScope();
|
||||||
|
if (!directorScope) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: form.name,
|
||||||
|
phone: form.phone,
|
||||||
|
role: 'DOCTOR',
|
||||||
|
hospitalId: directorScope.hospitalId,
|
||||||
|
departmentId: directorScope.departmentId,
|
||||||
|
groupId: form.groupId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isEdit.value) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
password: form.password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
@ -575,6 +662,9 @@ const handleSubmit = async () => {
|
|||||||
submitLoading.value = true;
|
submitLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = buildSubmitPayload();
|
const payload = buildSubmitPayload();
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateUser(currentId.value, payload);
|
await updateUser(currentId.value, payload);
|
||||||
ElMessage.success('更新成功');
|
ElMessage.success('更新成功');
|
||||||
@ -592,6 +682,11 @@ const handleSubmit = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
|
if (isDirector.value && row.role !== 'DOCTOR') {
|
||||||
|
ElMessage.warning('主任仅可删除本科室医生');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ElMessageBox.confirm(`确定要删除用户 "${row.name}" 吗?`, '警告', {
|
ElMessageBox.confirm(`确定要删除用户 "${row.name}" 吗?`, '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
@ -698,16 +793,24 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (route.query.action === 'create') {
|
if (route.query.action === 'create') {
|
||||||
await openCreateDialog();
|
await openCreateDialog();
|
||||||
if (route.query.hospitalId) {
|
if (!isDirector.value && route.query.hospitalId) {
|
||||||
form.hospitalId = Number(route.query.hospitalId);
|
form.hospitalId = Number(route.query.hospitalId);
|
||||||
await fetchDepartmentsForForm(form.hospitalId);
|
await fetchDepartmentsForForm(form.hospitalId);
|
||||||
}
|
}
|
||||||
if (route.query.departmentId) {
|
if (!isDirector.value && route.query.departmentId) {
|
||||||
form.departmentId = Number(route.query.departmentId);
|
form.departmentId = Number(route.query.departmentId);
|
||||||
await fetchGroupsForForm(form.departmentId);
|
await fetchGroupsForForm(form.departmentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canDeleteUser = (row) => {
|
||||||
|
if (userStore.role === 'SYSTEM_ADMIN') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDirector.value && row.role === 'DOCTOR';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user