Compare commits
169 Commits
3cabb5f803
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3d011951b | ||
| 7dc76d7b59 | |||
| b2dec2667a | |||
|
|
879d31b51d | ||
|
|
473a5f7f06 | ||
| 2eec988c56 | |||
| 8820048d55 | |||
| 6af7720470 | |||
|
|
5f134945ab | ||
| bc12cc1b08 | |||
| 17b8ea7192 | |||
|
|
2bfdd686c7 | ||
|
|
066cfaba46 | ||
| e8850e85fc | |||
| d083a3123a | |||
| 96c1927f8d | |||
| bdac9d0709 | |||
| 8faba1ea21 | |||
| dd9b77f6bb | |||
| d45955f6de | |||
| f905915f34 | |||
|
|
52951d7296 | ||
| 3c47979913 | |||
| 9aad809322 | |||
| b7850e5b8a | |||
|
|
effcdfbbe6 | ||
| 4277a369d2 | |||
| cf3f971741 | |||
| 75737cf95c | |||
| 4b544dc214 | |||
| 597e621b69 | |||
| 725ac4b76a | |||
| 8e8b35faa4 | |||
| 664ee0312c | |||
| cceaf7fb07 | |||
| d5d638b60b | |||
| 8de5ae3a4f | |||
| 8f20c48baa | |||
|
|
547cccbeb7 | ||
| 86e665bcae | |||
|
|
d1aa91f727 | ||
| bc021924e4 | |||
| 6179a89b6c | |||
| 7c12028f63 | |||
| befb4739ee | |||
| fe07cee58c | |||
|
|
066c457d90 | ||
| 39b608dfd0 | |||
| 6b600b44ca | |||
|
|
b26ad75299 | ||
| b69f312611 | |||
| c65db9abc3 | |||
| 1b4ad5e710 | |||
| e46e2be830 | |||
| f515b90c43 | |||
| 6aff10e240 | |||
|
|
5d02da03b4 | ||
| d99188bfb9 | |||
| c3776c642b | |||
| 46a99ecd55 | |||
| 81744b9b9e | |||
| 469b325f0e | |||
| 8a3fe5461e | |||
| b65841c0cc | |||
| 8ef334ba1b | |||
|
|
2492daa0ad | ||
| 8af06f6916 | |||
| 7008fb007f | |||
| dc039fcced | |||
|
|
fcb1d771f4 | ||
| 30ca81090a | |||
| e722841e60 | |||
| b4ab67aed9 | |||
| 6a8f82bb2e | |||
| 09761c8ce8 | |||
|
|
5e3affcf3a | ||
|
|
455f7938be | ||
|
|
9525b1d927 | ||
| 8810c678c9 | |||
| cd3155e63c | |||
| 45fdca65a7 | |||
| 491db8bc03 | |||
| c735bc3a78 | |||
| e2db4bd3a5 | |||
| fc0f5a11be | |||
| c5528ce1b7 | |||
| 9116ea4a84 | |||
| ce8b0b16b1 | |||
| 259c62b6b4 | |||
| 208b8fc41d | |||
| c611c0ce6f | |||
| 25c266babb | |||
| 04ad139eae | |||
|
|
6add091a7b | ||
|
|
a05b3a8d3c | ||
| 35bc735ecc | |||
|
|
fcd2d03424 | ||
| 87c7981ad9 | |||
| 9edf8936ba | |||
|
|
753bd8bb4b | ||
| 3e09b4cc10 | |||
| 55970622f1 | |||
| 98164c65a2 | |||
|
|
d63a34f4e6 | ||
| 20ab9f890f | |||
| 038213a26c | |||
| ff41aa9c04 | |||
| f60e070984 | |||
| 3f7169844c | |||
|
|
8b993d5ddd | ||
|
|
9cfa9a3417 | ||
|
|
b200f80d88 | ||
|
|
e57baaead6 | ||
|
|
2576f62f88 | ||
|
|
cd24fe007f | ||
|
|
3898880665 | ||
| 562e618aaa | |||
| ec4d57ea69 | |||
|
|
7a5b26607e | ||
|
|
50ceb98e83 | ||
|
|
2065395bd2 | ||
|
|
517dc41f01 | ||
|
|
6f8e677045 | ||
|
|
1747291f41 | ||
|
|
cff1e0145b | ||
|
|
3ab7ea1898 | ||
|
|
c5d75f053b | ||
|
|
730476e927 | ||
| d8a4487b2b | |||
| c93a5f69d8 | |||
|
|
b826afb17c | ||
|
|
2a5a157c57 | ||
|
|
ca9b145d3e | ||
|
|
7676f03c96 | ||
|
|
b5b91d8971 | ||
|
|
f1bddf3fbe | ||
| b1c966f69f | |||
| dba6350493 | |||
|
|
6fb5b5993a | ||
|
|
4c2b015210 | ||
|
|
9f9f193287 | ||
|
|
d34a314f02 | ||
| 8d45cfe9db | |||
| f9d897c081 | |||
| d2616ac2f9 | |||
| d2a6780c23 | |||
| fc32b83980 | |||
|
|
b936654a11 | ||
| 2a525f95b9 | |||
| 585b9bd720 | |||
|
|
faf73a5ac4 | ||
| 89bf85fd97 | |||
| f3d56bff45 | |||
|
|
cd6c015d8f | ||
|
|
dfdab41c00 | ||
|
|
f69de5e78f | ||
|
|
74892ea80f | ||
|
|
4bf74a1ff0 | ||
| 3a53837e50 | |||
|
|
2c2bb1adb0 | ||
|
|
a434dfdfff | ||
| 4c14d802c4 | |||
| a7602057e2 | |||
| 97af9d5eee | |||
| dfac362c37 | |||
| 3d31b3482a | |||
| 3acf8ad50a | |||
| 270004afee | |||
| 6c15f0d4d5 |
76
.github/copilot-instructions.md
vendored
76
.github/copilot-instructions.md
vendored
@@ -1,76 +0,0 @@
|
||||
# OpenHIS — AI 编码助手 指南
|
||||
|
||||
目的:帮助自动化/AI 编码代理快速上手本仓库,包含架构要点、关键文件、常用构建/运行命令以及项目约定。请只按照仓库内真实可见的内容提出修改建议或补充说明。
|
||||
|
||||
- **代码组织**: 本项目是一个 Java 后端(多模块 Maven)+ Vue3 前端(Vite)的大型应用。
|
||||
- 后端主模块目录:`openhis-server-new/`(顶层为 `pom`,包含多个子模块)。关键子模块示例:`openhis-application`, `openhis-domain`, `openhis-common`, `core-*` 系列。
|
||||
- 前端目录:`openhis-ui-vue3/`(Vite + Vue 3,使用 Pinia、Element Plus 等)。
|
||||
|
||||
- **大局观(Big Picture)**: 后端以 Spring Boot(Java 17)实现,使用多模块 Maven 管理公共库与业务模块;前端由单独仓库目录通过 Vite 构建并以环境变量(`VITE_APP_BASE_API`)与后端交互。后端扫描 `com.core` 与 `com.openhis` 包(见 `OpenHisApplication.java`),启动类位于:`openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`。
|
||||
|
||||
- **运行/构建(Windows PowerShell 示例)**:
|
||||
- 构建后端(从仓库根执行):
|
||||
|
||||
```powershell
|
||||
cd openhis-server-new
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
- 仅运行后端模块(开发时常用):
|
||||
|
||||
```powershell
|
||||
cd openhis-server-new/openhis-application
|
||||
mvn spring-boot:run
|
||||
# 或在 IDE 中运行 `com.openhis.OpenHisApplication` 的 main()
|
||||
```
|
||||
|
||||
- 前端启动与构建(需要 Node.js v16.x,仓库 README 建议 v16.15):
|
||||
|
||||
```powershell
|
||||
cd openhis-ui-vue3
|
||||
npm install
|
||||
npm run dev # 本地开发(热重载)
|
||||
npm run build:prod # 生产构建
|
||||
```
|
||||
|
||||
- **环境与配置**:
|
||||
- 后端配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`(数据库、端口、profile 等)。README 还提及 `application-druid.yml`(若存在请优先查看)。
|
||||
- 前端配置:多个 `.env.*` 文件(例如 `.env.development`, `.env.staging`, `.env.production`),关键变量:`VITE_APP_BASE_API`(例如 `/dev-api`),前端通过 `import.meta.env.VITE_APP_BASE_API` 拼接后端 URL(见 `src/utils/request.js`、多个视图与组件)。
|
||||
|
||||
- **重要约定 / 模式**:
|
||||
- 后端采用 Java 17、Spring Boot 2.5.x 家族,父 POM在 `openhis-server-new/pom.xml` 定义。常用依赖版本在该 POM 的 `<properties>` 中集中维护。
|
||||
- 模块间以 Maven 模块依赖与 `com.core` / `com.openhis` 包名分层(见 `pom.xml` 的 `<modules>` 与 `dependencyManagement`)。
|
||||
- 前端通过 Vite 插件配置(`openhis-ui-vue3/vite/plugins`)管理 svg、自动导入等。UI 框架为 Element Plus,状态管理为 Pinia。
|
||||
|
||||
- **集成点 & 外部依赖**:
|
||||
- 数据库:PostgreSQL(README 建议 v16.2),仓库根含一个大型初始化 SQL:`数据库初始话脚本(请使用navicat16版本导入).sql`,用于初始化表与演示数据。
|
||||
- 缓存/会话:Redis(需自行配置)。
|
||||
- 其他:Flowable(工作流),Druid(连接池监控),第三方服务通过特定配置类(例如 `YbServiceConfig` 在 `OpenHisApplication` 中启用)。
|
||||
|
||||
- **调试与常见位置**:
|
||||
- 启动类:`openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`。
|
||||
- 全局配置:`openhis-server-new/openhis-application/src/main/resources/`(`application.yml`、profile 文件等)。
|
||||
- 前端入口:`openhis-ui-vue3/src/main.js`、路由在 `openhis-ui-vue3/src/router/index.js`。
|
||||
- API 文档与监控路径(通常由后端暴露并被前端访问):
|
||||
- Swagger UI: `<VITE_APP_BASE_API>/swagger-ui/index.html`(前端视图在 `src/views/tool/swagger/index.vue`)。
|
||||
- Druid: `<VITE_APP_BASE_API>/druid/login.html`(见前端相关视图引用)。
|
||||
|
||||
- **为 AI 代理的具体建议(如何安全、有效地修改代码)**:
|
||||
- 修改后端时:优先在子模块(例如 `openhis-application`)本地运行 `mvn spring-boot:run` 验证启动与基础 API;大量改动前先执行 `mvn -T1C -DskipTests clean package` 在 CI 环境上验证构建(本地机器也可用)。
|
||||
- 修改前端时:检查/调整对应 `.env.*` 文件中的 `VITE_APP_BASE_API`,使用 `npm run dev` 本地联调后端接口(可通过代理或将 `VITE_APP_BASE_API` 指向后端地址)。
|
||||
- 修改数据库结构或 seed:请参考仓库根的 SQL 初始化脚本,任何 DDL/数据变更需同步该脚本并通知数据库管理员/运维。
|
||||
|
||||
- **举例(常见任务示例)**:
|
||||
- 本地联调前端 + 后端(PowerShell):
|
||||
|
||||
```powershell
|
||||
# 启动后端
|
||||
cd openhis-server-new/openhis-application
|
||||
mvn spring-boot:run
|
||||
|
||||
# 启动前端(另开终端)
|
||||
cd openhis-ui-vue3
|
||||
npm run dev
|
||||
```
|
||||
|
||||
如需我把这些内容合并为更短或更详细的版本,或把其中某部分(例如后端模块依赖关系图、关键 Java 包说明)展开,请告诉我要增强哪一节。
|
||||
@@ -1,26 +0,0 @@
|
||||
# 修复门诊预约界面专家号查询结果显示问题
|
||||
|
||||
## 问题分析
|
||||
1. 前端传递的参数正确:`type=expert`,后端正确转换为`ticketType=专家`
|
||||
2. 实际查询返回了5条记录,但COUNT查询只返回了1条记录
|
||||
3. 这导致前端只显示了1条记录,而不是全部5条
|
||||
4. 原因:MyBatis-Plus自动生成的COUNT查询和实际查询使用了不同的条件,特别是逻辑删除条件
|
||||
|
||||
## 解决方案
|
||||
1. 修改TicketMapper.xml中的自定义COUNT查询,显式添加`delete_flag = '0'`条件
|
||||
2. 在selectTicketPage和selectTicketPage_mpCount查询中都添加逻辑删除条件
|
||||
3. 确保两个查询使用完全相同的WHERE条件
|
||||
|
||||
## 修复步骤
|
||||
1. 修改`selectTicketPage`查询,添加逻辑删除条件`and delete_flag = '0'`
|
||||
2. 修改`selectTicketPage_mpCount`查询,添加逻辑删除条件`and delete_flag = '0'`
|
||||
3. 确保两个查询的WHERE条件完全一致
|
||||
4. 测试修复后的功能,确保专家号能正确显示全部5条记录
|
||||
|
||||
## 代码修改点
|
||||
- 文件:`d:/work/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/TicketMapper.xml`
|
||||
- 查询:`selectTicketPage` 和 `selectTicketPage_mpCount`
|
||||
- 修改内容:添加逻辑删除条件`and delete_flag = '0'`
|
||||
|
||||
## 预期效果
|
||||
修复后,COUNT查询和实际查询将使用完全相同的条件,包括逻辑删除条件,从而确保COUNT查询返回正确的总记录数,前端能显示所有5条专家号记录。
|
||||
@@ -1,30 +0,0 @@
|
||||
# 修复门诊预约界面专家号查询COUNT结果不正确问题
|
||||
|
||||
## 问题分析
|
||||
1. 前端传递的参数正确:`type=expert`,后端正确转换为`ticketType=专家`
|
||||
2. COUNT查询和实际查询的WHERE条件完全相同:`WHERE delete_flag = '0' AND ticket_type = '专家'`
|
||||
3. 但COUNT查询只返回1条记录,而实际查询返回5条记录
|
||||
4. 原因:MyBatis-Plus的分页插件在处理自定义COUNT查询时,存在bug,导致COUNT查询结果不正确
|
||||
|
||||
## 解决方案
|
||||
修改`TicketAppServiceImpl.java`中的`listTicket`方法,不使用MyBatis-Plus的自动分页功能,而是手动实现分页查询:
|
||||
1. 直接调用`ticketService.countTickets`方法获取总记录数
|
||||
2. 手动构建查询条件
|
||||
3. 确保COUNT查询和实际查询使用完全相同的条件
|
||||
|
||||
## 修复步骤
|
||||
1. 修改`TicketAppServiceImpl.java`中的`listTicket`方法
|
||||
2. 手动实现分页查询,包括:
|
||||
- 构建查询条件
|
||||
- 调用`countTickets`获取总记录数
|
||||
- 调用`selectTicketList`获取分页数据
|
||||
- 手动组装分页结果
|
||||
3. 测试修复后的功能,确保专家号能正确显示全部5条记录
|
||||
|
||||
## 代码修改点
|
||||
- 文件:`d:/work/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
|
||||
- 方法:`listTicket`
|
||||
- 修改内容:替换MyBatis-Plus的自动分页,改为手动分页实现
|
||||
|
||||
## 预期效果
|
||||
修复后,COUNT查询和实际查询将使用完全相同的条件,COUNT查询将返回正确的总记录数(5条),前端能显示所有5条专家号记录。
|
||||
@@ -1,32 +0,0 @@
|
||||
## 问题分析
|
||||
根据日志和代码分析,发现号源列表显示"没有更多数据了"的问题原因:
|
||||
|
||||
1. **后端查询正常**:成功查询到5条符合条件的专家号源记录
|
||||
2. **数据转换失败**:在`convertToDto`方法中,`fee`字段类型转换错误
|
||||
3. **响应返回空列表**:由于转换异常,最终返回给前端的号源列表为空
|
||||
|
||||
## 问题根源
|
||||
- `Ticket`实体类的`fee`字段为**BigDecimal类型**(数据库存储)
|
||||
- `TicketDto`类的`fee`字段为**String类型**(前端展示)
|
||||
- 在`convertToDto`方法中,直接将BigDecimal类型的`fee`赋值给String类型的`fee`,导致**ClassCastException**
|
||||
|
||||
## 修复方案
|
||||
修改`TicketAppServiceImpl.java`文件中的`convertToDto`方法,将BigDecimal类型的`fee`转换为String类型:
|
||||
|
||||
```java
|
||||
// 原代码
|
||||
dto.setFee(ticket.getFee());
|
||||
|
||||
// 修复后代码
|
||||
dto.setFee(ticket.getFee().toString());
|
||||
```
|
||||
|
||||
## 预期效果
|
||||
1. 修复后,后端能成功将`Ticket`实体转换为`TicketDto`
|
||||
2. 前端能接收到包含5条专家号源的完整列表
|
||||
3. 页面显示正常,不再出现"没有更多数据了"的提示
|
||||
|
||||
## 验证方法
|
||||
1. 重新启动项目,访问号源管理页面
|
||||
2. 选择"专家号"类型,查看是否能正确显示5条号源记录
|
||||
3. 检查日志,确认没有类型转换异常
|
||||
@@ -1,23 +0,0 @@
|
||||
# 修复门诊预约界面专家号查询问题
|
||||
|
||||
## 问题分析
|
||||
从日志中发现关键问题:
|
||||
- 前端传递的ticket_type值是英文:`general` (普通号) 和 `expert` (专家号)
|
||||
- 数据库中存储的ticket_type值是中文:`普通` 和 `专家`
|
||||
- 导致查询条件不匹配,无法查询到数据
|
||||
|
||||
## 解决方案
|
||||
需要在后端添加类型映射转换,将前端传递的英文类型转换为数据库中存储的中文类型。
|
||||
|
||||
## 修复步骤
|
||||
1. 修改 `TicketAppServiceImpl.java` 文件,在处理type参数时添加映射转换逻辑
|
||||
2. 添加从英文类型到中文类型的映射关系
|
||||
3. 测试修复后的功能,确保普通号和专家号都能正确查询
|
||||
|
||||
## 代码修改点
|
||||
- 文件:`d:/work/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
|
||||
- 方法:`listTicket` 中的type参数处理部分
|
||||
- 修改内容:添加类型映射转换,将 "general" 转换为 "普通","expert" 转换为 "专家"
|
||||
|
||||
## 预期效果
|
||||
修复后,前端选择"普通号"或"专家号"时,系统能正确查询到对应的号源数据,不再出现"没有更多数据了"的提示。
|
||||
@@ -1,23 +0,0 @@
|
||||
**问题分析**:
|
||||
后端返回的响应格式是`{code: 200, msg: "操作成功", data: {total: 5, limit: 20, page: 1, list: [5条记录]}}`,而前端可能期望直接访问`list`属性,导致只能显示1条数据。
|
||||
|
||||
**修复方案**:
|
||||
|
||||
1. 修改`TicketAppServiceImpl.java`的`listTicket`方法,确保返回的分页数据格式正确
|
||||
2. 调整响应结构,使其更符合前端期望
|
||||
3. 保持与现有代码的兼容性
|
||||
|
||||
**修改点**:
|
||||
|
||||
* `TicketAppServiceImpl.java`:优化`listTicket`方法的响应格式
|
||||
|
||||
* 确保分页信息和列表数据都能正确返回给前端
|
||||
|
||||
**预期效果**:
|
||||
|
||||
* 后端返回正确格式的响应数据
|
||||
|
||||
* 前端能够正确显示所有5条专家号数据
|
||||
|
||||
* 保持与现有代码的兼容性
|
||||
|
||||
188
AGENTS.md
Normal file
188
AGENTS.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# OpenHIS - AI Agent Development Guide
|
||||
|
||||
## 项目概览
|
||||
OpenHIS 是一个医院管理系统,采用 Java 17 + Spring Boot 后端和 Vue 3 + Vite 前端架构。
|
||||
|
||||
## 构建和运行命令
|
||||
|
||||
### 后端(Java/Spring Boot)
|
||||
```bash
|
||||
# 构建整个项目
|
||||
cd openhis-server-new
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 运行后端(开发模式)
|
||||
cd openhis-server-new/openhis-application
|
||||
mvn spring-boot:run
|
||||
|
||||
# 运行特定模块
|
||||
cd openhis-server-new/[module-name]
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### 前端(Vue 3 + Vite)
|
||||
```bash
|
||||
# 安装依赖
|
||||
cd openhis-ui-vue3
|
||||
npm install
|
||||
|
||||
# 开发服务器
|
||||
npm run dev
|
||||
|
||||
# 生产构建
|
||||
npm run build:prod
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:test
|
||||
|
||||
# 预览构建结果
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 测试
|
||||
项目当前没有配置正式的测试框架。如需添加测试:
|
||||
- 后端:考虑使用 JUnit 5 + Mockito
|
||||
- 前端:考虑使用 Vitest + Vue Test Utils
|
||||
|
||||
## 代码风格规范
|
||||
|
||||
### Java 后端规范
|
||||
- **Java 版本**: 17
|
||||
- **框架**: Spring Boot 2.5.15
|
||||
- **ORM**: MyBatis Plus 3.5.5
|
||||
- **数据库**: PostgreSQL
|
||||
- **包结构**:
|
||||
- `com.openhis` - 业务逻辑
|
||||
- `com.core` - 核心框架
|
||||
- **命名约定**:
|
||||
- 类名:PascalCase(如 `UserController`)
|
||||
- 方法名:camelCase(如 `getUserList`)
|
||||
- 常量:SCREAMING_SNAKE_CASE
|
||||
- 配置文件:kebab-case
|
||||
- **注解使用**:
|
||||
- 使用 `@Slf4j` 替代手动声明 logger
|
||||
- 使用 `@Data` 在实体类中
|
||||
- 使用 `@Service/@Controller/@Repository` 等 Spring 注解
|
||||
- **异常处理**:
|
||||
- 使用统一的异常处理机制
|
||||
- 自定义业务异常继承 `RuntimeException`
|
||||
|
||||
### Vue 前端规范
|
||||
- **框架**: Vue 3 + Composition API
|
||||
- **UI 库**: Element Plus
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 4
|
||||
- **构建工具**: Vite 5
|
||||
- **组件命名**: PascalCase
|
||||
- **文件命名**: kebab-case
|
||||
- **变量命名**: camelCase
|
||||
- **常量命名**: SCREAMING_SNAKE_CASE
|
||||
- **函数命名**:
|
||||
- 事件处理:`handle` 前缀
|
||||
- 数据获取:`get`/`load` 前缀
|
||||
- 提交操作:`submit` 前缀
|
||||
|
||||
### 导入顺序
|
||||
#### Java
|
||||
1. `java.*`
|
||||
2. `javax.*`
|
||||
3. 第三方库
|
||||
4. `com.core.*`
|
||||
5. `com.openhis.*`
|
||||
6. `*.*`(其他包)
|
||||
|
||||
#### JavaScript/Vue
|
||||
1. `vue` 相关
|
||||
2. 第三方库
|
||||
3. `@/` 别名导入
|
||||
4. 相对路径导入
|
||||
|
||||
### 代码格式
|
||||
#### Java
|
||||
- 缩进:4个空格
|
||||
- 行长度:120字符
|
||||
- 左大括号不换行
|
||||
|
||||
#### Vue/JavaScript
|
||||
- 缩进:2个空格
|
||||
- 字符串:优先使用单引号
|
||||
- 行长度:100字符
|
||||
|
||||
## 关键配置文件
|
||||
|
||||
### 后端配置
|
||||
- 主配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`
|
||||
- 环境配置:`application-{profile}.yml`
|
||||
- Maven 父 POM:`openhis-server-new/pom.xml`
|
||||
|
||||
### 前端配置
|
||||
- Vite 配置:`openhis-ui-vue3/vite.config.js`
|
||||
- 环境变量:`.env.*` 文件
|
||||
- 路由配置:`openhis-ui-vue3/src/router/index.js`
|
||||
|
||||
## 开发约定
|
||||
|
||||
### API 设计
|
||||
- RESTful API 风格
|
||||
- 统一响应格式
|
||||
- 使用 Swagger 文档
|
||||
- 错误码统一管理
|
||||
|
||||
### 数据库
|
||||
- 表名:snake_case
|
||||
- 字段名:snake_case
|
||||
- 主键:使用 `id`
|
||||
- 软删除:使用 `valid_flag` 字段
|
||||
|
||||
### 前端组件
|
||||
- 单一职责原则
|
||||
- Props 使用 camelCase
|
||||
- Events 使用 kebab-case
|
||||
- 使用 Composition API
|
||||
- 组件文档使用 JSDoc
|
||||
|
||||
### 状态管理
|
||||
- 模块化设计
|
||||
- 异步操作使用 actions
|
||||
- 避免在组件中直接修改状态
|
||||
|
||||
## 环境变量
|
||||
|
||||
### 前端
|
||||
- `VITE_APP_BASE_API`: API 基础路径
|
||||
- `VITE_APP_ENV`: 环境标识
|
||||
|
||||
### 后端
|
||||
- `spring.profiles.active`: 激活的配置文件
|
||||
- `core.name`: 应用名称
|
||||
- `core.version`: 应用版本
|
||||
|
||||
## 安全规范
|
||||
- 所有 API 接口需要权限验证
|
||||
- 敏感信息使用环境变量
|
||||
- SQL 注入防护
|
||||
- XSS 攻击防护
|
||||
|
||||
## 性能优化
|
||||
- 后端使用连接池(Druid)
|
||||
- 前端使用路由懒加载
|
||||
- 图片使用 WebP 格式
|
||||
- 大列表使用虚拟滚动
|
||||
|
||||
## 常用工具类
|
||||
- 后端:`com.core.common.utils.*`
|
||||
- 前端:`@/utils/*`
|
||||
|
||||
## 注意事项
|
||||
1. 修改数据库结构需要同步 SQL 脚本
|
||||
2. 新增功能需要添加权限配置
|
||||
3. 前端路由需要在权限系统中注册
|
||||
4. 接口变更需要更新 Swagger 文档
|
||||
5. 遵循现有代码风格,避免不必要的变化
|
||||
|
||||
## 故障排除
|
||||
- 后端端口:18080
|
||||
- 前端端口:81
|
||||
- API 前缀:`/openhis`
|
||||
- Swagger UI:`/openhis/swagger-ui/index.html`
|
||||
- Druid 监控:`/openhis/druid/login.html`
|
||||
376
CODEBUDDY.md
376
CODEBUDDY.md
@@ -1,376 +0,0 @@
|
||||
# CODEBUDDY.md
|
||||
|
||||
This file provides guidance to CodeBuddy Code when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a comprehensive Hospital Information System (HIS) built with a Java Spring Boot backend and Vue 3 frontend.
|
||||
|
||||
- **Backend**: Java 17, Spring Boot 2.5.15, multi-module Maven architecture
|
||||
- **Frontend**: Vue 3, Vite, Element Plus, Pinia state management
|
||||
- **Database**: PostgreSQL (recommended v16.2)
|
||||
- **Cache**: Redis
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── openhis-server-new/ # Backend multi-module Maven project
|
||||
│ ├── openhis-application/ # Main application module with startup class
|
||||
│ ├── openhis-domain/ # Business domain modules (administration, clinical, financial, etc.)
|
||||
│ ├── openhis-common/ # Shared utilities and common code
|
||||
│ ├── core-admin/ # Core administration module
|
||||
│ ├── core-framework/ # Framework configuration and security
|
||||
│ ├── core-system/ # System management module
|
||||
│ ├── core-quartz/ # Scheduled tasks
|
||||
│ ├── core-generator/ # Code generation utilities
|
||||
│ ├── core-common/ # Core utilities
|
||||
│ └── core-flowable/ # Workflow engine integration
|
||||
└── openhis-ui-vue3/ # Vue 3 frontend
|
||||
├── src/
|
||||
│ ├── api/ # API service layer
|
||||
│ ├── components/ # Reusable components
|
||||
│ ├── router/ # Vue Router configuration
|
||||
│ ├── store/ # Pinia state management
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── views/ # Page components
|
||||
└── vite/ # Vite plugins configuration
|
||||
```
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
### Backend (Java)
|
||||
|
||||
**Build the entire backend:**
|
||||
```bash
|
||||
cd openhis-server-new
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
**Run the backend application (development):**
|
||||
```bash
|
||||
cd openhis-server-new/openhis-application
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
**Alternative: Run directly from IDE:**
|
||||
- Run the main method in `openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
|
||||
|
||||
**Start scripts:**
|
||||
- Linux/Mac: `openhis-server-new/start.sh`
|
||||
- Windows: `openhis-server-new/start.bat`
|
||||
|
||||
### Frontend (Vue 3)
|
||||
|
||||
**Install dependencies:**
|
||||
```bash
|
||||
cd openhis-ui-vue3
|
||||
npm install
|
||||
```
|
||||
|
||||
**Development server (with hot reload):**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- Runs on port 81 by default
|
||||
- Proxies `/dev-api` requests to `http://localhost:18080/openhis`
|
||||
|
||||
**Build for production:**
|
||||
```bash
|
||||
npm run build:prod # Production build
|
||||
npm run build:stage # Staging build
|
||||
npm run build:test # Test environment build
|
||||
npm run build:dev # Development build
|
||||
npm run build:spug # Spug environment build
|
||||
```
|
||||
|
||||
**Preview production build:**
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
The backend uses a multi-module Maven architecture with clear separation of concerns:
|
||||
|
||||
1. **openhis-application**: Entry point with `OpenHisApplication.java` (d:\his\openhis-server-new\openhis-application\src\main\java\com\openhis\OpenHisApplication.java:20)
|
||||
- Scans `com.core` and `com.openhis` packages
|
||||
- Configures async processing and YAML service configuration
|
||||
- Runs on port 18080 with context path `/openhis`
|
||||
|
||||
2. **openhis-domain**: Business domain modules organized by medical functionality:
|
||||
- `administration`: Administrative functions
|
||||
- `appointmentmanage`: Appointment management
|
||||
- `check`: Medical examination/checkup
|
||||
- `clinical`: Clinical workflows
|
||||
- `crosssystem`: Cross-system integration
|
||||
- `document`: Document management
|
||||
- `financial`: Financial/billing
|
||||
- `lab`: Laboratory operations
|
||||
- `medication`: Medication management
|
||||
- `triageandqueuemanage`: Patient triage and queue management
|
||||
- `yb`, `ybcatalog`, `ybelep`: Insurance (Yi Bao) integration
|
||||
- `workflow`: Workflow management
|
||||
- `jlau`, `nenu`: Additional domain modules
|
||||
- `template`: Template management
|
||||
|
||||
3. **Core Modules** (com.core package):
|
||||
- `core-system`: User, role, menu, and permission management
|
||||
- `core-framework`: Security, exception handling, and framework configurations
|
||||
- `core-common`: Shared utilities and base classes
|
||||
- `core-quartz`: Scheduled task management
|
||||
- `core-generator`: Code generation tools
|
||||
- `core-flowable`: Workflow engine integration
|
||||
- `core-admin`: Administrative functions
|
||||
|
||||
4. **openhis-common**: Domain-specific shared code and utilities under `com.openhis.common` package
|
||||
|
||||
**Key Technologies:**
|
||||
- MyBatis-Plus 3.5.5 for ORM with enhanced CRUD operations
|
||||
- Druid 1.2.27 connection pool with monitoring at `/druid/*`
|
||||
- Flowable 6.8.0 for workflow management
|
||||
- LiteFlow 2.12.4.1 for business rule orchestration
|
||||
- Swagger 3.0.0 for API documentation
|
||||
- JWT 0.9.1 for authentication
|
||||
- Hutool 5.3.8 utility library
|
||||
- Fastjson2 2.0.58 for JSON processing
|
||||
- Pinyin4j 2.5.1 for Chinese character to Pinyin conversion
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
The frontend uses Vue 3 with composition API and modern tooling:
|
||||
|
||||
**Key Files:**
|
||||
- Entry point: `openhis-ui-vue3/src/main.js`
|
||||
- Router configuration: `openhis-ui-vue3/src/router/index.js`
|
||||
- Store initialization: `openhis-ui-vue3/src/store/store.js`
|
||||
- Vite configuration: `openhis-ui-vue3/vite.config.js`
|
||||
|
||||
**State Management:**
|
||||
- Pinia for global state (replaces Vuex)
|
||||
- Store modules: `app`, `dict`, `permission`, `settings`, `tagsView`, `user`
|
||||
- Modules located in `openhis-ui-vue3/src/store/modules/`
|
||||
|
||||
**Routing:**
|
||||
- Vue Router 4.3.0
|
||||
- Two types of routes:
|
||||
- `constantRoutes`: Public routes (login, 404, etc.)
|
||||
- `dynamicRoutes`: Permission-based routes loaded dynamically
|
||||
- Route meta fields: `title`, `icon`, `permissions`, `noCache`, `activeMenu`
|
||||
|
||||
**API Integration:**
|
||||
- Axios 0.27.2 for HTTP requests
|
||||
- Base API URL configured via environment variables (`VITE_APP_BASE_API`)
|
||||
- Proxy configuration in vite.config.js for development
|
||||
- `/dev-api` → `http://localhost:18080/openhis`
|
||||
- `/ybplugin` → `http://localhost:5000` (insurance plugin)
|
||||
- Request/response interceptors in `openhis-ui-vue3/src/utils/request.js`
|
||||
- API service files organized by module in `openhis-ui-vue3/src/api/`
|
||||
- `administration`, `appoinmentmanage`, `monitor`, `system`, `tool`
|
||||
- Shared APIs: `home.js`, `login.js`, `menu.js`, `public.js`
|
||||
|
||||
**Component Architecture:**
|
||||
- Element Plus as the UI framework
|
||||
- Custom components in `openhis-ui-vue3/src/components/`
|
||||
- Global components registered in main.js:
|
||||
- Pagination, TreeSelect, FileUpload, ImageUpload, ImagePreview
|
||||
- RightToolbar, Editor, DictTag
|
||||
|
||||
## Configuration
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
**Main configuration file:** `openhis-server-new/openhis-application/src/main/resources/application.yml`
|
||||
|
||||
**Environment-specific profiles:**
|
||||
- `application-dev.yml` - Development environment
|
||||
- `application-test.yml` - Test environment
|
||||
- `application-prd.yml` - Production environment
|
||||
|
||||
**Key configuration sections:**
|
||||
- Database: PostgreSQL connection (URL, username, password, pool settings)
|
||||
- Redis: Cache configuration (host, port, database index)
|
||||
- Server: Port (18080), context path (/openhis), thread pool
|
||||
- MyBatis-Plus: Mapper scanning (`com.core.**.domain,com.openhis.**.domain`), type aliases, logical delete
|
||||
- Logging: Debug levels for com.openhis and com.baomidou.mybatisplus
|
||||
- Swagger: API documentation at `/swagger-ui/index.html`
|
||||
- Druid: Database monitoring at `/druid/*` (credentials: openhis/123456)
|
||||
- Flowable: Workflow engine settings (schema update disabled)
|
||||
- LiteFlow: Business rule configuration at `config/flow.el.xml`
|
||||
- Token: JWT configuration (secret, expire time, header)
|
||||
- File upload: Max file size (10MB), max request size (20MB)
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
**Environment files** (in `openhis-ui-vue3/`):
|
||||
- `.env.dev` - Dev environment
|
||||
- `.env.development` - Development environment variables
|
||||
- `.env.staging` - Staging environment variables
|
||||
- `.env.production` - Production environment variables
|
||||
- `.env.test` - Test environment variables
|
||||
- `.env.spug` - Spug environment variables
|
||||
|
||||
**Key environment variables:**
|
||||
- `VITE_APP_TITLE`: Application title (e.g., "医院信息管理系统")
|
||||
- `VITE_APP_BASE_API`: Backend API base URL (e.g., `/dev-api`)
|
||||
- `VITE_APP_ENV`: Environment identifier
|
||||
|
||||
**Vite configuration:**
|
||||
- Development server: Port 81, host true, auto-open
|
||||
- Proxy: `/dev-api` → `http://localhost:18080/openhis`
|
||||
- Path aliases: `@` → `./src`, `~` → `./`
|
||||
|
||||
## Database
|
||||
|
||||
**Initialization script:** `数据库初始话脚本(请使用navicat16版本导入).sql` (located at repository root)
|
||||
- Use Navicat version 16 to import
|
||||
- Contains schema and initial demonstration data
|
||||
|
||||
**Database connection (dev environment):**
|
||||
- Type: PostgreSQL
|
||||
- URL: `jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=hisdev`
|
||||
- Driver: `org.postgresql.Driver`
|
||||
- Schema: `hisdev`
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Running Full Stack Locally
|
||||
|
||||
**Terminal 1 - Start backend:**
|
||||
```bash
|
||||
cd openhis-server-new/openhis-application
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
**Terminal 2 - Start frontend:**
|
||||
```bash
|
||||
cd openhis-ui-vue3
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Access the application at:
|
||||
- Frontend: http://localhost:81
|
||||
- Backend API: http://localhost:18080/openhis
|
||||
- Swagger UI: http://localhost:18080/openhis/swagger-ui/index.html
|
||||
- Druid monitoring: http://localhost:18080/openhis/druid/login.html
|
||||
|
||||
### Adding a New Backend Feature
|
||||
|
||||
1. Create domain entity in appropriate module under `openhis-domain/[module]/domain/`
|
||||
2. Create mapper interface in `openhis-domain/[module]/mapper/`
|
||||
3. Create mapper XML in `openhis-domain/[module]/resources/mapper/` (if custom SQL needed)
|
||||
4. Create service interface and implementation in `openhis-domain/[module]/service/`
|
||||
5. Create controller in `openhis-application/src/main/java/com/openhis/web/[module]/`
|
||||
6. Add MyBatis-Plus annotations if using enhanced features
|
||||
7. Test endpoints via Swagger UI at `http://localhost:18080/openhis/swagger-ui/index.html`
|
||||
|
||||
**Note:** Controllers are organized under `com.openhis.web` by business module (e.g., `web.administration`, `web.clinicalmanage`, `web.patientmanage`, etc.)
|
||||
|
||||
### Adding a New Frontend Page
|
||||
|
||||
1. Create Vue component in `openhis-ui-vue3/src/views/[module]/`
|
||||
2. Add API service methods in `openhis-ui-vue3/src/api/`
|
||||
3. Add route to `openhis-ui-vue3/src/router/index.js` (constantRoutes or dynamicRoutes)
|
||||
4. Add Pinia store module if state management needed
|
||||
5. Register global components if reusable
|
||||
|
||||
### Testing
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
cd openhis-server-new
|
||||
mvn test
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
- Run unit tests (if configured):
|
||||
```bash
|
||||
cd openhis-ui-vue3
|
||||
npm test
|
||||
```
|
||||
|
||||
## Key Patterns and Conventions
|
||||
|
||||
### Backend
|
||||
|
||||
- Package structure follows domain-driven design
|
||||
- Service layer uses `@Service` annotation
|
||||
- Controllers use `@RestController` with request mapping
|
||||
- MyBatis-Plus base mapper: `BaseMapper<T>`
|
||||
- Logical delete field: `validFlag` (1 = active, 0 = deleted)
|
||||
- Use `@EnableAsync` for async processing
|
||||
- JWT token stored in `Authorization` header
|
||||
|
||||
### Frontend
|
||||
|
||||
- Use Vue 3 Composition API (`<script setup>`)
|
||||
- Element Plus components with Chinese locale (zhCn)
|
||||
- API calls through centralized request utility in `src/utils/request.js`
|
||||
- Route-based permission control
|
||||
- Dictionary data through `useDict()` composable
|
||||
- Global properties: `$download`, `$downloadGet`, `$parseTime`, `$resetForm`, `$handleTree`, `$formatDateStr`
|
||||
- CSS in SCSS with global styles in `src/assets/styles/index.scss`
|
||||
- Registered global components: DictTag, Pagination, TreeSelect, FileUpload, ImageUpload, ImagePreview, RightToolbar, Editor
|
||||
- Hiprint plugin for printing functionality (window.hiprint)
|
||||
|
||||
## Important Files
|
||||
|
||||
### Backend
|
||||
- Startup class: `openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
|
||||
- Main config: `openhis-server-new/openhis-application/src/main/resources/application.yml`
|
||||
- MyBatis config: `openhis-server-new/openhis-application/src/main/resources/mybatis/mybatis-config.xml`
|
||||
- Parent POM: `openhis-server-new/pom.xml`
|
||||
|
||||
### Frontend
|
||||
- Entry point: `openhis-ui-vue3/src/main.js`
|
||||
- Router: `openhis-ui-vue3/src/router/index.js`
|
||||
- Request utils: `openhis-ui-vue3/src/utils/request.js`
|
||||
- Vite config: `openhis-ui-vue3/vite.config.js`
|
||||
- Environment files: `openhis-ui-vue3/.env.*`
|
||||
|
||||
## External Integrations
|
||||
|
||||
- **PostgreSQL 42.2.27**: Primary database
|
||||
- **MySQL Connector 9.4.0**: MySQL database support (alternative)
|
||||
- **Redis**: Caching and session management
|
||||
- **Flowable 6.8.0**: Workflow engine
|
||||
- **LiteFlow 2.12.4.1**: Business rule engine
|
||||
- **Swagger 3.0.0**: API documentation
|
||||
- **Druid 1.2.27**: Database connection pool and monitoring
|
||||
- **Element Plus 2.12.0**: Vue 3 UI component library
|
||||
- **Pinia 2.2.0**: State management
|
||||
- **Vite 5.0.4**: Build tool and dev server
|
||||
- **Hutool 5.3.8**: Java utility library
|
||||
- **Fastjson2 2.0.58**: JSON processing
|
||||
- **Pinyin4j 2.5.1**: Chinese character to Pinyin conversion
|
||||
- **iText 5.5.12**: PDF generation
|
||||
- **Apache POI 4.1.2**: Excel file processing
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### WebView Integration
|
||||
- Frontend supports WebView environment (e.g., embedded in desktop applications)
|
||||
- Chrome WebView integration with C# accessor (`chrome.webview.hostObjects.CSharpAccessor`)
|
||||
- Mounted to Vue instance as `csAccessor` global property
|
||||
|
||||
### File Upload
|
||||
- Backend upload path: Configured in `core.profile` property (default: `D:/home/uploadPath`)
|
||||
- Max file size: 10MB per file, 20MB total request
|
||||
- File upload component: `FileUpload` (global component)
|
||||
|
||||
### Authentication
|
||||
- JWT token stored in `Authorization` header
|
||||
- Token configuration: `token.secret`, `token.expireTime`, `token.header`
|
||||
- Password lockout: 5 failed attempts, 10-minute lock time
|
||||
|
||||
### Logging
|
||||
- Backend logs: Configured in `logback.xml`
|
||||
- Debug logging enabled for: `com.openhis`, `com.baomidou.mybatisplus`, `com.alibaba.druid`
|
||||
- Druid slow SQL threshold: 1000ms
|
||||
|
||||
### Code Generation
|
||||
- Backend code generator: `core-generator` module
|
||||
- Access via Swagger or `/tool/gen` route
|
||||
- Uses Velocity templates in `openhis-application/src/main/resources/vm/`
|
||||
674
LICENSE
674
LICENSE
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright 2022-2025 湖北天天数链技术有限公司
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
OpenHis Copyright (C) 2022-2025 湖北天天数链技术有限公司
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,223 +0,0 @@
|
||||
# MyBatis-Plus 自动填充处理器优化指南
|
||||
|
||||
## 概述
|
||||
本文档说明如何优化 `MybastisColumnsHandler` 以确保所有实体的审计字段(create_by、create_time、update_by、update_time)能够正确自动填充。
|
||||
|
||||
## 问题背景
|
||||
在 OpenHIS 系统中,当保存实体时可能会遇到以下错误:
|
||||
```
|
||||
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||
```
|
||||
|
||||
这是因为数据库表中的审计字段设置了 NOT NULL 约束,但在某些情况下自动填充机制未能正确设置这些字段。
|
||||
|
||||
## 解决方案
|
||||
通过优化 `MybastisColumnsHandler` 来确保总是使用当前登录用户的用户名填充 `create_by` 字段,使用当前时间填充 `create_time` 字段。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 1. 替换现有处理器
|
||||
将 `D:\his\openhis-server-new\core-framework\src\main\java\com\core\framework\handler\MybastisColumnsHandler.java` 文件替换为以下内容:
|
||||
|
||||
```java
|
||||
package com.core.framework.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.framework.config.TenantContext;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 用于自动填充创建时间和更新时间,以及创建人和更新人
|
||||
*/
|
||||
@Component
|
||||
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||
|
||||
// 设置数据新增时的字段自动赋值规则
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
// 填充创建时间
|
||||
Date currentTime = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||
|
||||
// 获取当前登录用户名
|
||||
String username = getCurrentUsername();
|
||||
|
||||
// 填充创建人
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||
|
||||
// 确保tenantId被设置
|
||||
Integer tenantId = getCurrentTenantId();
|
||||
if (tenantId == null) {
|
||||
throw new RuntimeException("无法获取当前租户ID,请确保用户已登录或正确设置租户上下文");
|
||||
}
|
||||
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
|
||||
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
|
||||
}
|
||||
|
||||
// 设置数据修改时的字段自动赋值规则
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
// 填充更新时间
|
||||
Date currentTime = new Date();
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
|
||||
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
|
||||
|
||||
// 填充更新人
|
||||
String username = getCurrentUsername();
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名
|
||||
* @return 当前登录用户名,如果无法获取则返回 "system"
|
||||
*/
|
||||
private String getCurrentUsername() {
|
||||
String username = "system"; // 默认值
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
username = loginUser.getUsername();
|
||||
} else {
|
||||
// 尝试从请求中获取用户信息
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
// 可以在这里添加额外的逻辑来从请求中获取用户信息
|
||||
// 例如从请求头、session等获取用户信息
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录异常但不中断处理流程
|
||||
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
|
||||
// 可以考虑记录日志
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户 ID
|
||||
*/
|
||||
private Integer getCurrentTenantId() {
|
||||
Integer result = null;
|
||||
|
||||
// 首先尝试从线程局部变量中获取租户ID(适用于定时任务等场景)
|
||||
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
|
||||
if (threadLocalTenantId != null) {
|
||||
result = threadLocalTenantId;
|
||||
} else {
|
||||
// 获取当前登录用户的租户ID(优先使用SecurityUtils中储存的LoginUser的租户ID)
|
||||
try {
|
||||
if (SecurityUtils.getAuthentication() != null) {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
result = loginUser.getTenantId();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录异常但不中断处理
|
||||
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
// 尝试从请求头中获取租户ID
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
if (request != null) {
|
||||
// 从请求头获取租户ID,假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
|
||||
String tenantIdHeader = request.getHeader("X-Tenant-ID");
|
||||
String requestMethodName = request.getHeader("Request-Method-Name");
|
||||
// 登录
|
||||
if ("login".equals(requestMethodName)) {
|
||||
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
|
||||
try {
|
||||
result = Integer.parseInt(tenantIdHeader);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有获取到租户ID,返回默认值
|
||||
if (result == null) {
|
||||
System.out.println("警告: 未能获取当前租户ID,将使用默认租户ID 1");
|
||||
result = 1; // 默认租户ID
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 验证处理器是否被正确扫描
|
||||
确保在主应用类或配置类中启用了自动填充功能:
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
|
||||
@EnableTransactionManagement // 启用事务管理
|
||||
public class OpenHisApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OpenHisApplication.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 测试验证
|
||||
创建一个简单的测试来验证自动填充是否正常工作:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
public class AuditFieldTest {
|
||||
@Autowired
|
||||
private PractitionerMapper practitionerMapper;
|
||||
|
||||
@Test
|
||||
public void testAuditFieldsAutoFill() {
|
||||
Practitioner practitioner = new Practitioner();
|
||||
practitioner.setName("Test Practitioner");
|
||||
|
||||
// 保存实体
|
||||
practitionerMapper.insert(practitioner);
|
||||
|
||||
// 验证审计字段是否被正确填充
|
||||
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||
assertThat(practitioner.getCreateBy()).isNotEqualTo("");
|
||||
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||
|
||||
// 清理测试数据
|
||||
practitionerMapper.deleteById(practitioner.getId());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全上下文**:确保在调用保存方法时用户已登录,这样 `SecurityUtils.getLoginUser()` 才能返回有效的用户对象。
|
||||
|
||||
2. **异常处理**:处理器中包含了异常处理,如果无法获取当前用户,将使用 "system" 作为默认值。
|
||||
|
||||
3. **租户ID**:处理器也处理租户ID的自动填充,这对于多租户系统很重要。
|
||||
|
||||
4. **兼容性**:处理器同时支持驼峰命名(createBy)和下划线命名(create_by)的字段,以兼容不同的配置。
|
||||
|
||||
## 总结
|
||||
通过优化 `MybastisColumnsHandler`,我们可以确保所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误,同时保持数据完整性和审计跟踪功能。
|
||||
184
QWEN.md
184
QWEN.md
@@ -1,184 +0,0 @@
|
||||
# Qwen Code Context for HIS (Hospital Information System)
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a comprehensive Hospital Information System (HIS) called OpenHIS, built with a Java Spring Boot backend and Vue 3 frontend. The system is designed to manage hospital operations including patient management, appointments, clinical workflows, billing, and administrative tasks.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
**Backend:**
|
||||
- Java 17
|
||||
- Spring Boot 2.5.15
|
||||
- PostgreSQL (recommended v16.2)
|
||||
- Redis
|
||||
- MyBatis-Plus 3.5.5 for ORM
|
||||
- Druid 1.2.27 for database connection pooling
|
||||
- Flowable 6.8.0 for workflow management
|
||||
- LiteFlow 2.12.4.1 for business rule orchestration
|
||||
- Swagger 3.0.0 for API documentation
|
||||
- JWT 0.9.1 for authentication
|
||||
|
||||
**Frontend:**
|
||||
- Vue 3 with Composition API
|
||||
- Vite 5.0.4 as build tool
|
||||
- Element Plus 2.12.0 as UI component library
|
||||
- Pinia 2.2.0 for state management
|
||||
- Axios 0.27.2 for HTTP requests
|
||||
- Sass for styling
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── openhis-server-new/ # Backend multi-module Maven project
|
||||
│ ├── openhis-application/ # Main application module with startup class
|
||||
│ ├── openhis-domain/ # Business domain modules (administration, clinical, financial, etc.)
|
||||
│ ├── openhis-common/ # Shared utilities and common code
|
||||
│ ├── core-admin/ # Core administration module
|
||||
│ ├── core-framework/ # Framework configuration and security
|
||||
│ ├── core-system/ # System management module
|
||||
│ ├── core-quartz/ # Scheduled tasks
|
||||
│ ├── core-generator/ # Code generation utilities
|
||||
│ ├── core-common/ # Core utilities
|
||||
│ └── core-flowable/ # Workflow engine integration
|
||||
├── openhis-ui-vue3/ # Vue 3 frontend
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API service layer
|
||||
│ │ ├── components/ # Reusable components
|
||||
│ │ ├── router/ # Vue Router configuration
|
||||
│ │ ├── store/ # Pinia state management
|
||||
│ │ ├── utils/ # Utility functions
|
||||
│ │ └── views/ # Page components
|
||||
│ └── vite/ # Vite plugins configuration
|
||||
├── sql/ # Database scripts
|
||||
├── 发版记录/ # Release records
|
||||
└── 迁移记录-DB变更记录/ # Database migration records
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Backend Setup
|
||||
|
||||
1. **Prerequisites:**
|
||||
- JDK 17 (required)
|
||||
- PostgreSQL v16.2 (required)
|
||||
- Redis (stable version)
|
||||
|
||||
2. **Database Setup:**
|
||||
- Import the database initialization script using Navicat 16 or later
|
||||
- Script location: `sql/20251224init脚本(使用Navicat Premium 17导入).sql`
|
||||
- Configure database connection in `application.yml` or `application-dev.yml`
|
||||
|
||||
3. **Build and Run:**
|
||||
```bash
|
||||
cd openhis-server-new
|
||||
mvn clean package -DskipTests
|
||||
cd openhis-application
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
Or run directly from IDE by executing `OpenHisApplication.java`
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
1. **Prerequisites:**
|
||||
- Node.js v16.15 (recommended)
|
||||
|
||||
2. **Installation and Run:**
|
||||
```bash
|
||||
cd openhis-ui-vue3
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **Access the application:**
|
||||
- Frontend: http://localhost:81
|
||||
- Backend API: http://localhost:18080/openhis
|
||||
- Swagger UI: http://localhost:18080/openhis/swagger-ui/index.html
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
The backend follows a multi-module Maven architecture with clear separation of concerns:
|
||||
|
||||
1. **openhis-application**: Entry point with `OpenHisApplication.java`
|
||||
- Scans `com.core` and `com.openhis` packages
|
||||
- Configured to run on port 18080 with context path `/openhis`
|
||||
|
||||
2. **openhis-domain**: Business domain modules organized by medical functionality:
|
||||
- `administration`: Administrative functions
|
||||
- `appointmentmanage`: Appointment management
|
||||
- `check`: Medical examination/checkup
|
||||
- `clinical`: Clinical workflows
|
||||
- `crosssystem`: Cross-system integration
|
||||
- `document`: Document management
|
||||
- `financial`: Financial/billing
|
||||
- `lab`: Laboratory operations
|
||||
- `medication`: Medication management
|
||||
- `triageandqueuemanage`: Patient triage and queue management
|
||||
- `yb`, `ybcatalog`, `ybelep`: Insurance (Yi Bao) integration
|
||||
- `workflow`: Workflow management
|
||||
|
||||
3. **Core Modules** (com.core package):
|
||||
- `core-system`: User, role, menu, and permission management
|
||||
- `core-framework`: Security, exception handling, and framework configurations
|
||||
- `core-common`: Shared utilities and base classes
|
||||
- `core-quartz`: Scheduled task management
|
||||
- `core-generator`: Code generation tools
|
||||
- `core-flowable`: Workflow engine integration
|
||||
- `core-admin`: Administrative functions
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
The frontend uses Vue 3 with composition API and modern tooling:
|
||||
|
||||
1. **State Management:** Pinia for global state with modules for app, dict, permission, settings, tagsView, and user
|
||||
|
||||
2. **Routing:** Vue Router 4.3.0 with public routes and dynamic permission-based routes
|
||||
|
||||
3. **API Integration:** Axios with request/response interceptors and API services organized by module
|
||||
|
||||
4. **Component Architecture:** Element Plus as UI framework with custom components in `src/components/`
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
- Main config: `openhis-server-new/openhis-application/src/main/resources/application.yml`
|
||||
- Environment-specific: `application-dev.yml`, `application-test.yml`, `application-prd.yml`
|
||||
- Database connection settings, Redis configuration, server settings, and MyBatis-Plus configuration
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
- Environment files: `.env.*` in `openhis-ui-vue3/`
|
||||
- Vite configuration: `vite.config.js`
|
||||
- Main entry: `src/main.js`
|
||||
- Router: `src/router/index.js`
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Backend Feature
|
||||
|
||||
1. Create domain entity in appropriate module under `openhis-domain/[module]/domain/`
|
||||
2. Create mapper interface in `openhis-domain/[module]/mapper/`
|
||||
3. Create service interface and implementation in `openhis-domain/[module]/service/`
|
||||
4. Create controller in `openhis-application/src/main/java/com/openhis/web/[module]/`
|
||||
5. Test endpoints via Swagger UI
|
||||
|
||||
### Adding a New Frontend Page
|
||||
|
||||
1. Create Vue component in `openhis-ui-vue3/src/views/[module]/`
|
||||
2. Add API service methods in `openhis-ui-vue3/src/api/`
|
||||
3. Add route to `openhis-ui-vue3/src/router/index.js`
|
||||
4. Add Pinia store module if state management needed
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The system uses logical deletion with a `validFlag` field (1 = active, 0 = deleted)
|
||||
- JWT tokens are stored in the `Authorization` header
|
||||
- The system supports WebView environments with C# accessor integration
|
||||
- File uploads are configured with max 10MB per file and 20MB total request size
|
||||
- Password lockout occurs after 5 failed attempts with a 10-minute lock time
|
||||
- The system includes a code generator accessible via `/tool/gen` route
|
||||
- Printing functionality is implemented using the hiprint plugin
|
||||
@@ -1,202 +0,0 @@
|
||||
# OpenHIS 系统审计字段填充最佳实践
|
||||
|
||||
## 概述
|
||||
本文档介绍如何在 OpenHIS 系统中确保所有实体的审计字段(create_by、create_time、update_by、update_time)能够正确自动填充。
|
||||
|
||||
## 自动填充机制
|
||||
|
||||
### 1. 基础实体类
|
||||
所有需要审计字段的实体类都应该继承自 `HisBaseEntity`:
|
||||
|
||||
```java
|
||||
import com.core.common.core.domain.HisBaseEntity;
|
||||
|
||||
@Data
|
||||
@TableName("adm_practitioner")
|
||||
public class Practitioner extends HisBaseEntity {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
// 其他业务字段...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自动填充处理器
|
||||
系统使用 `MybastisColumnsHandler` 来自动填充审计字段:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
// 填充创建时间和创建人
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
|
||||
|
||||
String username = getCurrentUsername(); // 获取当前用户名
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
// 填充更新时间和更新人
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
this.strictUpdateFill(metaObject, "update_time", Date.class, new Date());
|
||||
|
||||
String username = getCurrentUsername(); // 获取当前用户名
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||
}
|
||||
|
||||
private String getCurrentUsername() {
|
||||
String username = "system";
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
username = loginUser.getUsername();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return username;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 确保自动填充正常工作的要点
|
||||
|
||||
### 1. 检查实体类继承关系
|
||||
确保所有实体类都正确继承了 `HisBaseEntity`:
|
||||
|
||||
```java
|
||||
// 正确的做法
|
||||
public class Practitioner extends HisBaseEntity { ... }
|
||||
|
||||
// 如果不能继承 HisBaseEntity,则需要手动添加审计字段
|
||||
public class CustomEntity {
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Date updateTime;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 验证安全上下文
|
||||
确保在执行数据库操作时有有效的安全上下文:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class PractitionerService {
|
||||
public void savePractitioner(Practitioner practitioner) {
|
||||
// 确保调用此方法时用户已登录
|
||||
// SecurityUtils.getLoginUser() 应该能返回有效的 LoginUser 对象
|
||||
|
||||
// MyBatis-Plus 会在保存时自动调用 MybastisColumnsHandler
|
||||
practitionerMapper.insert(practitioner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 检查配置
|
||||
确保自动填充处理器被正确配置:
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
mybatis-plus:
|
||||
global-config:
|
||||
db-config:
|
||||
# 其他配置...
|
||||
configuration:
|
||||
# 其他配置...
|
||||
```
|
||||
|
||||
### 4. 手动填充(特殊情况)
|
||||
在某些特殊情况下,如果自动填充不工作,可以手动设置:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class PractitionerService {
|
||||
public void savePractitionerManually(Practitioner practitioner) {
|
||||
// 手动设置审计字段
|
||||
Date now = new Date();
|
||||
String currentUser = getCurrentUsername();
|
||||
|
||||
practitioner.setCreateTime(now);
|
||||
practitioner.setCreateBy(currentUser);
|
||||
practitioner.setUpdateTime(now);
|
||||
practitioner.setUpdateBy(currentUser);
|
||||
|
||||
practitionerMapper.insert(practitioner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题及解决方案
|
||||
|
||||
### 问题1:自动填充不生效
|
||||
**原因:**
|
||||
- 实体类没有继承 `HisBaseEntity`
|
||||
- `MybastisColumnsHandler` 没有被Spring管理(缺少@Component注解)
|
||||
- 没有有效的安全上下文
|
||||
|
||||
**解决方案:**
|
||||
- 确保实体类继承 `HisBaseEntity`
|
||||
- 检查 `MybastisColumnsHandler` 是否有 `@Component` 注解
|
||||
- 确保在调用保存方法时用户已登录
|
||||
|
||||
### 问题2:获取不到当前用户
|
||||
**原因:**
|
||||
- 用户未登录
|
||||
- 安全上下文配置错误
|
||||
|
||||
**解决方案:**
|
||||
- 在调用保存方法前确保用户已登录
|
||||
- 检查安全配置是否正确
|
||||
|
||||
### 问题3:批量操作时审计字段未填充
|
||||
**原因:**
|
||||
- 批量操作可能绕过了自动填充机制
|
||||
|
||||
**解决方案:**
|
||||
- 对于批量操作,手动设置审计字段
|
||||
- 或者使用 MyBatis-Plus 的批量操作方法,确保它们支持自动填充
|
||||
|
||||
## 测试验证
|
||||
|
||||
创建一个简单的测试来验证自动填充是否正常工作:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
public class AuditFieldTest {
|
||||
@Autowired
|
||||
private PractitionerMapper practitionerMapper;
|
||||
|
||||
@Test
|
||||
public void testAuditFieldsAutoFill() {
|
||||
Practitioner practitioner = new Practitioner();
|
||||
practitioner.setName("Test Practitioner");
|
||||
|
||||
// 保存实体
|
||||
practitionerMapper.insert(practitioner);
|
||||
|
||||
// 验证审计字段是否被正确填充
|
||||
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||
|
||||
// 清理测试数据
|
||||
practitionerMapper.deleteById(practitioner.getId());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
通过遵循以上最佳实践,可以确保 OpenHIS 系统中的所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误。
|
||||
@@ -1,113 +0,0 @@
|
||||
# 关于数据库审计字段(create_by, create_time等)的处理方案
|
||||
|
||||
## 问题描述
|
||||
在使用OpenHIS系统时,可能会遇到如下错误:
|
||||
```
|
||||
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||
```
|
||||
|
||||
## 问题分析
|
||||
1. 数据库表中的审计字段(如create_by, create_time)设置了NOT NULL约束
|
||||
2. 应用程序层面使用了MyBatis-Plus的自动填充功能来设置这些字段
|
||||
3. 当自动填充机制失效时,就会出现违反非空约束的错误
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案一:修复自动填充机制(推荐)
|
||||
系统已经实现了自动填充机制,位于 `MybastisColumnsHandler.java`:
|
||||
|
||||
```java
|
||||
// 设置数据新增时候的,字段自动赋值规则
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
// 同时填充驼峰和下划线命名的字段,以兼容不同的配置
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
|
||||
|
||||
String username = "system";
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
username = loginUser.getUsername();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
// 使用 fillStrategy 确保即使字段为 null 也会被填充
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||
// 如果 strictInsertFill 没有生效,使用 setFieldValByName 强制设置
|
||||
if (metaObject.hasGetter("createBy") && metaObject.getValue("createBy") == null) {
|
||||
this.setFieldValByName("createBy", username, metaObject);
|
||||
}
|
||||
if (metaObject.hasGetter("create_by") && metaObject.getValue("create_by") == null) {
|
||||
this.setFieldValByName("create_by", username, metaObject);
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
确保所有实体类都继承自 `HisBaseEntity` 或 `BaseEntity`,这样就能自动获得审计字段。
|
||||
|
||||
### 方案二:移除数据库约束(谨慎使用)
|
||||
如果确实需要允许审计字段为NULL,可以移除数据库约束:
|
||||
|
||||
```sql
|
||||
-- 移除 adm_practitioner 表中 create_by 列的 NOT NULL 约束
|
||||
ALTER TABLE "public"."adm_practitioner"
|
||||
ALTER COLUMN "create_by" DROP NOT NULL;
|
||||
|
||||
-- 同样处理 create_time 列(如果需要)
|
||||
ALTER TABLE "public"."adm_practitioner"
|
||||
ALTER COLUMN "create_time" DROP NOT NULL;
|
||||
```
|
||||
|
||||
### 方案三:批量修复所有表的约束
|
||||
如果多个表都存在这个问题,可以使用以下脚本:
|
||||
|
||||
```sql
|
||||
-- 为所有表的审计字段移除NOT NULL约束
|
||||
-- 注意:执行前请备份数据库!
|
||||
|
||||
-- 1. 检查所有包含审计字段的表
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
is_nullable
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
column_name IN ('create_by', 'create_time', 'update_by', 'update_time')
|
||||
AND table_schema = 'public'
|
||||
AND is_nullable = 'NO'; -- NO 表示 NOT NULL 约束
|
||||
|
||||
-- 2. 根据需要移除特定表的约束
|
||||
-- 示例:移除多个表的create_by约束
|
||||
ALTER TABLE "public"."adm_practitioner" ALTER COLUMN "create_by" DROP NOT NULL;
|
||||
ALTER TABLE "public"."adm_patient" ALTER COLUMN "create_by" DROP NOT NULL;
|
||||
-- 添加更多表的处理...
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 确保实体类继承基础类
|
||||
所有实体类应继承 `HisBaseEntity` 或 `BaseEntity`:
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("adm_practitioner")
|
||||
public class Practitioner extends HisBaseEntity {
|
||||
// 其他字段...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查安全上下文
|
||||
确保在保存数据时有有效的安全上下文,这样自动填充处理器才能获取到当前用户信息。
|
||||
|
||||
### 3. 验证自动填充配置
|
||||
确保 `MybastisColumnsHandler` 在Spring容器中被正确注册(使用@Component注解)。
|
||||
|
||||
## 总结
|
||||
- 推荐保持数据库中的NOT NULL约束,确保数据完整性
|
||||
- 依赖MyBatis-Plus的自动填充机制来设置审计字段
|
||||
- 确保所有实体类继承基础实体类
|
||||
- 在必要时才考虑移除数据库约束
|
||||
@@ -1,104 +0,0 @@
|
||||
-- 检查流水号(display_order)是否按“科室+医生+当天”正确递增
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. display_order 存的是纯数字(1, 2, 3...),不带时间戳前缀
|
||||
-- 2. 时间戳前缀(如 20260109)是在前端显示时加上的
|
||||
-- 3. 后端用 Redis key "ORG-{科室ID}-DOC-{医生ID}" 按天自增
|
||||
--
|
||||
-- 如何判断逻辑是否正确:
|
||||
-- 同一科室、同一医生、同一天的记录,display_order 应该递增(1, 2, 3...)
|
||||
-- 不同科室、不同医生、不同天的记录,可能都是 1(这是正常的)
|
||||
|
||||
-- ========================================
|
||||
-- 查询1:按“科室+医生+日期”分组,看每组内的 display_order 是否递增
|
||||
-- ========================================
|
||||
SELECT
|
||||
DATE(start_time) AS 日期,
|
||||
organization_id AS 科室ID,
|
||||
registrar_id AS 医生ID,
|
||||
COUNT(*) AS 该组记录数,
|
||||
MIN(display_order) AS 最小序号,
|
||||
MAX(display_order) AS 最大序号,
|
||||
STRING_AGG(display_order::text, ', ' ORDER BY start_time) AS 序号列表,
|
||||
STRING_AGG(id::text, ', ' ORDER BY start_time) AS 记录ID列表
|
||||
FROM adm_encounter
|
||||
WHERE delete_flag = '0'
|
||||
AND start_time >= CURRENT_DATE - INTERVAL '7 days' -- 只看最近7天
|
||||
AND display_order IS NOT NULL
|
||||
GROUP BY DATE(start_time), organization_id, registrar_id
|
||||
ORDER BY 日期 DESC, 科室ID, 医生ID;
|
||||
|
||||
-- ========================================
|
||||
-- 查询2:详细查看每条记录,看同组内的序号是否连续
|
||||
-- ========================================
|
||||
SELECT
|
||||
id AS 记录ID,
|
||||
DATE(start_time) AS 日期,
|
||||
organization_id AS 科室ID,
|
||||
registrar_id AS 医生ID,
|
||||
start_time AS 挂号时间,
|
||||
display_order AS 流水号,
|
||||
-- 计算:同组内的序号应该是 1, 2, 3...,看是否有重复或跳号
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY DATE(start_time), organization_id, registrar_id
|
||||
ORDER BY start_time
|
||||
) AS 应该是第几个,
|
||||
CASE
|
||||
WHEN display_order = ROW_NUMBER() OVER (
|
||||
PARTITION BY DATE(start_time), organization_id, registrar_id
|
||||
ORDER BY start_time
|
||||
) THEN '✓ 正常'
|
||||
ELSE '✗ 异常'
|
||||
END AS 是否正常
|
||||
FROM adm_encounter
|
||||
WHERE delete_flag = '0'
|
||||
AND start_time >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND display_order IS NOT NULL
|
||||
ORDER BY DATE(start_time) DESC, organization_id, registrar_id, start_time;
|
||||
|
||||
-- ========================================
|
||||
-- 查询3:只看今天的数据(最直观)
|
||||
-- ========================================
|
||||
SELECT
|
||||
id AS 记录ID,
|
||||
organization_id AS 科室ID,
|
||||
registrar_id AS 医生ID,
|
||||
start_time AS 挂号时间,
|
||||
display_order AS 流水号
|
||||
FROM adm_encounter
|
||||
WHERE delete_flag = '0'
|
||||
AND DATE(start_time) = CURRENT_DATE
|
||||
AND display_order IS NOT NULL
|
||||
ORDER BY organization_id, registrar_id, start_time;
|
||||
|
||||
-- ========================================
|
||||
-- 查询4:发现问题 - 找出同组内 display_order 重复的记录
|
||||
-- ========================================
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
DATE(start_time) AS reg_date,
|
||||
organization_id,
|
||||
registrar_id,
|
||||
start_time,
|
||||
display_order,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY DATE(start_time), organization_id, registrar_id
|
||||
ORDER BY start_time
|
||||
) AS should_be_order
|
||||
FROM adm_encounter
|
||||
WHERE delete_flag = '0'
|
||||
AND start_time >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND display_order IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
reg_date AS 日期,
|
||||
organization_id AS 科室ID,
|
||||
registrar_id AS 医生ID,
|
||||
COUNT(*) AS 重复数量,
|
||||
STRING_AGG(id::text || '->' || display_order::text, ', ') AS 问题记录
|
||||
FROM ranked
|
||||
WHERE display_order != should_be_order
|
||||
GROUP BY reg_date, organization_id, registrar_id
|
||||
ORDER BY reg_date DESC;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# 检查后端API返回数据结构
|
||||
|
||||
## 问题分析
|
||||
尽管我们更新了DTO和SQL查询,前端仍然没有显示创建时间,可能的原因:
|
||||
1. API响应中没有包含createTime字段
|
||||
2. SQL查询没有正确返回createTime字段
|
||||
3. 数据库中createTime字段本身为null
|
||||
4. JSON序列化问题
|
||||
|
||||
## 检查步骤
|
||||
|
||||
### 1. 检查数据库中数据
|
||||
首先检查数据库中sys_user表的createTime字段是否正确填充:
|
||||
```sql
|
||||
SELECT user_id, user_name, nick_name, create_time
|
||||
FROM sys_user
|
||||
WHERE create_time IS NOT NULL
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### 2. 检查API端点
|
||||
API端点是:GET /base-data-manage/practitioner/user-practitioner-page
|
||||
这个端点在PractitionerController中定义,调用practitionerAppService.getUserPractitionerPage()
|
||||
|
||||
### 3. 检查SQL查询
|
||||
在PractitionerAppMapper.xml中,我们已经添加了createTime字段:
|
||||
```xml
|
||||
T2.create_time
|
||||
```
|
||||
|
||||
### 4. 验证DTO映射
|
||||
UserAndPractitionerDto中已添加createTime字段:
|
||||
```java
|
||||
private Date createTime;
|
||||
```
|
||||
|
||||
### 5. 检查JSON序列化
|
||||
检查是否有@JsonFormat注解或其他序列化配置问题
|
||||
@@ -1,290 +0,0 @@
|
||||
# 深度排查 MyBatis-Plus 自动填充不生效问题
|
||||
|
||||
## 问题概述
|
||||
尽管对 MyBatis-Plus 的自动填充处理器进行了多次优化和配置,但 `create_by` 和 `create_time` 字段仍然没有被自动填充。
|
||||
|
||||
## 深度排查步骤
|
||||
|
||||
### 1. 检查 AOP 代理是否生效
|
||||
MyBatis-Plus 的自动填充功能依赖于 AOP 代理。如果实体类的方法被直接调用而非通过代理调用,自动填充可能不会生效。
|
||||
|
||||
### 2. 验证 Service 层实现
|
||||
确保使用的是 MyBatis-Plus 提供的通用 Service 方法,而不是自定义的 SQL。
|
||||
|
||||
### 3. 检查 @TableField 注解配置
|
||||
确认实体类中的字段注解配置正确。
|
||||
|
||||
### 4. 检查事务配置
|
||||
某些事务配置可能会影响 AOP 代理的生效。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案一:在 Service 层手动设置审计字段
|
||||
|
||||
创建一个工具类来统一处理审计字段的设置:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class AuditFieldUtil {
|
||||
|
||||
public static void setCreateInfo(Object entity) {
|
||||
if (entity == null) return;
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||
Date currentTime = new Date();
|
||||
|
||||
// 使用反射设置字段值
|
||||
Field createByField = getField(entity.getClass(), "createBy");
|
||||
if (createByField != null) {
|
||||
createByField.setAccessible(true);
|
||||
if (createByField.get(entity) == null || "".equals(createByField.get(entity))) {
|
||||
createByField.set(entity, username);
|
||||
}
|
||||
}
|
||||
|
||||
Field createTimeField = getField(entity.getClass(), "createTime");
|
||||
if (createTimeField != null) {
|
||||
createTimeField.setAccessible(true);
|
||||
if (createTimeField.get(entity) == null) {
|
||||
createTimeField.set(entity, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下划线命名的字段
|
||||
Field createByFieldUnderscore = getField(entity.getClass(), "create_by");
|
||||
if (createByFieldUnderscore != null) {
|
||||
createByFieldUnderscore.setAccessible(true);
|
||||
if (createByFieldUnderscore.get(entity) == null || "".equals(createByFieldUnderscore.get(entity))) {
|
||||
createByFieldUnderscore.set(entity, username);
|
||||
}
|
||||
}
|
||||
|
||||
Field createTimeFieldUnderscore = getField(entity.getClass(), "create_time");
|
||||
if (createTimeFieldUnderscore != null) {
|
||||
createTimeFieldUnderscore.setAccessible(true);
|
||||
if (createTimeFieldUnderscore.get(entity) == null) {
|
||||
createTimeFieldUnderscore.set(entity, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("设置审计字段时发生异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Field getField(Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
return clazz.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return getField(clazz.getSuperclass(), fieldName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后在 Service 实现中使用:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
|
||||
implements IPractitionerService {
|
||||
|
||||
@Autowired
|
||||
private AuditFieldUtil auditFieldUtil;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean save(Practitioner entity) {
|
||||
// 在保存前手动设置审计字段
|
||||
auditFieldUtil.setCreateInfo(entity);
|
||||
return super.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean saveBatch(Collection<Practitioner> entityList) {
|
||||
entityList.forEach(auditFieldUtil::setCreateInfo);
|
||||
return super.saveBatch(entityList);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案二:重写 BaseMapper 方法
|
||||
|
||||
如果 Service 层的方法不起作用,可以直接在 Mapper 层处理:
|
||||
|
||||
```java
|
||||
@Mapper
|
||||
public interface PractitionerMapper extends BaseMapper<Practitioner> {
|
||||
|
||||
@Insert({
|
||||
"<script>",
|
||||
"INSERT INTO adm_practitioner (",
|
||||
"id, active_flag, name, name_json, gender_enum, birth_date, deceased_date,",
|
||||
"phone, address, address_province, address_city, address_district, address_street,",
|
||||
"address_json, py_str, wb_str, bus_no, yb_no, user_id, tenant_id, delete_flag,",
|
||||
"create_by, create_time, update_by, update_time, org_id,",
|
||||
"phar_prac_cert_no, prsc_dr_cert_code, dr_profttl_code, kpd_code, signature, pos_no",
|
||||
") VALUES (",
|
||||
"#{id}, #{activeFlag}, #{name}, #{nameJson}, #{genderEnum}, #{birthDate}, #{deceasedDate},",
|
||||
"#{phone}, #{address}, #{addressProvince}, #{addressCity}, #{addressDistrict}, #{addressStreet},",
|
||||
"#{addressJson}, #{pyStr}, #{wbStr}, #{busNo}, #{ybNo}, #{userId}, #{tenantId}, #{deleteFlag},",
|
||||
"#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{orgId},",
|
||||
"#{pharPracCertNo}, #{prscDrCertCode}, #{drProfttlCode}, #{kpdCode}, #{signature}, #{posNo}",
|
||||
")",
|
||||
"</script>"
|
||||
})
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||
int insertWithAuditFields(Practitioner record);
|
||||
}
|
||||
```
|
||||
|
||||
### 方案三:使用 MyBatis 拦截器
|
||||
|
||||
创建一个 MyBatis 拦截器来自动填充字段:
|
||||
|
||||
```java
|
||||
@Intercepts({
|
||||
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
|
||||
})
|
||||
@Component
|
||||
public class AuditFieldInterceptor implements Interceptor {
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
Object[] args = invocation.getArgs();
|
||||
MappedStatement ms = (MappedStatement) args[0];
|
||||
Object parameter = args[1];
|
||||
|
||||
String sqlCommandType = ms.getSqlCommandType().toString();
|
||||
|
||||
if ("INSERT".equals(sqlCommandType)) {
|
||||
setCreateAuditFields(parameter);
|
||||
} else if ("UPDATE".equals(sqlCommandType)) {
|
||||
setUpdateAuditFields(parameter);
|
||||
}
|
||||
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
private void setCreateAuditFields(Object parameter) {
|
||||
if (parameter == null) return;
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||
Date currentTime = new Date();
|
||||
|
||||
// 设置 createBy 和 createTime
|
||||
setFieldValue(parameter, "createBy", username);
|
||||
setFieldValue(parameter, "create_time", username);
|
||||
setFieldValue(parameter, "createTime", currentTime);
|
||||
setFieldValue(parameter, "create_time", currentTime);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void setUpdateAuditFields(Object parameter) {
|
||||
if (parameter == null) return;
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||
Date currentTime = new Date();
|
||||
|
||||
// 设置 updateBy 和 updateTime
|
||||
setFieldValue(parameter, "updateBy", username);
|
||||
setFieldValue(parameter, "update_by", username);
|
||||
setFieldValue(parameter, "updateTime", currentTime);
|
||||
setFieldValue(parameter, "update_time", currentTime);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void setFieldValue(Object obj, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = getField(obj.getClass(), fieldName);
|
||||
if (field != null) {
|
||||
field.setAccessible(true);
|
||||
if (field.get(obj) == null) { // 只在原值为 null 时设置
|
||||
field.set(obj, value);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略无法设置的字段
|
||||
}
|
||||
}
|
||||
|
||||
private Field getField(Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
return clazz.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return getField(clazz.getSuperclass(), fieldName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
if (target instanceof Executor) {
|
||||
return Plugin.wrap(target, this);
|
||||
} else {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(Properties properties) {}
|
||||
}
|
||||
```
|
||||
|
||||
## 推荐实施顺序
|
||||
|
||||
1. 首先尝试方案一(Service 层手动设置),这是最简单且可控的方式
|
||||
2. 如果方案一不行,尝试方案三(MyBatis 拦截器),它在更底层起作用
|
||||
3. 方案二是最后的选择,需要重写具体的插入逻辑
|
||||
|
||||
## 验证方法
|
||||
|
||||
创建一个测试来验证自动填充是否生效:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
public class AuditFieldTest {
|
||||
|
||||
@Autowired
|
||||
private IPractitionerService practitionerService;
|
||||
|
||||
@Test
|
||||
public void testAuditFieldFill() {
|
||||
Practitioner practitioner = new Practitioner();
|
||||
practitioner.setName("Test Practitioner");
|
||||
|
||||
// 记录保存前的值
|
||||
System.out.println("保存前 - createBy: " + practitioner.getCreateBy());
|
||||
System.out.println("保存前 - createTime: " + practitioner.getCreateTime());
|
||||
|
||||
boolean success = practitionerService.save(practitioner);
|
||||
|
||||
// 从数据库重新查询以验证
|
||||
Practitioner saved = practitionerService.getById(practitioner.getId());
|
||||
System.out.println("保存后 - createBy: " + saved.getCreateBy());
|
||||
System.out.println("保存后 - createTime: " + saved.getCreateTime());
|
||||
|
||||
Assertions.assertTrue(success);
|
||||
Assertions.assertNotNull(saved.getCreateBy());
|
||||
Assertions.assertNotNull(saved.getCreateTime());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过这些方案,应该能够解决自动填充不生效的问题。
|
||||
@@ -1,143 +0,0 @@
|
||||
# 诊断 MyBatis-Plus 自动填充问题
|
||||
|
||||
## 问题现象
|
||||
尽管 `MybastisColumnsHandler` 已经实现并配置了自动填充功能,但 `create_by` 和 `create_time` 字段仍然没有被正确填充。
|
||||
|
||||
## 可能的原因及解决方案
|
||||
|
||||
### 1. 检查组件扫描配置
|
||||
确保 `MybastisColumnsHandler` 类被Spring容器正确管理:
|
||||
|
||||
```java
|
||||
@Component // 确保这个注解存在
|
||||
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查包扫描路径
|
||||
在主应用类中确保扫描到了处理器所在的包:
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
|
||||
@ComponentScan(basePackages = {"com.core", "com.openhis"}) // 确保扫描到处理器
|
||||
public class OpenHisApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OpenHisApplication.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 验证实体类配置
|
||||
确保实体类正确继承了 `HisBaseEntity` 并且字段上有正确的注解:
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("adm_practitioner")
|
||||
public class Practitioner extends HisBaseEntity {
|
||||
// 不需要在子类中重复定义 createBy, createTime 等字段
|
||||
// 因为它们已在 HisBaseEntity 中定义并带有 @TableField(fill = FieldFill.INSERT)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 检查安全上下文
|
||||
自动填充处理器依赖于安全上下文来获取当前用户。确保在执行保存操作时用户已登录:
|
||||
|
||||
```java
|
||||
// 在保存之前,确保用户已登录
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
// 用户未登录,可能需要手动设置审计字段
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 手动测试自动填充
|
||||
创建一个简单的测试来验证自动填充是否正常工作:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
public class AutoFillTest {
|
||||
|
||||
@Autowired
|
||||
private PractitionerMapper practitionerMapper;
|
||||
|
||||
@Test
|
||||
public void testAutoFill() {
|
||||
Practitioner practitioner = new Practitioner();
|
||||
practitioner.setName("Test Practitioner");
|
||||
|
||||
// 检查在保存前字段是否为空
|
||||
System.out.println("Before insert - createBy: " + practitioner.getCreateBy());
|
||||
System.out.println("Before insert - createTime: " + practitioner.getCreateTime());
|
||||
|
||||
// 执行插入操作
|
||||
int result = practitionerMapper.insert(practitioner);
|
||||
|
||||
// 检查保存后字段是否被填充
|
||||
System.out.println("After insert - createBy: " + practitioner.getCreateBy());
|
||||
System.out.println("After insert - createTime: " + practitioner.getCreateTime());
|
||||
|
||||
assertThat(result).isEqualTo(1);
|
||||
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 临时解决方案
|
||||
如果自动填充仍然不工作,可以在服务层手动设置这些字段:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
|
||||
implements IPractitionerService {
|
||||
|
||||
@Override
|
||||
public void savePractitioner(Practitioner practitioner) {
|
||||
// 手动设置审计字段
|
||||
if (practitioner.getCreateBy() == null || practitioner.getCreateBy().isEmpty()) {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
practitioner.setCreateBy(loginUser.getUsername());
|
||||
} else {
|
||||
practitioner.setCreateBy("system"); // 默认值
|
||||
}
|
||||
}
|
||||
|
||||
if (practitioner.getCreateTime() == null) {
|
||||
practitioner.setCreateTime(new Date());
|
||||
}
|
||||
|
||||
// 执行保存操作
|
||||
this.save(practitioner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 检查 MyBatis-Plus 版本兼容性
|
||||
确保使用的 MyBatis-Plus 版本与自动填充功能兼容。当前项目使用的是 3.5.5 版本,应该支持自动填充功能。
|
||||
|
||||
### 8. 调试自动填充处理器
|
||||
在 `MybastisColumnsHandler` 中添加日志来调试是否被调用:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
System.out.println("MybastisColumnsHandler.insertFill() called"); // 调试日志
|
||||
|
||||
Date currentTime = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||
|
||||
String username = getCurrentUsername();
|
||||
System.out.println("Setting createBy to: " + username); // 调试日志
|
||||
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||
|
||||
// ... 其他代码
|
||||
}
|
||||
```
|
||||
|
||||
通过以上步骤,应该能够诊断并解决自动填充不工作的问题。
|
||||
@@ -1,145 +0,0 @@
|
||||
package com.core.framework.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.framework.config.TenantContext;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 用于自动填充创建时间和更新时间,以及创建人和更新人
|
||||
*/
|
||||
@Component
|
||||
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||
|
||||
// 设置数据新增时的字段自动赋值规则
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
// 填充创建时间
|
||||
Date currentTime = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||
|
||||
// 获取当前登录用户名
|
||||
String username = getCurrentUsername();
|
||||
|
||||
// 填充创建人
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||
|
||||
// 确保tenantId被设置
|
||||
Integer tenantId = getCurrentTenantId();
|
||||
if (tenantId == null) {
|
||||
throw new RuntimeException("无法获取当前租户ID,请确保用户已登录或正确设置租户上下文");
|
||||
}
|
||||
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
|
||||
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
|
||||
}
|
||||
|
||||
// 设置数据修改时的字段自动赋值规则
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
// 填充更新时间
|
||||
Date currentTime = new Date();
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
|
||||
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
|
||||
|
||||
// 填充更新人
|
||||
String username = getCurrentUsername();
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名
|
||||
* @return 当前登录用户名,如果无法获取则返回 "system"
|
||||
*/
|
||||
private String getCurrentUsername() {
|
||||
String username = "system"; // 默认值
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
username = loginUser.getUsername();
|
||||
} else {
|
||||
// 尝试从请求中获取用户信息
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
// 可以在这里添加额外的逻辑来从请求中获取用户信息
|
||||
// 例如从请求头、session等获取用户信息
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录异常但不中断处理流程
|
||||
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
|
||||
// 可以考虑记录日志
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户 ID
|
||||
*/
|
||||
private Integer getCurrentTenantId() {
|
||||
Integer result = null;
|
||||
|
||||
// 首先尝试从线程局部变量中获取租户ID(适用于定时任务等场景)
|
||||
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
|
||||
if (threadLocalTenantId != null) {
|
||||
result = threadLocalTenantId;
|
||||
} else {
|
||||
// 获取当前登录用户的租户ID(优先使用SecurityUtils中储存的LoginUser的租户ID)
|
||||
try {
|
||||
if (SecurityUtils.getAuthentication() != null) {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
result = loginUser.getTenantId();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录异常但不中断处理
|
||||
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
// 尝试从请求头中获取租户ID
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
if (request != null) {
|
||||
// 从请求头获取租户ID,假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
|
||||
String tenantIdHeader = request.getHeader("X-Tenant-ID");
|
||||
String requestMethodName = request.getHeader("Request-Method-Name");
|
||||
// 登录
|
||||
if ("login".equals(requestMethodName)) {
|
||||
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
|
||||
try {
|
||||
result = Integer.parseInt(tenantIdHeader);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有获取到租户ID,返回默认值
|
||||
if (result == null) {
|
||||
System.out.println("警告: 未能获取当前租户ID,将使用默认租户ID 1");
|
||||
result = 1; // 默认租户ID
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.openhis;
|
||||
|
||||
/**
|
||||
* 示例类 - 引用 OpenHisApplication
|
||||
*/
|
||||
public class Fragment {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 引用 OpenHisApplication
|
||||
Class<?> applicationClass = com.openhis.OpenHisApplication.class;
|
||||
System.out.println("Application class: " + applicationClass.getName());
|
||||
}
|
||||
}
|
||||
434
md/需求/104-报卡管理界面_2026-02-05.md
Normal file
434
md/需求/104-报卡管理界面_2026-02-05.md
Normal file
@@ -0,0 +1,434 @@
|
||||
## 报卡管理界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:报卡管理界面
|
||||
**页面目标**:提供传染病报卡的审核、管理、筛选及批量操作功能,帮助疾控人员高效完成报卡审核工作
|
||||
**适用场景**:疾控中心工作人员日常审核医疗机构上报的传染病病例
|
||||
|
||||
- 医院CDC管理员日常审核传染病报卡
|
||||
- 批量处理待审核/退回的报卡
|
||||
- 按条件筛选统计报卡数据
|
||||
**页面类型**:数据管理列表页(含详情抽屉)
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 报卡数据概览统计(今日待审/本月失败/本月成功/本月上报)
|
||||
2. 多维度筛选报卡数据(时间/状态/科室/来源等)
|
||||
3. 报卡列表展示与批量操作(审核/退回/导出)
|
||||
4. 报卡详情查看与单条审核
|
||||
5. 审核记录追溯与意见填写
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 疾控人员可快速处理待审报卡
|
||||
- 支持批量审核提升工作效率
|
||||
- 完整记录审核过程便于追溯
|
||||
- 多维度筛选快速定位目标报卡
|
||||
**原型图地址**:https://static.pm-ai.cn/prototype/20260206/cc9991b716df0303fa3459042e33a1ea/index.html
|
||||
**流程图**:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["进入报卡管理界面"] --> B["查看报卡概览统计"]
|
||||
B --> C["点击统计卡片"]
|
||||
C --> D["悬停统计卡片"]
|
||||
D --> E["自动设置筛选项"]
|
||||
B --> F["鼠标悬停"]
|
||||
F --> G["筛选报卡数据"]
|
||||
G --> H["卡片上浮+阴影"]
|
||||
E --> I["设置筛选条件"]
|
||||
I --> K["点击重置按钮"]
|
||||
K --> J["清空条件"]
|
||||
I --> L["点击查询按钮"]
|
||||
L --> M["触发筛选"]
|
||||
M --> N["操作报卡列表"]
|
||||
N --> O["勾选报卡"]
|
||||
N --> P["处理单条报卡"]
|
||||
O --> Q["批量操作报卡"]
|
||||
P --> R["点击单条审核"]
|
||||
P --> S["点击单条查看"]
|
||||
Q --> T["点击批量审核"]
|
||||
Q --> U["点击批量退回"]
|
||||
R --> V{"报卡状态"}
|
||||
S --> W{"报卡状态"}
|
||||
V -- 已审核 --> X["打开查看抽屉"]
|
||||
V -- 待审核/失败 --> Y["校验选择状态"]
|
||||
W -- 任意 --> X
|
||||
T --> Z["校验选择状态"]
|
||||
U --> AA["校验选择状态"]
|
||||
Y -- 选择有效 --> AB["打开审核抽屉"]
|
||||
AB-->AN["展示审核记录"]
|
||||
AB-->AO["展示审核记录"]
|
||||
Y -- 未选择 --> AC["提示请选择报卡"]
|
||||
Z -- 选择有效 --> AD{"包含已审核项"}
|
||||
Z -- 未选择 --> AE["提示请选择报卡"]
|
||||
AD -- 是 --> AF["提示只能选择待审核报卡"]
|
||||
AD -- 否 --> AG["弹出审核弹窗"]
|
||||
AA -- 选择有效 --> AH{"包含已审核项"}
|
||||
AA -- 未选择 --> AI["提示请选择报卡"]
|
||||
AH -- 是 --> AF
|
||||
AH -- 否 --> AJ["弹出退回弹窗"]
|
||||
AG --> AK["加载报卡详情"]
|
||||
AJ --> AL["加载报卡详情"]
|
||||
AK -- 加载失败 --> AM["提示数据加载失败"]
|
||||
AL -- 加载失败 --> AM
|
||||
AK -- 成功 --> AN["展示审核记录"]
|
||||
AL -- 成功 --> AO["展示审核记录"]
|
||||
AN --> AP["填写审核意见"]
|
||||
AO --> AQ["填写退回原因"]
|
||||
AP --> AR{"意见是否为空"}
|
||||
AQ --> AS{"原因是否为空"}
|
||||
AR -- 是 --> AT["红字提示必填"]
|
||||
AS -- 是 --> AU["红字提示必填"]
|
||||
AR -- 否 --> AV["点击确认审核"]
|
||||
AS -- 否 --> AW["点击确认退回"]
|
||||
AV --> AX["点击审核通过"]
|
||||
AW --> AY["点击退回修改"]
|
||||
AX --> AZ["更新报卡状态"]
|
||||
AY --> BA["更新报卡状态"]
|
||||
AZ --> BB["生成审核记录"]
|
||||
BA --> BC["生成审核记录"]
|
||||
BB --> BD["按钮置灰"]
|
||||
BC --> BD["按钮置灰"]
|
||||
AX --> BE["提示操作失败,请检查网络"]
|
||||
AY --> BE
|
||||
AZ --> BF["刷新表格数据"]
|
||||
BA --> BF
|
||||
BF --> BG["刷新行状态"]
|
||||
BG --> BH["结束"]
|
||||
BE --> BH
|
||||
```
|
||||
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. **顶部导航栏**(固定高度60px)
|
||||
2. **报卡管理概览区**(统计卡片+快捷操作,高度自适应)
|
||||
3. **筛选控制区**(多条件组合筛选,折叠式布局)
|
||||
4. **报卡列表区**(表格展示,占主体60%高度)
|
||||
**布局特点**:上下层级结构,筛选区支持折叠/展开
|
||||
5. **抽屉详情区**
|
||||
|
||||
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部导航栏
|
||||
|
||||
**区域位置**:页面顶部固定
|
||||
**区域尺寸**:高度60px,100%宽度
|
||||
**区域功能**:展示系统标识和用户信息
|
||||
**包含元素**:
|
||||
|
||||
- **系统Logo**
|
||||
|
||||
- - 元素类型:图标+文字组合
|
||||
- 显示内容:"CDC"图标+"报卡管理"文字
|
||||
- 样式特征:蓝色主色调(#4a6fa5),左侧对齐
|
||||
|
||||
#### 2. 报卡管理概览
|
||||
|
||||
**区域位置**:导航栏下方
|
||||
|
||||
**区域尺寸**:100%宽度,自适应高度
|
||||
**区域功能**:关键数据统计与快速操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **统计卡片组**(4个)
|
||||
|
||||
- - 展示方式:网格布局(4列)
|
||||
- 数据字段:
|
||||
|
||||
| **字段名** | **类型** | **示例值** | **可操作** | **计算逻辑** |
|
||||
| ------------ | -------- | ---------- | ---------- | ------------------------- |
|
||||
| 今日待审核 | 数字 | 12 | 可点击 | 当天created_at+待审状态 |
|
||||
| 本月审核失败 | 数字 | 3 | 可点击 | 当月created_at+失败状态 |
|
||||
| 本月审核成功 | 数字 | 2 | 可点击 | 当月created_at+成功状态 |
|
||||
| 本月已上报 | 数字 | 156 | 可点击 | 当月created_at+已上报状态 |
|
||||
|
||||
o 交互行为:
|
||||
|
||||
- - - 悬停:卡片上浮5px+阴影加深
|
||||
- 点击:自动设置对应筛选项
|
||||
|
||||
- 样式特征:左侧状态色条(蓝/橙/红/绿)
|
||||
|
||||
#### 3. 筛选控制区
|
||||
|
||||
**区域功能**:多维度组合筛选报卡数据
|
||||
|
||||
**区域尺寸**:100%宽度,高度自适应(展开状态)
|
||||
**包含元素**:
|
||||
|
||||
- **筛选条件组**(横向排列→移动端垂直堆叠)
|
||||
|
||||
- - 登记来源(下拉单选):全部/门诊/住院/急诊/体检
|
||||
- 上报时间范围(双日期选择器)--默认值:最近一个月
|
||||
- 患者姓名(文本输入)
|
||||
- 审核状态(下拉单选):全部/待审核/审核通过/审核失败/已上报
|
||||
- 上报科室(树形下拉多选)--全部科室/取值于《科室管理》adm_organization表
|
||||
|
||||
- **操作按钮**:
|
||||
|
||||
- - “查询”(主按钮,触发筛选)
|
||||
- “重置”(次要按钮,清空条件)
|
||||
|
||||
#### 4. 报卡列表区
|
||||
|
||||
**区域功能**:展示报卡数据及操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **表格头部**
|
||||
|
||||
- - 全选复选框(联动所有行选择状态)
|
||||
- 列标题:报卡名称/病种名称/患者信息等11列
|
||||
|
||||
- **快捷操作按钮组**
|
||||
|
||||
- - 包含按钮:
|
||||
|
||||
- - “批量审核”蓝色按钮,(需选择条目)点击弹出填写审核备注弹窗
|
||||
- “批量退回修改”橙色警示按钮,(需选择条目)点击弹出退回原因填写弹窗
|
||||
- “导出当前”(按筛选条件)
|
||||
|
||||
- **表格行**
|
||||
|
||||
- - 数据字段:
|
||||
|
||||
取值于传染病报卡表(infectious_card)
|
||||
|
||||
| **列名** | **数据类型** | **示例值** | **说明** |
|
||||
| -------- | ------------ | ---------------- | --------------- |
|
||||
| 选择框 | boolean | - | 带全选功能 |
|
||||
| 报卡名称 | string | 传染病报告卡 | - |
|
||||
| 病种名称 | string | 病毒性肝炎 | - |
|
||||
| 报卡编号 | string | HOSP202601150001 | 唯一标识 |
|
||||
| 患者姓名 | string | 张某某 | 脱敏显示 |
|
||||
| 性别 | enum | 男 | 男/女/未知 |
|
||||
| 年龄 | number | 32 | - |
|
||||
| 上报科室 | string | 儿科 | - |
|
||||
| 登记来源 | string | 门诊 | - |
|
||||
| 上报时间 | datetime | 2026-01-31 09:23 | - |
|
||||
| 状态 | badge | 待审核 | 待审核<->已提交 |
|
||||
| 操作 | button | 审核/查看 | 根据状态禁用 |
|
||||
|
||||
|
||||
|
||||
- - 行操作按钮:
|
||||
|
||||
- - “审核”(主按钮,打开可编辑抽屉)
|
||||
- “查看”(次要按钮,打开只读抽屉)
|
||||
|
||||
- 交互规则:
|
||||
|
||||
- - 已审核通过的报卡禁用"审核"按钮
|
||||
- 行hover时显示浅蓝色背景
|
||||
|
||||
- 分页功能:
|
||||
|
||||
- - 实现分页功能(可以设置每页5/10/20条)
|
||||
|
||||
#### 5. 抽屉详情区
|
||||
|
||||
**区域位置**:右侧滑出
|
||||
**区域尺寸**:自适应布局
|
||||
**包含元素**:
|
||||
|
||||
· **报卡表单(****根据具体报卡登记的界面内容,比如《中华人民共和国传染病报告卡》内容和功能与需求编号102界面保持一致,建议用同一个界面)**
|
||||
|
||||
- - 字段分组:
|
||||
|
||||
- 1. 患者基本信息(姓名、身份证等)
|
||||
2. 临床信息(发病日期、诊断分类等)
|
||||
3. 疾病选择(甲/乙/丙类传染病复选框)
|
||||
4. 上报信息(报告单位、医生等)
|
||||
|
||||
· **审核记录**
|
||||
|
||||
- - 展示方式:时间轴列表
|
||||
- 数据字段:时间、操作人、操作类型、意见
|
||||
|
||||
· **操作按钮组**
|
||||
|
||||
- - 主按钮:审核通过(绿色)
|
||||
- 次按钮:退回修改(橙色)
|
||||
|
||||
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 批量审核流程
|
||||
|
||||
**触发条件**:勾选多行后点击"批量审核"
|
||||
**操作流程**:
|
||||
|
||||
1. 系统校验:至少选择1条非"已通过"状态的报卡
|
||||
|
||||
2. 弹出审核弹窗(500px居中模态框)
|
||||
|
||||
3. 填写审核意见(必填)
|
||||
|
||||
4. 点击"确认审核":
|
||||
|
||||
5. 批量更新状态为"审核通过",(自动批量写入每一条审核记录(插入infectious_audit 字段详表②、更改表infectious_card. Statu= 2和infectious_card. update_time= now()))
|
||||
|
||||
6. 刷新表格数据
|
||||
|
||||
7. - 成功:更新状态为"审核通过",添加审核记录
|
||||
- 失败:提示"审核失败,请检查网络"
|
||||
- 包含已审核项:提示"只能选择待审核报卡"
|
||||
- 未填写意见:阻止提交并红字提示
|
||||
|
||||
#### 2. 批量退回修改操作
|
||||
|
||||
**触发方式**:勾选多选框后点击"批量退回"
|
||||
**前置校验**:
|
||||
|
||||
- 至少勾选一条非"已审核"状态报卡
|
||||
- 选中已审核报卡时提示"只能操作待审核报卡"
|
||||
|
||||
**执行流程**:
|
||||
|
||||
1. 弹出模态框要求填写退回原因(必填)填写退回原因:阻止提交并红字提示
|
||||
2. 提交后批量更新选中报卡状态
|
||||
3. 每条生成审核记录(①、插入infectious_audit 字段详表②、更改表infectious_card. Statu=5和infectious_card. update_time= now())
|
||||
|
||||
#### 3.单卡审核流程
|
||||
|
||||
**触发方式**:点击操作列"审核"按钮
|
||||
**状态控制**:
|
||||
|
||||
- 待审核/审核失败:可操作
|
||||
- 审核通过:按钮禁用
|
||||
**执行流程**:
|
||||
|
||||
1. 右侧滑出审核抽屉
|
||||
2. 从行数据获取报卡ID自动填充患者报卡信息(异步加载报卡详情数据)
|
||||
3. 展示历史审核记录(如有)
|
||||
4. 填写审核意见/退回原因
|
||||
5. 点击"审核通过"或"退回修改"
|
||||
6. 更新表格行状态(生成审核记录(①、插入infectious_audit 字段详表②、更改表infectious_card. Statu=2/5和infectious_card. update_time= now()))
|
||||
|
||||
**异常处理**:
|
||||
|
||||
· 数据加载失败:提示"数据加载失败,请重试"
|
||||
|
||||
**·** 重复提交:按钮置灰防止重复点击
|
||||
|
||||
**状态变化**:
|
||||
|
||||
- 审核通过:表格行变绿色,按钮禁用,状态变成“审核通过”
|
||||
- 退回修改:表格行变橙色,生成退回记录,状态变成“审核失败”审核失败<->退回
|
||||
|
||||
**数据校验**:
|
||||
|
||||
- 必填字段红框提示
|
||||
- 身份证号格式校验
|
||||
- 日期逻辑校验(发病日期≤诊断日期)
|
||||
|
||||
#### 4. 筛选查询功能
|
||||
|
||||
**触发方式**:点击"查询"按钮
|
||||
**查询逻辑**:
|
||||
|
||||
- 登记来源:精确匹配
|
||||
- 患者姓名:模糊匹配
|
||||
- 时间范围:闭区间查询
|
||||
- 状态筛选:精确匹配
|
||||
- 上报科室:多选
|
||||
**性能优化**:
|
||||
- 500ms防抖处理
|
||||
- 分页加载(可以设置每页5/10/20条)实现分页功能
|
||||
|
||||
#### 5. 报卡详情查看
|
||||
|
||||
**触发条件**:点击行"查看"按钮
|
||||
**抽屉内容**:
|
||||
|
||||
- 只读表单(包含所有报卡字段)
|
||||
- 审核记录时间轴(倒序展示)
|
||||
- 关闭按钮(右上角×图标)
|
||||
**数据加载**:根据行数据获取报卡ID自动填充患者报卡信息(异步加载报卡详情数据)
|
||||
|
||||
#### 6. 筛选联动逻辑
|
||||
|
||||
| **操作** | **系统响应** |
|
||||
| ---------------------- | ------------------------------------------ |
|
||||
| 点击"今日待审核"统计卡 | 自动设置: - 时间=当天 - 状态=待审核 |
|
||||
| 本月审核失败 | 自动设置: - 时间=本月 - 状态=审核失败 |
|
||||
| 本月审核成功 | 自动设置: - 时间=本月 - 状态=审核成功 |
|
||||
| 本月已上报 | 自动设置: - 时间=本月 - 状态=已上报 |
|
||||
|
||||
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
**关键数据表**:
|
||||
|
||||
```
|
||||
|
||||
infectious_card 与报卡审核记录表(infectious_audit)采用 一对多 关联:
|
||||
① 、一张报卡可经历多次审核(初审、复审、退回、重审等)。
|
||||
② 、关联键:infectious_card.card_no → infectious_audit.card_id(FK)。
|
||||
*infectious_card. Statu增加状态5退回=审核失败(当批量退回修改/退回修改时更改表infectious_card. Statu=5 and infectious_card. update_time= now())
|
||||
|
||||
*infectious_audit 字段详表
|
||||
```
|
||||
|
||||
| **字段名** | **中文名称** | **取值说明** | **类型****(PG)** | **约束** |
|
||||
| ----------------- | ------------ | ------------------------------------------------------------ | ---------------- | ---------------------------------------- |
|
||||
| audit_id | 审核记录ID | 主键,自增 | BIGINT | PRIMARY KEY |
|
||||
| card_id | 报卡ID | 关联infectious_card.card_no | BIGINT | NOT NULL, FK |
|
||||
| audit_seq | 审核序号 | 第几次审核,从1开始 | SMALLINT | NOT NULL, ≥1 |
|
||||
| audit_type | 审核类型 | 1批量审核/2单审核通过/3批量退回修改/4单退回修改 /5其他 | CHAR(1) | NOT NULL, IN('1','2','3','4','5') |
|
||||
| audit_status_from | 审核前状态 | 同infectious_card.status(0暂存/1已提交=待审核/2已审核=审核通过/3已上报/4失败/5退回=审核失败) | CHAR(1) | NOT NULL, IN('0','1','2','3','4','5') |
|
||||
| audit_status_to | 审核后状态 | 同上,审核后的新状态 | CHAR(1) | NOT NULL, IN('0','1','2','3','4') |
|
||||
| audit_time | 审核时间 | 精确到秒,当前时间戳 | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| auditor_id | 审核人账号 | 登录账号 | VARCHAR(20) | NOT NULL |
|
||||
| auditor_name | 审核人姓名 | 登录账号的姓名 | VARCHAR(50) | NOT NULL |
|
||||
| audit_opinion | 审核意见 | 审核意见简述 | TEXT | |
|
||||
| Reason_for_return | 退回原因 | 退回原因简述 | TEXT | |
|
||||
| fail_reason_code | 失败原因码 | 字典:001必填项/002逻辑错误/003网络超时等 | VARCHAR(20) | |
|
||||
| fail_reason_desc | 失败详情 | 详细描述,可空 | TEXT | |
|
||||
| is_batch | 是否批量 | 0单条审核/1批量审核 | BOOLEAN | NOT NULL, DEFAULT false |
|
||||
| batch_size | 批量数量 | 批量时涉及报卡数 | INTEGER | NOT NULL, DEFAULT 1, ≥1 |
|
||||
| created_time | 记录创建时间 | 自动生成 | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_time | 记录更新时间 | 自动更新 | TIMESTAMP | NOT NULL, DEFAULT now(), ON UPDATE now() |
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:`#4a6fa5`(导航栏/主按钮)
|
||||
|
||||
- **状态色**:
|
||||
|
||||
- - 待审核:`rgba(74, 111, 165, 0.1)`
|
||||
- 审核失败:`rgba(231, 76, 60, 0.1)`
|
||||
|
||||
- **字体**:
|
||||
|
||||
- - 标题:`16px/1.5 #333`
|
||||
- 表格内容:`14px/1.5 #666`
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 审核记录需永久保存不可删除
|
||||
|
||||
|
||||
|
||||
### 七、补充说明
|
||||
|
||||
1. **日期格式**:统一使用`YYYY/MM/DD`(符合医疗系统惯例)
|
||||
2. **地址组件**:四级联动(省→市→区→街道)
|
||||
3. **职业选项**:使用国家标准职业分类
|
||||
4. **病种名称**:严格遵循《传染病报告卡》规范用词
|
||||
@@ -138,4 +138,26 @@ public class SysMenuController extends BaseController {
|
||||
String fullPath = menuService.generateFullPath(parentId, currentPath);
|
||||
return success(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新菜单缓存
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:menu:list')")
|
||||
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/refreshCache")
|
||||
public AjaxResult refreshCache() {
|
||||
menuService.refreshMenuCache();
|
||||
return success("菜单缓存已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新当前用户菜单缓存
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:menu:list')")
|
||||
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/refreshCurrentUserMenuCache")
|
||||
public AjaxResult refreshCurrentUserMenuCache() {
|
||||
menuService.clearMenuCacheByUserId(getUserId());
|
||||
return success("当前用户菜单缓存已刷新");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.core.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Excel额外表头信息注解
|
||||
@@ -14,7 +15,7 @@ public @interface ExcelExtra {
|
||||
/**
|
||||
* 表头名称
|
||||
*/
|
||||
String name();
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* 日期格式,如:yyyy-MM-dd HH:mm:ss
|
||||
@@ -35,4 +36,15 @@ public @interface ExcelExtra {
|
||||
* 是否导出
|
||||
*/
|
||||
boolean isExport() default true;
|
||||
|
||||
/**
|
||||
* 精度 默认:-1(默认不开启BigDecimal格式化)
|
||||
*/
|
||||
int scale() default -1;
|
||||
|
||||
/**
|
||||
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
|
||||
*/
|
||||
int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.core.common.enums;
|
||||
|
||||
/**
|
||||
* 角色枚举
|
||||
*
|
||||
* @author swb
|
||||
* @date 2026-01-29
|
||||
*/
|
||||
public enum RoleEnum {
|
||||
DOCTOR("doctor", "医生"),
|
||||
NURSE("nurse", "护士"),
|
||||
ADMIN("admin", "管理员");
|
||||
private final String code;
|
||||
private final String info;
|
||||
|
||||
RoleEnum(String code, String info) {
|
||||
this.code = code;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getInfo() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -33,67 +33,125 @@ public final class AgeCalculatorUtil {
|
||||
return period.getYears();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 当前年龄取得(床位列表表示年龄用)
|
||||
// */
|
||||
// public static String getAge(Date date) {
|
||||
// // 将 Date 转换为 LocalDateTime
|
||||
// LocalDateTime dateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
// LocalDateTime now = LocalDateTime.now();
|
||||
// int years = now.getYear() - dateTime.getYear();
|
||||
// if (years > 2) {
|
||||
// return String.format("%d岁", years);
|
||||
// }
|
||||
//
|
||||
// Period period = Period.between(dateTime.toLocalDate(), now.toLocalDate());
|
||||
// int months = period.getMonths();
|
||||
// int days = period.getDays();
|
||||
// long hours = ChronoUnit.HOURS.between(dateTime, now) - (days * 24L);
|
||||
//
|
||||
// if (hours < 0) {
|
||||
// hours += 24;
|
||||
// days--;
|
||||
// }
|
||||
// if (days < 0) {
|
||||
// months--;
|
||||
// days = getLastDayOfMonth(dateTime) - dateTime.getDayOfMonth() + now.getDayOfMonth();
|
||||
// }
|
||||
// if (months < 0) {
|
||||
// months += 12;
|
||||
// years--;
|
||||
// }
|
||||
// if (years < 0) {
|
||||
// return "1小时";
|
||||
// }
|
||||
//
|
||||
// if (years > 0 && months > 0) {
|
||||
// return String.format("%d岁%d月", years, months);
|
||||
// }
|
||||
// if (years > 0) {
|
||||
// return String.format("%d岁", years);
|
||||
// }
|
||||
// if (months > 0 && days > 0) {
|
||||
// return String.format("%d月%d天", months, days);
|
||||
// }
|
||||
// if (months > 0) {
|
||||
// return String.format("%d月", months);
|
||||
// }
|
||||
// if (days > 0 && hours > 0) {
|
||||
// return String.format("%d天%d小时", days, hours);
|
||||
// }
|
||||
// if (days > 0) {
|
||||
// return String.format("%d天", days);
|
||||
// }
|
||||
// if (hours > 0) {
|
||||
// return String.format("%d小时", hours);
|
||||
// }
|
||||
// return "1小时";
|
||||
// }
|
||||
/**
|
||||
* 当前年龄取得(床位列表表示年龄用)
|
||||
* 复刻Oracle函数FUN_GET_AGE的核心逻辑:返回年龄字符串
|
||||
*
|
||||
* @param birthDate 出生日期
|
||||
* @return 年龄字符串(如:29岁、3岁5月、2月15天、18天),出生日期晚于当前日期返回空字符串
|
||||
*/
|
||||
public static String getAge(Date date) {
|
||||
// 添加空值检查
|
||||
if (date == null) {
|
||||
public static String getAge(Date birthDate) {
|
||||
// 入参校验
|
||||
if (birthDate == null) {
|
||||
return "";
|
||||
}
|
||||
// 将 Date 转换为 LocalDateTime
|
||||
LocalDateTime dateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int years = now.getYear() - dateTime.getYear();
|
||||
if (years > 2) {
|
||||
return String.format("%d岁", years);
|
||||
|
||||
// 将Date转换为LocalDate(使用系统默认时区)
|
||||
LocalDate birthLocalDate = birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
LocalDate currentDate = LocalDate.now();
|
||||
|
||||
// 计算总天数(对应Oracle中的IDAY)
|
||||
long totalDays = ChronoUnit.DAYS.between(birthLocalDate, currentDate);
|
||||
|
||||
// 若出生日期晚于当前日期,返回空字符串
|
||||
if (totalDays < 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Period period = Period.between(dateTime.toLocalDate(), now.toLocalDate());
|
||||
int months = period.getMonths();
|
||||
int days = period.getDays();
|
||||
long hours = ChronoUnit.HOURS.between(dateTime, now) - (days * 24L);
|
||||
// 计算年份(复刻Oracle的闰年补偿逻辑:(当前年-出生年)/4 补偿闰年天数)
|
||||
int birthYear = birthLocalDate.getYear();
|
||||
int currentYear = currentDate.getYear();
|
||||
long leapYearCompensation = (currentYear - birthYear) / 4;
|
||||
long adjustedDays = totalDays - leapYearCompensation;
|
||||
|
||||
if (hours < 0) {
|
||||
hours += 24;
|
||||
days--;
|
||||
}
|
||||
if (days < 0) {
|
||||
months--;
|
||||
days = getLastDayOfMonth(dateTime) - dateTime.getDayOfMonth() + now.getDayOfMonth();
|
||||
}
|
||||
if (months < 0) {
|
||||
months += 12;
|
||||
years--;
|
||||
}
|
||||
if (years < 0) {
|
||||
return "1小时";
|
||||
}
|
||||
// 计算年、月、天(按365天/年、30天/月粗略折算,与Oracle逻辑一致)
|
||||
int iYear = (int) (adjustedDays / 365);
|
||||
long remainingDaysAfterYear = adjustedDays - iYear * 365;
|
||||
int iMonth = (int) (remainingDaysAfterYear / 30);
|
||||
int iDay = (int) (remainingDaysAfterYear - iMonth * 30);
|
||||
|
||||
if (years > 0 && months > 0) {
|
||||
return String.format("%d岁%d月", years, months);
|
||||
// 按原函数规则拼接返回字符串
|
||||
if (iYear <= 0) {
|
||||
// 小于1岁
|
||||
if (iMonth <= 0) {
|
||||
// 小于1个月,返回X天
|
||||
return iDay + "天";
|
||||
} else {
|
||||
// 1个月及以上,返回X月X天
|
||||
return iMonth + "月" + iDay + "天";
|
||||
}
|
||||
} else {
|
||||
// 1岁及以上
|
||||
if (iYear < 5) {
|
||||
// 1-4岁
|
||||
if (iMonth <= 0) {
|
||||
// 无整月,返回X岁X天
|
||||
return iYear + "岁" + iDay + "天";
|
||||
} else {
|
||||
// 有整月,返回X岁X月
|
||||
return iYear + "岁" + iMonth + "月";
|
||||
}
|
||||
} else {
|
||||
// 5岁及以上,仅返回X岁
|
||||
return iYear + "岁";
|
||||
}
|
||||
}
|
||||
if (years > 0) {
|
||||
return String.format("%d岁", years);
|
||||
}
|
||||
if (months > 0 && days > 0) {
|
||||
return String.format("%d月%d天", months, days);
|
||||
}
|
||||
if (months > 0) {
|
||||
return String.format("%d月", months);
|
||||
}
|
||||
if (days > 0 && hours > 0) {
|
||||
return String.format("%d天%d小时", days, hours);
|
||||
}
|
||||
if (days > 0) {
|
||||
return String.format("%d天", days);
|
||||
}
|
||||
if (hours > 0) {
|
||||
return String.format("%d小时", hours);
|
||||
}
|
||||
return "1小时";
|
||||
}
|
||||
|
||||
private static int getLastDayOfMonth(LocalDateTime dateTime) {
|
||||
int[] daysInMonth = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
if (isLeapYear(dateTime.getYear()) && dateTime.getMonthValue() == 2) {
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.core.common.utils;
|
||||
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -13,6 +15,8 @@ import java.util.Date;
|
||||
*/
|
||||
@Component
|
||||
public class AuditFieldUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuditFieldUtil.class);
|
||||
|
||||
/**
|
||||
* 为实体设置创建相关的审计字段
|
||||
@@ -65,8 +69,7 @@ public class AuditFieldUtil {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("设置创建审计字段时发生异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("设置创建审计字段时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +113,7 @@ public class AuditFieldUtil {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("设置更新审计字段时发生异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("设置更新审计字段时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.core.common.utils;
|
||||
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.text.ParseException;
|
||||
@@ -13,10 +15,13 @@ import java.util.Date;
|
||||
|
||||
/**
|
||||
* 时间工具类
|
||||
*
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DateUtils.class);
|
||||
|
||||
public static String YYYY = "yyyy";
|
||||
|
||||
public static String YYYY_MM = "yyyy-MM";
|
||||
@@ -227,7 +232,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
return endTime;
|
||||
}
|
||||
} catch (DateTimeParseException e) {
|
||||
e.printStackTrace();
|
||||
log.warn("日期解析失败: {}", strDate, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -250,7 +255,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
// 检查日期是否是未来的时间
|
||||
return dateToCheck.isAfter(currentDate);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.warn("日期解析失败: {}", dateString, e);
|
||||
// 解析失败或其他异常,返回 false 或根据需要处理异常
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package com.core.common.utils;
|
||||
|
||||
import com.core.common.annotation.Excel;
|
||||
import com.core.common.annotation.Excel.ColumnType;
|
||||
import com.core.common.annotation.Excel.Type;
|
||||
import com.core.common.annotation.ExcelExtra;
|
||||
import com.core.common.annotation.Excels;
|
||||
import com.core.common.config.CoreConfig;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.text.Convert;
|
||||
import com.core.common.exception.UtilException;
|
||||
import com.core.common.utils.file.FileTypeUtils;
|
||||
import com.core.common.utils.file.FileUtils;
|
||||
import com.core.common.utils.file.ImageUtils;
|
||||
import com.core.common.utils.poi.ExcelHandlerAdapter;
|
||||
import com.core.common.utils.reflect.ReflectUtils;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.RegExUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
@@ -30,17 +29,20 @@ import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import com.core.common.annotation.Excel;
|
||||
import com.core.common.annotation.Excel.ColumnType;
|
||||
import com.core.common.annotation.Excel.Type;
|
||||
import com.core.common.annotation.ExcelExtra;
|
||||
import com.core.common.annotation.Excels;
|
||||
import com.core.common.config.CoreConfig;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.text.Convert;
|
||||
import com.core.common.exception.UtilException;
|
||||
import com.core.common.utils.file.FileTypeUtils;
|
||||
import com.core.common.utils.file.FileUtils;
|
||||
import com.core.common.utils.file.ImageUtils;
|
||||
import com.core.common.utils.poi.ExcelHandlerAdapter;
|
||||
import com.core.common.utils.reflect.ReflectUtils;
|
||||
|
||||
/**
|
||||
* Excel相关处理
|
||||
@@ -1164,6 +1166,11 @@ public class NewExcelUtil<T> {
|
||||
ParameterizedType pt = (ParameterizedType)field.getGenericType();
|
||||
Class<?> subClass = (Class<?>)pt.getActualTypeArguments()[0];
|
||||
this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
|
||||
if (StringUtils.isNotEmpty(includeFields)) {
|
||||
this.subFields = this.subFields.stream().filter(f -> ArrayUtils.contains(includeFields, f.getName())).collect(Collectors.toList());
|
||||
} else if (StringUtils.isNotEmpty(excludeFields)) {
|
||||
this.subFields = this.subFields.stream().filter(f -> !ArrayUtils.contains(excludeFields, f.getName())).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,7 +1448,28 @@ public class NewExcelUtil<T> {
|
||||
}
|
||||
|
||||
try {
|
||||
// 计算表格体总列数
|
||||
int totalCols = 0;
|
||||
for (Object[] os : fields) {
|
||||
Field field = (Field)os[0];
|
||||
if (Collection.class.isAssignableFrom(field.getType()) && subFields != null) {
|
||||
long subCount = subFields.stream().filter(f -> f.isAnnotationPresent(Excel.class)).count();
|
||||
totalCols += subCount;
|
||||
} else {
|
||||
totalCols++;
|
||||
}
|
||||
}
|
||||
if (totalCols == 0) totalCols = 1;
|
||||
|
||||
int currentRowNum = rownum;
|
||||
int colIndex = 0;
|
||||
Row row = null;
|
||||
boolean hasVisible = false;
|
||||
|
||||
// 布局配置:Label占用1列,Value占用2列,共3列
|
||||
int labelCols = 1;
|
||||
int valueCols = 2;
|
||||
int itemCols = labelCols + valueCols;
|
||||
|
||||
for (Object[] os : extraFields) {
|
||||
Field field = (Field)os[0];
|
||||
@@ -1451,43 +1479,50 @@ public class NewExcelUtil<T> {
|
||||
if (isExtraFieldHidden(field.getName())) {
|
||||
continue;
|
||||
}
|
||||
hasVisible = true;
|
||||
|
||||
Row row = sheet.createRow(currentRowNum++);
|
||||
// 自动换行:如果不是行首,且剩余空间不足,则换行
|
||||
if (row == null) {
|
||||
row = sheet.createRow(currentRowNum);
|
||||
} else if (colIndex > 0 && colIndex + itemCols > totalCols) {
|
||||
currentRowNum++;
|
||||
row = sheet.createRow(currentRowNum);
|
||||
colIndex = 0;
|
||||
}
|
||||
|
||||
// 创建标签单元格(第0列)
|
||||
Cell labelCell = row.createCell(0);
|
||||
// 1. 创建 Label 单元格
|
||||
Cell labelCell = row.createCell(colIndex);
|
||||
labelCell.setCellValue(attr.name());
|
||||
labelCell.setCellStyle(styles.get("extraLabel"));
|
||||
|
||||
// 创建值单元格(第1列)
|
||||
Cell valueCell = row.createCell(1);
|
||||
// 2. 创建 Value 单元格
|
||||
int valueStartCol = colIndex + labelCols;
|
||||
Cell valueCell = row.createCell(valueStartCol);
|
||||
Object value = field.get(entity);
|
||||
String cellValue = formatExtraCellValue(value, attr);
|
||||
valueCell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue);
|
||||
valueCell.setCellStyle(styles.get("extraValue"));
|
||||
|
||||
// 创建合并区域(第1列到第2列)
|
||||
CellRangeAddress mergedRegion = new CellRangeAddress(row.getRowNum(), row.getRowNum(), 1, 2);
|
||||
sheet.addMergedRegion(mergedRegion);
|
||||
// 3. 合并 Value 单元格
|
||||
if (valueCols > 1) {
|
||||
int valueEndCol = valueStartCol + valueCols - 1;
|
||||
CellRangeAddress mergedRegion = new CellRangeAddress(row.getRowNum(), row.getRowNum(), valueStartCol, valueEndCol);
|
||||
sheet.addMergedRegion(mergedRegion);
|
||||
|
||||
// 设置边框
|
||||
RegionUtil.setBorderTop(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderBottom(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderLeft(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderRight(BorderStyle.THIN, mergedRegion, sheet);
|
||||
}
|
||||
|
||||
// 手动设置合并区域的边框,确保完整显示
|
||||
RegionUtil.setBorderTop(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderBottom(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderLeft(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderRight(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setTopBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
|
||||
RegionUtil.setBottomBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
|
||||
RegionUtil.setLeftBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
|
||||
RegionUtil.setRightBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
|
||||
colIndex += itemCols;
|
||||
}
|
||||
|
||||
// 设置列宽
|
||||
sheet.setColumnWidth(0, 15 * 256); // 标签列宽
|
||||
sheet.setColumnWidth(1, 20 * 256); // 值列宽
|
||||
sheet.setColumnWidth(2, 20 * 256); // 值列宽
|
||||
|
||||
// 更新当前行号,在额外表头和数据表头之间空一行
|
||||
rownum = currentRowNum + 1;
|
||||
if (hasVisible) {
|
||||
rownum = currentRowNum + 2;
|
||||
}
|
||||
subMergedFirstRowNum = rownum;
|
||||
subMergedLastRowNum = rownum;
|
||||
|
||||
@@ -1508,6 +1543,10 @@ public class NewExcelUtil<T> {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (value instanceof BigDecimal && attr.scale() >= 0) {
|
||||
return ((BigDecimal) value).setScale(attr.scale(), attr.roundingMode()).toString();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(attr.dateFormat())) {
|
||||
return parseDateToStr(attr.dateFormat(), value);
|
||||
}
|
||||
@@ -1808,7 +1847,7 @@ public class NewExcelUtil<T> {
|
||||
row = sheet.createRow(rowNo);
|
||||
}
|
||||
// 子字段也要排序
|
||||
List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
|
||||
List<Field> subFields = this.subFields;
|
||||
List<Field> sortedSubFields = subFields.stream().sorted(Comparator.comparing(subField -> {
|
||||
Excel subExcel = subField.getAnnotation(Excel.class);
|
||||
return subExcel.sort();
|
||||
@@ -1832,4 +1871,4 @@ public class NewExcelUtil<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.core.common.utils;
|
||||
|
||||
import com.core.common.constant.Constants;
|
||||
import com.core.common.core.text.Convert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@@ -20,10 +22,13 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端工具类
|
||||
*
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
public class ServletUtils {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ServletUtils.class);
|
||||
|
||||
/**
|
||||
* 获取String参数
|
||||
*/
|
||||
@@ -130,7 +135,7 @@ public class ServletUtils {
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().print(string);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("渲染响应字符串失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.core.common.utils.bean;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -12,6 +15,9 @@ import java.util.regex.Pattern;
|
||||
* @author system
|
||||
*/
|
||||
public class BeanUtils extends org.springframework.beans.BeanUtils {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BeanUtils.class);
|
||||
|
||||
/**
|
||||
* Bean方法名中属性名开始的下标
|
||||
*/
|
||||
@@ -37,7 +43,7 @@ public class BeanUtils extends org.springframework.beans.BeanUtils {
|
||||
try {
|
||||
copyProperties(src, dest);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("Bean属性复制失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ public class FlowDefinitionController extends BaseController {
|
||||
ImageIO.write(image, "png", os);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("读取流程图片失败, deployId: {}", deployId, e);
|
||||
} finally {
|
||||
try {
|
||||
if (os != null) {
|
||||
@@ -123,7 +123,7 @@ public class FlowDefinitionController extends BaseController {
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("关闭输出流失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ public class FlowTaskController extends BaseController {
|
||||
ImageIO.write(image, "png", os);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("读取流程图片失败", e);
|
||||
} finally {
|
||||
try {
|
||||
if (os != null) {
|
||||
@@ -219,7 +219,7 @@ public class FlowTaskController extends BaseController {
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("关闭输出流失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,7 +722,7 @@ public class FlowableUtils {
|
||||
// 反射设置属性值
|
||||
field.set(propertyDto, attribute.getValue());
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
log.warn("反射设置属性值失败", e);
|
||||
// 如果反射设置失败则忽略该属性
|
||||
}
|
||||
});
|
||||
@@ -730,7 +730,7 @@ public class FlowableUtils {
|
||||
return propertyDto;
|
||||
}).collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("解析流程属性失败", e);
|
||||
return Collections.emptyList(); // 如果发生异常则返回空列表
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ public class FlowDefinitionServiceImpl extends FlowServiceFactory implements IFl
|
||||
}
|
||||
return AjaxResult.success("流程启动成功");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("流程启动错误", e);
|
||||
return AjaxResult.error("流程启动错误");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class FlowInstanceServiceImpl extends FlowServiceFactory implements IFlow
|
||||
runtimeService.startProcessInstanceById(procDefId, variables);
|
||||
return AjaxResult.success("流程启动成功");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("流程启动错误, procDefId: {}", procDefId, e);
|
||||
return AjaxResult.error("流程启动错误");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.core.generator.service.IGenTableColumnService;
|
||||
import com.core.generator.service.IGenTableService;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -43,6 +45,8 @@ import java.util.Map;
|
||||
@RestController
|
||||
@RequestMapping("/tool/gen")
|
||||
public class GenController extends BaseController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GenController.class);
|
||||
@Autowired
|
||||
private IGenTableService genTableService;
|
||||
|
||||
@@ -436,7 +440,7 @@ public class GenController extends BaseController {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
|
||||
writer.write(str);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("写入文件失败: {}", filePath, e);
|
||||
} finally {
|
||||
is.close();
|
||||
}
|
||||
|
||||
@@ -165,4 +165,23 @@ public interface ISysMenuService {
|
||||
* @return 完整路径
|
||||
*/
|
||||
public String generateFullPath(Long parentId, String currentPath);
|
||||
|
||||
/**
|
||||
* 刷新菜单缓存
|
||||
*/
|
||||
public void refreshMenuCache();
|
||||
|
||||
/**
|
||||
* 根据用户ID清除菜单缓存
|
||||
*/
|
||||
public void clearMenuCacheByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 将菜单分配给角色
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param menuIds 菜单ID列表
|
||||
* @return 结果
|
||||
*/
|
||||
public int allocateMenuToRole(Long roleId, List<Long> menuIds);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.core.common.core.domain.entity.SysRole;
|
||||
import com.core.common.core.domain.entity.SysUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.common.utils.StringUtils;
|
||||
import com.core.system.domain.SysRoleMenu;
|
||||
import com.core.system.domain.vo.MetaVo;
|
||||
import com.core.system.domain.vo.RouterVo;
|
||||
import com.core.system.mapper.SysMenuMapper;
|
||||
@@ -54,12 +55,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 查询系统菜单列表
|
||||
*
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @return 菜单列表
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : #menu.menuName)")
|
||||
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : (#menu.menuName != null ? #menu.menuName : 'all') + ':' + (#menu.visible != null ? #menu.visible : 'all') + ':' + (#menu.status != null ? #menu.status : 'all'))")
|
||||
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
|
||||
List<SysMenu> menuList = null;
|
||||
// 管理员显示所有菜单信息
|
||||
@@ -110,11 +111,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询菜单
|
||||
*
|
||||
*
|
||||
* @param userId 用户名称
|
||||
* @return 菜单列表
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuTree:' + #userId")
|
||||
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
|
||||
List<SysMenu> menus = null;
|
||||
if (SecurityUtils.isAdmin(userId)) {
|
||||
@@ -405,11 +407,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 新增保存菜单信息
|
||||
*
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@org.springframework.cache.annotation.Caching(evict = {
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||
})
|
||||
@@ -419,19 +422,27 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
if (sysMenu != null){
|
||||
return -1;
|
||||
}
|
||||
return menuMapper.insertMenu(menu);
|
||||
|
||||
int rows = menuMapper.insertMenu(menu);
|
||||
|
||||
// 如果是管理员创建菜单,自动分配给所有角色(可选逻辑)
|
||||
// 或者,可以将新菜单分配给创建者所属的角色
|
||||
// 这里我们暂时不自动分配,因为这可能不符合安全最佳实践
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改保存菜单信息
|
||||
*
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.Caching(evict = {
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId")
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId"),
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #menu.updateBy")
|
||||
})
|
||||
public int updateMenu(SysMenu menu) {
|
||||
//路径Path唯一性判断(排除当前菜单本身)
|
||||
@@ -450,14 +461,13 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 删除菜单管理信息
|
||||
*
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.Caching(evict = {
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menuId")
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||
})
|
||||
public int deleteMenuById(Long menuId) {
|
||||
return menuMapper.deleteMenuById(menuId);
|
||||
@@ -776,4 +786,50 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
return normalizePath(fullPath.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新菜单缓存
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||
public void refreshMenuCache() {
|
||||
log.info("菜单缓存已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID清除菜单缓存
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #userId")
|
||||
public void clearMenuCacheByUserId(Long userId) {
|
||||
log.info("清除用户 {} 的菜单树缓存", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将菜单分配给角色
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param menuIds 菜单ID列表
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||
public int allocateMenuToRole(Long roleId, List<Long> menuIds) {
|
||||
// 先删除该角色现有的所有菜单权限
|
||||
roleMenuMapper.deleteRoleMenuByRoleId(roleId);
|
||||
|
||||
// 重新分配菜单给角色
|
||||
if (menuIds != null && !menuIds.isEmpty()) {
|
||||
List<SysRoleMenu> roleMenuList = new ArrayList<>();
|
||||
for (Long menuId : menuIds) {
|
||||
SysRoleMenu rm = new SysRoleMenu();
|
||||
rm.setRoleId(roleId);
|
||||
rm.setMenuId(menuId);
|
||||
roleMenuList.add(rm);
|
||||
}
|
||||
return roleMenuMapper.batchRoleMenu(roleMenuList);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.core.system.domain.SysUserConfig;
|
||||
import com.core.common.utils.StringUtils;
|
||||
import com.core.system.mapper.SysUserConfigMapper;
|
||||
import com.core.system.service.ISysUserConfigService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,13 +15,15 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户配置Service业务层处理
|
||||
*
|
||||
* @author
|
||||
*
|
||||
* @author
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Service
|
||||
public class SysUserConfigServiceImpl extends ServiceImpl<SysUserConfigMapper, SysUserConfig> implements ISysUserConfigService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(SysUserConfigServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private SysUserConfigMapper sysUserConfigMapper;
|
||||
|
||||
@@ -170,8 +174,7 @@ public class SysUserConfigServiceImpl extends ServiceImpl<SysUserConfigMapper, S
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录错误日志以便调试
|
||||
System.err.println("保存用户配置时发生错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("保存用户配置时发生错误, userId: {}, configKey: {}", userId, configKey, e);
|
||||
throw e; // 重新抛出异常让上层处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Web Layer - API Controllers
|
||||
|
||||
**Module**: `openhis-application/web`
|
||||
**Role**: API endpoint layer - all REST controllers for frontend communication
|
||||
|
||||
## OVERVIEW
|
||||
46 web modules serving REST APIs for all business functionality.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
web/
|
||||
├── [module-name]/
|
||||
│ ├── controller/ # REST endpoints (@RestController)
|
||||
│ ├── dto/ # Data transfer objects
|
||||
│ ├── mapper/ # MyBatis mappers (if module-specific)
|
||||
│ └── appservice/ # Application service layer
|
||||
│ └── impl/
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| API endpoints | `*/controller/*Controller.java` |
|
||||
| Request/Response schemas | `*/dto/*.java` |
|
||||
| Business logic orchestration | `*/appservice/*.java` |
|
||||
|
||||
## CONVENTIONS
|
||||
- Controllers: `@RestController`, `@RequestMapping("/module-name")`
|
||||
- Standard response: `AjaxResult` from core-common
|
||||
- DTO naming: `XxxRequest`, `XxxResponse`, `XxxDTO`
|
||||
- Service pattern: interface in `appservice/`, impl in `appservice/impl/`
|
||||
- API naming: `listXxx()`, `getXxx()`, `addXxx()`, `updateXxx()`, `deleteXxx()`
|
||||
|
||||
## ANTI-PATTERNS
|
||||
- Never put business logic in controllers - delegate to appservice
|
||||
- Never return raw entities - use DTOs
|
||||
- Never bypass `AjaxResult` wrapper
|
||||
- Never create module-specific mappers without justification
|
||||
@@ -104,4 +104,8 @@ public class InstrumentManageDto {
|
||||
/** 备注 */
|
||||
private String remarks;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Integer getInstrumentTypeEnum() {
|
||||
return instrumentTypeEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,19 @@ public class InstrumentManageInitDto {
|
||||
private List<InstrumentType> InstrumentTypeList;
|
||||
private List<InstrumentStatusEnumOption> InstrumentStatusEnumList;
|
||||
|
||||
// 手动添加 setter 方法
|
||||
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
|
||||
this.statusFlagOptions = statusFlagOptions;
|
||||
}
|
||||
|
||||
public void setInstrumentTypeList(List<InstrumentType> InstrumentTypeList) {
|
||||
this.InstrumentTypeList = InstrumentTypeList;
|
||||
}
|
||||
|
||||
public void setInstrumentStatusEnumList(List<InstrumentStatusEnumOption> InstrumentStatusEnumList) {
|
||||
this.InstrumentStatusEnumList = InstrumentStatusEnumList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
|
||||
@@ -13,4 +13,13 @@ import java.util.List;
|
||||
public class InstrumentStatusRequest {
|
||||
private List<Long> ids;
|
||||
private String type;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,36 @@ public class LisConfigManageDto {
|
||||
|
||||
private List<ActivityDefSpecimenDef> activityDefSpecimenDefs;
|
||||
|
||||
// 手动添加 getter 和 setter 方法
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public List<ActivityDefDeviceDef> getActivityDefDeviceDefs() {
|
||||
return activityDefDeviceDefs;
|
||||
}
|
||||
|
||||
public void setActivityDefDeviceDefs(List<ActivityDefDeviceDef> activityDefDeviceDefs) {
|
||||
this.activityDefDeviceDefs = activityDefDeviceDefs;
|
||||
}
|
||||
|
||||
public List<ActivityDefObservationDef> getActivityDefObservationDefs() {
|
||||
return activityDefObservationDefs;
|
||||
}
|
||||
|
||||
public void setActivityDefObservationDefs(List<ActivityDefObservationDef> activityDefObservationDefs) {
|
||||
this.activityDefObservationDefs = activityDefObservationDefs;
|
||||
}
|
||||
|
||||
public List<ActivityDefSpecimenDef> getActivityDefSpecimenDefs() {
|
||||
return activityDefSpecimenDefs;
|
||||
}
|
||||
|
||||
public void setActivityDefSpecimenDefs(List<ActivityDefSpecimenDef> activityDefSpecimenDefs) {
|
||||
this.activityDefSpecimenDefs = activityDefSpecimenDefs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,16 @@ public class LisConfigManageInitDto {
|
||||
|
||||
private List<SpecimenDefinition> specimenDefs;
|
||||
|
||||
// 手动添加 setter 方法
|
||||
public void setDeviceDefs(List<DeviceDefinition> deviceDefs) {
|
||||
this.deviceDefs = deviceDefs;
|
||||
}
|
||||
|
||||
public void setObservationDefs(List<ObservationDefinition> observationDefs) {
|
||||
this.observationDefs = observationDefs;
|
||||
}
|
||||
|
||||
public void setSpecimenDefs(List<SpecimenDefinition> specimenDefs) {
|
||||
this.specimenDefs = specimenDefs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,16 @@ public class ObservationDefManageDto {
|
||||
/** 删除状态) */
|
||||
private String deleteFlag;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Long getInstrumentId() {
|
||||
return instrumentId;
|
||||
}
|
||||
|
||||
public Integer getStatusEnum() {
|
||||
return statusEnum;
|
||||
}
|
||||
|
||||
|
||||
public Integer getObservationTypeEnum() {
|
||||
return observationTypeEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,19 @@ public class ObservationDefManageInitDto {
|
||||
private List<ObservationTypeEnumOption> ObservationTypeList;
|
||||
private List<InstrumentEnumOption> instrumentEnumOptionList;
|
||||
|
||||
// 手动添加 setter 方法
|
||||
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
|
||||
this.statusFlagOptions = statusFlagOptions;
|
||||
}
|
||||
|
||||
public void setObservationTypeList(List<ObservationTypeEnumOption> ObservationTypeList) {
|
||||
this.ObservationTypeList = ObservationTypeList;
|
||||
}
|
||||
|
||||
public void setInstrumentEnumOptionList(List<InstrumentEnumOption> instrumentEnumOptionList) {
|
||||
this.instrumentEnumOptionList = instrumentEnumOptionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
|
||||
@@ -13,4 +13,13 @@ import java.util.List;
|
||||
public class ObservationDefStatusRequest {
|
||||
private List<Long> ids;
|
||||
private String type;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,8 @@ public class ReportResultManageDto {
|
||||
private String authoredTime; // 开单时间
|
||||
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Integer getGenderEnum() {
|
||||
return genderEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,12 @@ public class SampleCollectManageDto {
|
||||
private String authoredTime; // 开单时间
|
||||
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Integer getGenderEnum() {
|
||||
return genderEnum;
|
||||
}
|
||||
|
||||
public Integer getCollectionStatusEnum() {
|
||||
return collectionStatusEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,13 @@ import java.util.List;
|
||||
public class SampleCollectStatusRequest {
|
||||
private List<Long> ids;
|
||||
private String type;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,12 @@ public class SpecimenDefManageDto {
|
||||
private Integer statusEnum;
|
||||
private String statusEnumText;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Integer getSpecimenTypeEnum() {
|
||||
return specimenTypeEnum;
|
||||
}
|
||||
|
||||
public Integer getStatusEnum() {
|
||||
return statusEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ public class SpecimenDefManageInitDto {
|
||||
private List<statusEnumOption> statusFlagOptions;
|
||||
private List<SpecimenType> SpecimenTypeList;
|
||||
|
||||
// 手动添加 setter 方法
|
||||
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
|
||||
this.statusFlagOptions = statusFlagOptions;
|
||||
}
|
||||
|
||||
public void setSpecimenTypeList(List<SpecimenType> SpecimenTypeList) {
|
||||
this.SpecimenTypeList = SpecimenTypeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
|
||||
@@ -13,4 +13,13 @@ import java.util.List;
|
||||
public class SpecimenDefStatusRequest {
|
||||
private List<Long> ids;
|
||||
private String type;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +69,13 @@ public class AdjustPriceDataVo {
|
||||
private Long locationId;
|
||||
|
||||
private BigDecimal finalTotalQuantity;
|
||||
|
||||
// 手动添加 getter 方法
|
||||
public Long getItemId() {
|
||||
return itemId;
|
||||
}
|
||||
|
||||
public Integer getCategoryType() {
|
||||
return categoryType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,17 @@ public interface IDoctorScheduleAppService {
|
||||
|
||||
R<?> getTodayDoctorScheduleList();
|
||||
|
||||
R<?> getTodayMySchedule();
|
||||
|
||||
R<?> getDoctorScheduleListByDeptId(Long deptId);
|
||||
|
||||
R<?> getDoctorScheduleListByDeptIdAndDateRange(Long deptId, String startDate, String endDate);
|
||||
|
||||
R<?> addDoctorSchedule(DoctorSchedule doctorSchedule);
|
||||
|
||||
R<?> addDoctorScheduleWithDate(DoctorSchedule doctorSchedule, String scheduledDate);
|
||||
|
||||
R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule);
|
||||
|
||||
R<?> removeDoctorSchedule(Integer doctorScheduleId);
|
||||
}
|
||||
|
||||
@@ -5,4 +5,6 @@ import com.openhis.web.appointmentmanage.dto.SchedulePoolDto;
|
||||
|
||||
public interface ISchedulePoolAppService {
|
||||
R<?> addSchedulePool(SchedulePoolDto schedulePoolDto);
|
||||
|
||||
R<?> list(SchedulePoolDto schedulePoolDto);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
package com.openhis.web.appointmentmanage.appservice.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.appointmentmanage.domain.DoctorSchedule;
|
||||
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
|
||||
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||
import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper;
|
||||
import com.openhis.appointmentmanage.service.IDoctorScheduleService;
|
||||
import com.openhis.appointmentmanage.service.ISchedulePoolService;
|
||||
import com.openhis.appointmentmanage.service.IScheduleSlotService;
|
||||
import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
@Resource
|
||||
private IDoctorScheduleService doctorScheduleService;
|
||||
|
||||
|
||||
@Resource
|
||||
private ISchedulePoolService schedulePoolService;
|
||||
|
||||
@Resource
|
||||
private IScheduleSlotService scheduleSlotService;
|
||||
|
||||
@Resource
|
||||
private DoctorScheduleMapper doctorScheduleMapper;
|
||||
|
||||
|
||||
@Override
|
||||
public R<?> getDoctorScheduleList() {
|
||||
List<DoctorSchedule> list = doctorScheduleService.list();
|
||||
@@ -30,47 +43,48 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getTodayDoctorScheduleList() {
|
||||
// 获取今天的日期
|
||||
LocalDate today = LocalDate.now();
|
||||
DayOfWeek dayOfWeek = today.getDayOfWeek();
|
||||
|
||||
// 将 Java 的 DayOfWeek 转换为字符串表示
|
||||
String weekdayStr = convertDayOfWeekToString(dayOfWeek);
|
||||
|
||||
// 查询今天排班的医生
|
||||
LambdaQueryWrapper<DoctorSchedule> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(DoctorSchedule::getWeekday, weekdayStr) // 根据星期几查询
|
||||
.eq(DoctorSchedule::getIsStopped, false); // 只查询未停止的排班
|
||||
|
||||
List<DoctorSchedule> list = doctorScheduleService.list(queryWrapper);
|
||||
public R<?> getDoctorScheduleListByDeptId(Long deptId) {
|
||||
List<DoctorSchedule> list = doctorScheduleService.list(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DoctorSchedule>()
|
||||
.eq("dept_id", deptId));
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 DayOfWeek 转换为字符串表示
|
||||
* @param dayOfWeek DayOfWeek枚举
|
||||
* @return 对应的字符串表示
|
||||
*/
|
||||
private String convertDayOfWeekToString(DayOfWeek dayOfWeek) {
|
||||
switch (dayOfWeek) {
|
||||
case MONDAY:
|
||||
return "1"; // 或者 "星期一" 或 "Monday",取决于数据库中的实际存储格式
|
||||
case TUESDAY:
|
||||
return "2";
|
||||
case WEDNESDAY:
|
||||
return "3";
|
||||
case THURSDAY:
|
||||
return "4";
|
||||
case FRIDAY:
|
||||
return "5";
|
||||
case SATURDAY:
|
||||
return "6";
|
||||
case SUNDAY:
|
||||
return "7";
|
||||
default:
|
||||
return "1"; // 默认为星期一
|
||||
@Override
|
||||
public R<?> getDoctorScheduleListByDeptIdAndDateRange(Long deptId, String startDate, String endDate) {
|
||||
// 联表查询 adm_doctor_schedule LEFT JOIN adm_schedule_pool,
|
||||
// 通过 schedule_date 获取具体出诊日期,解决按星期匹配导致日期错位的问题
|
||||
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectScheduleWithDateByDeptAndRange(
|
||||
deptId, startDate, endDate);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public R<?> getTodayDoctorScheduleList() {
|
||||
// 联表查询 adm_schedule_pool,按今日具体日期查询排班,替代原来按星期匹配的方式
|
||||
String todayStr = LocalDate.now().toString(); // yyyy-MM-dd
|
||||
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectTodaySchedule(todayStr);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public R<?> getTodayMySchedule() {
|
||||
// 联表查询 adm_schedule_pool,按今日具体日期 + 医生ID 查询个人排班
|
||||
String todayStr = LocalDate.now().toString(); // yyyy-MM-dd
|
||||
|
||||
// 从 SecurityUtils 获取当前登录医生ID
|
||||
Long currentDoctorId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
|
||||
if (currentDoctorId != null) {
|
||||
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectTodayMySchedule(todayStr,
|
||||
currentDoctorId);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
// 如果未绑定医生,则返回空列表
|
||||
return R.ok(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,6 +92,11 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
if (ObjectUtil.isEmpty(doctorSchedule)) {
|
||||
return R.fail("医生排班不能为空");
|
||||
}
|
||||
|
||||
if (doctorSchedule.getLimitNumber() == null || doctorSchedule.getLimitNumber() <= 0) {
|
||||
return R.fail("限号数量必须大于0");
|
||||
}
|
||||
|
||||
// 创建新对象,排除id字段(数据库id列是GENERATED ALWAYS,由数据库自动生成)
|
||||
DoctorSchedule newSchedule = new DoctorSchedule();
|
||||
newSchedule.setWeekday(doctorSchedule.getWeekday());
|
||||
@@ -88,33 +107,316 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
newSchedule.setEndTime(doctorSchedule.getEndTime());
|
||||
newSchedule.setLimitNumber(doctorSchedule.getLimitNumber());
|
||||
// call_sign_record 字段不能为null,设置默认值为空字符串
|
||||
newSchedule.setCallSignRecord(doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
|
||||
newSchedule.setCallSignRecord(
|
||||
doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
|
||||
newSchedule.setRegisterItem(doctorSchedule.getRegisterItem() != null ? doctorSchedule.getRegisterItem() : "");
|
||||
newSchedule.setRegisterFee(doctorSchedule.getRegisterFee() != null ? doctorSchedule.getRegisterFee() : 0);
|
||||
newSchedule.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
|
||||
newSchedule
|
||||
.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
|
||||
newSchedule.setDiagnosisFee(doctorSchedule.getDiagnosisFee() != null ? doctorSchedule.getDiagnosisFee() : 0);
|
||||
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : false);
|
||||
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : true);
|
||||
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
||||
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
||||
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
||||
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
||||
|
||||
// 不设置id字段,让数据库自动生成
|
||||
// 使用自定义的insertWithoutId方法,确保INSERT语句不包含id字段
|
||||
int result = doctorScheduleMapper.insertWithoutId(newSchedule);
|
||||
boolean save = result > 0;
|
||||
if (save) {
|
||||
// 返回保存后的实体对象,包含数据库生成的ID
|
||||
return R.ok(newSchedule);
|
||||
|
||||
if (result > 0) {
|
||||
// 创建号源池,并传入正确的医生ID
|
||||
SchedulePool pool = createSchedulePool(newSchedule, doctorSchedule.getDoctorId());
|
||||
boolean poolSaved = schedulePoolService.save(pool);
|
||||
|
||||
if (poolSaved) {
|
||||
// 创建号源槽
|
||||
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
|
||||
newSchedule.getStartTime(), newSchedule.getEndTime());
|
||||
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
||||
|
||||
if (slotsSaved) {
|
||||
// 不更新available_num字段,因为它是数据库生成列
|
||||
// pool.setAvailableNum(newSchedule.getLimitNumber());
|
||||
// schedulePoolService.updateById(pool);
|
||||
|
||||
return R.ok(newSchedule);
|
||||
} else {
|
||||
throw new RuntimeException("创建号源槽失败");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("创建号源池失败");
|
||||
}
|
||||
} else {
|
||||
return R.fail("保存失败");
|
||||
return R.fail("保存排班信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> addDoctorScheduleWithDate(DoctorSchedule doctorSchedule, String scheduledDate) {
|
||||
if (ObjectUtil.isEmpty(doctorSchedule)) {
|
||||
return R.fail("医生排班不能为空");
|
||||
}
|
||||
|
||||
if (doctorSchedule.getLimitNumber() == null || doctorSchedule.getLimitNumber() <= 0) {
|
||||
return R.fail("限号数量必须大于0");
|
||||
}
|
||||
|
||||
// 创建新对象,排除id字段(数据库id列是GENERATED ALWAYS,由数据库自动生成)
|
||||
DoctorSchedule newSchedule = new DoctorSchedule();
|
||||
newSchedule.setWeekday(doctorSchedule.getWeekday());
|
||||
newSchedule.setTimePeriod(doctorSchedule.getTimePeriod());
|
||||
newSchedule.setDoctor(doctorSchedule.getDoctor());
|
||||
newSchedule.setClinic(doctorSchedule.getClinic());
|
||||
newSchedule.setStartTime(doctorSchedule.getStartTime());
|
||||
newSchedule.setEndTime(doctorSchedule.getEndTime());
|
||||
newSchedule.setLimitNumber(doctorSchedule.getLimitNumber());
|
||||
// call_sign_record 字段不能为null,设置默认值为空字符串
|
||||
newSchedule.setCallSignRecord(
|
||||
doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
|
||||
newSchedule.setRegisterItem(doctorSchedule.getRegisterItem() != null ? doctorSchedule.getRegisterItem() : "");
|
||||
newSchedule.setRegisterFee(doctorSchedule.getRegisterFee() != null ? doctorSchedule.getRegisterFee() : 0);
|
||||
newSchedule
|
||||
.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
|
||||
newSchedule.setDiagnosisFee(doctorSchedule.getDiagnosisFee() != null ? doctorSchedule.getDiagnosisFee() : 0);
|
||||
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : true);
|
||||
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
||||
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
||||
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
||||
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
||||
|
||||
// 不设置id字段,让数据库自动生成
|
||||
// 使用自定义的insertWithoutId方法,确保INSERT语句不包含id字段
|
||||
int result = doctorScheduleMapper.insertWithoutId(newSchedule);
|
||||
|
||||
if (result > 0) {
|
||||
// 创建号源池,并传入正确的医生ID和具体日期
|
||||
SchedulePool pool = createSchedulePoolWithDate(newSchedule, doctorSchedule.getDoctorId(), scheduledDate);
|
||||
boolean poolSaved = schedulePoolService.save(pool);
|
||||
|
||||
if (poolSaved) {
|
||||
// 创建号源槽
|
||||
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
|
||||
newSchedule.getStartTime(), newSchedule.getEndTime());
|
||||
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
||||
|
||||
if (slotsSaved) {
|
||||
return R.ok(newSchedule);
|
||||
} else {
|
||||
throw new RuntimeException("创建号源槽失败");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("创建号源池失败");
|
||||
}
|
||||
} else {
|
||||
return R.fail("保存排班信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule) {
|
||||
if (ObjectUtil.isEmpty(doctorSchedule) || ObjectUtil.isEmpty(doctorSchedule.getId())) {
|
||||
return R.fail("医生排班ID不能为空");
|
||||
}
|
||||
// 注意:此为核心更新,暂未处理号源池和号源槽的同步更新
|
||||
int result = doctorScheduleMapper.updateDoctorSchedule(doctorSchedule);
|
||||
return result > 0 ? R.ok(result) : R.fail("更新排班信息失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建号源池
|
||||
*/
|
||||
private SchedulePool createSchedulePool(DoctorSchedule schedule, Long doctorId) {
|
||||
SchedulePool pool = new SchedulePool();
|
||||
// 生成唯一池编码
|
||||
pool.setPoolCode("POOL_" + System.currentTimeMillis());
|
||||
pool.setHospitalId(1L); // 默认医院ID,实际项目中应从上下文获取
|
||||
pool.setDoctorId(doctorId); // 使用正确的医生ID
|
||||
pool.setDoctorName(schedule.getDoctor());
|
||||
pool.setDeptId(schedule.getDeptId());
|
||||
pool.setClinicRoom(schedule.getClinic());
|
||||
// 设置出诊日期,这里假设是下周的对应星期
|
||||
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
|
||||
pool.setShift(schedule.getTimePeriod());
|
||||
pool.setStartTime(schedule.getStartTime());
|
||||
pool.setEndTime(schedule.getEndTime());
|
||||
pool.setTotalQuota(schedule.getLimitNumber());
|
||||
pool.setBookedNum(0);
|
||||
pool.setLockedNum(0);
|
||||
// 不设置available_num,因为它是数据库生成列
|
||||
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
||||
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
||||
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
|
||||
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
||||
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
||||
pool.setSupportChannel("");
|
||||
pool.setStatus(1); // 1表示可用
|
||||
|
||||
// 设置时间字段
|
||||
java.util.Date now = new java.util.Date();
|
||||
java.util.Date tomorrow = new java.util.Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 明天的时间
|
||||
|
||||
pool.setReleaseTime(now);
|
||||
pool.setDeadlineTime(tomorrow); // 截止时间为明天
|
||||
pool.setScheduleId(schedule.getId());
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建号源池(使用具体日期)
|
||||
*/
|
||||
private SchedulePool createSchedulePoolWithDate(DoctorSchedule schedule, Long doctorId, String scheduledDateStr) {
|
||||
SchedulePool pool = new SchedulePool();
|
||||
// 生成唯一池编码
|
||||
pool.setPoolCode("POOL_" + System.currentTimeMillis());
|
||||
pool.setHospitalId(1L); // 默认医院ID,实际项目中应从上下文获取
|
||||
pool.setDoctorId(doctorId); // 使用正确的医生ID
|
||||
pool.setDoctorName(schedule.getDoctor());
|
||||
pool.setDeptId(schedule.getDeptId());
|
||||
pool.setClinicRoom(schedule.getClinic());
|
||||
|
||||
// 使用传入的具体日期
|
||||
if (scheduledDateStr != null && !scheduledDateStr.isEmpty()) {
|
||||
try {
|
||||
LocalDate scheduledDate = LocalDate.parse(scheduledDateStr);
|
||||
pool.setScheduleDate(scheduledDate);
|
||||
} catch (Exception e) {
|
||||
// 如果解析失败,回退到原来的计算方式
|
||||
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供具体日期,使用原来的计算方式
|
||||
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
|
||||
}
|
||||
|
||||
pool.setShift(schedule.getTimePeriod());
|
||||
pool.setStartTime(schedule.getStartTime());
|
||||
pool.setEndTime(schedule.getEndTime());
|
||||
pool.setTotalQuota(schedule.getLimitNumber());
|
||||
pool.setBookedNum(0);
|
||||
pool.setLockedNum(0);
|
||||
// 不设置available_num,因为它是数据库生成列
|
||||
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
||||
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
||||
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
|
||||
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
||||
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
||||
pool.setSupportChannel("");
|
||||
pool.setStatus(1); // 1表示可用
|
||||
|
||||
// 设置时间字段
|
||||
java.util.Date now = new java.util.Date();
|
||||
java.util.Date tomorrow = new java.util.Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 明天的时间
|
||||
|
||||
pool.setReleaseTime(now);
|
||||
pool.setDeadlineTime(tomorrow); // 截止时间为明天
|
||||
pool.setScheduleId(schedule.getId());
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建号源槽
|
||||
*/
|
||||
private List<ScheduleSlot> createScheduleSlots(Integer poolId, Integer limitNumber, LocalTime startTime,
|
||||
LocalTime endTime) {
|
||||
List<ScheduleSlot> slots = new ArrayList<>();
|
||||
|
||||
// 计算时间间隔
|
||||
long totalTimeMinutes = startTime.until(endTime, java.time.temporal.ChronoUnit.MINUTES);
|
||||
long interval = totalTimeMinutes / limitNumber;
|
||||
|
||||
for (int i = 1; i <= limitNumber; i++) {
|
||||
ScheduleSlot slot = new ScheduleSlot();
|
||||
slot.setPoolId(poolId);
|
||||
slot.setSeqNo(i); // 序号
|
||||
slot.setStatus(0); // 0表示可用
|
||||
// 计算预计叫号时间,均匀分布在开始时间和结束时间之间
|
||||
LocalTime expectTime = startTime.plusMinutes(interval * (i - 1));
|
||||
slot.setExpectTime(expectTime);
|
||||
java.util.Date now = new java.util.Date();
|
||||
slot.setCreateTime(now);
|
||||
slot.setUpdateTime(now);
|
||||
|
||||
slots.add(slot);
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据星期几计算具体日期(下周的对应星期)
|
||||
*/
|
||||
private LocalDate calculateScheduleDate(String weekday) {
|
||||
// 这里简单实现,实际项目中可能需要更复杂的日期计算逻辑
|
||||
LocalDate today = LocalDate.now();
|
||||
int currentDayOfWeek = today.getDayOfWeek().getValue(); // 1=Monday, 7=Sunday
|
||||
int targetDayOfWeek = getDayOfWeekNumber(weekday); // 假设weekday是中文如"周一"
|
||||
|
||||
// 计算到下周对应星期的天数差
|
||||
int daysToAdd = targetDayOfWeek - currentDayOfWeek + 7; // 加7确保是下周
|
||||
return today.plusDays(daysToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将中文星期转换为数字(1=周一,7=周日)
|
||||
*/
|
||||
private int getDayOfWeekNumber(String weekday) {
|
||||
switch (weekday) {
|
||||
case "周一":
|
||||
return 1;
|
||||
case "周二":
|
||||
return 2;
|
||||
case "周三":
|
||||
return 3;
|
||||
case "周四":
|
||||
return 4;
|
||||
case "周五":
|
||||
return 5;
|
||||
case "周六":
|
||||
return 6;
|
||||
case "周日":
|
||||
return 7;
|
||||
default:
|
||||
return 1; // 默认周一
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public R<?> removeDoctorSchedule(Integer doctorScheduleId) {
|
||||
if (doctorScheduleId == null && ObjectUtil.isEmpty(doctorScheduleId)) {
|
||||
if (doctorScheduleId == null) {
|
||||
return R.fail("排班id不能为空");
|
||||
}
|
||||
boolean remove = doctorScheduleService.removeById(doctorScheduleId);
|
||||
return R.ok(remove);
|
||||
|
||||
// 1. 根据排班ID找到关联的号源池
|
||||
List<SchedulePool> pools = schedulePoolService.list(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SchedulePool>()
|
||||
.eq("schedule_id", doctorScheduleId));
|
||||
|
||||
if (ObjectUtil.isNotEmpty(pools)) {
|
||||
List<Long> poolIds = pools.stream().map(SchedulePool::getId).collect(java.util.stream.Collectors.toList());
|
||||
|
||||
// 2. 根据号源池ID找到所有关联的号源槽
|
||||
List<ScheduleSlot> slots = scheduleSlotService.list(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ScheduleSlot>()
|
||||
.in("pool_id", poolIds));
|
||||
|
||||
if (ObjectUtil.isNotEmpty(slots)) {
|
||||
List<Integer> slotIds = slots.stream().map(ScheduleSlot::getId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
// 3. 逻辑删除所有号源槽
|
||||
scheduleSlotService.removeByIds(slotIds);
|
||||
}
|
||||
|
||||
// 4. 逻辑删除所有号源池
|
||||
schedulePoolService.removeByIds(poolIds);
|
||||
}
|
||||
|
||||
// 5. 逻辑删除主排班记录
|
||||
boolean removed = doctorScheduleService.removeById(doctorScheduleId);
|
||||
|
||||
return R.ok(removed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,15 @@ public class SchedulePoolAppServiceImpl implements ISchedulePoolAppService {
|
||||
boolean save = schedulePoolService.save(schedulePool);
|
||||
return R.ok(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> list(SchedulePoolDto schedulePoolDto) {
|
||||
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SchedulePool> wrapper =
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
|
||||
wrapper.like(ObjectUtil.isNotEmpty(schedulePoolDto.getDoctorName()), SchedulePool::getDoctorName, schedulePoolDto.getDoctorName());
|
||||
wrapper.eq(ObjectUtil.isNotNull(schedulePoolDto.getDeptId()), SchedulePool::getDeptId, schedulePoolDto.getDeptId());
|
||||
wrapper.ge(ObjectUtil.isNotEmpty(schedulePoolDto.getQueryBeginDate()), SchedulePool::getScheduleDate, schedulePoolDto.getQueryBeginDate());
|
||||
wrapper.le(ObjectUtil.isNotEmpty(schedulePoolDto.getQueryEndDate()), SchedulePool::getScheduleDate, schedulePoolDto.getQueryEndDate());
|
||||
return R.ok(schedulePoolService.list(wrapper));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
||||
if (result > 0) {
|
||||
// 4. 预约成功后,更新排班表状态
|
||||
DoctorSchedule schedule = new DoctorSchedule();
|
||||
schedule.setId(Math.toIntExact(slotId)); // 对应 XML 中的 WHERE id = #{id}
|
||||
schedule.setId(slotId); // 对应 XML 中的 WHERE id = #{id}
|
||||
schedule.setIsStopped(true); // 设置为已预约
|
||||
schedule.setStopReason("booked"); // 设置停用原因
|
||||
|
||||
@@ -113,8 +113,8 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
||||
try {
|
||||
ticketService.cancelTicket(slotId);
|
||||
DoctorSchedule schedule = new DoctorSchedule();
|
||||
schedule.setId(Math.toIntExact(slotId)); // 对应 WHERE id = #{id}
|
||||
schedule.setIsStopped(false); // 设置为 false (数据库对应 0)
|
||||
schedule.setId(slotId); // 对应 WHERE id = #{id}
|
||||
schedule.setIsStopped(false); // 设置为 false
|
||||
schedule.setStopReason(""); // 将原因清空 (设为空字符串)
|
||||
// 3. 调用自定义更新方法
|
||||
int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule);
|
||||
@@ -235,7 +235,11 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
||||
|
||||
// 日期处理:LocalDateTime 转 Date
|
||||
if (schedule.getCreateTime() != null) {
|
||||
ZonedDateTime zdt = schedule.getCreateTime().atZone(ZoneId.systemDefault());
|
||||
// 1. 先转成 Instant
|
||||
Instant instant = schedule.getCreateTime().toInstant();
|
||||
// 2. 结合时区转成 ZonedDateTime
|
||||
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
|
||||
// 3. 再转回 Date (如果 DTO 需要的是 Date)
|
||||
dto.setAppointmentDate(Date.from(zdt.toInstant()));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,53 @@ public class DoctorScheduleController {
|
||||
return R.ok(doctorScheduleAppService.getDoctorScheduleList());
|
||||
}
|
||||
|
||||
/*
|
||||
* 根据科室ID获取医生排班List
|
||||
*
|
||||
* */
|
||||
@GetMapping("/list-by-dept/{deptId}")
|
||||
public R<?> getDoctorScheduleListByDeptId(@PathVariable Long deptId) {
|
||||
return R.ok(doctorScheduleAppService.getDoctorScheduleListByDeptId(deptId));
|
||||
}
|
||||
|
||||
/*
|
||||
* 根据科室ID和日期范围获取医生排班List
|
||||
*
|
||||
* */
|
||||
@GetMapping("/list-by-dept-and-date")
|
||||
public R<?> getDoctorScheduleListByDeptIdAndDateRange(@RequestParam Long deptId,
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(doctorScheduleAppService.getDoctorScheduleListByDeptIdAndDateRange(deptId, startDate, endDate));
|
||||
}
|
||||
|
||||
/*
|
||||
* 新增医生排班
|
||||
*
|
||||
* */
|
||||
@PostMapping("/add")
|
||||
public R<?> addDoctorSchedule(@RequestBody DoctorSchedule doctorSchedule) {
|
||||
return R.ok(doctorScheduleAppService.addDoctorSchedule(doctorSchedule));
|
||||
return doctorScheduleAppService.addDoctorSchedule(doctorSchedule);
|
||||
}
|
||||
|
||||
/*
|
||||
* 新增医生排班(带具体日期)
|
||||
*
|
||||
* */
|
||||
@PostMapping("/add-with-date")
|
||||
public R<?> addDoctorScheduleWithDate(@RequestBody DoctorSchedule doctorSchedule) {
|
||||
// 从DoctorSchedule对象中获取scheduledDate字段
|
||||
String scheduledDate = doctorSchedule.getScheduledDate();
|
||||
return doctorScheduleAppService.addDoctorScheduleWithDate(doctorSchedule, scheduledDate);
|
||||
}
|
||||
|
||||
/*
|
||||
* 修改医生排班
|
||||
*
|
||||
* */
|
||||
@PutMapping("/update")
|
||||
public R<?> updateDoctorSchedule(@RequestBody DoctorSchedule doctorSchedule) {
|
||||
return doctorScheduleAppService.updateDoctorSchedule(doctorSchedule);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -49,4 +89,13 @@ public class DoctorScheduleController {
|
||||
return R.ok(doctorScheduleAppService.getTodayDoctorScheduleList());
|
||||
}
|
||||
|
||||
/*
|
||||
* 获取当前登录医生今日排班List
|
||||
*
|
||||
* */
|
||||
@GetMapping("/today-my-schedule")
|
||||
public R<?> getTodayMySchedule() {
|
||||
return doctorScheduleAppService.getTodayMySchedule();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ package com.openhis.web.appointmentmanage.controller;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.appointmentmanage.appservice.ISchedulePoolAppService;
|
||||
import com.openhis.web.appointmentmanage.dto.SchedulePoolDto;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@@ -20,8 +18,18 @@ public class SchedulePoolController {
|
||||
* 新增号源
|
||||
*
|
||||
* */
|
||||
@PostMapping("/add")
|
||||
public R<?> addSchedulePool(@RequestBody SchedulePoolDto schedulePoolDto) {
|
||||
return R.ok(schedulePoolAppService.addSchedulePool(schedulePoolDto));
|
||||
return schedulePoolAppService.addSchedulePool(schedulePoolDto);
|
||||
}
|
||||
|
||||
/*
|
||||
* 查询号源
|
||||
*
|
||||
* */
|
||||
@GetMapping("/list")
|
||||
public R<?> list(SchedulePoolDto schedulePoolDto) {
|
||||
return schedulePoolAppService.list(schedulePoolDto);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@ import java.time.LocalTime;
|
||||
@Data
|
||||
public class SchedulePoolDto {
|
||||
/** id */
|
||||
private Integer id;
|
||||
private Long id;
|
||||
|
||||
/** 业务编号 */
|
||||
private String poolCode;
|
||||
|
||||
/** 医院ID */
|
||||
private Integer hospitalId;
|
||||
private Long hospitalId;
|
||||
|
||||
/** 科室ID */
|
||||
private Integer deptId;
|
||||
private Long deptId;
|
||||
|
||||
/** 医生ID */
|
||||
private Integer doctorId;
|
||||
private Long doctorId;
|
||||
|
||||
/** 医生姓名 */
|
||||
private String doctorName;
|
||||
@@ -86,17 +86,23 @@ public class SchedulePoolDto {
|
||||
private Integer version;
|
||||
|
||||
/** 操作人ID */
|
||||
private Integer opUserId;
|
||||
private Long opUserId;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 排班ID */
|
||||
private Integer scheduleId;
|
||||
private Long scheduleId;
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 查询开始日期 */
|
||||
private String queryBeginDate;
|
||||
|
||||
/** 查询结束日期 */
|
||||
private String queryEndDate;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,19 @@ public interface IOrganizationAppService {
|
||||
/**
|
||||
* 查询机构树
|
||||
*
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param classEnumList 科室分类列表(逗号分隔的值)
|
||||
* @param sortField 排序字段
|
||||
* @param sortOrder 排序方向
|
||||
* @param request 请求数据
|
||||
* @param sortField 排序字段
|
||||
* @param sortOrder 排序方向
|
||||
* @param request 请求数据
|
||||
* @return 机构树分页列表
|
||||
*/
|
||||
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, List<String> classEnumList,
|
||||
String sortField, String sortOrder, HttpServletRequest request);
|
||||
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum,
|
||||
List<String> classEnumList,
|
||||
String sortField, String sortOrder, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 机构信息详情
|
||||
@@ -67,4 +68,15 @@ public interface IOrganizationAppService {
|
||||
*/
|
||||
R<?> inactiveOrg(Long orgId);
|
||||
|
||||
/**
|
||||
* 获取挂号科室列表
|
||||
*
|
||||
* @param pageNum 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 机构/科室名称
|
||||
* @param orgName 机构名称
|
||||
* @return 挂号科室列表
|
||||
*/
|
||||
R<?> getRegisterOrganizations(Integer pageNum, Integer pageSize, String name, String orgName);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.openhis.web.basedatamanage.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.AssignSeqUtil;
|
||||
@@ -27,8 +26,6 @@ import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.baomidou.mybatisplus.core.toolkit.StringUtils.camelToUnderline;
|
||||
|
||||
@Service
|
||||
public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
|
||||
@@ -39,8 +36,9 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
private AssignSeqUtil assignSeqUtil;
|
||||
|
||||
@Override
|
||||
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, List<String> classEnumList,
|
||||
String sortField, String sortOrder, HttpServletRequest request) {
|
||||
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum,
|
||||
List<String> classEnumList,
|
||||
String sortField, String sortOrder, HttpServletRequest request) {
|
||||
|
||||
// 使用Page对象进行分页查询
|
||||
Page<Organization> page = new Page<>(pageNo, pageSize);
|
||||
@@ -63,24 +61,24 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
if (i == 0) {
|
||||
// 第一个条件
|
||||
wrapper.and(subWrapper -> {
|
||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||
.or() // 或者
|
||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||
.or() // 或者
|
||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||
.or() // 或者
|
||||
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||
.or() // 或者
|
||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||
.or() // 或者
|
||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||
.or() // 或者
|
||||
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
||||
});
|
||||
} else {
|
||||
// 后续条件使用OR连接
|
||||
wrapper.or(subWrapper -> {
|
||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||
.or() // 或者
|
||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||
.or() // 或者
|
||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||
.or() // 或者
|
||||
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||
.or() // 或者
|
||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||
.or() // 或者
|
||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||
.or() // 或者
|
||||
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -113,7 +111,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
private List<OrganizationDto> buildTree(List<Organization> records) {
|
||||
// 按b_no的层级排序,确保父节点先处理
|
||||
List<Organization> sortedRecords = records.stream()
|
||||
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
|
||||
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
|
||||
|
||||
Map<String, OrganizationDto> nodeMap = new HashMap<>();
|
||||
List<OrganizationDto> tree = new ArrayList<>();
|
||||
@@ -158,17 +156,20 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
public R<?> getOrgInfo(Long orgId) {
|
||||
Organization organization = organizationService.getById(orgId);
|
||||
if (organization == null) {
|
||||
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] {"机构信息"}));
|
||||
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] { "机构信息" }));
|
||||
}
|
||||
|
||||
|
||||
// 转换为DTO对象,确保数据格式一致
|
||||
OrganizationDto organizationDto = new OrganizationDto();
|
||||
BeanUtils.copyProperties(organization, organizationDto);
|
||||
organizationDto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, organizationDto.getTypeEnum()));
|
||||
organizationDto
|
||||
.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, organizationDto.getTypeEnum()));
|
||||
organizationDto.setClassEnum_dictText(formatClassEnumDictText(organizationDto.getClassEnum()));
|
||||
organizationDto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, organizationDto.getActiveFlag()));
|
||||
|
||||
return R.ok(organizationDto, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息查询"}));
|
||||
organizationDto
|
||||
.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, organizationDto.getActiveFlag()));
|
||||
|
||||
return R.ok(organizationDto,
|
||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息查询" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +196,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
// 如果传了上级科室 把当前的code拼到后边
|
||||
if (StringUtils.isNotEmpty(organization.getBusNo())) {
|
||||
organization.setBusNo(String.format(CommonConstants.Common.MONTAGE_FORMAT, organization.getBusNo(),
|
||||
CommonConstants.Common.POINT, code));
|
||||
CommonConstants.Common.POINT, code));
|
||||
} else {
|
||||
organization.setBusNo(code);
|
||||
}
|
||||
@@ -204,7 +205,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
}
|
||||
// 返回机构id
|
||||
return R.ok(organization.getId(),
|
||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息更新添加"}));
|
||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息更新添加" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,8 +225,8 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
// 删除机构信息
|
||||
boolean deleteOrgSuccess = organizationService.removeByIds(orgIdList);
|
||||
return deleteOrgSuccess
|
||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00005, new Object[] {"机构信息"}))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息"}));
|
||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00005, new Object[] { "机构信息" }))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] { "机构信息" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,8 +239,9 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
public R<?> activeOrg(Long orgId) {
|
||||
// 机构启用
|
||||
boolean result = organizationService.activeOrg(orgId);
|
||||
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息启用"}))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息启用"}));
|
||||
return result
|
||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息启用" }))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] { "机构信息启用" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,8 +254,9 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
public R<?> inactiveOrg(Long orgId) {
|
||||
// 机构停用
|
||||
boolean result = organizationService.inactiveOrg(orgId);
|
||||
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息停用"}))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息停用"}));
|
||||
return result
|
||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息停用" }))
|
||||
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] { "机构信息停用" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,6 +288,62 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
return String.join(",", dictTexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取挂号科室列表
|
||||
*
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 机构/科室名称
|
||||
* @param orgName 机构名称
|
||||
* @return 挂号科室列表
|
||||
*/
|
||||
@Override
|
||||
public R<?> getRegisterOrganizations(Integer pageNo, Integer pageSize, String name, String orgName) {
|
||||
// 使用Page对象进行分页查询
|
||||
Page<Organization> page = new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
|
||||
|
||||
// 创建查询条件,只查询register_flag为1的组织机构
|
||||
LambdaQueryWrapper<Organization> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Organization::getRegisterFlag, 1); // 只获取挂号科室
|
||||
queryWrapper.eq(Organization::getDeleteFlag, "0"); // 确保未删除
|
||||
|
||||
// 添加名称过滤条件
|
||||
if (StringUtils.isNotEmpty(name)) {
|
||||
queryWrapper.like(Organization::getName, name);
|
||||
}
|
||||
|
||||
// 如果有机构名称筛选
|
||||
if (StringUtils.isNotEmpty(orgName)) {
|
||||
// 这里假设 orgName 是父机构名称,如果需要更复杂的关联查询可在此扩展
|
||||
// 当前逻辑暂保持与原逻辑一致的过滤方式或根据需求调整
|
||||
}
|
||||
|
||||
// 按编码排序
|
||||
queryWrapper.orderByAsc(Organization::getBusNo);
|
||||
|
||||
// 执行分页查询
|
||||
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
|
||||
|
||||
// 转换为DTO对象并设置字典文本
|
||||
List<OrganizationDto> organizationDtoList = resultPage.getRecords().stream().map(org -> {
|
||||
OrganizationDto dto = new OrganizationDto();
|
||||
BeanUtils.copyProperties(org, dto);
|
||||
dto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, dto.getTypeEnum()));
|
||||
dto.setClassEnum_dictText(formatClassEnumDictText(dto.getClassEnum()));
|
||||
dto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, dto.getActiveFlag()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 创建返回分页对象
|
||||
Page<OrganizationDto> finalResult = new Page<>();
|
||||
finalResult.setRecords(organizationDtoList);
|
||||
finalResult.setTotal(resultPage.getTotal());
|
||||
finalResult.setSize(resultPage.getSize());
|
||||
finalResult.setCurrent(resultPage.getCurrent());
|
||||
|
||||
return R.ok(finalResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验字段是否为指定类中的有效属性
|
||||
*/
|
||||
|
||||
@@ -64,6 +64,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
|
||||
.add(new OrgLocInitDto.locationFormOption(LocationForm.CABINET.getValue(), LocationForm.CABINET.getInfo()));
|
||||
chargeItemStatusOptions.add(
|
||||
new OrgLocInitDto.locationFormOption(LocationForm.PHARMACY.getValue(), LocationForm.PHARMACY.getInfo()));
|
||||
chargeItemStatusOptions.add(
|
||||
new OrgLocInitDto.locationFormOption(LocationForm.WAREHOUSE.getValue(), LocationForm.WAREHOUSE.getInfo()));
|
||||
|
||||
// 获取科室下拉选列表
|
||||
List<Organization> organizationList = organizationService.getList(OrganizationType.DEPARTMENT.getValue(), null);
|
||||
@@ -89,6 +91,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
|
||||
locationList = locationService.getCabinetList();
|
||||
} else if (LocationForm.PHARMACY.getValue().equals(locationForm)) {
|
||||
locationList = locationService.getPharmacyList();
|
||||
} else if (LocationForm.WAREHOUSE.getValue().equals(locationForm)) {
|
||||
locationList = locationService.getWarehouseList();
|
||||
}
|
||||
List<OrgLocInitDto.locationOption> locationOptions = locationList.stream()
|
||||
.map(location -> new OrgLocInitDto.locationOption(location.getId(), location.getName()))
|
||||
|
||||
@@ -42,24 +42,24 @@ public class OrganizationController {
|
||||
/**
|
||||
* 机构分页列表
|
||||
*
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param classEnum 科室分类(支持多选,逗号分隔)
|
||||
* @param sortField 排序字段
|
||||
* @param sortOrder 排序方向
|
||||
* @param request 请求对象
|
||||
* @param request 请求对象
|
||||
* @return 机构分页列表
|
||||
*/
|
||||
@GetMapping(value = "/organization")
|
||||
public R<?> getOrganizationPage(@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "typeEnum", required = false) Integer typeEnum,
|
||||
@RequestParam(value = "classEnum", required = false) String classEnum,
|
||||
@RequestParam(value = "sortField", required = false) String sortField,
|
||||
@RequestParam(value = "sortOrder", required = false) String sortOrder, HttpServletRequest request) {
|
||||
@RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "typeEnum", required = false) Integer typeEnum,
|
||||
@RequestParam(value = "classEnum", required = false) String classEnum,
|
||||
@RequestParam(value = "sortField", required = false) String sortField,
|
||||
@RequestParam(value = "sortOrder", required = false) String sortOrder, HttpServletRequest request) {
|
||||
|
||||
// 解析classEnum参数,支持逗号分隔的多个值
|
||||
List<String> classEnumList = null;
|
||||
@@ -67,10 +67,10 @@ public class OrganizationController {
|
||||
classEnumList = Arrays.asList(classEnum.split(","));
|
||||
}
|
||||
|
||||
Page<OrganizationDto> organizationTree =
|
||||
iOrganizationAppService.getOrganizationTree(pageNo, pageSize, name, typeEnum, classEnumList, sortField, sortOrder, request);
|
||||
Page<OrganizationDto> organizationTree = iOrganizationAppService.getOrganizationTree(pageNo, pageSize, name,
|
||||
typeEnum, classEnumList, sortField, sortOrder, request);
|
||||
return R.ok(organizationTree,
|
||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00009, new Object[] {"机构信息"}));
|
||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00009, new Object[] { "机构信息" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,4 +130,21 @@ public class OrganizationController {
|
||||
return iOrganizationAppService.inactiveOrg(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取挂号科室列表
|
||||
*
|
||||
* @param pageNum 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 机构/科室名称
|
||||
* @param orgName 机构名称
|
||||
* @return 挂号科室列表
|
||||
*/
|
||||
@GetMapping("/register-organizations")
|
||||
public R<?> getRegisterOrganizations(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String orgName) {
|
||||
return iOrganizationAppService.getRegisterOrganizations(pageNum, pageSize, name, orgName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.cardmanagement.dto.*;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 报卡管理 Service接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
public interface ICardManageAppService {
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
*
|
||||
* @return 统计数据
|
||||
*/
|
||||
CardStatisticsDto getStatistics();
|
||||
|
||||
/**
|
||||
* 分页查询报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
R<?> getCardPage(CardQueryDto queryParams);
|
||||
|
||||
/**
|
||||
* 获取报卡详情
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 报卡详情
|
||||
*/
|
||||
InfectiousCardDto getCardDetail(String cardNo);
|
||||
|
||||
/**
|
||||
* 获取审核记录
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 审核记录列表
|
||||
*/
|
||||
List<AuditRecordDto> getAuditRecords(String cardNo);
|
||||
|
||||
/**
|
||||
* 批量审核
|
||||
*
|
||||
* @param batchAuditDto 批量审核参数
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> batchAudit(BatchAuditDto batchAuditDto);
|
||||
|
||||
/**
|
||||
* 批量退回
|
||||
*
|
||||
* @param batchReturnDto 批量退回参数
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> batchReturn(BatchReturnDto batchReturnDto);
|
||||
|
||||
/**
|
||||
* 单条审核通过
|
||||
*
|
||||
* @param auditDto 审核参数
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> auditPass(SingleAuditDto auditDto);
|
||||
|
||||
/**
|
||||
* 单条退回
|
||||
*
|
||||
* @param returnDto 退回参数
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> auditReturn(SingleReturnDto returnDto);
|
||||
|
||||
/**
|
||||
* 导出报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @param response 响应
|
||||
*/
|
||||
void exportCards(CardQueryDto queryParams, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 获取科室树
|
||||
*
|
||||
* @return 科室树数据
|
||||
*/
|
||||
R<?> getDeptTree();
|
||||
|
||||
/**
|
||||
* 获取医生个人报卡统计数据
|
||||
*
|
||||
* @return 统计数据
|
||||
*/
|
||||
DoctorCardStatisticsDto getDoctorCardStatistics();
|
||||
|
||||
/**
|
||||
* 分页查询医生个人报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
R<?> getDoctorCardPage(DoctorCardQueryDto queryParams);
|
||||
|
||||
/**
|
||||
* 提交报卡
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> submitCard(String cardNo);
|
||||
|
||||
/**
|
||||
* 撤回报卡
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> withdrawCard(String cardNo);
|
||||
|
||||
/**
|
||||
* 删除报卡(状态变为作废)
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> deleteCard(String cardNo);
|
||||
|
||||
/**
|
||||
* 批量提交报卡
|
||||
*
|
||||
* @param cardNos 卡片编号列表
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> batchSubmitCards(List<String> cardNos);
|
||||
|
||||
/**
|
||||
* 批量删除报卡
|
||||
*
|
||||
* @param cardNos 卡片编号列表
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> batchDeleteCards(List<String> cardNos);
|
||||
|
||||
/**
|
||||
* 导出报卡为Word文档
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @param response 响应
|
||||
*/
|
||||
void exportCardToWord(String cardNo, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 更新医生报卡
|
||||
*
|
||||
* @param updateDto 更新参数
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> updateDoctorCard(DoctorCardUpdateDto updateDto);
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.openhis.infectious.domain.InfectiousAudit;
|
||||
import com.openhis.infectious.domain.InfectiousCard;
|
||||
import com.openhis.web.cardmanagement.appservice.ICardManageAppService;
|
||||
import com.openhis.web.cardmanagement.dto.*;
|
||||
import com.openhis.web.cardmanagement.mapper.InfectiousAuditMapper;
|
||||
import com.openhis.web.cardmanagement.mapper.InfectiousCardMapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 报卡管理 Service 实现
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class CardManageAppServiceImpl implements ICardManageAppService {
|
||||
|
||||
private final InfectiousCardMapper infectiousCardMapper;
|
||||
private final InfectiousAuditMapper infectiousAuditMapper;
|
||||
|
||||
@Override
|
||||
public CardStatisticsDto getStatistics() {
|
||||
CardStatisticsDto dto = new CardStatisticsDto();
|
||||
dto.setTodayPending(infectiousCardMapper.countTodayPending());
|
||||
dto.setMonthFailed(infectiousCardMapper.countMonthFailed());
|
||||
dto.setMonthSuccess(infectiousCardMapper.countMonthSuccess());
|
||||
dto.setMonthReported(infectiousCardMapper.countMonthReported());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getCardPage(CardQueryDto queryParams) {
|
||||
Page<InfectiousCard> page = new Page<>(queryParams.getPageNo(), queryParams.getPageSize());
|
||||
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 登记来源
|
||||
if (queryParams.getRegistrationSource() != null) {
|
||||
wrapper.eq(InfectiousCard::getRegistrationSource, queryParams.getRegistrationSource());
|
||||
}
|
||||
|
||||
// 状态
|
||||
if (StringUtils.hasText(queryParams.getStatus())) {
|
||||
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||
}
|
||||
|
||||
// 患者姓名模糊查询
|
||||
if (StringUtils.hasText(queryParams.getPatientName())) {
|
||||
wrapper.like(InfectiousCard::getPatName, queryParams.getPatientName());
|
||||
}
|
||||
|
||||
// 科室
|
||||
if (queryParams.getDeptId() != null) {
|
||||
wrapper.eq(InfectiousCard::getDeptId, queryParams.getDeptId());
|
||||
}
|
||||
|
||||
// 时间范围
|
||||
if (StringUtils.hasText(queryParams.getStartDate())) {
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(queryParams.getStartDate() + "T00:00:00");
|
||||
wrapper.ge(InfectiousCard::getCreateTime, startDateTime);
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getEndDate())) {
|
||||
LocalDateTime endDateTime = LocalDateTime.parse(queryParams.getEndDate() + "T23:59:59");
|
||||
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
|
||||
}
|
||||
|
||||
// 按创建时间倒序
|
||||
wrapper.orderByDesc(InfectiousCard::getCreateTime);
|
||||
|
||||
IPage<InfectiousCard> result = infectiousCardMapper.selectPage(page, wrapper);
|
||||
|
||||
// 转换为 DTO
|
||||
List<InfectiousCardDto> list = result.getRecords().stream().map(this::convertToDto).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("list", list);
|
||||
resultMap.put("total", result.getTotal());
|
||||
return R.ok(resultMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfectiousCardDto getCardDetail(String cardNo) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToDto(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditRecordDto> getAuditRecords(String cardNo) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<InfectiousAudit> records = infectiousAuditMapper.selectByCardId(card.getId());
|
||||
return records.stream().map(this::convertAuditToDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> batchAudit(BatchAuditDto batchAuditDto) {
|
||||
if (batchAuditDto.getCardNos() == null || batchAuditDto.getCardNos().isEmpty()) {
|
||||
return R.fail("请选择要审核的报卡");
|
||||
}
|
||||
|
||||
String auditorId = SecurityUtils.getUserId().toString();
|
||||
String auditorName = SecurityUtils.getUsername();
|
||||
|
||||
int successCount = 0;
|
||||
for (String cardNo : batchAuditDto.getCardNos()) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) continue;
|
||||
if ("2".equals(card.getStatus()) || "3".equals(card.getStatus())) continue;
|
||||
|
||||
// 更新状态为已审核
|
||||
String oldStatus = card.getStatus();
|
||||
card.setStatus("2");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
// 创建审核记录
|
||||
createAuditRecord(card.getId(), oldStatus, "2", "1", batchAuditDto.getAuditOpinion(),
|
||||
null, auditorId, auditorName, true, batchAuditDto.getCardNos().size());
|
||||
successCount++;
|
||||
}
|
||||
|
||||
return R.ok("批量审核成功,共审核" + successCount + "条");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> batchReturn(BatchReturnDto batchReturnDto) {
|
||||
if (batchReturnDto.getCardNos() == null || batchReturnDto.getCardNos().isEmpty()) {
|
||||
return R.fail("请选择要退回的报卡");
|
||||
}
|
||||
|
||||
String auditorId = SecurityUtils.getUserId().toString();
|
||||
String auditorName = SecurityUtils.getUsername();
|
||||
|
||||
int successCount = 0;
|
||||
for (String cardNo : batchReturnDto.getCardNos()) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) continue;
|
||||
if ("2".equals(card.getStatus()) || "3".equals(card.getStatus())) continue;
|
||||
|
||||
// 更新状态为退回 (审核失败)
|
||||
String oldStatus = card.getStatus();
|
||||
card.setStatus("5");
|
||||
card.setReturnReason(batchReturnDto.getReturnReason());
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
// 创建审核记录
|
||||
createAuditRecord(card.getId(), oldStatus, "5", "3", null,
|
||||
batchReturnDto.getReturnReason(), auditorId, auditorName, true, batchReturnDto.getCardNos().size());
|
||||
successCount++;
|
||||
}
|
||||
|
||||
return R.ok("批量退回成功,共退回" + successCount + "条");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> auditPass(SingleAuditDto auditDto) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(auditDto.getCardNo());
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
String auditorId = SecurityUtils.getUserId().toString();
|
||||
String auditorName = SecurityUtils.getUsername();
|
||||
|
||||
// 更新状态
|
||||
String oldStatus = card.getStatus();
|
||||
card.setStatus("2");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
// 创建审核记录
|
||||
createAuditRecord(card.getId(), oldStatus, "2", "2", auditDto.getAuditOpinion(),
|
||||
null, auditorId, auditorName, false, 1);
|
||||
|
||||
return R.ok("审核通过");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> auditReturn(SingleReturnDto returnDto) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(returnDto.getCardNo());
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
String auditorId = SecurityUtils.getUserId().toString();
|
||||
String auditorName = SecurityUtils.getUsername();
|
||||
|
||||
// 更新状态
|
||||
String oldStatus = card.getStatus();
|
||||
card.setStatus("5");
|
||||
card.setReturnReason(returnDto.getReturnReason());
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
// 创建审核记录
|
||||
createAuditRecord(card.getId(), oldStatus, "5", "4", null,
|
||||
returnDto.getReturnReason(), auditorId, auditorName, false, 1);
|
||||
|
||||
return R.ok("已退回");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportCards(CardQueryDto queryParams, HttpServletResponse response) {
|
||||
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 应用查询条件
|
||||
if (queryParams.getRegistrationSource() != null) {
|
||||
wrapper.eq(InfectiousCard::getRegistrationSource, queryParams.getRegistrationSource());
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getStatus())) {
|
||||
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getPatientName())) {
|
||||
wrapper.like(InfectiousCard::getPatName, queryParams.getPatientName());
|
||||
}
|
||||
if (queryParams.getDeptId() != null) {
|
||||
wrapper.eq(InfectiousCard::getDeptId, queryParams.getDeptId());
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getStartDate())) {
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(queryParams.getStartDate() + "T00:00:00");
|
||||
wrapper.ge(InfectiousCard::getCreateTime, startDateTime);
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getEndDate())) {
|
||||
LocalDateTime endDateTime = LocalDateTime.parse(queryParams.getEndDate() + "T23:59:59");
|
||||
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
|
||||
}
|
||||
wrapper.orderByDesc(InfectiousCard::getCreateTime);
|
||||
|
||||
List<InfectiousCard> cards = infectiousCardMapper.selectList(wrapper);
|
||||
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
Sheet sheet = workbook.createSheet("报卡列表");
|
||||
|
||||
// 创建表头
|
||||
Row headerRow = sheet.createRow(0);
|
||||
String[] headers = {"报卡编号", "患者姓名", "性别", "年龄", "疾病名称", "科室", "上报时间", "状态"};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers[i]);
|
||||
}
|
||||
|
||||
// 填充数据
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
InfectiousCard card = cards.get(i);
|
||||
Row row = sheet.createRow(i + 1);
|
||||
row.createCell(0).setCellValue(card.getCardNo());
|
||||
row.createCell(1).setCellValue(card.getPatName());
|
||||
row.createCell(2).setCellValue("1".equals(card.getSex()) ? "男" : "2".equals(card.getSex()) ? "女" : "未知");
|
||||
row.createCell(3).setCellValue(card.getAge() != null ? card.getAge() + "岁" : "");
|
||||
row.createCell(4).setCellValue(card.getDiseaseName());
|
||||
row.createCell(5).setCellValue(card.getDeptName());
|
||||
row.createCell(6).setCellValue(card.getCreateTime() != null ? dateFormat.format(card.getCreateTime()) : "");
|
||||
row.createCell(7).setCellValue(getStatusText(card.getStatus()));
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" +
|
||||
URLEncoder.encode("报卡列表.xlsx", StandardCharsets.UTF_8));
|
||||
workbook.write(response.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
log.error("导出报卡列表失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getDeptTree() {
|
||||
// 返回科室树数据,实际应从科室服务获取
|
||||
return R.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoctorCardStatisticsDto getDoctorCardStatistics() {
|
||||
Long doctorId = SecurityUtils.getUserId();
|
||||
|
||||
DoctorCardStatisticsDto dto = new DoctorCardStatisticsDto();
|
||||
Integer totalCount = infectiousCardMapper.countByDoctorId(doctorId);
|
||||
Integer pendingFailedCount = infectiousCardMapper.countPendingFailedByDoctorId(doctorId);
|
||||
Integer reportedCount = infectiousCardMapper.countReportedByDoctorId(doctorId);
|
||||
|
||||
dto.setTotalCount(totalCount != null ? totalCount : 0);
|
||||
dto.setPendingFailedCount(pendingFailedCount != null ? pendingFailedCount : 0);
|
||||
dto.setReportedCount(reportedCount != null ? reportedCount : 0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getDoctorCardPage(DoctorCardQueryDto queryParams) {
|
||||
Long doctorId = SecurityUtils.getUserId();
|
||||
|
||||
Page<InfectiousCard> page = new Page<>(queryParams.getPageNo(), queryParams.getPageSize());
|
||||
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 只查询当前医生的报卡
|
||||
wrapper.eq(InfectiousCard::getDoctorId, doctorId);
|
||||
|
||||
// 状态筛选
|
||||
if (StringUtils.hasText(queryParams.getStatus())) {
|
||||
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (StringUtils.hasText(queryParams.getStartDate())) {
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(queryParams.getStartDate() + "T00:00:00");
|
||||
wrapper.ge(InfectiousCard::getCreateTime, startDateTime);
|
||||
}
|
||||
if (StringUtils.hasText(queryParams.getEndDate())) {
|
||||
LocalDateTime endDateTime = LocalDateTime.parse(queryParams.getEndDate() + "T23:59:59");
|
||||
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
|
||||
}
|
||||
|
||||
// 关键词搜索(患者姓名或报卡名称)
|
||||
if (StringUtils.hasText(queryParams.getKeyword())) {
|
||||
wrapper.and(w -> w
|
||||
.like(InfectiousCard::getPatName, queryParams.getKeyword())
|
||||
.or()
|
||||
.like(InfectiousCard::getDiseaseName, queryParams.getKeyword())
|
||||
);
|
||||
}
|
||||
|
||||
// 按创建时间倒序
|
||||
wrapper.orderByDesc(InfectiousCard::getCreateTime);
|
||||
|
||||
IPage<InfectiousCard> result = infectiousCardMapper.selectPage(page, wrapper);
|
||||
|
||||
// 转换为 DTO
|
||||
List<DoctorCardListDto> list = result.getRecords().stream()
|
||||
.map(this::convertToDoctorCardListDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("list", list);
|
||||
resultMap.put("total", result.getTotal());
|
||||
return R.ok(resultMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> submitCard(String cardNo) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
// 验证权限:只能提交自己的报卡
|
||||
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
|
||||
return R.fail("无权操作此报卡");
|
||||
}
|
||||
|
||||
// 验证状态:只有暂存状态可以提交
|
||||
if (!"0".equals(card.getStatus())) {
|
||||
return R.fail("只能提交暂存状态的报卡");
|
||||
}
|
||||
|
||||
// 更新状态为已提交
|
||||
card.setStatus("1");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
return R.ok("提交成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> withdrawCard(String cardNo) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
// 验证权限:只能撤回自己的报卡
|
||||
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
|
||||
return R.fail("无权操作此报卡");
|
||||
}
|
||||
|
||||
// 验证状态:只有已提交状态可以撤回
|
||||
if (!"1".equals(card.getStatus())) {
|
||||
return R.fail("只能撤回已提交状态的报卡");
|
||||
}
|
||||
|
||||
// 更新状态为暂存
|
||||
card.setStatus("0");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
return R.ok("撤回成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> deleteCard(String cardNo) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
// 验证权限:只能删除自己的报卡
|
||||
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
|
||||
return R.fail("无权操作此报卡");
|
||||
}
|
||||
|
||||
// 验证状态:只有暂存状态可以删除
|
||||
if (!"0".equals(card.getStatus())) {
|
||||
return R.fail("只能删除暂存状态的报卡");
|
||||
}
|
||||
|
||||
// 更新状态为作废
|
||||
card.setStatus("6");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
|
||||
return R.ok("删除成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> batchSubmitCards(List<String> cardNos) {
|
||||
if (cardNos == null || cardNos.isEmpty()) {
|
||||
return R.fail("请选择要提交的报卡");
|
||||
}
|
||||
|
||||
Long doctorId = SecurityUtils.getUserId();
|
||||
int successCount = 0;
|
||||
|
||||
for (String cardNo : cardNos) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) continue;
|
||||
|
||||
// 验证权限:只能提交自己的报卡
|
||||
if (!card.getDoctorId().equals(doctorId)) continue;
|
||||
|
||||
// 验证状态:只有暂存状态可以提交
|
||||
if (!"0".equals(card.getStatus())) continue;
|
||||
|
||||
// 更新状态为已提交
|
||||
card.setStatus("1");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (successCount == 0) {
|
||||
return R.fail("没有可提交的报卡,只能提交暂存状态的报卡");
|
||||
}
|
||||
|
||||
return R.ok("批量提交成功,共提交" + successCount + "条");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> batchDeleteCards(List<String> cardNos) {
|
||||
if (cardNos == null || cardNos.isEmpty()) {
|
||||
return R.fail("请选择要删除的报卡");
|
||||
}
|
||||
|
||||
Long doctorId = SecurityUtils.getUserId();
|
||||
int successCount = 0;
|
||||
|
||||
for (String cardNo : cardNos) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) continue;
|
||||
|
||||
// 验证权限:只能删除自己的报卡
|
||||
if (!card.getDoctorId().equals(doctorId)) continue;
|
||||
|
||||
// 验证状态:只有暂存状态可以删除
|
||||
if (!"0".equals(card.getStatus())) continue;
|
||||
|
||||
// 更新状态为作废
|
||||
card.setStatus("6");
|
||||
card.setUpdateTime(new Date());
|
||||
infectiousCardMapper.updateById(card);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (successCount == 0) {
|
||||
return R.fail("没有可删除的报卡,只能删除暂存状态的报卡");
|
||||
}
|
||||
|
||||
return R.ok("批量删除成功,共删除" + successCount + "条");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> updateDoctorCard(DoctorCardUpdateDto updateDto) {
|
||||
// 获取当前登录用户信息
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
Long currentUserId = loginUser.getUserId();
|
||||
|
||||
// 查询报卡
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(updateDto.getCardNo());
|
||||
if (card == null) {
|
||||
return R.fail("报卡不存在");
|
||||
}
|
||||
|
||||
// 验证是否当前医生的报卡 - 根据 doctorId 字段验证
|
||||
if (!currentUserId.equals(card.getDoctorId())) {
|
||||
return R.fail("只能修改自己的报卡");
|
||||
}
|
||||
|
||||
// 验证状态是否允许修改(只能修改暂存状态的报卡)
|
||||
if (!"0".equals(card.getStatus())) {
|
||||
return R.fail("只能修改暂存状态的报卡");
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
card.setPhone(updateDto.getPhone());
|
||||
card.setOnsetDate(updateDto.getOnsetDate());
|
||||
card.setDiagDate(updateDto.getDiagDate());
|
||||
card.setDiseaseType(updateDto.getDiseaseType()); // 使用 diseaseType 字段
|
||||
card.setAddressProv(updateDto.getAddressProv());
|
||||
card.setAddressCity(updateDto.getAddressCity());
|
||||
card.setAddressCounty(updateDto.getAddressCounty());
|
||||
card.setAddressHouse(updateDto.getAddressHouse());
|
||||
card.setUpdateTime(new Date());
|
||||
card.setUpdateBy(loginUser.getUsername()); // 使用username作为更新者
|
||||
|
||||
card.setUpdateTime(new Date());
|
||||
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
|
||||
|
||||
card.setUpdateTime(new Date());
|
||||
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
|
||||
|
||||
card.setUpdateTime(new Date());
|
||||
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
|
||||
|
||||
int rows = infectiousCardMapper.updateById(card);
|
||||
if (rows > 0) {
|
||||
return R.ok("更新成功");
|
||||
}
|
||||
return R.fail("更新失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportCardToWord(String cardNo, HttpServletResponse response) {
|
||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||
if (card == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证权限:只能导出自己的报卡
|
||||
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证状态:只有已上报状态可以导出
|
||||
if (!"3".equals(card.getStatus())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: 实现 Word 导出逻辑,使用 Apache POI 或其他库
|
||||
// 这里简化为返回文本内容
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" +
|
||||
URLEncoder.encode("传染病报告卡-" + cardNo + ".docx", StandardCharsets.UTF_8));
|
||||
|
||||
// 实际应生成 Word 文档内容
|
||||
response.getWriter().write("报卡编号:" + cardNo);
|
||||
} catch (IOException e) {
|
||||
log.error("导出报卡失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为医生报卡列表 DTO
|
||||
*/
|
||||
private DoctorCardListDto convertToDoctorCardListDto(InfectiousCard card) {
|
||||
DoctorCardListDto dto = new DoctorCardListDto();
|
||||
BeanUtils.copyProperties(card, dto);
|
||||
dto.setCardName(getCardName(card.getCardNameCode()));
|
||||
dto.setSubmitTime(card.getCreateTime() != null ?
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm").format(card.getCreateTime()) : null);
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报卡名称
|
||||
*/
|
||||
private String getCardName(Integer cardNameCode) {
|
||||
if (cardNameCode == null) return "中华人民共和国传染病报告卡";
|
||||
switch (cardNameCode) {
|
||||
case 1: return "中华人民共和国传染病报告卡";
|
||||
case 2: return "甲类传染病报告卡";
|
||||
case 3: return "乙类传染病报告卡";
|
||||
case 4: return "丙类传染病报告卡";
|
||||
default: return "中华人民共和国传染病报告卡";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换审核记录为 DTO
|
||||
*/
|
||||
private AuditRecordDto convertAuditToDto(InfectiousAudit audit) {
|
||||
AuditRecordDto dto = new AuditRecordDto();
|
||||
BeanUtils.copyProperties(audit, dto);
|
||||
dto.setCardId(audit.getCardId() != null ? audit.getCardId().toString() : null);
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为报卡 DTO
|
||||
*/
|
||||
private InfectiousCardDto convertToDto(InfectiousCard card) {
|
||||
InfectiousCardDto dto = new InfectiousCardDto();
|
||||
BeanUtils.copyProperties(card, dto);
|
||||
dto.setStatusText(getStatusText(card.getStatus()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建审核记录
|
||||
*/
|
||||
private void createAuditRecord(Long cardId, String statusFrom, String statusTo, String auditType,
|
||||
String auditOpinion, String returnReason, String auditorId, String auditorName,
|
||||
Boolean isBatch, Integer batchSize) {
|
||||
InfectiousAudit audit = new InfectiousAudit();
|
||||
audit.setCardId(cardId);
|
||||
audit.setAuditSeq(infectiousAuditMapper.getNextAuditSeq(cardId));
|
||||
audit.setAuditType(auditType);
|
||||
audit.setAuditStatusFrom(statusFrom);
|
||||
audit.setAuditStatusTo(statusTo);
|
||||
audit.setAuditTime(LocalDateTime.now());
|
||||
audit.setAuditorId(auditorId);
|
||||
audit.setAuditorName(auditorName);
|
||||
audit.setAuditOpinion(auditOpinion);
|
||||
audit.setReasonForReturn(returnReason);
|
||||
audit.setIsBatch(isBatch);
|
||||
audit.setBatchSize(batchSize);
|
||||
infectiousAuditMapper.insert(audit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
private String getStatusText(String status) {
|
||||
switch (status) {
|
||||
case "0": return "暂存";
|
||||
case "1": return "已提交";
|
||||
case "2": return "审核通过";
|
||||
case "3": return "已上报";
|
||||
case "4": return "失败";
|
||||
case "5": return "审核失败";
|
||||
case "6": return "作废";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.cardmanagement.appservice.ICardManageAppService;
|
||||
import com.openhis.web.cardmanagement.dto.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 报卡管理 Controller
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/card-management")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class CardManageController {
|
||||
|
||||
private final ICardManageAppService cardManageAppService;
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
*
|
||||
* @return 统计数据
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public R<CardStatisticsDto> getStatistics() {
|
||||
return R.ok(cardManageAppService.getStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public R<?> getCardPage(CardQueryDto queryParams) {
|
||||
return cardManageAppService.getCardPage(queryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报卡详情
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 报卡详情
|
||||
*/
|
||||
@GetMapping("/detail/{cardNo}")
|
||||
public R<InfectiousCardDto> getCardDetail(@PathVariable String cardNo) {
|
||||
return R.ok(cardManageAppService.getCardDetail(cardNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核记录
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 审核记录列表
|
||||
*/
|
||||
@GetMapping("/audit-records/{cardNo}")
|
||||
public R<List<AuditRecordDto>> getAuditRecords(@PathVariable String cardNo) {
|
||||
return R.ok(cardManageAppService.getAuditRecords(cardNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量审核
|
||||
*
|
||||
* @param batchAuditDto 批量审核参数
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/batch-audit")
|
||||
public R<?> batchAudit(@RequestBody BatchAuditDto batchAuditDto) {
|
||||
return cardManageAppService.batchAudit(batchAuditDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量退回
|
||||
*
|
||||
* @param batchReturnDto 批量退回参数
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/batch-return")
|
||||
public R<?> batchReturn(@RequestBody BatchReturnDto batchReturnDto) {
|
||||
return cardManageAppService.batchReturn(batchReturnDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条审核通过
|
||||
*
|
||||
* @param auditDto 审核参数
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/audit-pass")
|
||||
public R<?> auditPass(@RequestBody SingleAuditDto auditDto) {
|
||||
return cardManageAppService.auditPass(auditDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条退回
|
||||
*
|
||||
* @param returnDto 退回参数
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/audit-return")
|
||||
public R<?> auditReturn(@RequestBody SingleReturnDto returnDto) {
|
||||
return cardManageAppService.auditReturn(returnDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @param response 响应
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void exportCards(CardQueryDto queryParams, HttpServletResponse response) {
|
||||
cardManageAppService.exportCards(queryParams, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取科室树
|
||||
*
|
||||
* @return 科室树数据
|
||||
*/
|
||||
@GetMapping("/dept-tree")
|
||||
public R<?> getDeptTree() {
|
||||
return cardManageAppService.getDeptTree();
|
||||
}
|
||||
|
||||
// ==================== 医生个人报卡管理 ====================
|
||||
|
||||
/**
|
||||
* 获取医生个人报卡统计数据
|
||||
*
|
||||
* @return 统计数据
|
||||
*/
|
||||
@GetMapping("/doctor/statistics")
|
||||
public R<DoctorCardStatisticsDto> getDoctorCardStatistics() {
|
||||
return R.ok(cardManageAppService.getDoctorCardStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询医生个人报卡列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
@GetMapping("/doctor/page")
|
||||
public R<?> getDoctorCardPage(DoctorCardQueryDto queryParams) {
|
||||
return cardManageAppService.getDoctorCardPage(queryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交报卡
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/doctor/submit/{cardNo}")
|
||||
public R<?> submitCard(@PathVariable String cardNo) {
|
||||
return cardManageAppService.submitCard(cardNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回报卡
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/doctor/withdraw/{cardNo}")
|
||||
public R<?> withdrawCard(@PathVariable String cardNo) {
|
||||
return cardManageAppService.withdrawCard(cardNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除报卡(状态变为作废)
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @return 结果
|
||||
*/
|
||||
@DeleteMapping("/doctor/{cardNo}")
|
||||
public R<?> deleteCard(@PathVariable String cardNo) {
|
||||
return cardManageAppService.deleteCard(cardNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量提交报卡
|
||||
*
|
||||
* @param cardNos 卡片编号列表
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/doctor/batch-submit")
|
||||
public R<?> batchSubmitCards(@RequestBody List<String> cardNos) {
|
||||
return cardManageAppService.batchSubmitCards(cardNos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除报卡
|
||||
*
|
||||
* @param cardNos 卡片编号列表
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/doctor/batch-delete")
|
||||
public R<?> batchDeleteCards(@RequestBody List<String> cardNos) {
|
||||
return cardManageAppService.batchDeleteCards(cardNos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新医生报卡
|
||||
*
|
||||
* @param updateDto 更新参数
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/doctor/update")
|
||||
public R<?> updateDoctorCard(@RequestBody DoctorCardUpdateDto updateDto) {
|
||||
return cardManageAppService.updateDoctorCard(updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出报卡为Word文档
|
||||
*
|
||||
* @param cardNo 卡片编号
|
||||
* @param response 响应
|
||||
*/
|
||||
@GetMapping("/doctor/export-word/{cardNo}")
|
||||
public void exportCardToWord(@PathVariable String cardNo, HttpServletResponse response) {
|
||||
cardManageAppService.exportCardToWord(cardNo, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审核记录DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class AuditRecordDto {
|
||||
|
||||
/** 审核记录ID */
|
||||
private Long auditId;
|
||||
|
||||
/** 报卡ID */
|
||||
private String cardId;
|
||||
|
||||
/** 审核序号 */
|
||||
private Integer auditSeq;
|
||||
|
||||
/** 审核类型 */
|
||||
private String auditType;
|
||||
|
||||
/** 审核前状态 */
|
||||
private String auditStatusFrom;
|
||||
|
||||
/** 审核后状态 */
|
||||
private String auditStatusTo;
|
||||
|
||||
/** 审核时间 */
|
||||
private LocalDateTime auditTime;
|
||||
|
||||
/** 审核人账号 */
|
||||
private String auditorId;
|
||||
|
||||
/** 审核人姓名 */
|
||||
private String auditorName;
|
||||
|
||||
/** 审核意见 */
|
||||
private String auditOpinion;
|
||||
|
||||
/** 退回原因 */
|
||||
private String reasonForReturn;
|
||||
|
||||
/** 是否批量 */
|
||||
private Boolean isBatch;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量审核参数DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class BatchAuditDto {
|
||||
|
||||
/** 卡片编号列表 */
|
||||
private List<String> cardNos;
|
||||
|
||||
/** 审核意见 */
|
||||
private String auditOpinion;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量退回参数DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class BatchReturnDto {
|
||||
|
||||
/** 卡片编号列表 */
|
||||
private List<String> cardNos;
|
||||
|
||||
/** 退回原因 */
|
||||
private String returnReason;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 报卡查询参数DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class CardQueryDto {
|
||||
|
||||
/** 当前页 */
|
||||
private Integer pageNo = 1;
|
||||
|
||||
/** 每页条数 */
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/** 登记来源 */
|
||||
private Integer registrationSource;
|
||||
|
||||
/** 开始日期 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束日期 */
|
||||
private String endDate;
|
||||
|
||||
/** 患者姓名 */
|
||||
private String patientName;
|
||||
|
||||
/** 审核状态 */
|
||||
private String status;
|
||||
|
||||
/** 科室ID */
|
||||
private Long deptId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 报卡统计数据DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class CardStatisticsDto {
|
||||
|
||||
/** 今日待审核 */
|
||||
private Integer todayPending;
|
||||
|
||||
/** 本月审核失败 */
|
||||
private Integer monthFailed;
|
||||
|
||||
/** 本月审核成功 */
|
||||
private Integer monthSuccess;
|
||||
|
||||
/** 本月已上报 */
|
||||
private Integer monthReported;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 医生个人报卡列表DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-08
|
||||
*/
|
||||
@Data
|
||||
public class DoctorCardListDto {
|
||||
|
||||
/** 卡片ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 卡片编号 */
|
||||
private String cardNo;
|
||||
|
||||
/** 患者姓名 */
|
||||
private String patName;
|
||||
|
||||
/** 身份证号 */
|
||||
private String idNo;
|
||||
|
||||
/** 联系电话 */
|
||||
private String phone;
|
||||
|
||||
/** 就诊卡号(暂时不展示,字段保留) */
|
||||
private String visitCardNo;
|
||||
|
||||
/** 报卡名称 */
|
||||
private String cardName;
|
||||
|
||||
/** 提交时间 */
|
||||
private String submitTime;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 医生个人报卡查询参数
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-08
|
||||
*/
|
||||
@Data
|
||||
public class DoctorCardQueryDto {
|
||||
|
||||
/** 页码 */
|
||||
private Integer pageNo = 1;
|
||||
|
||||
/** 每页数量 */
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/** 开始日期 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束日期 */
|
||||
private String endDate;
|
||||
|
||||
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回) */
|
||||
private String status;
|
||||
|
||||
/** 患者姓名或报卡名称 */
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright ©2026 CJB-CNIT Team. All rights reserved
|
||||
*/
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 医生个人报卡统计数据
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-08
|
||||
*/
|
||||
@Data
|
||||
public class DoctorCardStatisticsDto {
|
||||
|
||||
/** 总报卡数 */
|
||||
private Integer totalCount;
|
||||
|
||||
/** 待处理失败数 */
|
||||
private Integer pendingFailedCount;
|
||||
|
||||
/** 已成功上报数 */
|
||||
private Integer reportedCount;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class DoctorCardUpdateDto {
|
||||
private String cardNo;
|
||||
private String phone;
|
||||
private LocalDate onsetDate;
|
||||
private LocalDateTime diagDate;
|
||||
private String diseaseType; // 修改为diseaseType,对应InfectiousCard中的diseaseType字段
|
||||
private String addressProv;
|
||||
private String addressCity;
|
||||
private String addressCounty;
|
||||
private String addressHouse;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 传染病报卡详情DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class InfectiousCardDto {
|
||||
|
||||
/** 卡片编号 */
|
||||
private String cardNo;
|
||||
|
||||
/** 患者姓名 */
|
||||
private String patName;
|
||||
|
||||
/** 家长姓名 */
|
||||
private String parentName;
|
||||
|
||||
/** 证件号码 */
|
||||
private String idNo;
|
||||
|
||||
/** 性别(1男/2女/0未知) */
|
||||
private String sex;
|
||||
|
||||
/** 出生日期 */
|
||||
private LocalDate birthday;
|
||||
|
||||
/** 实足年龄 */
|
||||
private Integer age;
|
||||
|
||||
/** 年龄单位(1岁/2月/3天) */
|
||||
private String ageUnit;
|
||||
|
||||
/** 工作单位 */
|
||||
private String workplace;
|
||||
|
||||
/** 联系电话 */
|
||||
private String phone;
|
||||
|
||||
/** 紧急联系人电话 */
|
||||
private String contactPhone;
|
||||
|
||||
/** 现住址省 */
|
||||
private String addressProv;
|
||||
|
||||
/** 现住址市 */
|
||||
private String addressCity;
|
||||
|
||||
/** 现住址县 */
|
||||
private String addressCounty;
|
||||
|
||||
/** 现住址街道 */
|
||||
private String addressTown;
|
||||
|
||||
/** 现住址村/居委 */
|
||||
private String addressVillage;
|
||||
|
||||
/** 现住址门牌号 */
|
||||
private String addressHouse;
|
||||
|
||||
/** 病人属于 */
|
||||
private String patientbelong;
|
||||
|
||||
/** 职业 */
|
||||
private String occupation;
|
||||
|
||||
/** 疾病编码 */
|
||||
private String diseaseCode;
|
||||
|
||||
/** 疾病名称 */
|
||||
private String diseaseName;
|
||||
|
||||
/** 分型 */
|
||||
private String diseaseSubtype;
|
||||
|
||||
/** 病例分类 */
|
||||
private String diseaseType;
|
||||
|
||||
/** 发病日期 */
|
||||
private LocalDate onsetDate;
|
||||
|
||||
/** 诊断日期 */
|
||||
private LocalDateTime diagDate;
|
||||
|
||||
/** 死亡日期 */
|
||||
private LocalDate deathDate;
|
||||
|
||||
/** 订正病名 */
|
||||
private String revisedDiseaseName;
|
||||
|
||||
/** 退卡原因 */
|
||||
private String returnReason;
|
||||
|
||||
/** 报告单位 */
|
||||
private String reportOrg;
|
||||
|
||||
/** 联系电话 */
|
||||
private String reportOrgPhone;
|
||||
|
||||
/** 报告医生 */
|
||||
private String reportDoc;
|
||||
|
||||
/** 填卡日期 */
|
||||
private LocalDate reportDate;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 状态文本 */
|
||||
private String statusText;
|
||||
|
||||
/** 报卡名称代码 */
|
||||
private Integer cardNameCode;
|
||||
|
||||
/** 登记来源 */
|
||||
private Integer registrationSource;
|
||||
|
||||
/** 科室名称 */
|
||||
private String deptName;
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单条审核参数DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class SingleAuditDto {
|
||||
|
||||
/** 卡片编号 */
|
||||
private String cardNo;
|
||||
|
||||
/** 审核意见 */
|
||||
private String auditOpinion;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.openhis.web.cardmanagement.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单条退回参数DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Data
|
||||
public class SingleReturnDto {
|
||||
|
||||
/** 卡片编号 */
|
||||
private String cardNo;
|
||||
|
||||
/** 退回原因 */
|
||||
private String returnReason;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.openhis.web.cardmanagement.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.openhis.infectious.domain.InfectiousAudit;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审核记录Mapper
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfectiousAuditMapper extends BaseMapper<InfectiousAudit> {
|
||||
|
||||
/**
|
||||
* 根据报卡ID查询审核记录
|
||||
*/
|
||||
@Select("SELECT * FROM infectious_audit WHERE card_id = #{cardId} ORDER BY audit_time DESC")
|
||||
List<InfectiousAudit> selectByCardId(@Param("cardId") Long cardId);
|
||||
|
||||
/**
|
||||
* 获取下一个审核序号
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(audit_seq), 0) + 1 FROM infectious_audit WHERE card_id = #{cardId}")
|
||||
Integer getNextAuditSeq(@Param("cardId") Long cardId);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.openhis.web.cardmanagement.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.openhis.infectious.domain.InfectiousCard;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 传染病报卡Mapper
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-03-05
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfectiousCardMapper extends BaseMapper<InfectiousCard> {
|
||||
|
||||
/**
|
||||
* 统计今日待审核数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE(create_time) = CURRENT_DATE AND status = '1'")
|
||||
Integer countTodayPending();
|
||||
|
||||
/**
|
||||
* 统计本月审核失败数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '5'")
|
||||
Integer countMonthFailed();
|
||||
|
||||
/**
|
||||
* 统计本月审核成功数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '2'")
|
||||
Integer countMonthSuccess();
|
||||
|
||||
/**
|
||||
* 统计本月已上报数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '3'")
|
||||
Integer countMonthReported();
|
||||
|
||||
/**
|
||||
* 根据卡片编号查询
|
||||
*/
|
||||
@Select("SELECT * FROM infectious_card WHERE card_no = #{cardNo}")
|
||||
InfectiousCard selectByCardNo(@Param("cardNo") String cardNo);
|
||||
|
||||
/**
|
||||
* 统计医生个人总报卡数
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId}")
|
||||
Integer countByDoctorId(@Param("doctorId") Long doctorId);
|
||||
|
||||
/**
|
||||
* 统计医生待处理失败数(状态为0暂存或4失败)
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status IN ('0', '4')")
|
||||
Integer countPendingFailedByDoctorId(@Param("doctorId") Long doctorId);
|
||||
|
||||
/**
|
||||
* 统计医生已成功上报数(状态为3已上报)
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status = '3'")
|
||||
Integer countReportedByDoctorId(@Param("doctorId") Long doctorId);
|
||||
}
|
||||
@@ -247,6 +247,10 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
||||
if (EncounterStatus.CANCELLED.getValue().equals(byId.getStatusEnum())) {
|
||||
return R.fail(null, "该患者已经退号,请勿重复退号");
|
||||
}
|
||||
// 只有待诊状态才能退号
|
||||
if (!EncounterStatus.PLANNED.getValue().equals(byId.getStatusEnum())) {
|
||||
return R.fail(null, "该患者医生已接诊,不能退号!");
|
||||
}
|
||||
iEncounterService.returnRegister(cancelRegPaymentDto.getEncounterId());
|
||||
// 查询账户信息
|
||||
Account account = iAccountService
|
||||
|
||||
@@ -66,7 +66,10 @@ public class OutpatientRefundController {
|
||||
* @return 患者账单列表
|
||||
*/
|
||||
@GetMapping(value = "/patient-payment")
|
||||
public R<?> getEncounterPatientPayment(@RequestParam Long encounterId) {
|
||||
public R<?> getEncounterPatientPayment(@RequestParam(required = false) Long encounterId) {
|
||||
if (encounterId == null) {
|
||||
return R.fail(null, "请先选择患者后再进行退费操作");
|
||||
}
|
||||
return outpatientRefundAppService.getEncounterPatientPayment(encounterId);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
||||
// 导出到Excel
|
||||
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
||||
} catch (IOException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
log.error("导出Excel失败", e);
|
||||
return R.fail("导出Excel失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public class CheckPartAppServiceImpl implements ICheckPartAppService {
|
||||
// 导出到Excel
|
||||
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
||||
} catch (IOException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
log.error("导出Excel失败", e);
|
||||
return R.fail("导出Excel失败:" + e.getMessage());
|
||||
}
|
||||
return R.ok(null, "导出Excel成功");
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package com.openhis.web.check.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.controller.BaseController;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.check.domain.CheckMethod;
|
||||
import com.openhis.check.domain.CheckPackage;
|
||||
import com.openhis.check.domain.CheckPackageDetail;
|
||||
import com.openhis.check.domain.CheckPart;
|
||||
import com.openhis.check.domain.CheckType;
|
||||
import com.openhis.check.service.ICheckMethodService;
|
||||
import com.openhis.check.service.ICheckPackageDetailService;
|
||||
import com.openhis.check.service.ICheckPackageService;
|
||||
import com.openhis.check.service.ICheckPartService;
|
||||
import com.openhis.check.service.ICheckTypeService;
|
||||
@@ -20,7 +25,8 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 检查类型管理Controller
|
||||
@@ -30,7 +36,7 @@ import java.util.List;
|
||||
* @updated 2025-11-26 - 增加套餐设置相关接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping({"/system/check-type", "/system"})
|
||||
@RequestMapping({ "/system/check-type", "/system" })
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class CheckTypeController extends BaseController {
|
||||
@@ -39,39 +45,196 @@ public class CheckTypeController extends BaseController {
|
||||
private final ICheckMethodService checkMethodService;
|
||||
private final ICheckPartService checkPartService;
|
||||
private final ICheckPackageService checkPackageService;
|
||||
private final ICheckPackageDetailService checkPackageDetailService;
|
||||
private final ICheckPackageAppService checkPackageAppService;
|
||||
|
||||
/**
|
||||
* 获取检查类型列表
|
||||
* 获取所有检查类型列表(不分页,用于下拉选项)
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
public AjaxResult getAllCheckTypes() {
|
||||
List<CheckType> list = checkTypeService.list(
|
||||
new QueryWrapper<CheckType>().orderByAsc("id"));
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查类型列表(支持分页)
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public AjaxResult list() {
|
||||
List<CheckType> list = checkTypeService.list();
|
||||
return AjaxResult.success(list);
|
||||
public AjaxResult list(
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
|
||||
if (pageSize > 10)
|
||||
pageSize = 10;
|
||||
|
||||
// 1. 分页查询父节点(NULL + 0 都算父)
|
||||
Page<CheckType> parentPage = checkTypeService.page(
|
||||
new Page<>(pageNo, pageSize),
|
||||
new QueryWrapper<CheckType>()
|
||||
.and(w -> w.isNull("parent_id").or().eq("parent_id", 0))
|
||||
.orderByAsc("id"));
|
||||
|
||||
if (parentPage.getRecords().isEmpty()) {
|
||||
return AjaxResult.success(parentPage);
|
||||
}
|
||||
|
||||
// 2. 父ID列表(注意类型)
|
||||
List<Long> parentIds = parentPage.getRecords()
|
||||
.stream()
|
||||
.map(CheckType::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 3. 查询子节点
|
||||
List<CheckType> children = checkTypeService.list(
|
||||
new QueryWrapper<CheckType>().in("parent_id", parentIds));
|
||||
|
||||
// 4. 分组
|
||||
Map<Long, List<CheckType>> childMap = children.stream().collect(Collectors.groupingBy(CheckType::getParentId));
|
||||
|
||||
// 5. 拼接父 + 子
|
||||
List<CheckType> result = new ArrayList<>();
|
||||
for (CheckType parent : parentPage.getRecords()) {
|
||||
result.add(parent);
|
||||
List<CheckType> list = childMap.get(parent.getId());
|
||||
if (list != null && !list.isEmpty()) {
|
||||
result.addAll(list);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 返回(total 是父节点总数)
|
||||
Page<CheckType> page = new Page<>(pageNo, pageSize, parentPage.getTotal());
|
||||
page.setRecords(result);
|
||||
|
||||
return AjaxResult.success(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查方法列表
|
||||
*/
|
||||
@GetMapping({"/method/list", "/check-method/list"})
|
||||
@GetMapping({ "/method/list", "/check-method/list" })
|
||||
public AjaxResult methodList() {
|
||||
List<CheckMethod> list = checkMethodService.list();
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取检查部位列表
|
||||
*/
|
||||
@GetMapping({"/part/list", "/check-part/list"})
|
||||
@GetMapping({ "/part/list", "/check-part/list" })
|
||||
public AjaxResult partList() {
|
||||
List<CheckPart> list = checkPartService.list();
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前登录用户过滤可见的检查套餐项目(差异化显示)
|
||||
* <p>
|
||||
* 过滤规则:
|
||||
* 1. 全院套餐(packageLevel="1"):任何医生可见
|
||||
* 2. 科室套餐(packageLevel="2"):仅当前科室医生可见(需传 deptName)
|
||||
* 3. 个人套餐(packageLevel="3"):仅当前登录用户可见
|
||||
* </p>
|
||||
*
|
||||
* @param deptName 当前登录用户所在科室名称(由前端传入)
|
||||
* @return 可用套餐列表(含明细检查项目)
|
||||
*/
|
||||
@GetMapping({ "/package/filtered", "/check-package/filtered" })
|
||||
public AjaxResult getFilteredPackages(@RequestParam(required = false) String deptName) {
|
||||
// 获取当前登录用户名,用于匹配个人套餐
|
||||
String currentUsername;
|
||||
try {
|
||||
currentUsername = SecurityUtils.getUsername();
|
||||
} catch (Exception e) {
|
||||
currentUsername = null;
|
||||
}
|
||||
|
||||
// 查询未停用(isDisabled=0)的全部套餐
|
||||
LambdaQueryWrapper<CheckPackage> wrapper = new LambdaQueryWrapper<CheckPackage>()
|
||||
.eq(CheckPackage::getIsDisabled, 0)
|
||||
.orderByAsc(CheckPackage::getNumber);
|
||||
List<CheckPackage> allPackages = checkPackageService.list(wrapper);
|
||||
|
||||
// 按照三级规则筛选出本次登录用户可见的套餐
|
||||
final String finalUsername = currentUsername;
|
||||
final String finalDept = (deptName != null) ? deptName.trim() : "";
|
||||
List<CheckPackage> visiblePackages = allPackages.stream().filter(pkg -> {
|
||||
String level = pkg.getPackageLevel(); // 1:全院 2:科室 3:个人
|
||||
if ("1".equals(level)) {
|
||||
// 全院套餐 - 所有人可见
|
||||
return true;
|
||||
} else if ("2".equals(level)) {
|
||||
// 科室套餐 - department 字段包含当前科室
|
||||
String dept = pkg.getDepartment();
|
||||
return dept != null && !finalDept.isEmpty() && dept.contains(finalDept);
|
||||
} else if ("3".equals(level)) {
|
||||
// 个人套餐 - user 字段包含当前账号
|
||||
String user = pkg.getUser();
|
||||
return user != null && finalUsername != null && user.contains(finalUsername);
|
||||
}
|
||||
return false;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
if (visiblePackages.isEmpty()) {
|
||||
return AjaxResult.success(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 批量查明细,拼装返回结构
|
||||
List<Long> packageIds = visiblePackages.stream().map(CheckPackage::getId).collect(Collectors.toList());
|
||||
List<CheckPackageDetail> details = checkPackageDetailService.list(
|
||||
new LambdaQueryWrapper<CheckPackageDetail>().in(CheckPackageDetail::getPackageId, packageIds));
|
||||
// 按套餐ID分组明细
|
||||
Map<Long, List<CheckPackageDetail>> detailMap = details.stream()
|
||||
.collect(Collectors.groupingBy(CheckPackageDetail::getPackageId));
|
||||
|
||||
// 构造返回视图:每个套餐是一个分组,下挂若干检查项目
|
||||
List<Map<String, Object>> result = visiblePackages.stream().map(pkg -> {
|
||||
Map<String, Object> group = new LinkedHashMap<>();
|
||||
group.put("packageId", pkg.getId());
|
||||
group.put("packageName", pkg.getPackageName());
|
||||
group.put("packageLevel", pkg.getPackageLevel()); // 1/2/3
|
||||
group.put("packageLevelText", parseLevelText(pkg.getPackageLevel()));
|
||||
group.put("packagePrice", pkg.getPackagePrice());
|
||||
group.put("serviceFee", pkg.getServiceFee());
|
||||
group.put("packagePriceEnabled", pkg.getPackagePriceEnabled());
|
||||
// 明细列表
|
||||
List<Map<String, Object>> items = (detailMap.getOrDefault(pkg.getId(), Collections.emptyList()))
|
||||
.stream().map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", d.getId());
|
||||
item.put("packageId", d.getPackageId());
|
||||
item.put("itemCode", d.getItemCode());
|
||||
item.put("itemName", d.getItemName());
|
||||
item.put("unitPrice", d.getUnitPrice()); // 单价
|
||||
item.put("amount", d.getAmount()); // 金额
|
||||
item.put("serviceCharge", d.getServiceCharge()); // 服务费
|
||||
item.put("quantity", d.getQuantity());
|
||||
item.put("orderNum", d.getOrderNum());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
group.put("items", items);
|
||||
return group;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
/** 套餐级别文字映射 */
|
||||
private String parseLevelText(String level) {
|
||||
if ("1".equals(level))
|
||||
return "全院";
|
||||
if ("2".equals(level))
|
||||
return "科室";
|
||||
if ("3".equals(level))
|
||||
return "个人";
|
||||
return "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查套餐列表(支持分页和筛选)
|
||||
*/
|
||||
@GetMapping({"/package/list", "/check-package/list"})
|
||||
@GetMapping({ "/package/list", "/check-package/list" })
|
||||
public AjaxResult packageList(
|
||||
@RequestParam(required = false) Integer pageNo,
|
||||
@RequestParam(required = false) Integer pageSize,
|
||||
@@ -83,9 +246,9 @@ public class CheckTypeController extends BaseController {
|
||||
@RequestParam(required = false) String user,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
|
||||
|
||||
LambdaQueryWrapper<CheckPackage> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
|
||||
// 添加筛选条件
|
||||
if (organization != null && !organization.isEmpty()) {
|
||||
wrapper.eq(CheckPackage::getOrganization, organization);
|
||||
@@ -111,20 +274,20 @@ public class CheckTypeController extends BaseController {
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
wrapper.le(CheckPackage::getMaintainDate, LocalDate.parse(endDate));
|
||||
}
|
||||
|
||||
|
||||
// 按更新时间倒序排列
|
||||
wrapper.orderByDesc(CheckPackage::getUpdateTime);
|
||||
|
||||
|
||||
// 如果需要分页
|
||||
if (pageNo != null && pageSize != null) {
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<CheckPackage> page =
|
||||
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNo, pageSize);
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<CheckPackage> result =
|
||||
checkPackageService.page(page, wrapper);
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<CheckPackage> page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
|
||||
pageNo, pageSize);
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<CheckPackage> result = checkPackageService
|
||||
.page(page, wrapper);
|
||||
return AjaxResult.success(result);
|
||||
} else {
|
||||
List<CheckPackage> list = checkPackageService.list(wrapper);
|
||||
return AjaxResult.success(list);
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +318,7 @@ public class CheckTypeController extends BaseController {
|
||||
/**
|
||||
* 根据ID获取检查套餐详情
|
||||
*/
|
||||
@GetMapping({"/package/{id}", "/check-package/{id}"})
|
||||
@GetMapping({ "/package/{id}", "/check-package/{id}" })
|
||||
public R<?> getCheckPackageById(@PathVariable Long id) {
|
||||
return checkPackageAppService.getCheckPackageById(id);
|
||||
}
|
||||
@@ -163,7 +326,7 @@ public class CheckTypeController extends BaseController {
|
||||
/**
|
||||
* 新增检查套餐
|
||||
*/
|
||||
@PostMapping({"/package", "/check-package"})
|
||||
@PostMapping({ "/package", "/check-package" })
|
||||
public R<?> addCheckPackage(@Valid @RequestBody CheckPackageDto checkPackageDto) {
|
||||
return checkPackageAppService.addCheckPackage(checkPackageDto);
|
||||
}
|
||||
@@ -171,7 +334,7 @@ public class CheckTypeController extends BaseController {
|
||||
/**
|
||||
* 更新检查套餐
|
||||
*/
|
||||
@PutMapping({"/package", "/check-package"})
|
||||
@PutMapping({ "/package", "/check-package" })
|
||||
public R<?> updateCheckPackage(@Valid @RequestBody CheckPackageDto checkPackageDto) {
|
||||
return checkPackageAppService.updateCheckPackage(checkPackageDto);
|
||||
}
|
||||
@@ -179,7 +342,7 @@ public class CheckTypeController extends BaseController {
|
||||
/**
|
||||
* 删除检查套餐
|
||||
*/
|
||||
@DeleteMapping({"/package/{id}", "/check-package/{id}"})
|
||||
@DeleteMapping({ "/package/{id}", "/check-package/{id}" })
|
||||
public R<?> deleteCheckPackage(@PathVariable Long id) {
|
||||
return checkPackageAppService.deleteCheckPackage(id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
package com.openhis.web.check.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.controller.BaseController;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.page.TableDataInfo;
|
||||
import com.core.common.utils.AssignSeqUtil;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.administration.domain.ChargeItem;
|
||||
import com.openhis.administration.service.IChargeItemService;
|
||||
import com.openhis.check.domain.ExamApply;
|
||||
import com.openhis.check.domain.ExamApplyItem;
|
||||
import com.openhis.check.service.IExamApplyItemService;
|
||||
import com.openhis.check.service.IExamApplyService;
|
||||
import com.openhis.common.constant.CommonConstants;
|
||||
import com.openhis.common.enums.AssignSeqEnum;
|
||||
import com.openhis.common.enums.ChargeItemStatus;
|
||||
import com.openhis.common.enums.GenerateSource;
|
||||
import com.openhis.common.enums.RequestStatus;
|
||||
import com.openhis.web.check.dto.ExamApplyDto;
|
||||
import com.openhis.web.check.dto.ExamApplyItemDto;
|
||||
import com.openhis.workflow.domain.ServiceRequest;
|
||||
import com.openhis.workflow.service.IServiceRequestService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检查申请主表 Controller
|
||||
* <p>
|
||||
* 核心职责:检查申请单的增删查,保存时同步写入门诊医嘱表(wor_service_request)
|
||||
* 和费用项表(adm_charge_item),删除时级联清理,保证业务数据闭环。
|
||||
* </p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/exam/apply")
|
||||
public class ExamApplyController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IExamApplyService examApplyService;
|
||||
|
||||
@Autowired
|
||||
private IExamApplyItemService examApplyItemService;
|
||||
|
||||
@Autowired
|
||||
private IServiceRequestService serviceRequestService;
|
||||
|
||||
@Autowired
|
||||
private IChargeItemService chargeItemService;
|
||||
|
||||
@Autowired
|
||||
private AssignSeqUtil assignSeqUtil;
|
||||
|
||||
/**
|
||||
* 查询检查申请单列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ExamApply examApply) {
|
||||
startPage();
|
||||
LambdaQueryWrapper<ExamApply> wrapper = new LambdaQueryWrapper<>();
|
||||
if (examApply.getVisitNo() != null) {
|
||||
wrapper.eq(ExamApply::getVisitNo, examApply.getVisitNo());
|
||||
}
|
||||
wrapper.orderByDesc(ExamApply::getApplyTime);
|
||||
List<ExamApply> list = examApplyService.list(wrapper);
|
||||
|
||||
// 为每条申请单计算总金额
|
||||
for (ExamApply apply : list) {
|
||||
List<ExamApplyItem> items = examApplyItemService.list(
|
||||
new LambdaQueryWrapper<ExamApplyItem>()
|
||||
.eq(ExamApplyItem::getApplyNo, apply.getApplyNo()));
|
||||
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
|
||||
for (ExamApplyItem item : items) {
|
||||
if (item.getItemFee() != null) {
|
||||
totalAmount = totalAmount.add(item.getItemFee());
|
||||
}
|
||||
}
|
||||
|
||||
apply.setTotalAmount(totalAmount);
|
||||
}
|
||||
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取申请单详细信息(包含明细)
|
||||
*/
|
||||
@GetMapping(value = "/{applyNo}")
|
||||
public AjaxResult getInfo(@PathVariable("applyNo") String applyNo) {
|
||||
ExamApply examApply = examApplyService.getById(applyNo);
|
||||
if (examApply == null) {
|
||||
return AjaxResult.error("未找到申请单信息");
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("data", examApply);
|
||||
|
||||
// 挂载项目明细
|
||||
List<ExamApplyItem> items = examApplyItemService.list(
|
||||
new LambdaQueryWrapper<ExamApplyItem>().eq(ExamApplyItem::getApplyNo, applyNo));
|
||||
ajax.put("items", items);
|
||||
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增检查申请单
|
||||
* <p>
|
||||
* 核心级联保存逻辑:
|
||||
* 1. 生成申请单号,保存 exam_apply 主表
|
||||
* 2. 批量保存 exam_apply_item 明细表
|
||||
* 3. 为每条明细写入 wor_service_request(门诊医嘱),使检查进入医嘱体系
|
||||
* 4. 为每条医嘱写入 adm_charge_item(费用项),使检查费用进入收费系统
|
||||
* 5. 回写 exam_apply_item.service_request_id,建立双向关联
|
||||
* </p>
|
||||
*/
|
||||
@PostMapping
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AjaxResult add(@RequestBody @Validated ExamApplyDto dto) {
|
||||
|
||||
// ========== 1. 生成申请单号并保存主表 ==========
|
||||
String dateStr = DateUtil.format(new Date(), "yyyyMMdd");
|
||||
String random4 = String.format("%04d", (int) (Math.random() * 10000));
|
||||
String applyNo = "EX" + dateStr + random4;
|
||||
|
||||
ExamApply examApply = new ExamApply();
|
||||
BeanUtils.copyProperties(dto, examApply);
|
||||
examApply.setApplyNo(applyNo);
|
||||
examApply.setApplyTime(LocalDateTime.now());
|
||||
examApply.setCreateTime(new Date());
|
||||
examApply.setApplyStatus(0); // 0=已开单
|
||||
|
||||
// 操作员工号取当前登录用户
|
||||
try {
|
||||
examApply.setOperatorId(SecurityUtils.getUsername());
|
||||
} catch (Exception e) {
|
||||
examApply.setOperatorId("system");
|
||||
}
|
||||
examApplyService.save(examApply);
|
||||
|
||||
// ========== 2. 批量保存明细 + 写入门诊医嘱 + 写入费用项 ==========
|
||||
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
|
||||
// 获取当前登录用户信息,用于写入医嘱和费用项
|
||||
Long currentUserId = null;
|
||||
Long currentOrgId = null;
|
||||
Integer tenantId = null;
|
||||
String currentUsername = "system";
|
||||
try {
|
||||
currentUserId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
currentOrgId = SecurityUtils.getLoginUser().getOrgId();
|
||||
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
currentUsername = SecurityUtils.getUsername();
|
||||
} catch (Exception e) {
|
||||
// 获取失败时使用默认值
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
int seq = 1;
|
||||
|
||||
for (ExamApplyItemDto itemDto : dto.getItems()) {
|
||||
// ----- 2a. 保存检查明细 -----
|
||||
ExamApplyItem item = new ExamApplyItem();
|
||||
BeanUtils.copyProperties(itemDto, item);
|
||||
item.setApplyNo(applyNo);
|
||||
item.setItemSeq(seq++);
|
||||
item.setItemStatus(0); // 同主表状态:已开单
|
||||
examApplyItemService.save(item);
|
||||
|
||||
// ----- 2b. 写入门诊医嘱表 wor_service_request -----
|
||||
ServiceRequest serviceRequest = new ServiceRequest();
|
||||
// 生成唯一服务编码(SR + 日期 + 4位序列)
|
||||
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
|
||||
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue()); // 草稿/待发送
|
||||
serviceRequest.setIntentEnum(1); // 意图:医嘱
|
||||
serviceRequest.setQuantity(BigDecimal.ONE); // 检查默认数量1
|
||||
serviceRequest.setUnitCode("次"); // 单位
|
||||
|
||||
// 通过 basedOnTable + basedOnId 关联回检查申请主表
|
||||
serviceRequest.setBasedOnTable("exam_apply");
|
||||
serviceRequest.setBasedOnId(examApply.getId());
|
||||
|
||||
// 检查申请不走诊疗定义,设置为0占位(数据库有NOT NULL约束)
|
||||
serviceRequest.setActivityId(0L);
|
||||
|
||||
// 患者和就诊信息 —— 使用前端传递的数字型ID
|
||||
if (dto.getPatientIdNum() != null) {
|
||||
serviceRequest.setPatientId(dto.getPatientIdNum());
|
||||
}
|
||||
if (dto.getEncounterId() != null) {
|
||||
serviceRequest.setEncounterId(dto.getEncounterId());
|
||||
}
|
||||
|
||||
serviceRequest.setRequesterId(currentUserId); // 开单医生
|
||||
serviceRequest.setOrgId(currentOrgId); // 执行科室
|
||||
serviceRequest.setAuthoredTime(now); // 签发时间
|
||||
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 来源=医生开立
|
||||
|
||||
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
||||
if (itemDto.getItemName() != null) {
|
||||
serviceRequest
|
||||
.setContentJson("{\"adviceName\":\"" + itemDto.getItemName().replace("\"", "\\\"") + "\"}");
|
||||
}
|
||||
|
||||
// 租户和审计字段
|
||||
if (tenantId != null) {
|
||||
serviceRequest.setTenantId(tenantId);
|
||||
}
|
||||
serviceRequest.setCreateBy(currentUsername);
|
||||
serviceRequest.setCreateTime(now);
|
||||
|
||||
serviceRequestService.save(serviceRequest);
|
||||
|
||||
// ----- 2c. 写入费用项表 adm_charge_item -----
|
||||
ChargeItem chargeItem = new ChargeItem();
|
||||
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix()
|
||||
.concat(serviceRequest.getBusNo())); // 费用项编码 = CI + 服务编码
|
||||
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 草稿状态,待收费
|
||||
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
||||
|
||||
// 关联医嘱表
|
||||
chargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);
|
||||
chargeItem.setServiceId(serviceRequest.getId());
|
||||
|
||||
// 患者和就诊
|
||||
if (dto.getPatientIdNum() != null) {
|
||||
chargeItem.setPatientId(dto.getPatientIdNum());
|
||||
}
|
||||
if (dto.getEncounterId() != null) {
|
||||
chargeItem.setEncounterId(dto.getEncounterId());
|
||||
}
|
||||
|
||||
chargeItem.setEntererId(currentUserId); // 开立人
|
||||
chargeItem.setRequestingOrgId(currentOrgId); // 开立科室
|
||||
chargeItem.setEnteredDate(now); // 开立时间
|
||||
|
||||
// 以下字段均有 NOT NULL 约束,检查申请不走定价/账户体系,用0占位
|
||||
chargeItem.setDefinitionId(0L); // 费用定价ID
|
||||
chargeItem.setAccountId(0L); // 关联账户ID
|
||||
chargeItem.setContextEnum(2); // 类型:2=诊疗
|
||||
chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION); // 产品来源表
|
||||
chargeItem.setProductId(0L); // 产品ID
|
||||
|
||||
// 金额:单价和总价取检查项目费用
|
||||
BigDecimal fee = itemDto.getItemFee() != null ? itemDto.getItemFee() : BigDecimal.ZERO;
|
||||
chargeItem.setQuantityValue(BigDecimal.ONE); // 数量
|
||||
chargeItem.setQuantityUnit("次"); // 单位
|
||||
chargeItem.setUnitPrice(fee); // 单价
|
||||
chargeItem.setTotalPrice(fee); // 总价 = 单价 × 1
|
||||
|
||||
// 租户和审计字段
|
||||
if (tenantId != null) {
|
||||
chargeItem.setTenantId(tenantId);
|
||||
}
|
||||
chargeItem.setCreateBy(currentUsername);
|
||||
chargeItem.setCreateTime(now);
|
||||
|
||||
chargeItemService.save(chargeItem);
|
||||
|
||||
// ----- 2d. 回写明细表的医嘱关联ID -----
|
||||
item.setServiceRequestId(serviceRequest.getId());
|
||||
examApplyItemService.updateById(item);
|
||||
}
|
||||
}
|
||||
|
||||
return AjaxResult.success("开单成功", applyNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改检查申请单
|
||||
* <p>
|
||||
* 采用「先删旧关联再重建」策略:
|
||||
* 1. 更新 exam_apply 主表基本字段
|
||||
* 2. 删除旧的明细、医嘱、费用项
|
||||
* 3. 用新提交的明细重新创建医嘱和费用项
|
||||
* </p>
|
||||
*/
|
||||
@PutMapping
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AjaxResult edit(@RequestBody @Validated ExamApplyDto dto) {
|
||||
String applyNo = dto.getApplyNo();
|
||||
if (applyNo == null || applyNo.isEmpty()) {
|
||||
return AjaxResult.error("修改时申请单号不能为空");
|
||||
}
|
||||
|
||||
// ========== 1. 更新主表基本信息 ==========
|
||||
ExamApply examApply = examApplyService.getById(applyNo);
|
||||
if (examApply == null) {
|
||||
return AjaxResult.error("未找到该申请单");
|
||||
}
|
||||
// 只更新可编辑字段,不覆盖系统字段(applyNo, createTime等)
|
||||
examApply.setNatureofCost(dto.getNatureofCost());
|
||||
examApply.setApplyDeptCode(dto.getApplyDeptCode());
|
||||
examApply.setPerformDeptCode(dto.getPerformDeptCode());
|
||||
examApply.setApplyDocCode(dto.getApplyDocCode());
|
||||
examApply.setClinicDesc(dto.getClinicDesc());
|
||||
examApply.setContraindication(dto.getContraindication());
|
||||
examApply.setClinicalDiag(dto.getClinicalDiag());
|
||||
examApply.setMedicalHistorySummary(dto.getMedicalHistorySummary());
|
||||
examApply.setPurposeDesc(dto.getPurposeDesc());
|
||||
examApply.setPurposeofInspection(dto.getPurposeofInspection());
|
||||
examApply.setInspectionArea(dto.getInspectionArea());
|
||||
examApply.setInspectionMethod(dto.getInspectionMethod());
|
||||
examApply.setApplyRemark(dto.getApplyRemark());
|
||||
examApply.setIsUrgent(dto.getIsUrgent());
|
||||
examApplyService.updateById(examApply);
|
||||
|
||||
// ========== 2. 删除旧的明细、医嘱、费用项 ==========
|
||||
List<ExamApplyItem> oldItems = examApplyItemService.list(
|
||||
new LambdaQueryWrapper<ExamApplyItem>().eq(ExamApplyItem::getApplyNo, applyNo));
|
||||
for (ExamApplyItem oldItem : oldItems) {
|
||||
if (oldItem.getServiceRequestId() != null) {
|
||||
// 删除旧费用项
|
||||
chargeItemService.deleteByServiceTableAndId(
|
||||
CommonConstants.TableName.WOR_SERVICE_REQUEST,
|
||||
oldItem.getServiceRequestId());
|
||||
// 删除旧医嘱
|
||||
serviceRequestService.removeById(oldItem.getServiceRequestId());
|
||||
}
|
||||
}
|
||||
// 删除旧明细
|
||||
examApplyItemService.remove(
|
||||
new LambdaQueryWrapper<ExamApplyItem>().eq(ExamApplyItem::getApplyNo, applyNo));
|
||||
|
||||
// ========== 3. 用新提交的明细重新创建 ==========
|
||||
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
|
||||
Long currentUserId = null;
|
||||
Long currentOrgId = null;
|
||||
Integer tenantId = null;
|
||||
String currentUsername = "system";
|
||||
try {
|
||||
currentUserId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
currentOrgId = SecurityUtils.getLoginUser().getOrgId();
|
||||
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
currentUsername = SecurityUtils.getUsername();
|
||||
} catch (Exception e) {
|
||||
// 获取失败时使用默认值
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
int seq = 1;
|
||||
|
||||
for (ExamApplyItemDto itemDto : dto.getItems()) {
|
||||
// 保存新明细
|
||||
ExamApplyItem item = new ExamApplyItem();
|
||||
BeanUtils.copyProperties(itemDto, item);
|
||||
item.setApplyNo(applyNo);
|
||||
item.setItemSeq(seq++);
|
||||
item.setItemStatus(0);
|
||||
examApplyItemService.save(item);
|
||||
|
||||
// 写入门诊医嘱
|
||||
ServiceRequest serviceRequest = new ServiceRequest();
|
||||
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
|
||||
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());
|
||||
serviceRequest.setIntentEnum(1);
|
||||
serviceRequest.setQuantity(BigDecimal.ONE);
|
||||
serviceRequest.setUnitCode("次");
|
||||
serviceRequest.setBasedOnTable("exam_apply");
|
||||
serviceRequest.setBasedOnId(examApply.getId());
|
||||
serviceRequest.setActivityId(0L);
|
||||
|
||||
if (dto.getPatientIdNum() != null) {
|
||||
serviceRequest.setPatientId(dto.getPatientIdNum());
|
||||
}
|
||||
if (dto.getEncounterId() != null) {
|
||||
serviceRequest.setEncounterId(dto.getEncounterId());
|
||||
}
|
||||
serviceRequest.setRequesterId(currentUserId);
|
||||
serviceRequest.setOrgId(currentOrgId);
|
||||
serviceRequest.setAuthoredTime(now);
|
||||
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
||||
|
||||
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
||||
if (itemDto.getItemName() != null) {
|
||||
serviceRequest
|
||||
.setContentJson("{\"adviceName\":\"" + itemDto.getItemName().replace("\"", "\\\"") + "\"}");
|
||||
}
|
||||
|
||||
if (tenantId != null) {
|
||||
serviceRequest.setTenantId(tenantId);
|
||||
}
|
||||
serviceRequest.setCreateBy(currentUsername);
|
||||
serviceRequest.setCreateTime(now);
|
||||
serviceRequestService.save(serviceRequest);
|
||||
|
||||
// 写入费用项
|
||||
ChargeItem chargeItem = new ChargeItem();
|
||||
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix()
|
||||
.concat(serviceRequest.getBusNo()));
|
||||
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue());
|
||||
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
||||
chargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);
|
||||
chargeItem.setServiceId(serviceRequest.getId());
|
||||
|
||||
if (dto.getPatientIdNum() != null) {
|
||||
chargeItem.setPatientId(dto.getPatientIdNum());
|
||||
}
|
||||
if (dto.getEncounterId() != null) {
|
||||
chargeItem.setEncounterId(dto.getEncounterId());
|
||||
}
|
||||
chargeItem.setEntererId(currentUserId);
|
||||
chargeItem.setRequestingOrgId(currentOrgId);
|
||||
chargeItem.setEnteredDate(now);
|
||||
chargeItem.setDefinitionId(0L);
|
||||
chargeItem.setAccountId(0L);
|
||||
chargeItem.setContextEnum(2);
|
||||
chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
|
||||
chargeItem.setProductId(0L);
|
||||
|
||||
BigDecimal fee = itemDto.getItemFee() != null ? itemDto.getItemFee() : BigDecimal.ZERO;
|
||||
chargeItem.setQuantityValue(BigDecimal.ONE);
|
||||
chargeItem.setQuantityUnit("次");
|
||||
chargeItem.setUnitPrice(fee);
|
||||
chargeItem.setTotalPrice(fee);
|
||||
|
||||
if (tenantId != null) {
|
||||
chargeItem.setTenantId(tenantId);
|
||||
}
|
||||
chargeItem.setCreateBy(currentUsername);
|
||||
chargeItem.setCreateTime(now);
|
||||
chargeItemService.save(chargeItem);
|
||||
|
||||
// 回写明细表的医嘱关联ID
|
||||
item.setServiceRequestId(serviceRequest.getId());
|
||||
examApplyItemService.updateById(item);
|
||||
}
|
||||
}
|
||||
|
||||
return AjaxResult.success("修改成功", applyNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除检查申请单
|
||||
* <p>
|
||||
* 级联操作:
|
||||
* 1. 查询并删除关联的门诊医嘱(wor_service_request)和费用项(adm_charge_item)
|
||||
* 2. 删除检查明细(exam_apply_item)
|
||||
* 3. 删除主表(exam_apply)
|
||||
* </p>
|
||||
*/
|
||||
@DeleteMapping("/{applyNo}")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AjaxResult remove(@PathVariable String applyNo) {
|
||||
// 1. 查询该申请单的所有明细,获取关联的医嘱ID
|
||||
List<ExamApplyItem> items = examApplyItemService.list(
|
||||
new LambdaQueryWrapper<ExamApplyItem>().eq(ExamApplyItem::getApplyNo, applyNo));
|
||||
|
||||
for (ExamApplyItem item : items) {
|
||||
if (item.getServiceRequestId() != null) {
|
||||
// 删除费用项(按医嘱表名+医嘱ID定位)
|
||||
chargeItemService.deleteByServiceTableAndId(
|
||||
CommonConstants.TableName.WOR_SERVICE_REQUEST,
|
||||
item.getServiceRequestId());
|
||||
// 删除门诊医嘱
|
||||
serviceRequestService.removeById(item.getServiceRequestId());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 删除明细表
|
||||
examApplyItemService.remove(
|
||||
new LambdaQueryWrapper<ExamApplyItem>().eq(ExamApplyItem::getApplyNo, applyNo));
|
||||
|
||||
// 3. 删除主表
|
||||
examApplyService.removeById(applyNo);
|
||||
|
||||
return AjaxResult.success("删除/作废成功");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.openhis.web.check.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检查申请数据传输对象
|
||||
*/
|
||||
@Data
|
||||
public class ExamApplyDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 申请单号 (EXYYYYMMDD####) */
|
||||
private String applyNo;
|
||||
|
||||
/** 患者主索引 EMPI */
|
||||
private String patientId;
|
||||
|
||||
/** 门诊就诊流水号 */
|
||||
private String visitNo;
|
||||
|
||||
/** 申请科室代码 */
|
||||
private String applyDeptCode;
|
||||
|
||||
/** 执行科室代码 */
|
||||
private String performDeptCode;
|
||||
|
||||
/** 申请医生工号 */
|
||||
private String applyDocCode;
|
||||
|
||||
/** 申请时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime applyTime;
|
||||
|
||||
/** 就诊卡号 */
|
||||
private String medicalrecordNumber;
|
||||
|
||||
/** 费用性质 */
|
||||
private String natureofCost;
|
||||
|
||||
/** 诊断描述 */
|
||||
private String clinicDesc;
|
||||
|
||||
/** 禁忌症 */
|
||||
private String contraindication;
|
||||
|
||||
/** 病史摘要 */
|
||||
private String medicalHistorySummary;
|
||||
|
||||
/** 体格检查 */
|
||||
private String purposeofInspection;
|
||||
|
||||
/** 申检部位 */
|
||||
private String inspectionArea;
|
||||
|
||||
/** 检查方法 */
|
||||
private String inspectionMethod;
|
||||
|
||||
/** 备注 */
|
||||
private String applyRemark;
|
||||
|
||||
/** 检查大类代码 */
|
||||
private String examTypeCode;
|
||||
|
||||
/** 临床诊断 */
|
||||
private String clinicalDiag;
|
||||
|
||||
/** 检查目的 */
|
||||
private String purposeDesc;
|
||||
|
||||
/** 加急标志 0 普通 1 加急 */
|
||||
private Integer isUrgent;
|
||||
|
||||
/** 妊娠状态 0未知 1未孕 2可能孕 3孕妇 */
|
||||
private Integer pregnancyState;
|
||||
|
||||
/** 过敏史 */
|
||||
private String allergyDesc;
|
||||
|
||||
/** 申请单状态 0已开单 1已收费 2已预约 3已签到 4部分报告 5已完告 6作废 */
|
||||
private Integer applyStatus;
|
||||
|
||||
/** 就诊ID(用于写入门诊医嘱和费用项关联) */
|
||||
private Long encounterId;
|
||||
|
||||
/** 患者数字ID(用于写入门诊医嘱和费用项,与 patientId 的 EMPI 字符串不同) */
|
||||
private Long patientIdNum;
|
||||
|
||||
/**
|
||||
* 绑定的明细项目列表
|
||||
*/
|
||||
private List<ExamApplyItemDto> items;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.openhis.web.check.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 检查申请明细项目数据传输对象
|
||||
*/
|
||||
@Data
|
||||
public class ExamApplyItemDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 项目代码 (院内收费项目) */
|
||||
private String itemCode;
|
||||
|
||||
/** 项目名称 */
|
||||
private String itemName;
|
||||
|
||||
/** 国家项目代码 */
|
||||
private String nationalItemCode;
|
||||
|
||||
/** 检查部位代码 */
|
||||
private String bodyPartCode;
|
||||
|
||||
/** 检查方法代码 */
|
||||
private String examMethodCode;
|
||||
|
||||
/** 对比剂药品 */
|
||||
private String contrastDrug;
|
||||
|
||||
/** 对比剂剂量 */
|
||||
private String contrastDose;
|
||||
|
||||
/** 执行科室代码 */
|
||||
private String performDeptCode;
|
||||
|
||||
/** 预约号 */
|
||||
private String appointmentNo;
|
||||
|
||||
/** 项目单价 */
|
||||
private BigDecimal itemFee;
|
||||
|
||||
/** 行状态 0 已开单 1 已收费 ... */
|
||||
private Integer itemStatus;
|
||||
|
||||
/** 检查备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -21,9 +21,11 @@ public interface ISurgeryAppService {
|
||||
* @param surgeryDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @param plannedTimeStart 计划开始时间
|
||||
* @param plannedTimeEnd 计划结束时间
|
||||
* @return 手术列表
|
||||
*/
|
||||
IPage<SurgeryDto> getSurgeryPage(SurgeryDto surgeryDto, Integer pageNo, Integer pageSize);
|
||||
IPage<SurgeryDto> getSurgeryPage(SurgeryDto surgeryDto, Integer pageNo, Integer pageSize, String plannedTimeStart, String plannedTimeEnd);
|
||||
|
||||
/**
|
||||
* 根据ID查询手术详情
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.openhis.web.clinicalmanage.appservice;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.clinicalmanage.dto.OpCreateScheduleDto;
|
||||
import com.openhis.web.clinicalmanage.dto.OpScheduleDto;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 手术安排业务层接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-28
|
||||
*/
|
||||
public interface ISurgicalScheduleAppService {
|
||||
|
||||
/**
|
||||
* 分页查询手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @return 手术安排列表
|
||||
*/
|
||||
IPage<OpScheduleDto> getSurgerySchedulePage(OpScheduleDto opScheduleDto, Integer pageNo, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 根据ID查询手术安排详情
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 手术安排详情
|
||||
*/
|
||||
R<OpScheduleDto> getSurgeryScheduleDetail(Long scheduleId);
|
||||
|
||||
/**
|
||||
* 新增手术安排
|
||||
*
|
||||
* @param opCreateScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> addSurgerySchedule(OpCreateScheduleDto opCreateScheduleDto);
|
||||
|
||||
/**
|
||||
* 修改手术安排
|
||||
*
|
||||
* @param opScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> updateSurgerySchedule(OpScheduleDto opScheduleDto);
|
||||
|
||||
/**
|
||||
* 删除手术安排
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 结果
|
||||
*/
|
||||
R<?> deleteSurgerySchedule(Long scheduleId);
|
||||
|
||||
/**
|
||||
* 导出手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param response 响应对象
|
||||
* @throws IOException 异常
|
||||
*/
|
||||
void exportSurgerySchedule(OpScheduleDto opScheduleDto, HttpServletResponse response) throws IOException;
|
||||
}
|
||||
@@ -47,8 +47,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.core.framework.datasource.DynamicDataSourceContextHolder.log;
|
||||
|
||||
@@ -102,10 +103,12 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
||||
* @param surgeryDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @param plannedTimeStart 计划开始时间
|
||||
* @param plannedTimeEnd 计划结束时间
|
||||
* @return 手术列表
|
||||
*/
|
||||
@Override
|
||||
public IPage<SurgeryDto> getSurgeryPage(SurgeryDto surgeryDto, Integer pageNo, Integer pageSize) {
|
||||
public IPage<SurgeryDto> getSurgeryPage(SurgeryDto surgeryDto, Integer pageNo, Integer pageSize, String plannedTimeStart, String plannedTimeEnd) {
|
||||
QueryWrapper<SurgeryDto> queryWrapper = HisQueryUtils.buildQueryWrapper(surgeryDto, null,
|
||||
new HashSet<String>() {{
|
||||
add("surgery_no");
|
||||
@@ -115,6 +118,37 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
||||
add("anesthetist_name");
|
||||
add("org_name");
|
||||
}}, null);
|
||||
|
||||
// 添加计划时间范围查询
|
||||
if (plannedTimeStart != null && !plannedTimeStart.isEmpty()) {
|
||||
try {
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(plannedTimeStart, DateTimeFormatter.ISO_DATE_TIME);
|
||||
queryWrapper.ge("planned_time", startDateTime);
|
||||
} catch (Exception e) {
|
||||
// 如果解析失败,尝试使用日期格式解析
|
||||
try {
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(plannedTimeStart + "T00:00:00", DateTimeFormatter.ISO_DATE_TIME);
|
||||
queryWrapper.ge("planned_time", startDateTime);
|
||||
} catch (Exception ex) {
|
||||
log.error("解析计划开始时间失败: {}", plannedTimeStart, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (plannedTimeEnd != null && !plannedTimeEnd.isEmpty()) {
|
||||
try {
|
||||
LocalDateTime endDateTime = LocalDateTime.parse(plannedTimeEnd, DateTimeFormatter.ISO_DATE_TIME);
|
||||
queryWrapper.le("planned_time", endDateTime);
|
||||
} catch (Exception e) {
|
||||
// 如果解析失败,尝试使用日期格式解析
|
||||
try {
|
||||
LocalDateTime endDateTime = LocalDateTime.parse(plannedTimeEnd + "T23:59:59", DateTimeFormatter.ISO_DATE_TIME);
|
||||
queryWrapper.le("planned_time", endDateTime);
|
||||
} catch (Exception ex) {
|
||||
log.error("解析计划结束时间失败: {}", plannedTimeEnd, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
return surgeryAppMapper.getSurgeryPage(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
@@ -499,94 +533,94 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
||||
|
||||
/**
|
||||
* 填充手术记录中的名称字段
|
||||
* 根据ID反向查询用户表、机构表、手术室表、患者表、就诊表,填充对应的名称字段
|
||||
* 优化:使用批量查询减少数据库访问次数
|
||||
*
|
||||
* @param surgery 手术实体对象
|
||||
*/
|
||||
private void fillSurgeryNameFields(Surgery surgery) {
|
||||
// 收集所有需要查询的ID
|
||||
Set<Long> practitionerIds = new HashSet<>();
|
||||
Set<Long> orgIds = new HashSet<>();
|
||||
Set<Long> otherIds = new HashSet<>();
|
||||
|
||||
// 收集Practitioner IDs
|
||||
if (surgery.getMainSurgeonId() != null) practitionerIds.add(surgery.getMainSurgeonId());
|
||||
if (surgery.getAnesthetistId() != null) practitionerIds.add(surgery.getAnesthetistId());
|
||||
if (surgery.getAssistant1Id() != null) practitionerIds.add(surgery.getAssistant1Id());
|
||||
if (surgery.getAssistant2Id() != null) practitionerIds.add(surgery.getAssistant2Id());
|
||||
if (surgery.getScrubNurseId() != null) practitionerIds.add(surgery.getScrubNurseId());
|
||||
if (surgery.getApplyDoctorId() != null) practitionerIds.add(surgery.getApplyDoctorId());
|
||||
|
||||
// 收集Organization IDs
|
||||
if (surgery.getOrgId() != null) orgIds.add(surgery.getOrgId());
|
||||
if (surgery.getApplyDeptId() != null) orgIds.add(surgery.getApplyDeptId());
|
||||
|
||||
// 批量查询并缓存结果
|
||||
Map<Long, String> practitionerNameMap = new HashMap<>();
|
||||
Map<Long, String> orgNameMap = new HashMap<>();
|
||||
|
||||
// 批量查询Practitioner
|
||||
if (!practitionerIds.isEmpty()) {
|
||||
List<com.openhis.administration.domain.Practitioner> practitioners = practitionerService.listByIds(practitionerIds);
|
||||
for (com.openhis.administration.domain.Practitioner p : practitioners) {
|
||||
practitionerNameMap.put(p.getId(), p.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询Organization
|
||||
if (!orgIds.isEmpty()) {
|
||||
List<Organization> orgs = organizationService.listByIds(orgIds);
|
||||
for (Organization o : orgs) {
|
||||
orgNameMap.put(o.getId(), o.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 填充患者姓名
|
||||
if (surgery.getPatientId() != null) {
|
||||
if (surgery.getPatientId() != null && surgery.getPatientName() == null) {
|
||||
Patient patient = patientService.getById(surgery.getPatientId());
|
||||
if (patient != null) {
|
||||
surgery.setPatientName(patient.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 填充主刀医生姓名(使用practitionerId查询Practitioner表)
|
||||
if (surgery.getMainSurgeonId() != null) {
|
||||
com.openhis.administration.domain.Practitioner mainSurgeon = practitionerService.getById(surgery.getMainSurgeonId());
|
||||
if (mainSurgeon != null) {
|
||||
surgery.setMainSurgeonName(mainSurgeon.getName());
|
||||
}
|
||||
// 使用缓存填充名称
|
||||
if (surgery.getMainSurgeonId() != null && surgery.getMainSurgeonName() == null) {
|
||||
surgery.setMainSurgeonName(practitionerNameMap.get(surgery.getMainSurgeonId()));
|
||||
}
|
||||
|
||||
// 填充麻醉医生姓名(使用practitionerId查询Practitioner表)
|
||||
if (surgery.getAnesthetistId() != null) {
|
||||
com.openhis.administration.domain.Practitioner anesthetist = practitionerService.getById(surgery.getAnesthetistId());
|
||||
if (anesthetist != null) {
|
||||
surgery.setAnesthetistName(anesthetist.getName());
|
||||
}
|
||||
if (surgery.getAnesthetistId() != null && surgery.getAnesthetistName() == null) {
|
||||
surgery.setAnesthetistName(practitionerNameMap.get(surgery.getAnesthetistId()));
|
||||
}
|
||||
|
||||
// 填充助手1姓名(使用practitionerId查询Practitioner表)
|
||||
if (surgery.getAssistant1Id() != null) {
|
||||
com.openhis.administration.domain.Practitioner assistant1 = practitionerService.getById(surgery.getAssistant1Id());
|
||||
if (assistant1 != null) {
|
||||
surgery.setAssistant1Name(assistant1.getName());
|
||||
}
|
||||
if (surgery.getAssistant1Id() != null && surgery.getAssistant1Name() == null) {
|
||||
surgery.setAssistant1Name(practitionerNameMap.get(surgery.getAssistant1Id()));
|
||||
}
|
||||
|
||||
// 填充助手2姓名(使用practitionerId查询Practitioner表)
|
||||
if (surgery.getAssistant2Id() != null) {
|
||||
com.openhis.administration.domain.Practitioner assistant2 = practitionerService.getById(surgery.getAssistant2Id());
|
||||
if (assistant2 != null) {
|
||||
surgery.setAssistant2Name(assistant2.getName());
|
||||
}
|
||||
if (surgery.getAssistant2Id() != null && surgery.getAssistant2Name() == null) {
|
||||
surgery.setAssistant2Name(practitionerNameMap.get(surgery.getAssistant2Id()));
|
||||
}
|
||||
|
||||
// 填充巡回护士姓名(使用practitionerId查询Practitioner表)
|
||||
if (surgery.getScrubNurseId() != null) {
|
||||
com.openhis.administration.domain.Practitioner scrubNurse = practitionerService.getById(surgery.getScrubNurseId());
|
||||
if (scrubNurse != null) {
|
||||
surgery.setScrubNurseName(scrubNurse.getName());
|
||||
}
|
||||
if (surgery.getScrubNurseId() != null && surgery.getScrubNurseName() == null) {
|
||||
surgery.setScrubNurseName(practitionerNameMap.get(surgery.getScrubNurseId()));
|
||||
}
|
||||
if (surgery.getApplyDoctorId() != null && surgery.getApplyDoctorName() == null) {
|
||||
surgery.setApplyDoctorName(practitionerNameMap.get(surgery.getApplyDoctorId()));
|
||||
}
|
||||
|
||||
// 填充手术室名称
|
||||
if (surgery.getOperatingRoomId() != null) {
|
||||
if (surgery.getOperatingRoomId() != null && surgery.getOperatingRoomName() == null) {
|
||||
OperatingRoom operatingRoom = operatingRoomService.getById(surgery.getOperatingRoomId());
|
||||
if (operatingRoom != null) {
|
||||
surgery.setOperatingRoomName(operatingRoom.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 填充执行科室名称
|
||||
if (surgery.getOrgId() != null) {
|
||||
Organization org = organizationService.getById(surgery.getOrgId());
|
||||
if (org != null) {
|
||||
surgery.setOrgName(org.getName());
|
||||
}
|
||||
// 使用缓存填充组织名称
|
||||
if (surgery.getOrgId() != null && surgery.getOrgName() == null) {
|
||||
surgery.setOrgName(orgNameMap.get(surgery.getOrgId()));
|
||||
}
|
||||
if (surgery.getApplyDeptId() != null && surgery.getApplyDeptName() == null) {
|
||||
surgery.setApplyDeptName(orgNameMap.get(surgery.getApplyDeptId()));
|
||||
}
|
||||
|
||||
// 填充申请科室名称(如果还没有设置)
|
||||
if (surgery.getApplyDeptId() != null && (surgery.getApplyDeptName() == null || surgery.getApplyDeptName().isEmpty())) {
|
||||
Organization applyDept = organizationService.getById(surgery.getApplyDeptId());
|
||||
if (applyDept != null) {
|
||||
surgery.setApplyDeptName(applyDept.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 填充申请医生姓名(如果还没有设置) - 使用practitionerId查询Practitioner表
|
||||
if (surgery.getApplyDoctorId() != null && (surgery.getApplyDoctorName() == null || surgery.getApplyDoctorName().isEmpty())) {
|
||||
com.openhis.administration.domain.Practitioner applyDoctor = practitionerService.getById(surgery.getApplyDoctorId());
|
||||
if (applyDoctor != null) {
|
||||
surgery.setApplyDoctorName(applyDoctor.getName());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("填充手术名称字段完成 - patientName: {}, mainSurgeonName: {}, anesthetistName: {}, assistant1Name: {}, assistant2Name: {}, scrubNurseName: {}, operatingRoomName: {}, orgName: {}",
|
||||
surgery.getPatientName(), surgery.getMainSurgeonName(), surgery.getAnesthetistName(), surgery.getAssistant1Name(),
|
||||
surgery.getAssistant2Name(), surgery.getScrubNurseName(), surgery.getOperatingRoomName(), surgery.getOrgName());
|
||||
log.debug("填充手术名称字段完成 - patientName: {}, mainSurgeonName: {}, orgName: {}",
|
||||
surgery.getPatientName(), surgery.getMainSurgeonName(), surgery.getOrgName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.openhis.web.clinicalmanage.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.administration.domain.Patient;
|
||||
import com.openhis.administration.service.IPatientService;
|
||||
import com.openhis.surgicalschedule.domain.OpSchedule;
|
||||
import com.openhis.surgicalschedule.service.IOpScheduleService;
|
||||
import com.openhis.web.clinicalmanage.appservice.ISurgicalScheduleAppService;
|
||||
import com.openhis.web.clinicalmanage.dto.OpCreateScheduleDto;
|
||||
import com.openhis.web.clinicalmanage.dto.OpScheduleDto;
|
||||
import com.openhis.web.clinicalmanage.mapper.SurgicalScheduleAppMapper;
|
||||
import com.openhis.web.regdoctorstation.mapper.RequestFormManageAppMapper;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 手术安排业务层实现类
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-28
|
||||
*/
|
||||
@Service
|
||||
public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppService {
|
||||
|
||||
@Resource
|
||||
private IOpScheduleService opScheduleService;
|
||||
|
||||
@Resource
|
||||
private IPatientService patientService;
|
||||
|
||||
@Resource
|
||||
private SurgicalScheduleAppMapper surgicalScheduleAppMapper;
|
||||
|
||||
|
||||
@Resource
|
||||
private RequestFormManageAppMapper requestFormManageAppMapper;
|
||||
/**
|
||||
* 分页查询手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @return 手术安排列表
|
||||
*/
|
||||
@Override
|
||||
public IPage<OpScheduleDto> getSurgerySchedulePage(OpScheduleDto opScheduleDto, Integer pageNo, Integer pageSize) {
|
||||
return surgicalScheduleAppMapper.getSurgerySchedulePage(new Page<>(pageNo, pageSize), opScheduleDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询手术安排详情
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 手术安排详情
|
||||
*/
|
||||
@Override
|
||||
public R<OpScheduleDto> getSurgeryScheduleDetail(Long scheduleId) {
|
||||
OpScheduleDto opScheduleDto = surgicalScheduleAppMapper.getSurgeryScheduleDetail(scheduleId);
|
||||
if (opScheduleDto == null) {
|
||||
return R.fail("手术安排信息不存在");
|
||||
}
|
||||
return R.ok(opScheduleDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增手术安排
|
||||
*
|
||||
* @param opCreateScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> addSurgerySchedule(OpCreateScheduleDto opCreateScheduleDto) {
|
||||
// 校验患者是否存在
|
||||
if (opCreateScheduleDto.getPatientId() != null) {
|
||||
Patient patient = patientService.getById(opCreateScheduleDto.getPatientId());
|
||||
if (patient == null) {
|
||||
return R.fail("患者信息不存在");
|
||||
}
|
||||
}
|
||||
//校验该时段内手术间是否被占用
|
||||
LocalDateTime scheduleDate = opCreateScheduleDto.getEntryTime();//入室时间
|
||||
String roomCode = opCreateScheduleDto.getRoomCode();//手术室编号
|
||||
LocalDateTime endTime = opCreateScheduleDto.getEndTime();//手术结束时间
|
||||
Boolean scheduleConflict = surgicalScheduleAppMapper.isScheduleConflict(scheduleDate, endTime, roomCode);
|
||||
if (scheduleConflict) {
|
||||
return R.fail("该时段内手术间被占用");
|
||||
}
|
||||
|
||||
LoginUser loginUser = new LoginUser();
|
||||
//获取当前登录用户信息
|
||||
loginUser = SecurityUtils.getLoginUser();
|
||||
// 当前登录用户ID
|
||||
Long userId = loginUser.getUserId();
|
||||
|
||||
// 转换为实体对象
|
||||
OpSchedule opSchedule = new OpSchedule();
|
||||
BeanUtils.copyProperties(opCreateScheduleDto, opSchedule);
|
||||
|
||||
// 设置创建者ID
|
||||
opSchedule.setCreatorId(userId);
|
||||
//设置创建人名称
|
||||
opSchedule.setCreateBy(loginUser.getUsername());
|
||||
//设置创建时间
|
||||
opSchedule.setCreateTime(new Date());
|
||||
// 设置租户ID
|
||||
opSchedule.setTenantId(loginUser.getTenantId());
|
||||
//设置手术状态
|
||||
opSchedule.setOperStatus(0);
|
||||
//修改申请表状态为已排期
|
||||
|
||||
// 保存手术安排
|
||||
boolean saved = opScheduleService.save(opSchedule);
|
||||
//修改申请单状态为已排期
|
||||
|
||||
if (!saved) {
|
||||
return R.fail("新增手术安排失败");
|
||||
}
|
||||
|
||||
return R.ok("新增手术安排成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手术安排
|
||||
*
|
||||
* @param opScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> updateSurgerySchedule(OpScheduleDto opScheduleDto) {
|
||||
// 校验手术安排是否存在
|
||||
if (opScheduleDto.getScheduleId() == null) {
|
||||
return R.fail("排程号不能为空");
|
||||
}
|
||||
OpSchedule existSchedule = opScheduleService.getById(opScheduleDto.getScheduleId());
|
||||
if (existSchedule == null) {
|
||||
return R.fail("手术安排信息不存在");
|
||||
}
|
||||
|
||||
// 转换为实体对象
|
||||
OpSchedule opSchedule = new OpSchedule();
|
||||
BeanUtils.copyProperties(opScheduleDto, opSchedule);
|
||||
|
||||
// 更新时间
|
||||
opSchedule.setUpdateTime(new Date());
|
||||
|
||||
// 更新手术安排
|
||||
boolean updated = opScheduleService.updateById(opSchedule);
|
||||
if (!updated) {
|
||||
return R.fail("修改手术安排失败");
|
||||
}
|
||||
|
||||
return R.ok("修改手术安排成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除手术安排
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> deleteSurgerySchedule(Long scheduleId) {
|
||||
// 校验手术安排是否存在
|
||||
OpSchedule existSchedule = opScheduleService.getById(scheduleId);
|
||||
if (existSchedule == null) {
|
||||
return R.fail("手术安排信息不存在");
|
||||
}
|
||||
|
||||
// 逻辑删除手术安排
|
||||
boolean deleted = opScheduleService.removeById(scheduleId);
|
||||
if (!deleted) {
|
||||
return R.fail("删除手术安排失败");
|
||||
}
|
||||
|
||||
return R.ok(null, "删除手术安排成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param response 响应对象
|
||||
* @throws IOException 异常
|
||||
*/
|
||||
@Override
|
||||
public void exportSurgerySchedule(OpScheduleDto opScheduleDto, HttpServletResponse response) throws IOException {
|
||||
// 查询所有符合条件的手术安排
|
||||
List<OpScheduleDto> scheduleList = surgicalScheduleAppMapper.getSurgeryScheduleList(opScheduleDto);
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("text/csv; charset=utf-8");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("手术安排列表.csv", "UTF-8"));
|
||||
|
||||
// 写入CSV文件
|
||||
try (OutputStream outputStream = response.getOutputStream();
|
||||
PrintWriter writer = new PrintWriter(outputStream)) {
|
||||
|
||||
// 写入UTF-8 BOM,解决Excel乱码问题
|
||||
outputStream.write(0xEF);
|
||||
outputStream.write(0xBB);
|
||||
outputStream.write(0xBF);
|
||||
|
||||
// 写入表头(与前端表格完全一致)
|
||||
writer.println("ID,卫生机构,姓名,就诊卡号,手术单号,手术名称,申请科室,手术类型,手术性质,主刀医生,麻醉方法,安排时间,操作人");
|
||||
|
||||
// 写入数据
|
||||
int index = 0;
|
||||
for (OpScheduleDto schedule : scheduleList) {
|
||||
index++;
|
||||
|
||||
// 转换手术类型
|
||||
String surgeryType = convertSurgeryNature(schedule.getSurgeryNature());
|
||||
|
||||
// 转换麻醉方法
|
||||
String anesthesiaMethod = convertAnesMethod(schedule.getAnesMethod());
|
||||
|
||||
// 格式化安排时间
|
||||
String formattedDate = formatScheduleDate(schedule.getScheduleDate());
|
||||
|
||||
writer.printf("%d,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
|
||||
index, // 序号从1开始
|
||||
schedule.getOrgName() != null ? schedule.getOrgName() : "",
|
||||
schedule.getPatientName() != null ? schedule.getPatientName() : "",
|
||||
schedule.getVisitId() != null ? schedule.getVisitId().toString() : "",
|
||||
schedule.getOperCode() != null ? schedule.getOperCode() : "",
|
||||
schedule.getOperName() != null ? schedule.getOperName() : "",
|
||||
schedule.getApplyDeptName() != null ? schedule.getApplyDeptName() : "",
|
||||
surgeryType,
|
||||
surgeryType, // 手术性质和手术类型使用相同的转换
|
||||
schedule.getSurgeonName() != null ? schedule.getSurgeonName() : "",
|
||||
anesthesiaMethod,
|
||||
formattedDate,
|
||||
schedule.getCreateByName() != null ? schedule.getCreateByName() : ""
|
||||
);
|
||||
}
|
||||
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换手术性质/类型
|
||||
*/
|
||||
private String convertSurgeryNature(String surgeryNature) {
|
||||
if (surgeryNature == null) return "";
|
||||
|
||||
switch (surgeryNature) {
|
||||
case "1": return "择期手术";
|
||||
case "2": return "急诊手术";
|
||||
case "3": return "限期手术";
|
||||
case "4": return "日间手术";
|
||||
default: return surgeryNature;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换麻醉方法
|
||||
*/
|
||||
private String convertAnesMethod(String anesMethod) {
|
||||
if (anesMethod == null) return "";
|
||||
|
||||
switch (anesMethod) {
|
||||
case "1": return "全身麻醉";
|
||||
case "2": return "椎管内麻醉";
|
||||
case "3": return "神经阻滞麻醉";
|
||||
case "4": return "局部浸润麻醉";
|
||||
case "5": return "表面麻醉";
|
||||
case "6": return "复合麻醉";
|
||||
case "7": return "基础麻醉";
|
||||
default: return anesMethod;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化安排时间
|
||||
*/
|
||||
private String formatScheduleDate(LocalDate scheduleDate) {
|
||||
if (scheduleDate == null) return "";
|
||||
|
||||
// 格式化为 yyyy-MM-dd
|
||||
return scheduleDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,18 @@ public class SurgeryController {
|
||||
* @param surgeryDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @param plannedTimeStart 计划开始时间
|
||||
* @param plannedTimeEnd 计划结束时间
|
||||
* @return 手术列表
|
||||
*/
|
||||
@GetMapping(value = "/surgery-page")
|
||||
public R<IPage<SurgeryDto>> getSurgeryPage(SurgeryDto surgeryDto,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
|
||||
IPage<SurgeryDto> page = surgeryAppService.getSurgeryPage(surgeryDto, pageNo, pageSize);
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(value = "plannedTimeStart", required = false) String plannedTimeStart,
|
||||
@RequestParam(value = "plannedTimeEnd", required = false) String plannedTimeEnd) {
|
||||
// 将时间范围参数传递给服务层
|
||||
IPage<SurgeryDto> page = surgeryAppService.getSurgeryPage(surgeryDto, pageNo, pageSize, plannedTimeStart, plannedTimeEnd);
|
||||
return R.ok(page);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.openhis.web.clinicalmanage.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.clinicalmanage.appservice.ISurgicalScheduleAppService;
|
||||
import com.openhis.web.clinicalmanage.dto.OpCreateScheduleDto;
|
||||
import com.openhis.web.clinicalmanage.dto.OpScheduleDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 手术安排Controller
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-28
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/clinical-manage/surgery-schedule")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class SurgicalScheduleController {
|
||||
|
||||
private final ISurgicalScheduleAppService surgicalScheduleAppService;
|
||||
|
||||
/**
|
||||
* 分页查询手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param pageNo 当前页
|
||||
* @param pageSize 每页条数
|
||||
* @return 手术安排列表
|
||||
*/
|
||||
@GetMapping(value = "/page")
|
||||
public R<IPage<OpScheduleDto>> getSurgerySchedulePage(OpScheduleDto opScheduleDto,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
|
||||
IPage<OpScheduleDto> page = surgicalScheduleAppService.getSurgerySchedulePage(opScheduleDto, pageNo, pageSize);
|
||||
return R.ok(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询手术安排详情
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 手术安排详情
|
||||
*/
|
||||
@GetMapping(value = "/{scheduleId}")
|
||||
public R<OpScheduleDto> getSurgeryScheduleDetail(@PathVariable Long scheduleId) {
|
||||
return surgicalScheduleAppService.getSurgeryScheduleDetail(scheduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增手术安排
|
||||
*
|
||||
* @param opCreateScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping(value = "/create")
|
||||
public R<?> addSurgerySchedule(@RequestBody OpCreateScheduleDto opCreateScheduleDto) {
|
||||
return surgicalScheduleAppService.addSurgerySchedule(opCreateScheduleDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手术安排
|
||||
*
|
||||
* @param opScheduleDto 手术安排信息
|
||||
* @return 结果
|
||||
*/
|
||||
@PutMapping(value = "/update")
|
||||
public R<?> updateSurgerySchedule(@RequestBody OpScheduleDto opScheduleDto) {
|
||||
return surgicalScheduleAppService.updateSurgerySchedule(opScheduleDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除手术安排
|
||||
*
|
||||
* @param scheduleId 手术安排ID
|
||||
* @return 结果
|
||||
*/
|
||||
@DeleteMapping(value = "/{scheduleId}")
|
||||
public R<?> deleteSurgerySchedule(@PathVariable Long scheduleId) {
|
||||
return surgicalScheduleAppService.deleteSurgerySchedule(scheduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出手术安排列表
|
||||
*
|
||||
* @param opScheduleDto 查询条件
|
||||
* @param response 响应对象
|
||||
* @throws IOException 异常
|
||||
*/
|
||||
@GetMapping(value = "/export")
|
||||
public void exportSurgerySchedule(OpScheduleDto opScheduleDto, HttpServletResponse response) throws IOException {
|
||||
surgicalScheduleAppService.exportSurgerySchedule(opScheduleDto, response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.openhis.web.clinicalmanage.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class OpCreateScheduleDto {
|
||||
/**
|
||||
* 申请单ID
|
||||
*/
|
||||
private Long applyId;
|
||||
|
||||
/**
|
||||
* 患者ID
|
||||
*/
|
||||
private Long patientId;
|
||||
|
||||
/**
|
||||
* 就诊ID
|
||||
*/
|
||||
private Long visitId;
|
||||
|
||||
/**
|
||||
* 手术编码
|
||||
*/
|
||||
private String operCode;
|
||||
|
||||
/**
|
||||
* 手术名称
|
||||
*/
|
||||
private String operName;
|
||||
|
||||
/**
|
||||
* 术前诊断
|
||||
*/
|
||||
private String preoperativeDiagnosis;
|
||||
|
||||
/**
|
||||
* 术后诊断
|
||||
*/
|
||||
private String postoperativeDiagnosis;
|
||||
|
||||
/**
|
||||
* 手术安排日期
|
||||
*/
|
||||
private LocalDate scheduleDate;
|
||||
|
||||
/**
|
||||
* 手术台次序号
|
||||
*/
|
||||
private Integer sequenceNo;
|
||||
|
||||
/**
|
||||
* 是否首台手术 1-是 0-否
|
||||
*/
|
||||
private Integer isFirstSurgery;
|
||||
|
||||
/**
|
||||
* 是否有药物过敏 1-是 0-否
|
||||
*/
|
||||
private Integer isAllergyMedication;
|
||||
|
||||
/**
|
||||
* 过敏备注
|
||||
*/
|
||||
private String allergyRemark;
|
||||
|
||||
/**
|
||||
* 手术性质
|
||||
*/
|
||||
private String surgeryNature;
|
||||
|
||||
/**
|
||||
* 手术部位
|
||||
*/
|
||||
private String surgerySite;
|
||||
|
||||
/**
|
||||
* 入院时间
|
||||
*/
|
||||
private LocalDateTime admissionTime;
|
||||
|
||||
/**
|
||||
* 入手术室时间
|
||||
*/
|
||||
private LocalDateTime entryTime;
|
||||
|
||||
/**
|
||||
* 手术室编码
|
||||
*/
|
||||
private String roomCode;
|
||||
|
||||
/**
|
||||
* 手术台号
|
||||
*/
|
||||
private String tableNo;
|
||||
|
||||
/**
|
||||
* 麻醉方式
|
||||
*/
|
||||
private String anesMethod;
|
||||
|
||||
/**
|
||||
* 麻醉医生1编码
|
||||
*/
|
||||
private String anesDoctor1Code;
|
||||
|
||||
/**
|
||||
* 麻醉医生2编码
|
||||
*/
|
||||
private String anesDoctor2Code;
|
||||
|
||||
/**
|
||||
* 麻醉医生3编码
|
||||
*/
|
||||
private String anesDoctor3Code;
|
||||
|
||||
/**
|
||||
* 洗手护士编码
|
||||
*/
|
||||
private String scrubNurseCode;
|
||||
|
||||
/**
|
||||
* 巡回护士1编码
|
||||
*/
|
||||
private String circuNurse1Code;
|
||||
|
||||
/**
|
||||
* 巡回护士2编码
|
||||
*/
|
||||
private String circuNurse2Code;
|
||||
|
||||
/**
|
||||
* 洗手护士1编码
|
||||
*/
|
||||
private String scrubNurse1Code;
|
||||
|
||||
/**
|
||||
* 洗手护士2编码
|
||||
*/
|
||||
private String scrubNurse2Code;
|
||||
|
||||
/**
|
||||
* 主刀医生编码
|
||||
*/
|
||||
private String surgeonCode;
|
||||
|
||||
/**
|
||||
* 助手1编码
|
||||
*/
|
||||
private String assistant1Code;
|
||||
|
||||
/**
|
||||
* 助手2编码
|
||||
*/
|
||||
private String assistant2Code;
|
||||
|
||||
/**
|
||||
* 助手3编码
|
||||
*/
|
||||
private String assistant3Code;
|
||||
|
||||
/**
|
||||
* 手术开始时间
|
||||
*/
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 手术结束时间
|
||||
*/
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 麻醉开始时间
|
||||
*/
|
||||
private LocalDateTime anesStart;
|
||||
|
||||
/**
|
||||
* 麻醉结束时间
|
||||
*/
|
||||
private LocalDateTime anesEnd;
|
||||
|
||||
/**
|
||||
* 手术状态
|
||||
*/
|
||||
private Integer operStatus;
|
||||
|
||||
/**
|
||||
* 是否植入耗材 1-是 0-否
|
||||
*/
|
||||
private Integer implantFlag;
|
||||
|
||||
/**
|
||||
* 植入物序列号
|
||||
*/
|
||||
private String implantSerial;
|
||||
|
||||
/**
|
||||
* 失血量(ml)
|
||||
*/
|
||||
private Integer bloodLoss;
|
||||
|
||||
/**
|
||||
* 输血量(ml)
|
||||
*/
|
||||
private Integer bloodTrans;
|
||||
|
||||
/**
|
||||
* 感染诊断
|
||||
*/
|
||||
private String infectionDiagnosis;
|
||||
|
||||
/**
|
||||
* 隔离类型
|
||||
*/
|
||||
private String isolationType;
|
||||
|
||||
/**
|
||||
* 患者体重(kg)
|
||||
*/
|
||||
private BigDecimal patientWeight;
|
||||
|
||||
/**
|
||||
* 患者身高(cm)
|
||||
*/
|
||||
private BigDecimal patientHeight;
|
||||
|
||||
/**
|
||||
* 沟通信息
|
||||
*/
|
||||
private String communicationInfo;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 创建人ID
|
||||
*/
|
||||
private String creatorId;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
private Integer tenantId;
|
||||
|
||||
/**
|
||||
* 是否删除(默认为0,1表示删除)
|
||||
*/
|
||||
private Integer deleteFlag;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.openhis.web.clinicalmanage.dto;
|
||||
|
||||
import com.openhis.surgicalschedule.domain.OpSchedule;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
|
||||
/**
|
||||
* 手术安排DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-28
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class OpScheduleDto extends OpSchedule {
|
||||
|
||||
/**
|
||||
* 患者姓名
|
||||
*/
|
||||
private String patientName;
|
||||
|
||||
/**
|
||||
* 就诊ID
|
||||
*/
|
||||
private Long encounterId;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 年龄
|
||||
*/
|
||||
private Integer age;
|
||||
|
||||
/**
|
||||
* 卫生机构名称
|
||||
*/
|
||||
private String orgName;
|
||||
|
||||
/**
|
||||
* 申请科室名称
|
||||
*/
|
||||
private String applyDeptName;
|
||||
|
||||
/**
|
||||
* 主刀医生姓名
|
||||
*/
|
||||
private String surgeonName;
|
||||
|
||||
/**
|
||||
* 申请时间开始
|
||||
*/
|
||||
private String applyTime;
|
||||
|
||||
/**
|
||||
* 申请医生姓名
|
||||
*/
|
||||
private String applyDoctorName;
|
||||
|
||||
/**
|
||||
* 卫生机构
|
||||
*/
|
||||
private String orgId;
|
||||
|
||||
/**
|
||||
* 手术类型
|
||||
*/
|
||||
private String surgeryType;
|
||||
/**
|
||||
* 申请科室
|
||||
*/
|
||||
private Long applyDeptId;
|
||||
/**
|
||||
* 手术单号
|
||||
*/
|
||||
private String surgeryNo;
|
||||
|
||||
/**
|
||||
* 创建人名称
|
||||
*/
|
||||
private String createByName;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user