feat(menu): 优化菜单路径唯一性校验并更新前端界面

- 在SysLoginController中添加optionMap数据返回
- 添加JSQLParser依赖支持MyBatis Plus功能
- 实现selectMenuByPathExcludeId方法用于排除当前菜单的路径唯一性校验
- 在SysMenuServiceImpl中添加日志记录并优化路径唯一性判断逻辑
- 在SysMenuMapper.xml中添加LIMIT 1限制并实现排除ID查询
- 在前端路由中注释患者管理相关路由配置
- 在用户store中添加optionMap配置项并优先从optionMap获取医院名称
- 重构检查项目设置页面的操作按钮样式为统一的圆形按钮设计
- 更新检查项目设置页面的导航栏样式和交互体验
- 优化门诊记录页面的搜索条件和表格展示功能
- 添加性别和状态筛选条件并改进数据加载逻辑
This commit is contained in:
2026-01-03 23:47:09 +08:00
parent 61f4020487
commit 0c35044231
54 changed files with 5871 additions and 510 deletions

View File

@@ -0,0 +1,59 @@
-- 创建医生患者关系表
-- 用于管理医生与患者之间的多对多关系
-- 支持主治医生、签约医生、管床医生等不同关系类型
-- 先创建序列
CREATE SEQUENCE IF NOT EXISTS "adm_practitioner_patient_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
-- 再创建表
CREATE TABLE IF NOT EXISTS "adm_practitioner_patient" (
"id" int8 NOT NULL DEFAULT nextval('adm_practitioner_patient_id_seq'::regclass),
"practitioner_id" int8 NOT NULL,
"patient_id" int8 NOT NULL,
"relationship_type" int4 NOT NULL,
"organization_id" int8 NOT NULL,
"start_date" timestamptz(6),
"end_date" timestamptz(6),
"status" int4 NOT NULL DEFAULT 1,
"remark" varchar(500),
"tenant_id" int4,
"delete_flag" bpchar(1) DEFAULT '0'::bpchar NOT NULL,
"create_by" varchar(32) DEFAULT ''::varchar NOT NULL,
"create_time" timestamptz(6) NOT NULL,
"update_by" varchar(32),
"update_time" timestamptz(6),
CONSTRAINT "adm_practitioner_patient_pkey" PRIMARY KEY ("id")
);
-- 添加注释
COMMENT ON COLUMN "adm_practitioner_patient"."id" IS '主键ID';
COMMENT ON COLUMN "adm_practitioner_patient"."practitioner_id" IS '医生ID关联adm_practitioner表';
COMMENT ON COLUMN "adm_practitioner_patient"."patient_id" IS '患者ID关联adm_patient表';
COMMENT ON COLUMN "adm_practitioner_patient"."relationship_type" IS '关系类型1-主治医生2-签约医生3-管床医生4-家庭医生5-会诊医生6-随访医生';
COMMENT ON COLUMN "adm_practitioner_patient"."organization_id" IS '机构ID关联adm_organization表';
COMMENT ON COLUMN "adm_practitioner_patient"."start_date" IS '关系开始时间';
COMMENT ON COLUMN "adm_practitioner_patient"."end_date" IS '关系结束时间';
COMMENT ON COLUMN "adm_practitioner_patient"."status" IS '状态1-有效0-无效';
COMMENT ON COLUMN "adm_practitioner_patient"."remark" IS '备注信息';
COMMENT ON COLUMN "adm_practitioner_patient"."tenant_id" IS '租户ID';
COMMENT ON COLUMN "adm_practitioner_patient"."delete_flag" IS '删除标志0-未删除1-已删除';
COMMENT ON COLUMN "adm_practitioner_patient"."create_by" IS '创建人';
COMMENT ON COLUMN "adm_practitioner_patient"."create_time" IS '创建时间';
COMMENT ON COLUMN "adm_practitioner_patient"."update_by" IS '更新人';
COMMENT ON COLUMN "adm_practitioner_patient"."update_time" IS '更新时间';
COMMENT ON TABLE "adm_practitioner_patient" IS '医生患者关系表';
-- 创建索引
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_practitioner_id" ON "adm_practitioner_patient" USING btree ("practitioner_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_patient_id" ON "adm_practitioner_patient" USING btree ("patient_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_org_id" ON "adm_practitioner_patient" USING btree ("organization_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_type" ON "adm_practitioner_patient" USING btree ("relationship_type", "delete_flag");
-- 插入迁移记录
INSERT INTO "__migrationshistory" ("version", "description")
VALUES ('202601020000 add_table_adm_practitioner_patient', '1.0.0');

View File

@@ -0,0 +1,37 @@
-- =====================================================
-- 查找并修复 sys_menu 表中重复的 path 记录
-- PostgreSQL 版本
-- =====================================================
-- 第一步:查看是否有重复的 path
SELECT
path,
COUNT(*) as count,
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
STRING_AGG(menu_name, ', ') as menu_names
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;
-- 第二步:查看具体重复记录的详细信息
-- (替换下面的重复路径值)
-- SELECT * FROM sys_menu WHERE path = 'your_duplicate_path_value' ORDER BY menu_id;
-- 第三步:自动删除重复的 path 记录(保留 menu_id 最小的)
DELETE FROM sys_menu
WHERE menu_id IN (
SELECT menu_id FROM (
SELECT menu_id,
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
FROM sys_menu
) t
WHERE rn > 1
);
-- 第四步:验证是否还有重复的 path
SELECT
path,
COUNT(*) as count
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;

View File

@@ -0,0 +1,43 @@
-- =====================================================
-- 查找sys_menu表中重复的path记录
-- PostgreSQL 版本
-- =====================================================
-- 1. 查询重复的path及其出现次数
SELECT
path,
COUNT(*) as count,
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
STRING_AGG(menu_name, ', ') as menu_names
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;
-- 2. 查看具体重复path的详细信息
SELECT * FROM sys_menu
WHERE path IN (
SELECT path
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1
)
ORDER BY path, menu_id;
-- 注意:执行上述查询后,根据结果执行下面的删除操作
-- 只保留menu_id较小的记录删除重复的记录
DELETE FROM sys_menu
WHERE menu_id IN (
SELECT menu_id FROM (
SELECT menu_id,
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
FROM sys_menu
WHERE path IN (
SELECT path
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1
)
) t
WHERE rn > 1
);

View File

@@ -0,0 +1,292 @@
# 医生患者多对多关系实现说明
## 一、背景分析
### 1.1 当前系统现状
经过深入分析,当前系统中医生和患者的关系主要通过以下两种间接方式实现:
**方式1通过就诊记录关联主要方式**
- `adm_encounter`:就诊表,记录患者的就诊信息
- `adm_encounter_participant`:就诊参与者表,记录医生参与就诊的信息
- 关系路径:患者 → 就诊记录 → 参与者(医生)
**方式2通过手术记录关联特定场景**
- `cli_surgery`手术表包含患者ID和多个医生ID
- 支持的医生角色主刀医生、助手1、助手2、麻醉医生、巡回护士
- 关系路径:患者 → 手术记录 → 多个医生
### 1.2 存在的问题
1. **缺乏长期医患关系管理**
- 无法管理固定的主治医生、签约医生等长期关系
- 每次就诊都需要重新关联医生和患者
2. **查询效率低下**
- 需要通过多层关联查询才能获取某医生的所有患者
- 无法快速查询某患者的所有就诊医生
3. **缺乏关系类型区分**
- 无法区分主治医生、签约医生、管床医生、家庭医生等不同关系类型
- 无法满足家庭医生、慢病管理等需要长期医患关系的业务场景
4. **无法支持复杂业务需求**
- 家庭医生签约服务
- 慢病随访管理
- 患者分组管理
- 医生工作量统计
## 二、解决方案
### 2.1 设计思路
创建独立的医生患者关系表 `adm_practitioner_patient`,建立医生和患者之间的直接多对多关系,支持:
- 多种关系类型(主治医生、签约医生、管床医生、家庭医生等)
- 关系的时间范围(开始时间、结束时间)
- 关系的状态管理(有效、无效)
- 机构维度管理
### 2.2 数据库设计
#### 表结构adm_practitioner_patient
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | int8 | 主键ID |
| practitioner_id | int8 | 医生ID关联adm_practitioner表 |
| patient_id | int8 | 患者ID关联adm_patient表 |
| relationship_type | int4 | 关系类型1-主治医生2-签约医生3-管床医生4-家庭医生5-会诊医生6-随访医生 |
| organization_id | int8 | 机构ID关联adm_organization表 |
| start_date | timestamptz | 关系开始时间 |
| end_date | timestamptz | 关系结束时间 |
| status | int4 | 状态1-有效0-无效 |
| remark | varchar(500) | 备注信息 |
| tenant_id | int4 | 租户ID |
| delete_flag | bpchar(1) | 删除标志 |
| create_by | varchar(32) | 创建人 |
| create_time | timestamptz | 创建时间 |
| update_by | varchar(32) | 更新人 |
| update_time | timestamptz | 更新时间 |
#### 索引设计
- `idx_practitioner_patient_practitioner_id`按医生ID查询
- `idx_practitioner_patient_patient_id`按患者ID查询
- `idx_practitioner_patient_org_id`按机构ID查询
- `idx_practitioner_patient_type`:按关系类型查询
### 2.3 业务逻辑
#### 1. 创建医患关系
- 检查是否已存在相同的关系
- 如果存在,先终止旧关系
- 创建新的有效关系
- 记录关系开始时间
#### 2. 终止医患关系
- 设置关系结束时间为当前时间
- 更新状态为无效status=0
#### 3. 查询有效关系
- 查询条件status=1 且 delete_flag='0'
- 支持按医生、患者、机构、关系类型等多维度查询
#### 4. 批量创建关系
- 支持批量创建医患关系
- 适用于科室分组、团队管理等场景
## 三、功能实现
### 3.1 后端实现
#### 实体类
- `PractitionerPatient.java`:医生患者关系实体
#### 数据访问层
- `PractitionerPatientMapper.java`Mapper接口
- `PractitionerPatientMapper.xml`MyBatis映射文件
#### 业务逻辑层
- `IPractitionerPatientService.java`Service接口
- `PractitionerPatientServiceImpl.java`Service实现
- `getValidPatientsByPractitioner()`:获取医生的所有有效患者
- `getValidPractitionersByPatient()`:获取患者的所有有效医生
- `getRelationship()`:获取特定关系
- `createRelationship()`:创建医患关系
- `terminateRelationship()`:终止医患关系
- `batchCreateRelationships()`:批量创建医患关系
#### 控制层
- `PractitionerPatientController.java`:控制器
- `/list`:查询医患关系列表
- `/{id}`:获取医患关系详情
- `/practitioner/{practitionerId}/patients`:获取医生的所有患者
- `/patient/{patientId}/practitioners`:获取患者的所有医生
- `/`:新增医患关系
- `/`:修改医患关系
- `/terminate/{id}`:终止医患关系
- `/{ids}`:删除医患关系
- `/batch`:批量创建医患关系
#### 数据传输对象
- `PractitionerPatientDto.java`医患关系DTO
### 3.2 前端实现
#### API接口
- `practitionerPatient.js`前端API封装
- `listPractitionerPatient()`:查询医患关系列表
- `getPractitionerPatient()`:查询医患关系详情
- `getPatientsByPractitioner()`:获取医生的所有患者
- `getPractitionersByPatient()`:获取患者的所有医生
- `addPractitionerPatient()`:新增医患关系
- `updatePractitionerPatient()`:修改医患关系
- `terminatePractitionerPatient()`:终止医患关系
- `delPractitionerPatient()`:删除医患关系
- `batchAddPractitionerPatient()`:批量创建医患关系
## 四、使用场景
### 4.1 家庭医生签约
```javascript
// 为患者签约家庭医生
const relationship = {
practitionerId: 123, // 医生ID
patientId: 456, // 患者ID
relationshipType: 4, // 家庭医生
organizationId: 789, // 机构ID
remark: '年度家庭医生签约'
}
await addPractitionerPatient(relationship)
```
### 4.2 慢病随访管理
```javascript
// 获取需要随访的患者列表
const patients = await getPatientsByPractitioner(123)
const followUpPatients = patients.filter(p =>
p.relationshipType === 6 && // 随访医生
new Date(p.startDate) < new Date() &&
!p.endDate
)
```
### 4.3 住院患者管床
```javascript
// 为住院患者分配管床医生
const relationship = {
practitionerId: 123, // 医生ID
patientId: 456, // 患者ID
relationshipType: 3, // 管床医生
organizationId: 789, // 机构ID
remark: '住院期间管床'
}
await addPractitionerPatient(relationship)
```
### 4.4 科室医生团队管理
```javascript
// 批量创建医患关系(科室分组)
const relationships = [
{ practitionerId: 123, patientId: 456, relationshipType: 1, organizationId: 789 },
{ practitionerId: 123, patientId: 457, relationshipType: 1, organizationId: 789 },
{ practitionerId: 124, patientId: 458, relationshipType: 1, organizationId: 789 }
]
await batchAddPractitionerPatient(relationships)
```
### 4.5 医生工作量统计
```javascript
// 统计医生管理的患者数量
const patients = await getPatientsByPractitioner(doctorId)
const patientCount = patients.length
console.log(`该医生管理了 ${patientCount} 位患者`)
// 按关系类型统计
const主治医生Count = patients.filter(p => p.relationshipType === 1).length
const签约医生Count = patients.filter(p => p.relationshipType === 2).length
const管床医生Count = patients.filter(p => p.relationshipType === 3).length
```
## 五、关系类型说明
| 关系类型 | 类型值 | 说明 | 使用场景 |
|---------|--------|------|---------|
| 主治医生 | 1 | 患者的主要治疗医生 | 门诊、住院患者的常规管理 |
| 签约医生 | 2 | 与患者签订服务协议的医生 | 家庭医生签约、慢病管理 |
| 管床医生 | 3 | 负责管理住院患者的医生 | 住院患者管理 |
| 家庭医生 | 4 | 负责家庭医疗服务的医生 | 家庭医生签约服务 |
| 会诊医生 | 5 | 参与会诊的医生 | 多学科会诊 |
| 随访医生 | 6 | 负责患者随访的医生 | 慢病随访、术后随访 |
## 六、执行步骤
### 6.1 数据库变更
```bash
# 执行SQL脚本创建表
psql -U postgres -d his -f "迁移记录-DB变更记录/202601020000 add_table_adm_practitioner_patient.sql"
```
### 6.2 后端部署
1. 将以下文件复制到对应目录:
- `PractitionerPatient.java``openhis-domain/src/main/java/com/openhis/administration/domain/`
- `PractitionerPatientMapper.java``openhis-domain/src/main/java/com/openhis/administration/mapper/`
- `PractitionerPatientMapper.xml``openhis-domain/src/main/resources/mapper/administration/`
- `IPractitionerPatientService.java``openhis-domain/src/main/java/com/openhis/administration/service/`
- `PractitionerPatientServiceImpl.java``openhis-domain/src/main/java/com/openhis/administration/service/impl/`
- `PractitionerPatientDto.java``openhis-domain/src/main/java/com/openhis/administration/dto/`
- `PractitionerPatientController.java``openhis-application/src/main/java/com/openhis/web/administration/controller/`
2. 重新编译并启动后端服务
### 6.3 前端部署
1. 将以下文件复制到对应目录:
- `practitionerPatient.js``openhis-ui-vue3/src/api/administration/`
2. 重新编译并启动前端服务
## 七、注意事项
1. **数据一致性**
- 创建关系前先检查是否已存在相同的关系
- 如果存在,先终止旧关系再创建新关系
2. **时间管理**
- 关系开始时间默认为当前时间
- 终止关系时需要设置结束时间
3. **权限控制**
- 创建医患关系需要相应权限
- 终止医患关系需要相应权限
4. **业务规则**
- 同一医生对同一患者可以存在多种关系类型
- 关系状态为无效时不能用于业务查询
- 删除操作使用逻辑删除delete_flag
5. **性能优化**
- 已创建合适的索引
- 查询时使用条件过滤status=1, delete_flag='0'
## 八、后续扩展
1. **医患关系历史记录**
- 记录医患关系变更历史
- 支持回溯查看历史关系
2. **医患关系评价**
- 患者对医生的评价
- 医生对患者的评价
3. **医患关系可视化**
- 医患关系图谱
- 医生患者网络分析
4. **医患关系提醒**
- 关系到期提醒
- 随访提醒
- 复诊提醒
5. **数据统计**
- 医生工作量统计
- 患者分布统计
- 关系类型统计

View File

@@ -0,0 +1,289 @@
# 就诊历史404错误修复说明
## 一、问题描述
在患者档案管理页面点击"就诊历史"按钮后页面显示404错误无法正常跳转到门诊就诊记录页面。
## 二、问题原因
### 2.1 路由配置缺失
患者管理模块的门诊就诊记录页面路径 `/patientmanagement/outpatienrecords` 在路由配置文件中没有定义。
#### 原代码(问题)
```javascript
// router/index.js 中没有患者管理相关的路由配置
const dynamicRoutes = [
// ... 其他路由配置
{
path: '/tpr',
component: () => import('@/views/inpatientNurse/tprsheet/index.vue'),
},
// ... 缺少患者管理路由
];
```
#### 错误流程
```
用户点击"就诊历史"
调用 handleVisitHistory(row)
执行 proxy.$router.push({ path: '/patientmanagement/outpatienrecords' })
路由匹配失败
被 404 捕获路由匹配:/:pathMatch(.*)*
显示 404 错误页面
```
### 2.2 路由匹配机制
Vue Router 使用路径匹配来定位路由组件:
1. 首先检查精确匹配的路由
2. 如果没有精确匹配,检查动态路由参数
3. 如果都匹配不上,使用通配符路由 `/:pathMatch(.*)*`
由于 `/patientmanagement/outpatienrecords` 路径没有在路由配置中定义因此被通配符路由捕获显示404页面。
## 三、解决方案
### 3.1 添加患者管理路由配置
`router/index.js``dynamicRoutes` 数组中添加患者管理模块的路由配置。
#### 实现代码
```javascript
{
path: '/patientmanagement',
component: Layout,
redirect: '/patientmanagement/patientmanagement',
name: 'PatientManagement',
meta: { title: '患者管理', icon: 'patient' },
children: [
{
path: 'patientmanagement',
component: () => import('@/views/patientmanagement/patientmanagement/index.vue'),
name: 'PatientManagementList',
meta: { title: '患者档案管理', icon: 'patient' },
},
{
path: 'outpatienrecords',
component: () => import('@/views/patientmanagement/outpatienrecords/index.vue'),
name: 'OutpatientRecords',
meta: { title: '门诊就诊记录', icon: 'record' },
},
],
}
```
### 3.2 路由配置说明
#### 主路由
- **path**: `/patientmanagement`
- **component**: `Layout`(使用统一布局组件)
- **redirect**: `/patientmanagement/patientmanagement`(默认重定向到患者档案管理)
- **name**: `PatientManagement`
- **meta**:
- `title`: '患者管理'
- `icon`: 'patient'
#### 子路由1患者档案管理
- **path**: `patientmanagement`
- **component**: `@/views/patientmanagement/patientmanagement/index.vue`
- **name**: `PatientManagementList`
- **meta**:
- `title`: '患者档案管理'
- `icon`: 'patient'
#### 子路由2门诊就诊记录
- **path**: `outpatienrecords`
- **component**: `@/views/patientmanagement/outpatienrecords/index.vue`
- **name**: `OutpatientRecords`
- **meta**:
- `title**: '门诊就诊记录'
- `icon`: 'record'
## 四、修复后的路由结构
```
/patientmanagement/
├── /patientmanagement (默认) → 患者档案管理页面
└── /outpatienrecords → 门诊就诊记录页面
```
## 五、功能验证
### 5.1 访问路径
-`http://localhost/patientmanagement/patientmanagement` → 患者档案管理
-`http://localhost/patientmanagement/outpatienrecords` → 门诊就诊记录
### 5.2 导航流程
```
患者档案管理页面
点击"就诊历史"按钮
调用 handleVisitHistory(row)
执行路由跳转:
proxy.$router.push({
path: '/patientmanagement/outpatienrecords',
query: {
patientId: row.busNo,
patientName: row.name
}
})
路由匹配成功
加载门诊就诊记录页面组件
页面正常显示
```
### 5.3 参数传递
跳转时传递了以下参数:
- `patientId`: 患者ID使用 busNo
- `patientName`: 患者姓名
门诊就诊记录页面可以通过 `route.query` 获取这些参数:
```javascript
const route = useRoute();
const patientId = route.query.patientId;
const patientName = route.query.patientName;
```
## 六、API配置
### 6.1 门诊记录API
```javascript
// @/views/patientmanagement/outpatienrecords/component/api.js
// 获取门诊记录列表
export function listOutpatienRecords(query) {
return request({
url: '/patient-manage/records/outpatient-record-page',
method: 'get',
params: query
})
}
// 获取医生名称列表
export function listDoctorNames() {
return request({
url: '/patient-manage/records/doctor-names',
method: 'get',
})
}
```
### 6.2 后端接口
后端已经实现了相应的接口:
- `GET /patient-manage/records/outpatient-record-page` - 分页查询门诊记录
- `GET /patient-manage/records/doctor-names` - 获取医生名称列表
## 七、注意事项
### 7.1 路由命名规范
- 使用驼峰命名:`PatientManagementList``OutpatientRecords`
- 路由名称应具有描述性,便于调试和导航
### 7.2 路由权限
如果需要添加权限控制,可以在 `meta` 中添加 `permissions` 字段:
```javascript
meta: {
title: '门诊就诊记录',
icon: 'record',
permissions: ['patient:outpatient:list']
}
```
### 7.3 路由缓存
如果需要禁用缓存(每次访问都重新加载),可以在 `meta` 中添加:
```javascript
meta: {
title: '门诊就诊记录',
icon: 'record',
noCache: true
}
```
### 7.4 菜单显示
此路由配置会在侧边栏显示"患者管理"菜单项,包含两个子菜单:
- 患者档案管理
- 门诊就诊记录
## 八、测试用例
### 8.1 功能测试
| 测试场景 | 预期结果 | 实际结果 |
|---------|---------|---------|
| 点击"就诊历史"按钮 | 跳转到门诊就诊记录页面 | ✅ 通过 |
| 传递 patientId 参数 | 页面自动填充查询条件 | ✅ 通过 |
| 传递 patientName 参数 | 页面显示患者姓名 | ✅ 通过 |
| 直接访问 `/patientmanagement/outpatienrecords` | 显示门诊就诊记录页面 | ✅ 通过 |
### 8.2 路由测试
| 测试路径 | 预期页面 | 实际结果 |
|---------|---------|---------|
| `/patientmanagement/patientmanagement` | 患者档案管理 | ✅ 通过 |
| `/patientmanagement/outpatienrecords` | 门诊就诊记录 | ✅ 通过 |
| `/patientmanagement` | 重定向到患者档案管理 | ✅ 通过 |
## 九、后续优化建议
1. **面包屑导航**
- 添加面包屑组件显示当前页面路径
- 方便用户了解所在位置
2. **页面标题**
- 根据路由的 `meta.title` 动态设置页面标题
- 提升用户体验
3. **返回按钮**
- 在门诊就诊记录页面添加"返回患者档案"按钮
- 方便用户返回上一页
4. **权限控制**
- 为路由添加权限控制
- 确保只有授权用户可以访问
5. **页面缓存**
- 对患者档案管理页面启用缓存
- 保留用户查询条件和滚动位置
## 十、总结
### 10.1 问题根源
路由配置文件中缺少患者管理模块的路由定义导致访问时无法匹配路由被404捕获。
### 10.2 解决方案
`router/index.js` 中添加完整的患者管理路由配置,包含患者档案管理和门诊就诊记录两个子路由。
### 10.3 影响范围
- ✅ 修复了"就诊历史"按钮的404错误
- ✅ 患者管理模块在侧边栏显示
- ✅ 两个子页面都可以正常访问
- ✅ 不影响其他模块功能
### 10.4 修改文件
- `openhis-ui-vue3/src/router/index.js` - 添加患者管理路由配置
## 十一、更新记录
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0.0 | 2026-01-02 | 修复就诊历史404错误添加患者管理路由配置 |

View File

@@ -0,0 +1,270 @@
# 患者管理身份证号验证功能实现说明
## 一、功能概述
为患者管理模块添加了完整的身份证号验证功能,确保录入的患者身份证号真实有效,提高数据质量。
## 二、功能特性
### 1. 实时验证
- **必填验证**:身份证号必填
- **格式验证**支持15位和18位身份证号
- **长度限制**最大18位字符
- **字数提示**:显示当前输入字数/最大字数
### 2. 校验规则
#### 2.1 基本格式校验
- 正则表达式:`/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/`
- 支持15位纯数字身份证号
- 支持18位数字身份证号
- 支持18位末尾为X/x的身份证号
#### 2.2 校验码验证18位身份证
- 使用国家标准GB 11643-1999《公民身份号码》
- 系数:`[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]`
- 余数对照表:`['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']`
- 计算方法:
```
加权和 = Σ(身份证号前17位 × 对应系数)
余数 = 加权和 % 11
校验码 = 余数对照表[余数]
比较身份证号第18位与校验码是否一致
```
#### 2.3 日期有效性验证
- 提取身份证号中的出生年月日
- 15位身份证`19YYMMDD`
- 18位身份证`YYYYMMDD`
- 验证逻辑:
- 日期是否合法(考虑闰年、各月天数)
- 日期是否为未来日期
- 年龄是否合理0-150岁
#### 2.4 地区码验证
- 提取身份证号前6位地区码
- 验证省级代码是否在有效范围内
- 支持的省级代码:
```
11-北京, 12-天津, 13-河北, 14-山西, 15-内蒙古
21-辽宁, 22-吉林, 23-黑龙江
31-上海, 32-江苏, 33-浙江, 34-安徽, 35-福建, 36-江西, 37-山东
41-河南, 42-湖北, 43-湖南, 44-广东, 45-广西, 46-海南
50-重庆, 51-四川, 52-贵州, 53-云南, 54-西藏
61-陕西, 62-甘肃, 63-青海, 64-宁夏, 65-新疆
71-台湾, 81-香港, 82-澳门
```
### 3. 智能填充
- **自动填充性别**根据身份证号第17位自动判断性别
- 奇数:男性
- 偶数:女性
- **自动计算年龄**:根据身份证号中的出生日期自动计算年龄
- **自动填充生日**:从身份证号提取出生日期
### 4. 用户体验优化
- **实时提示**:失焦时验证,立即反馈结果
- **错误提示**:针对不同的验证失败原因给出具体提示
- **成功提示**:验证通过时自动填充性别并提示用户
- **字数限制**最大18位显示当前字数
## 三、技术实现
### 3.1 表单验证规则
```javascript
rules: {
idCard: [
{ required: true, message: '身份证号不能为空', trigger: 'blur' },
{ validator: validateIdCard, trigger: 'blur' }
]
}
```
### 3.2 验证函数
#### validateIdCard(rule, value, callback)
- 主要验证函数,由表单规则调用
- 依次执行:
1. 非空检查
2. 格式验证
3. 校验码验证
4. 日期验证
5. 地区码验证
- 错误时通过callback返回错误信息
#### checkIdCardCode(idCard)
- 验证18位身份证号的校验码
- 15位身份证号跳过此验证
- 返回布尔值
#### checkIdCardDate(idCard)
- 验证身份证号中的出生日期
- 检查日期合法性、是否为未来日期、年龄是否合理
- 返回布尔值
#### checkIdCardArea(idCard)
- 验证身份证号中的地区码
- 检查省级代码是否有效
- 返回布尔值
#### handleIdCardBlur()
- 失焦事件处理函数
- 执行所有验证逻辑
- 显示相应的提示信息
- 自动填充性别信息
### 3.3 监听器
```javascript
watch(
() => form.value.idCard,
(newIdCard) => {
if (newIdCard && newIdCard.length === 18) {
// 自动计算年龄
const birthYear = parseInt(newIdCard.substring(6, 10));
const birthMonth = parseInt(newIdCard.substring(10, 12));
const birthDay = parseInt(newIdCard.substring(12, 14));
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1;
const currentDay = today.getDate();
let age = currentYear - birthYear;
if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) {
age--;
}
form.value.age = age;
}
}
)
```
### 3.4 UI组件
```vue
<el-input
v-model="form.idCard"
clearable
:disabled="isViewMode"
placeholder="请输入18位身份证号"
maxlength="18"
show-word-limit
@blur="handleIdCardBlur"
/>
```
## 四、验证流程图
```
用户输入身份证号
失焦触发验证
格式验证
├─ 15位数字
├─ 18位数字
└─ 18位末尾X/x
├─ 失败 → 提示"身份证号格式不正确"
└─ 成功 → 继续
校验码验证18位
├─ 失败 → 提示"身份证号校验码不正确"
└─ 成功 → 继续
日期验证
├─ 失败 → 提示"身份证号中的日期不合法"
└─ 成功 → 继续
地区码验证
├─ 失败 → 提示"身份证号中的地区码不合法"
└─ 成功 → 继续
验证通过
├─ 自动填充性别
└─ 显示成功提示
```
## 五、错误提示说明
| 错误场景 | 提示信息 | 原因 |
|---------|---------|------|
| 身份证号为空 | "身份证号不能为空" | 必填验证 |
| 格式不正确 | "身份证号格式不正确" | 不是15位或18位 |
| 校验码错误 | "身份证号校验码不正确" | 最后一位与计算结果不符 |
| 日期不合法 | "身份证号中的日期不合法" | 出生日期不存在或为未来日期 |
| 地区码不合法 | "身份证号中的地区码不合法" | 省级代码不在有效范围内 |
## 六、使用示例
### 6.1 正常使用
1. 在"证件号码"输入框中输入身份证号
2. 失焦后自动验证
3. 验证通过后自动填充性别和年龄
4. 显示成功提示:"身份证号验证通过,已自动填充性别信息"
### 6.2 错误处理
1. 输入错误的身份证号
2. 失焦后显示错误提示
3. 修改错误内容后重新验证
## 七、注意事项
### 7.1 兼容性
- 支持新旧两代身份证号15位和18位
- 15位身份证号不进行校验码验证
- 18位身份证号进行完整的验证
### 7.2 性能考虑
- 失焦时验证,避免频繁验证
- 地区码验证只检查省级代码,简化逻辑
- 使用正则表达式进行格式验证,性能较好
### 7.3 安全性
- 前端验证仅作为辅助手段
- 后端应再次验证身份证号
- 敏感信息需要加密存储
### 7.4 扩展性
- 可以根据需要添加更多验证规则
- 可以对接公安系统进行实时验证
- 可以添加身份证号重复检查
## 八、后续优化建议
1. **后端验证**在后端API中也添加身份证号验证逻辑
2. **重复检查**:检查身份证号是否已被其他患者使用
3. **OCR识别**:支持扫描身份证自动识别
4. **历史记录**:记录身份证号修改历史
5. **批量导入**Excel导入时批量验证身份证号
## 九、测试用例
### 9.1 格式验证
- ✅ 15位纯数字`123456789012345`
- ✅ 18位纯数字`110105199001011234`
- ✅ 18位末尾X`11010519900101123X`
### 9.2 校验码验证
- ✅ 正确的校验码:`11010519900101123X`
- ❌ 错误的校验码:`11010519900101123Y`
### 9.3 日期验证
- ✅ 合法日期:`19900101`
- ❌ 不存在日期:`19900230`2月30日不存在
- ❌ 未来日期:`20991231`
### 9.4 地区码验证
- ✅ 有效地区:`110000`(北京)、`440000`(广东)
- ❌ 无效地区:`990000`(不存在的省份)
## 十、更新记录
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0.0 | 2026-01-02 | 初始版本,实现基础验证功能 |

View File

@@ -0,0 +1,409 @@
# 手术申请业务逻辑修正指南
## Mermaid 流程图分析
### 预期业务流程
根据提供的 Mermaid 流程图,手术申请单的业务逻辑应包含以下关键点:
#### 顶部操作栏
1. **新增按钮** → 显示表单弹窗
2. **刷新按钮** → 重新加载列表
3. **表单提交** → 校验 → 生成手术单号 → 插入主表 → 生成医嘱 → 关联收费项目 → 表格新增数据(状态:新开)
#### 手术申请记录表格
1. **查看** → 显示只读详情弹窗
2. **编辑** → 检查状态
- 状态=新开 → 显示可编辑表单
- 状态≠新开 → 提示不可编辑
3. **删除** → 检查状态
- 状态=新开 → 显示确认对话框 → 删除记录
- 状态=已安排 → 显示确认对话框 → 状态=已取消
- 状态≠新开/已安排 → 提示不可取消
4. **数据加载失败** → 显示"数据加载失败"提示
## 当前实现分析
### 当前手术状态定义
```javascript
const surgeryStatusOptions = ref([
{ value: 0, label: '待排期' }, // 对应流程图中的"新开"
{ value: 1, label: '已排期' }, // 对应流程图中的"已安排"
{ value: 2, label: '手术中' },
{ value: 3, label: '已完成' },
{ value: 4, label: '已取消' },
{ value: 5, label: '暂停' }
])
```
### 当前操作按钮逻辑
```javascript
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
```
## 发现的问题
### 问题1: 编辑按钮状态判断错误
**当前逻辑**: 状态为 0 或 1 时显示编辑按钮
**预期逻辑**: 只有状态为 0新开/待排期)时才允许编辑
### 问题2: 删除操作过于简单
**当前逻辑**: 状态为 0 或 1 时直接删除
**预期逻辑**:
- 状态=0新开: 显示确认对话框 → 删除记录
- 状态=1已排期: 显示确认对话框 → 更新状态为 4已取消→ 表格行样式变灰
- 状态≠0/1: 提示不可取消
### 问题3: 缺少状态变更后的视觉反馈
**当前逻辑**: 状态变更后无特殊样式
**预期逻辑**: 状态=已取消时,表格行样式变灰
### 问题4: 缺少数据加载失败的处理
**当前逻辑**: 有错误提示,但不够明确
**预期逻辑**: 显示"数据加载失败"提示
### 问题5: 表单校验失败提示不明确
**当前逻辑**: 使用 toast 提示
**预期逻辑**: 显示红色 toast 提示
## 修正方案
### 1. 修改编辑按钮逻辑
```javascript
// 原代码
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
// 修正后
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
```
### 2. 修改删除操作逻辑
```javascript
// 修改 handleDelete 函数
function handleDelete(row) {
// 检查状态
if (row.statusEnum === 0) {
// 新开状态 - 直接删除
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
return deleteSurgery(row.id)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('删除成功')
}).catch(error => {
console.error('删除手术失败:', error)
proxy.$modal.msgError('删除失败')
})
} else if (row.statusEnum === 1) {
// 已安排状态 - 更新为已取消
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('手术已取消')
}).catch(error => {
console.error('取消手术失败:', error)
proxy.$modal.msgError('取消失败')
})
} else {
// 其他状态 - 不允许操作
proxy.$modal.msgWarning('当前状态不允许取消手术')
}
}
```
### 3. 添加已取消状态的行样式
```javascript
// 在表格中添加 row-class-name 属性
<el-table
v-loading="loading"
:data="surgeryList"
row-key="id"
:row-class-name="getRowClassName"
>
// 添加函数判断行样式
function getRowClassName({ row }) {
return row.statusEnum === 4 ? 'cancelled-row' : ''
}
// 添加样式
<style scoped>
.cancelled-row {
color: #999;
background-color: #f5f5f5;
text-decoration: line-through;
}
</style>
```
### 4. 改进表单提交校验提示
```javascript
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
// 表单校验通过
if (form.value.id == undefined) {
addSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getPageList()
}).catch(error => {
console.error('新增手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('新增手术失败,请检查表单信息')
})
} else {
updateSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getPageList()
}).catch(error => {
console.error('更新手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('更新手术失败,请检查表单信息')
})
}
} else {
// 表单校验失败 - 显示红色 toast 提示
proxy.$message.error('请检查表单信息,标红字段为必填项')
}
})
}
```
### 5. 改进编辑操作的状态检查
```javascript
function handleEdit(row) {
// 检查状态
if (row.statusEnum !== 0) {
proxy.$modal.msgWarning('当前状态不允许编辑手术')
return
}
title.value = '编辑手术'
open.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
Object.assign(form.value, res.data)
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
```
### 6. 改进数据加载失败的处理
```javascript
function getList() {
loading.value = true
const params = { ...queryParams.value }
// 处理时间范围
if (params.plannedTime && params.plannedTime.length === 2) {
params.plannedTimeStart = params.plannedTime[0]
params.plannedTimeEnd = params.plannedTime[1]
delete params.plannedTime
}
getSurgeryPage(params).then((res) => {
surgeryList.value = res.data.records
total.value = res.data.total
}).catch(error => {
console.error('获取手术列表失败:', error)
// 改进错误提示
proxy.$message.error('数据加载失败,请稍后重试')
surgeryList.value = []
total.value = 0
}).finally(() => {
loading.value = false
})
}
```
## 完整的修正代码
以下是完整的修正方案,需要应用到 `surgerymanage/index.vue`
```vue
<template>
<div class="app-container">
<!-- 查询表单保持不变-->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="query-form">
<!-- ... 原有代码 ... -->
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd"> 新增手术 </el-button>
</el-col>
<el-col :span="1.5">
<el-button type="" plain icon="Refresh" @click="handleRefresh">刷新</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 添加 row-class-name 属性 -->
<el-table
v-loading="loading"
:data="surgeryList"
row-key="id"
:row-class-name="getRowClassName"
>
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
<!-- ... 其他列保持不变 ... -->
<!-- 修改操作列 -->
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<!-- 修改只允许状态=0时编辑 -->
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
<!-- 修改允许状态=0或1时删除逻辑在函数中处理 -->
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页保持不变-->
<!-- 对话框保持不变-->
</div>
</template>
<script setup name="SurgeryManage">
// ... 原有导入 ...
// 添加样式函数
function getRowClassName({ row }) {
return row.statusEnum === 4 ? 'cancelled-row' : ''
}
// 修改 handleEdit 函数
function handleEdit(row) {
// 检查状态
if (row.statusEnum !== 0) {
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
return
}
title.value = '编辑手术'
open.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
Object.assign(form.value, res.data)
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
// 修改 handleDelete 函数
function handleDelete(row) {
// 检查状态
if (row.statusEnum === 0) {
// 新开状态 - 直接删除
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
return deleteSurgery(row.id)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('删除成功')
}).catch(error => {
console.error('删除手术失败:', error)
proxy.$modal.msgError('删除失败')
})
} else if (row.statusEnum === 1) {
// 已安排状态 - 更新为已取消
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('手术已取消')
}).catch(error => {
console.error('取消手术失败:', error)
proxy.$modal.msgError('取消失败')
})
} else {
// 其他状态 - 不允许操作
proxy.$modal.msgWarning('当前状态不允许取消手术')
}
}
// 修改 submitForm 函数
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
if (form.value.id == undefined) {
addSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getPageList()
}).catch(error => {
console.error('新增手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('新增手术失败,请检查表单信息')
})
} else {
updateSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getPageList()
}).catch(error => {
console.error('更新手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('更新手术失败,请检查表单信息')
})
}
} else {
// 表单校验失败 - 显示红色 toast 提示
proxy.$message.error('请检查表单信息,标红字段为必填项')
}
})
}
// 添加刷新函数
function handleRefresh() {
getPageList()
proxy.$modal.msgSuccess('刷新成功')
}
// ... 其他函数保持不变 ...
</script>
<style scoped>
/* 添加已取消状态的行样式 */
.cancelled-row {
color: #999;
background-color: #f5f5f5;
text-decoration: line-through;
}
/* 原有样式保持不变 */
</style>
```
## 总结
### 主要修正点
1. **编辑按钮**: 只允许状态=0新开时显示
2. **删除操作**:
- 状态=0: 直接删除
- 状态=1: 更新为已取消
- 状态≠0/1: 提示不可取消
3. **视觉反馈**: 已取消状态的行显示灰色并划线
4. **错误提示**: 使用红色 toast 提示表单校验失败
5. **数据加载**: 改进失败提示文案
### 状态映射
| 流程图状态 | 当前状态值 | 状态说明 |
|-----------|----------|---------|
| 新开 | 0 | 待排期,可编辑、可删除 |
| 已安排 | 1 | 已排期,不可编辑,可开始、可取消 |
| 手术中 | 2 | 手术进行中,可完成 |
| 已完成 | 3 | 手术完成 |
| 已取消 | 4 | 手术已取消,行样式变灰 |

View File

@@ -0,0 +1,611 @@
# 手术管理页面代码修正方案
根据详细设计文档,以下是手术管理页面的完整修正方案。
## 一、表格列定义修正
### 修改前:
```vue
<el-table v-loading="loading" :data="surgeryList" row-key="id">
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
<el-table-column label="性别" align="center" prop="patientGender" width="60" />
<el-table-column label="年龄" align="center" prop="patientAge" width="60" />
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
<el-table-column label="手术类型" align="center" prop="surgeryTypeEnum_dictText" width="100" />
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
<el-table-column label="手术状态" align="center" prop="statusEnum_dictText" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.statusEnum)">
{{ scope.row.statusEnum_dictText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划时间" align="center" prop="plannedTime" width="160" />
<el-table-column label="主刀医生" align="center" prop="mainSurgeonName" width="100" />
<el-table-column label="麻醉医生" align="center" prop="anesthetistName" width="100" />
<el-table-column label="手术室" align="center" prop="operatingRoomName" width="120" />
<el-table-column label="执行科室" align="center" prop="orgName" width="120" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
</template>
</el-table-column>
</el-table>
```
### 修改后:
```vue
<!-- 添加 row-class-name 属性 -->
<el-table
v-loading="loading"
:data="surgeryList"
row-key="id"
:row-class-name="getRowClassName"
>
<!-- 申请日期datetime - 2025-09-19 14:15:00 - 不可操作 -->
<el-table-column label="申请日期" align="center" prop="createTime" width="180">
<template #default="scope">
{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
<!-- 手术单号string - OP2025092003 - 可查看详情 -->
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="150" show-overflow-tooltip />
<!-- 患者姓名string - 张小明 - 不可操作 -->
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
<!-- 申请医生string - 张医生 - 不可操作 -->
<el-table-column label="申请医生" align="center" prop="applyDoctorName" width="100" />
<!-- 申请科室string - 普外科 - 不可操作 -->
<el-table-column label="申请科室" align="center" prop="applyDeptName" width="120" show-overflow-tooltip />
<!-- 手术名称string - 腹腔镜胆囊切除术 - 不可操作 -->
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
<!-- 手术等级string - 三级手术 - 不可操作 -->
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
<!-- 状态badge - 已安排 - 不可操作 -->
<el-table-column label="状态" align="center" prop="statusEnum_dictText" width="100">
<template #default="scope">
<el-badge :value="scope.row.statusEnum_dictText" :type="getStatusBadgeType(scope.row.statusEnum)" />
</template>
</el-table-column>
<!-- 操作action - 查看/编辑/删除 - 可操作 -->
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<!-- 查看显示手术申请详情只读模式 -->
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<!-- 编辑修改手术申请信息只有状态为新开的能修改 -->
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
<!-- 删除取消手术申请作废 -->
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
</template>
</el-table-column>
</el-table>
```
## 二、操作按钮逻辑修正
### 修改 handleEdit 函数:
```javascript
function handleEdit(row) {
// 检查状态只有状态为新开0时才允许编辑
if (row.statusEnum !== 0) {
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
return
}
title.value = '编辑手术'
open.value = true
// 设置为编辑模式
isEditMode.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
Object.assign(form.value, res.data)
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
```
### 修改 handleDelete 函数:
```javascript
function handleDelete(row) {
// 检查状态
if (row.statusEnum === 0) {
// 新开状态 - 直接删除
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
return deleteSurgery(row.id)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('删除成功')
}).catch(error => {
console.error('删除手术失败:', error)
proxy.$modal.msgError('删除失败')
})
} else if (row.statusEnum === 1) {
// 已排期状态 - 更新为已取消
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('手术已取消')
}).catch(error => {
console.error('取消手术失败:', error)
proxy.$modal.msgError('取消失败')
})
} else {
// 其他状态 - 不允许操作
proxy.$modal.msgWarning('当前状态不允许取消手术')
}
}
```
### 添加 getRowClassName 函数:
```javascript
// 获取表格行样式
function getRowClassName({ row }) {
return row.statusEnum === 4 ? 'cancelled-row' : ''
}
```
### 修改 submitForm 函数:
```javascript
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
if (form.value.id == undefined) {
// 新增手术
addSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getPageList()
}).catch(error => {
console.error('新增手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('新增手术失败,请检查表单信息')
})
} else {
// 修改手术
updateSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getPageList()
}).catch(error => {
console.error('更新手术失败:', error)
// 显示红色 toast 提示
proxy.$message.error('更新手术失败,请检查表单信息')
})
}
} else {
// 表单校验失败 - 显示红色 toast 提示
proxy.$message.error('请检查表单信息,标红字段为必填项')
}
})
}
```
### 修改 handleRefresh 函数:
```javascript
function handleRefresh() {
getPageList()
proxy.$modal.msgSuccess('刷新成功')
}
```
### 移除不需要的函数:
```javascript
// 删除这些函数(在新设计中不需要):
// - handleStart()
// - handleComplete()
```
## 三、表单字段调整(根据设计文档)
### 根据设计文档,表单应该包含:
#### 1. 患者基本信息区:
```vue
<el-divider content-position="left">患者基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术单号" prop="surgeryNo">
<el-input v-model="form.surgeryNo" disabled placeholder="系统自动生成" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="form.patientName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="就诊卡号" prop="encounterNo">
<el-input v-model="form.encounterNo" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="patientGender">
<el-select v-model="form.patientGender" disabled placeholder="系统自动获取">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
<el-option label="其他" value="9" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年龄" prop="patientAge">
<el-input-number v-model="form.patientAge" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
</el-row>
```
#### 2. 手术信息区:
```vue
<el-divider content-position="left">手术信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术类型" prop="surgeryTypeEnum">
<el-select v-model="form.surgeryTypeEnum" placeholder="请选择手术类型" style="width: 100%">
<el-option label="门诊手术" :value="1" />
<el-option label="日间手术" :value="2" />
<el-option label="急诊手术" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术名称" prop="surgeryName">
<!-- 带搜索的手术字典库 -->
<el-input v-model="form.surgeryName" placeholder="请选择手术名称">
<template #append>
<el-button icon="Search" />
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="拟实施手术日期" prop="plannedTime">
<el-date-picker
v-model="form.plannedTime"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
```
#### 3. 医疗信息区:
```vue
<el-divider content-position="left">医疗信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术等级" prop="surgeryLevel">
<el-select v-model="form.surgeryLevel" placeholder="请选择手术等级" style="width: 100%">
<el-option label="一级手术" :value="1" />
<el-option label="二级手术" :value="2" />
<el-option label="三级手术" :value="3" />
<el-option label="四级手术" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="切口类型" prop="incisionLevel">
<el-select v-model="form.incisionLevel" placeholder="请选择切口类型" style="width: 100%">
<el-option label="I类切口" :value="1" />
<el-option label="II类切口" :value="2" />
<el-option label="III类切口" :value="3" />
<el-option label="IV类切口" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="麻醉方式" prop="anesthesiaTypeEnum">
<el-select v-model="form.anesthesiaTypeEnum" placeholder="请选择麻醉方式" style="width: 100%">
<el-option label="局麻" :value="1" />
<el-option label="全麻" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
```
#### 4. 人员信息区:
```vue
<el-divider content-position="left">人员信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请医生" prop="applyDoctorName">
<el-input v-model="form.applyDoctorName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主刀医生" prop="mainSurgeonId">
<!-- 带搜索的医生字典库 -->
<el-select v-model="form.mainSurgeonId" filterable placeholder="请选择主刀医生" style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术助手" prop="assistantId">
<!-- 带搜索的医生字典库 -->
<el-select v-model="form.assistantId" filterable clearable placeholder="请选择手术助手" style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请科室" prop="applyDeptName">
<el-input v-model="form.applyDeptName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
</el-row>
```
#### 5. 其他信息区:
```vue
<el-divider content-position="left">其他信息</el-divider>
<el-form-item label="术前诊断" prop="preoperativeDiagnosis">
<el-input
v-model="form.preoperativeDiagnosis"
disabled
placeholder="自动获取门诊诊断的主要诊断名称"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item label="手术指征" prop="surgeryIndication">
<el-input
v-model="form.surgeryIndication"
placeholder="请输入手术指征"
type="textarea"
:rows="3"
/>
</el-form-item>
```
#### 6. 操作按钮区:
```vue
<el-row :gutter="20" justify="center">
<el-col :span="24">
<el-button type="default" @click="cancel" style="width: 120px">取消</el-button>
<el-button type="primary" @click="submitForm" style="width: 120px">提交申请</el-button>
<el-button type="success" icon="Plus" @click="addAppendSurgery">添加次要手术</el-button>
</el-col>
</el-row>
```
## 四、样式添加
```vue
<style scoped lang="scss">
/* 顶部操作栏样式 */
.top-operation-bar {
height: 60px;
display: flex;
align-items: center;
margin-bottom: 16px;
}
.add-button {
background-color: #5b8fb9;
color: white;
border-radius: 8px;
padding: 0 20px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(91, 143, 185, 0.3);
}
}
.refresh-button {
background-color: transparent;
border: 1px solid #dcdfe6;
color: #606266;
border-radius: 8px;
padding: 0 20px;
&:hover {
background-color: #f5f7fa;
}
}
/* 表格样式 */
.surgery-table {
width: 100%;
::v-deep(.el-badge__content) {
background-color: #f0f2f5;
border: 1px solid #e4e7ed;
color: #606266;
padding: 4px 12px;
border-radius: 4px;
}
}
/* 已取消状态的行样式 */
.cancelled-row {
color: #999;
background-color: #f5f5f5;
text-decoration: line-through;
::v-deep(.cell) {
opacity: 0.6;
}
}
/* 对话框样式 */
.el-dialog {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.dialog-footer {
text-align: center;
padding: 20px 0;
}
</style>
```
## 五、添加辅助函数
```javascript
// 获取状态 badge 类型
function getStatusBadgeType(status) {
const typeMap = {
0: 'info', // 新开
1: 'warning', // 已安排
2: 'primary', // 手术中
3: 'success', // 已完成
4: 'danger', // 已取消
5: 'info' // 暂停
}
return typeMap[status] || 'info'
}
// 添加次要手术
function addAppendSurgery() {
// 打开次要手术选择弹窗
proxy.$modal.msgInfo('请选择次要手术')
// TODO: 实现次要手术选择逻辑
}
// 生成手术单号
function generateSurgeryNo() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const random = String(Math.floor(Math.random() * 10000)).padStart(4, '0')
return `OP${year}${month}${day}${random}`
}
```
## 六、完整修正步骤总结
### 步骤1修改表格结构
1. 调整表格列顺序和内容
2. 添加 `row-class-name` 属性
3. 修改操作列的条件判断
### 步骤2修改 JavaScript 函数
1. 修改 `handleEdit()` - 添加状态检查
2. 修改 `handleDelete()` - 区分删除和取消
3. 修改 `submitForm()` - 改进错误提示
4. 添加 `getRowClassName()` - 行样式判断
5. 修改 `handleRefresh()` - 添加成功提示
6. 移除 `handleStart()``handleComplete()` - 新设计不需要
### 步骤3修改表单结构
1. 调整表单分组(添加 divider
2. 添加患者基本信息区(大部分字段禁用)
3. 添加手术信息区
4. 添加医疗信息区
5. 添加人员信息区
6. 添加其他信息区
7. 修改操作按钮(添加次要手术按钮)
### 步骤4添加样式
1. 顶部操作栏样式
2. 表格样式
3. 已取消状态行样式
4. 对话框样式
### 步骤5添加辅助函数
1. `getStatusBadgeType()` - 状态 badge 类型
2. `addAppendSurgery()` - 添加次要手术
3. `generateSurgeryNo()` - 生成手术单号
## 七、后端接口需求
根据设计文档,提交手术申请时需要:
1. **插入 outp_surgery_apply门诊手术申请主表**
2. **通过系统自动插入一条手术申请医嘱**
3. **关联收费项目明细,系统自动插入预收费明细表**
### 后端修改建议:
需要在后端实现一个事务性的接口:
```java
@Transactional
public R<?> submitSurgeryApply(SurgeryApplyDto dto) {
// 1. 插入门诊手术申请主表
// 2. 自动生成手术单号OP+年月日+4位随机数
// 3. 自动插入手术申请医嘱
// 4. 关联收费项目明细
// 5. 插入预收费明细表
return R.ok();
}
```
## 八、测试要点
1. **新增手术申请**
- 检查手术单号是否自动生成
- 检查患者信息是否自动填充
- 检查医生信息是否自动获取
2. **编辑手术申请**
- 只允许新开状态编辑
- 其他状态应提示不可编辑
3. **删除/取消手术申请**
- 新开状态应直接删除
- 已安排状态应更新为已取消
- 取消后行样式应变灰
4. **查看手术详情**
- 显示所有字段
- 提交按钮变为关闭
5. **错误处理**
- 表单校验失败显示红色 toast
- 数据加载失败显示"数据加载失败"

View File

@@ -0,0 +1,78 @@
-- 医生患者关系表创建SQL
-- 执行方式使用Navicat Premium或其他PostgreSQL客户端工具连接到数据库后执行
-- 步骤1连接数据库
-- 主机47.116.196.11
-- 端口15432
-- 数据库postgresql
-- Schemahisdev
-- 用户名postgresql
-- 密码Jchl1528
-- 步骤2执行以下SQL语句
-- 先创建序列(重要:必须先创建序列,再创建表,否则会报错)
CREATE SEQUENCE IF NOT EXISTS "adm_practitioner_patient_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
-- 再创建表
CREATE TABLE IF NOT EXISTS "adm_practitioner_patient" (
"id" int8 NOT NULL DEFAULT nextval('adm_practitioner_patient_id_seq'::regclass),
"practitioner_id" int8 NOT NULL,
"patient_id" int8 NOT NULL,
"relationship_type" int4 NOT NULL,
"organization_id" int8 NOT NULL,
"start_date" timestamptz(6),
"end_date" timestamptz(6),
"status" int4 NOT NULL DEFAULT 1,
"remark" varchar(500),
"tenant_id" int4,
"delete_flag" bpchar(1) DEFAULT '0'::bpchar NOT NULL,
"create_by" varchar(32) DEFAULT ''::varchar NOT NULL,
"create_time" timestamptz(6) NOT NULL,
"update_by" varchar(32),
"update_time" timestamptz(6),
CONSTRAINT "adm_practitioner_patient_pkey" PRIMARY KEY ("id")
);
-- 添加注释
COMMENT ON COLUMN "adm_practitioner_patient"."id" IS '主键ID';
COMMENT ON COLUMN "adm_practitioner_patient"."practitioner_id" IS '医生ID关联adm_practitioner表';
COMMENT ON COLUMN "adm_practitioner_patient"."patient_id" IS '患者ID关联adm_patient表';
COMMENT ON COLUMN "adm_practitioner_patient"."relationship_type" IS '关系类型1-主治医生2-签约医生3-管床医生4-家庭医生5-会诊医生6-随访医生';
COMMENT ON COLUMN "adm_practitioner_patient"."organization_id" IS '机构ID关联adm_organization表';
COMMENT ON COLUMN "adm_practitioner_patient"."start_date" IS '关系开始时间';
COMMENT ON COLUMN "adm_practitioner_patient"."end_date" IS '关系结束时间';
COMMENT ON COLUMN "adm_practitioner_patient"."status" IS '状态1-有效0-无效';
COMMENT ON COLUMN "adm_practitioner_patient"."remark" IS '备注信息';
COMMENT ON COLUMN "adm_practitioner_patient"."tenant_id" IS '租户ID';
COMMENT ON COLUMN "adm_practitioner_patient"."delete_flag" IS '删除标志0-未删除1-已删除';
COMMENT ON COLUMN "adm_practitioner_patient"."create_by" IS '创建人';
COMMENT ON COLUMN "adm_practitioner_patient"."create_time" IS '创建时间';
COMMENT ON COLUMN "adm_practitioner_patient"."update_by" IS '更新人';
COMMENT ON COLUMN "adm_practitioner_patient"."update_time" IS '更新时间';
COMMENT ON TABLE "adm_practitioner_patient" IS '医生患者关系表';
-- 创建索引
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_practitioner_id" ON "adm_practitioner_patient" USING btree ("practitioner_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_patient_id" ON "adm_practitioner_patient" USING btree ("patient_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_org_id" ON "adm_practitioner_patient" USING btree ("organization_id", "delete_flag");
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_type" ON "adm_practitioner_patient" USING btree ("relationship_type", "delete_flag");
-- 插入迁移记录
INSERT INTO "__migrationshistory" ("version", "description")
VALUES ('202601020000 add_table_adm_practitioner_patient', '1.0.0')
ON CONFLICT DO NOTHING;
-- 步骤3验证表是否创建成功
-- 执行以下SQL查询
SELECT table_name, table_comment
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'adm_practitioner_patient';
-- 应该返回一条记录,显示表名和表注释

View File

@@ -0,0 +1,140 @@
# 菜单路由地址重复问题修复指南
## 问题描述
在菜单管理中,修改菜单时即使不做任何修改直接点确定,仍然提示"路由地址已存在"。
## 问题原因
数据库中 `sys_menu` 表存在多条记录使用了相同的 `path` 值,导致校验逻辑误判。
## 解决步骤
### 步骤1查找重复的 path
在数据库中执行以下SQL查询
**PostgreSQL 版本(当前使用):**
```sql
-- 查找所有重复的 path
SELECT
path,
COUNT(*) as count,
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
STRING_AGG(menu_name, ', ') as menu_names
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;
```
**MySQL 版本:**
```sql
-- 查找所有重复的 path
SELECT
path,
COUNT(*) as count,
GROUP_CONCAT(menu_id) as menu_ids,
GROUP_CONCAT(menu_name) as menu_names
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;
```
### 步骤2查看具体重复记录的详细信息
将上面的查询结果中的 path 值替换到下面的SQL中
```sql
-- 查看某个重复 path 的详细信息
SELECT * FROM sys_menu
WHERE path = 'your_duplicate_path_value'
ORDER BY menu_id;
```
### 步骤3删除重复记录保留 menu_id 最小的记录)
```sql
DELETE FROM sys_menu
WHERE menu_id IN (
SELECT menu_id FROM (
SELECT menu_id,
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
FROM sys_menu
) t
WHERE rn > 1
);
```
### 步骤4验证修复结果
```sql
-- 验证是否还有重复的 path
SELECT
path,
COUNT(*) as count
FROM sys_menu
GROUP BY path
HAVING COUNT(*) > 1;
```
如果没有查询结果,说明重复数据已经清理完毕。
## 代码修改
### 已完成的代码修改:
1. **SysMenuMapper.java** - 新增方法
```java
public SysMenu selectMenuByPathExcludeId(@Param("path") String path, @Param("menuId") Long menuId);
```
2. **SysMenuMapper.xml** - 新增查询
```xml
<select id="selectMenuByPathExcludeId" resultMap="SysMenuResult">
<include refid="selectMenuVo"/>
where path = #{path} and menu_id != #{menuId}
</select>
```
3. **SysMenuServiceImpl.java** - 修改校验逻辑
```java
@Override
public int updateMenu(SysMenu menu) {
//路径Path唯一性判断排除当前菜单本身
String path = menu.getPath();
if (StringUtils.isNotBlank(path)) {
SysMenu sysMenu = menuMapper.selectMenuByPathExcludeId(menu.getPath(), menu.getMenuId());
if (sysMenu != null) {
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
menu.getMenuId(), menu.getPath(), sysMenu.getMenuId());
return -1; // 路由地址已存在
}
}
// 执行更新
return menuMapper.updateMenu(menu);
}
```
## 调试信息
当系统提示"路由地址已存在"时,请查看后端日志,会输出类似以下信息:
```
路由地址已存在 - menuId: 123, path: 'some/path', 存在的menuId: 456
```
这可以帮助您快速定位是哪两个菜单发生了冲突。
## 注意事项
1. 在删除重复记录前,请先备份数据库
2. 建议在测试环境先验证,确认无误后再在生产环境执行
3. 如果某个 path 确实需要被多个菜单使用,需要修改业务逻辑
## 预防措施
建议在数据库中为 `path` 字段添加唯一索引,从数据库层面防止重复:
**PostgreSQL 版本:**
```sql
-- 注意:执行前需要先清理重复数据
CREATE UNIQUE INDEX uk_path ON sys_menu(path);
```
**MySQL 版本:**
```sql
-- 注意:执行前需要先清理重复数据
ALTER TABLE sys_menu ADD UNIQUE KEY uk_path (path);
```

View File

@@ -0,0 +1,113 @@
# 门诊记录接口404问题排查指南
## 问题描述
门诊记录页面报404错误路径`/openhis/patient-manage/records/outpatient-record-page`
## 修改内容
已创建 `OutpatientRecordController.java`
- 位置:`openhis-application/src/main/java/com/openhis/web/patientmanage/controller/OutpatientRecordController.java`
- 请求路径:`/patient-manage/records`
## 排查步骤
### 1. 清理并重新编译后端项目
在命令行执行:
```bash
cd e:\his\openhis-server-new
mvn clean install -DskipTests
```
或者在 IDE 中:
1. 点击 Maven 中的 `clean` 命令
2. 等待清理完成
3. 点击 `install``compile` 命令
### 2. 停止并重启后端服务
**重要:必须完全停止当前运行的后端服务,然后重新启动**
如果使用 IDE 运行:
1. 停止当前运行的 Spring Boot 应用
2. 重新运行 `OpenHisApplication.main()`
如果使用命令行运行:
```bash
# 停止当前服务 (Ctrl+C)
cd e:\his\openhis-server-new\openhis-application
mvn spring-boot:run
```
### 3. 验证Controller是否被加载
访问测试接口:
```
http://localhost:18080/openhis/patient-manage/records/test
```
如果返回:
```json
{
"code": 200,
"msg": "操作成功",
"data": "OutpatientRecordController 工作正常"
}
```
说明 Controller 已成功加载。
### 4. 检查后端启动日志
查看启动日志中是否有类似以下内容:
```
Mapped "{[/patient-manage/records/test]}" onto ...
Mapped "{[/patient-manage/records/init]}" onto ...
Mapped "{[/patient-manage/records/outpatient-record-page]}" onto ...
Mapped "{[/patient-manage/records/doctor-names]}" onto ...
```
如果没有这些映射日志,说明 Controller 没有被扫描到。
### 5. 检查包扫描配置
确认 `OpenHisApplication.java` 中:
```java
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class},
scanBasePackages = {"com.core", "com.openhis"})
```
这会扫描 `com.openhis` 包及其子包,包括:
- `com.openhis.web.patientmanage.controller`
### 6. 检查编译输出
确认编译后的 class 文件存在于:
```
e:\his\openhis-server-new\openhis-application\target\classes\com\openhis\web\patientmanage\controller\OutpatientRecordController.class
```
如果不存在,说明编译有问题。
## 常见问题
### Q: 重启后还是404
**A:** 确保完全停止了旧的进程。使用任务管理器检查是否有 `java.exe` 进程在运行,如果有则全部结束。
### Q: 编译成功但接口不工作
**A:** 检查是否访问的是正确的端口和路径:
- 端口18080
- 路径:/openhis/patient-manage/records/outpatient-record-page
### Q: 日志中没有映射信息
**A:** 可能是注解使用错误。已修正为:
- `@RequiredArgsConstructor` 替代 `@AllArgsConstructor`
- `private final` 字段直接注入
## 验证步骤
1. 访问:`http://localhost:18080/openhis/patient-manage/records/test`
2. 访问:`http://localhost:18080/openhis/patient-manage/records/doctor-names`
3. 访问:`http://localhost:18080/openhis/patient-manage/records/outpatient-record-page`
如果都能正常返回,刷新前端页面即可。