Compare commits
198 Commits
b8d7e3cdf1
...
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 | |||
|
|
3cabb5f803 | ||
|
|
a55884a710 | ||
| dfac362c37 | |||
|
|
897afd4da2 | ||
| 3d31b3482a | |||
| 3acf8ad50a | |||
|
|
fa06a52d71 | ||
| d1b290881f | |||
| 5781e39c20 | |||
| 9ed43c9413 | |||
| 270004afee | |||
|
|
96941cb4e0 | ||
| 6c15f0d4d5 | |||
|
|
063be326eb | ||
| 5534a71c7d | |||
| 669d669422 | |||
| 98fe9f3301 | |||
| 6f7d723c6b | |||
| 0a08088ada | |||
|
|
48309fcaa4 | ||
|
|
28160e082c | ||
|
|
29ecfd90f2 | ||
| f690b78b18 | |||
| 6f71c678bd | |||
| 1c781c1224 | |||
|
|
638f853af6 | ||
|
|
96a8f75aa1 | ||
| 15a6445e26 | |||
| 0e4b0ad6fd | |||
|
|
7da461a9cb | ||
| b0040bcd48 | |||
| fa5394cc35 | |||
| 81cc8b08a0 | |||
| 0cecf3bcad |
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());
|
||||
}
|
||||
}
|
||||
505
md/需求/102-门诊医生站传染病报告卡登记-2026-1-28.md
Normal file
505
md/需求/102-门诊医生站传染病报告卡登记-2026-1-28.md
Normal file
@@ -0,0 +1,505 @@
|
||||
## 门诊医生站传染性报卡登记PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:门诊医生站传染性报卡登记
|
||||
**页面目标**:帮助医生完成法定传染病病例的电子报告卡填写与提交
|
||||
**适用场景**:医生确诊或疑似发现法定传染病病例时,进行报卡登记
|
||||
**页面类型**:表单页(复杂表单)
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 患者基本信息录入(含身份验证)
|
||||
2. 传染病分类选择与疾病诊断信息登记
|
||||
3. 病例分类与流行病学信息记录
|
||||
4. 数据校验与表单提交
|
||||
5. 地址四级联动选择(省-市-区县-街道)
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 规范传染病报告流程,确保数据完整准确
|
||||
- 减少手工填写错误,提高上报效率
|
||||
- 自动关联患者基本信息,减少重复录入
|
||||
- 内置校验规则防止漏报错报
|
||||
|
||||
**原型图地址**:https://static.pm-ai.cn/prototype/20260128/6041dcc237645108aa9e917e8d57705f/index.html
|
||||
**流程图**:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A(["开始报卡"]) --> B["填写患者基本信息"]
|
||||
B --> C{"身份证格式错误"}
|
||||
C -- 是 --> D["提示请输入有效身份证号码"]
|
||||
C -- 否 --> E{"患者年龄≤14岁"}
|
||||
E -- 是 --> F["显示家长姓名输入框"]
|
||||
E -- 否 --> G["隐藏家长姓名输入框"]
|
||||
F --> H["填写现住地址"]
|
||||
G --> H
|
||||
H --> I{"地址加载失败"}
|
||||
I -- 是 --> J["显示手动输入选项"]
|
||||
I -- 否 --> K["选择疾病分类"]
|
||||
J --> K
|
||||
K --> L{"选择特定疾病"}
|
||||
L -- 是 --> M["显示疾病分型选择"]
|
||||
L -- 否 --> N["跳过分型选择"]
|
||||
M --> O["填写发病/诊断日期"]
|
||||
N --> O
|
||||
O --> P{"日期逻辑错误"}
|
||||
P -- 是 --> Q["提示发病日期不能晚于诊断日期"]
|
||||
P -- 否 --> R["填写报告信息"]
|
||||
Q --> R
|
||||
R --> S["表单校验"]
|
||||
S --> T{"校验失败"}
|
||||
T -- 是 --> U["显示错误提示"]
|
||||
T -- 否 --> V["保存报卡"]
|
||||
U --> S
|
||||
V --> W{"点击重置按钮"}
|
||||
W -- 是 --> X["保留关键信息重置其他字段"]
|
||||
X --> S
|
||||
V --> Y{"点击关闭按钮"}
|
||||
Y -- 是 --> Z{"确认关闭"}
|
||||
Z -- 是 --> AA(["结束流程"])
|
||||
Z -- 否 --> V
|
||||
```
|
||||
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. **顶部标题区**(5%):展示表单标题和卡片编号
|
||||
2. **患者信息区**(30%):患者基本信息、联系方式、现住地址等
|
||||
3. **疾病信息区**(50%):疾病分类选择、发病/诊断日期、疾病分型等
|
||||
4. **报告信息区**(10%):报告单位、医生、填卡日期等
|
||||
|
||||
**操作按钮区**(5%):保存、重置、关闭按钮
|
||||
**布局特点**:上下布局,采用响应式网格,表单分组清晰,必填项高亮标识
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 标题区
|
||||
|
||||
**区域位置**:页面顶部
|
||||
**区域尺寸**:高度60px
|
||||
**区域功能**:展示表单标题和唯一编号标识
|
||||
**包含元素**:
|
||||
|
||||
- 表单标题
|
||||
|
||||
- - 元素类型:标题文本
|
||||
- 默认内容:“中华人民共和国传染病报告卡”
|
||||
- 样式要求:20px字号,深蓝色(#2c3e50),居中加粗
|
||||
|
||||
- 卡片编号
|
||||
|
||||
- - 元素类型:输入框
|
||||
- 默认值:空
|
||||
- 提示文字:“单位自编,与网络直报一致”
|
||||
- 交互行为:支持手动输入12位编号
|
||||
- 样式要求:12px灰色文字,带下划线分隔线
|
||||
|
||||
#### 2. 患者基本信息区
|
||||
|
||||
**区域位置**:标题区下方
|
||||
**区域功能**:采集患者核心身份信息、联系方式、居住地等
|
||||
**包含元素**:
|
||||
|
||||
- 患者姓名输入框
|
||||
|
||||
- - 元素类型:文本输入框,自动引入当前就诊患者信息的姓名
|
||||
- 校验规则:必填项,支持中文姓名2-10字
|
||||
|
||||
- 家长姓名输入框
|
||||
|
||||
- - 元素类型:文本输入框
|
||||
- 条件显示:当系统计算年龄≤14岁时自动显示必填标识
|
||||
|
||||
- 身份证号输入框
|
||||
|
||||
- - 元素类型:文本输入框,自动引入当前就诊患者信息的身份证号
|
||||
- 校验规则:必填项,自动校验18位身份证格式
|
||||
|
||||
- 性别选择
|
||||
|
||||
- - 元素类型:单选按钮组
|
||||
- 选项:男/女/未知,自动匹配当前就诊患者信息的性别
|
||||
- 默认值:必填项
|
||||
|
||||
- 出生日期输入
|
||||
|
||||
- - 元素类型:复合输入区域
|
||||
- 包含:年(4位)/月(2位)/日(2位)三个输入框,自动匹配当前就诊患者信息的出生年月
|
||||
- 联动逻辑:自动计算实足年龄并填充到年龄输入框
|
||||
|
||||
- 工作单位输入框
|
||||
|
||||
- - 元素类型:文本输入框,自动引入当前就诊患者信息的工作单位
|
||||
- 特殊场景:学生自动关联学校信息
|
||||
|
||||
- 联系电话
|
||||
|
||||
- - 元素类型:电话输入框,自动引入当前就诊患者信息的联系方式
|
||||
- 校验规则:必填,11位手机号或带区号固话
|
||||
|
||||
- 紧急联系人电话
|
||||
|
||||
- - 元素类型:电话输入框
|
||||
- 校验规则:必填,11位手机号或带区号固话
|
||||
|
||||
- 病人属于
|
||||
|
||||
- - 复选框类型:通过现地址自动判断
|
||||
- 校验规则:必填
|
||||
|
||||
- 职业
|
||||
|
||||
- - 下拉选项类型:取值于字典管理的字典名称为“职业”维护的数据
|
||||
- 校验规则:必填
|
||||
|
||||
|
||||
|
||||
#### 3. 现住地址选择区
|
||||
|
||||
**区域功能**:四级联动地址选择(省-市-区县-街道)
|
||||
**交互逻辑**:
|
||||
|
||||
1. 省份选择后动态加载对应城市
|
||||
2. 城市选择后动态加载区县
|
||||
3. 区县选择后动态加载街道
|
||||
4. 村(居)和门牌号为手动输入
|
||||
**数据要求**:
|
||||
|
||||
- 初始默认值:省-市-区县-街道(自动引入当前就诊患者信息的现住址)
|
||||
- 异常处理:当上级未选择时禁用下级选择
|
||||
|
||||
**字典取值跟新增患者的现住址保持一致(患者管理-)患者列表)**
|
||||

|
||||
|
||||
|
||||
#### 4. 疾病信息区
|
||||
|
||||
**区域功能**:选择传染病类型及相关临床信息
|
||||
**包含元素**:
|
||||
|
||||
- **疾病分类选择**:
|
||||
|
||||
- - 布局方式:网格布局(3列)
|
||||
- 分类:甲类/乙类/丙类传染病
|
||||
- 交互行为:多选但同类别互斥
|
||||
- 特殊处理:选择炭疽/肺结核/病毒性肝炎/疟疾/梅毒/血吸虫病等疾病时激活分型选择
|
||||
|
||||
- **疾病复选框互斥逻辑:**
|
||||
|
||||
- - 选择炭疽病时显示分型选项(肺炭疽/皮肤炭疽/胃肠炭疽/未分型)
|
||||
- 选择肺结核时显示分型选项(涂阳/仅培阳/菌阴/未痰检)
|
||||
- 选择病毒性肝炎时显示分型选项(甲/乙/丙/戊型)
|
||||
- 选择疟疾时显示分型选项(间日疟/恶性疟/三日疟/卵形疟/未分型)
|
||||
- 选择梅毒时显示分型选项(Ⅰ期/Ⅱ期/Ⅲ期/胎传/隐性)
|
||||
- 选择血吸虫病时显示分型选项(急性/慢性/晚期/未分型)
|
||||
|
||||
- **分型选择**:
|
||||
|
||||
- - 元素类型:动态下拉框
|
||||
- 数据源:根据疾病类型动态加载
|
||||
- 示例:肺结核→涂阳/仅培阳/菌阴/未痰检
|
||||
|
||||
- **其他法定管理以及重点监测传染病输入框:**
|
||||
|
||||
- - 手动输入非列表疾病
|
||||
- 自动关联传染病代码库
|
||||
|
||||
- **发病日期**:
|
||||
|
||||
- - 元素类型:日期选择器
|
||||
- 验证规则:不得晚于诊断日期
|
||||
|
||||
- **诊断日期**:
|
||||
|
||||
- - 元素类型:日期选择器
|
||||
- 取值:默认当前系统时间
|
||||
|
||||
- **死亡日期**:
|
||||
|
||||
- - 元素类型:日期选择器
|
||||
- 填写规则:根据实际情况填写
|
||||
|
||||
- **病例分类**
|
||||
|
||||
- - 复选框类型: 1疑似病例/2临床诊断病例/3确诊病例/4病原携带/5阳性检测结果
|
||||
- 校验规则:必填
|
||||
|
||||
|
||||
|
||||
#### 5. 报告信息区
|
||||
|
||||
**区域功能**:记录报告单位和责任人信息等
|
||||
**包含元素**:
|
||||
|
||||
- **报告单位**:
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:当前登录医院
|
||||
- 交互行为:只读
|
||||
|
||||
- **联系电话**:
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:当前登录医院的联系电话
|
||||
- 交互行为:可编辑
|
||||
|
||||
- **报告医生**:
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:当前登录医生
|
||||
- 验证规则:必填
|
||||
|
||||
- **填卡日期**
|
||||
|
||||
- - 默认当前系统日期,显示为"YYYY-MM-DD"格式
|
||||
|
||||
- **修订病名**
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:空
|
||||
- 填写:自定义编辑
|
||||
|
||||
- **退卡原因**
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:空
|
||||
- 填写:自定义编辑
|
||||
|
||||
- **备注**
|
||||
|
||||
- - 元素类型:文本输入
|
||||
- 默认值:空
|
||||
- 填写:自定义编辑
|
||||
|
||||
#### 6. 操作按钮区
|
||||
|
||||
**区域位置**:页面底部
|
||||
**包含元素**:
|
||||
|
||||
- **保存按钮**:
|
||||
|
||||
- - 元素类型:主要操作按钮
|
||||
- 交互行为:触发表单验证,通过后保存
|
||||
- 样式特征:蓝色(#3498db),圆角8px
|
||||
|
||||
- **重置按钮**:
|
||||
|
||||
- - 交互行为:清除非基础信息字段
|
||||
- 特殊处理:保留患者姓名、身份证等关键信息
|
||||
|
||||
- **关闭按钮**:
|
||||
|
||||
- - 交互行为:二次确认后关闭页面
|
||||
- 样式特征:红色(#e74c3c)
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 地址联动选择
|
||||
|
||||
**触发条件**:选择省级行政区
|
||||
**操作流程**:
|
||||
|
||||
1. 选择省份→加载该省下所有城市
|
||||
2. 选择城市→加载该市所有区县
|
||||
3. 选择区县→加载街道列表
|
||||
**异常处理**:网络错误时显示"加载失败,请手动输入"
|
||||
|
||||
#### 2. 疾病分型联动
|
||||
|
||||
**触发条件**:选择特定疾病
|
||||
**数据映射**:
|
||||
|
||||
| **疾病类型** | **分型选项** |
|
||||
| ------------ | ---------------------------------- |
|
||||
| 肺结核 | 涂阳/仅培阳/菌阴/未痰检 |
|
||||
| 梅毒 | I期/II期/III期/胎传/隐性 |
|
||||
| 炭疽 | 肺炭疽/皮肤炭疽/胃肠炭疽/未分型 |
|
||||
| 病毒性肝炎 | 甲/乙/丙/戊型 |
|
||||
| 疟疾 | 间日疟/恶性疟/三日疟/卵形疟/未分型 |
|
||||
| 血吸虫病 | 急性/慢性/晚期/未分型 |
|
||||
|
||||
#### 3. 表单验证
|
||||
|
||||
**全局验证**:
|
||||
|
||||
1. 提交时检查必填字段
|
||||
2. 验证身份证号格式
|
||||
3. 确保至少选择一种疾病
|
||||
**字段级验证**:
|
||||
|
||||
- 电话号码:11位数字,错误提示“请输入有效的联系电话”
|
||||
- 发病日期≤诊断日期≤填卡日期,错误提示“发病日期不能晚于诊断日期”
|
||||
- 身份证号18位且符合校验算法,错误提示“请输入有效的身份证号码”
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
**传染病报卡表(infectious_card)**
|
||||
|
||||
| **字段** | **类型** | **国标含义** | **来源****/****说明** |
|
||||
|---------------------| -------------- |-----------------|--------------------------------------|
|
||||
| card_no | VARCHAR(20) PK | 卡片编号 | 机构代码+年月日+4位流水 |
|
||||
| visit_id | BIGINT FK | 本次就诊ID | adm_encounter.id |
|
||||
| diag_id | BIGINT FK | 诊断记录唯一ID | adm_encounter_diagnosis.condition_id |
|
||||
| pat_id | BIGINT FK | 患者主索引 | adm_patient.id |
|
||||
| id_type | TINYINT | 证件类型 | |
|
||||
| id_no | VARCHAR(30) | 证件号码 | 18位校验 |
|
||||
| pat_name | VARCHAR(50) | 患者姓名 | |
|
||||
| parent_name | VARCHAR(50) | 家长姓名 | ≤14岁必填 |
|
||||
| sex | CHAR(1) | 性别 | 1男/2女/0未知 |
|
||||
| birthday | DATE | 出生日期 | |
|
||||
| age | INT | 实足年龄 | 函数计算 |
|
||||
| age_unit | CHAR(1) | 年龄单位 | 岁/月/天-》1岁/2月/3天 |
|
||||
| workplace | VARCHAR(100) | 工作单位 | 学生填学校 |
|
||||
| phone | VARCHAR(20) | 联系电话 | 患者本人电话 |
|
||||
| contact_phone | VARCHAR(20) | 紧急联系人电话 | |
|
||||
| address_prov | VARCHAR(6) | 现住址省 | GB2260 |
|
||||
| address_city | VARCHAR(6) | 现住址市 | 同上 |
|
||||
| address_county | VARCHAR(6) | 现住址县 | 同上 |
|
||||
| address_town | VARCHAR(9) | 现住址街道 | 同上 |
|
||||
| address_village | VARCHAR(80) | 现住址村/居委 | |
|
||||
| address_house | VARCHAR(40) | 现住址门牌号 | |
|
||||
| patient_belong | TINYINT | 病人属于 | 系统判定,1本县区/2本市其他/3本省其他/4外省/5港澳台/6外籍 |
|
||||
| occupation | VARCHAR(4) | 职业 | GB/T 6565,取值于字典管理的字典名称为“职业”维护的数据 |
|
||||
| disease_code | VARCHAR(8) | 疾病名称 | WS 218-2020,见下表 |
|
||||
| disease_type | VARCHAR(8) | 分型 | 见下表,6类必分型疾病必填 |
|
||||
| other_disease | VARCHAR(50) | 其他法定管理以及重点监测传染病 | |
|
||||
| case_class | TINYINT | 病例分类 | 1疑似病例/2临床诊断病例/3确诊病例/4病原携带/5阳性检测结果 |
|
||||
| onset_date | DATE | 发病日期 | 默认诊断时间,病原携带者填初检日期 |
|
||||
| diag_date | DATETIME | 诊断日期 | 精确到小时 |
|
||||
| death_date | DATE | 死亡日期 | 死亡病例必填 |
|
||||
| correct_name | VARCHAR(50) | 订正病名 | 订正报告必填 |
|
||||
| withdraw_reason | VARCHAR(100) | 退卡原因 | 退卡时必填 |
|
||||
| report_org | VARCHAR(18) | 报告单位 | 统一信用代码(医院名称) |
|
||||
| report_org_phone | VARCHAR(20) | 联系电话 | 报告单位电话:医院总值班/防保科座机 |
|
||||
| report_doc | VARCHAR(20) | 报告医生 | 医生姓名 |
|
||||
| report_date | DATE | 填卡日期 | 当天日期 |
|
||||
| status | TINYINT | 状态 | 0暂存1已提交2已审核3已上报4失败5作废 |
|
||||
| fail_msg | VARCHAR(500) | 失败原因 | 国家平台返回 |
|
||||
| xml_content | TEXT | 上报XML | 日志 |
|
||||
| create_time | DATETIME | 创建时间 | |
|
||||
| update_time | DATETIME | 更新时间 | |
|
||||
| card_name_code | TINYINT | 报卡名称代码 | 数值对照(取值于字典管理-》报卡名称代码)1-中华人民共和国传染病报告卡 |
|
||||
| registration source | TINYINT | 登记来源 | 1门诊/2住院 |
|
||||
| dept_id | TINYINT | 科室ID | 患者当前就诊科室 |
|
||||
| doctor_id | TINYINT | 医生ID | 患者当前开单医生 |
|
||||
|
||||
**甲类传染病(2 种)―― 01xxxx**
|
||||
|
||||
| **disease_code** | **疾病名称** | **国家平台码** |
|
||||
| ---------------- | ------------ | -------------- |
|
||||
| 0101 | 鼠疫 | 甲类 |
|
||||
| 0102 | 霍乱 | 甲类 |
|
||||
|
||||
存值示例:`0101`(鼠疫)、`0102`(霍乱)
|
||||
|
||||
|
||||
|
||||
**乙类传染病(27 种)―― 02xxxx**
|
||||
|
||||
| **disease_code** | **疾病名称** | **国家平台码** |
|
||||
| ---------------- | -------------------- | ------------------ |
|
||||
| 0201 | 传染性非典型肺炎 | 乙类(按甲类管理) |
|
||||
| 0202 | 艾滋病 | 乙类 |
|
||||
| 0203 | 病毒性肝炎 | 乙类 |
|
||||
| 0204 | 脊髓灰质炎 | 乙类(按甲类管理) |
|
||||
| 0205 | 人感染高致病性禽流感 | 乙类(按甲类管理) |
|
||||
| 0206 | 麻疹 | 乙类 |
|
||||
| 0207 | 流行性出血热 | 乙类 |
|
||||
| 0208 | 狂犬病 | 乙类 |
|
||||
| 0209 | 流行性乙型脑炎 | 乙类 |
|
||||
| 0210 | 登革热 | 乙类 |
|
||||
| 0211 | 炭疽 | 乙类(按甲类管理) |
|
||||
| 0212 | 细菌性和阿米巴性痢疾 | 乙类 |
|
||||
| 0213 | 肺结核 | 乙类 |
|
||||
| 0214 | 伤寒和副伤寒 | 乙类 |
|
||||
| 0215 | 流行性脑脊髓膜炎 | 乙类 |
|
||||
| 0216 | 百日咳 | 乙类 |
|
||||
| 0217 | 白喉 | 乙类 |
|
||||
| 0218 | 新生儿破伤风 | 乙类 |
|
||||
| 0219 | 猩红热 | 乙类 |
|
||||
| 0220 | 布鲁氏菌病 | 乙类 |
|
||||
| 0221 | 淋病 | 乙类 |
|
||||
| 0222 | 梅毒 | 乙类 |
|
||||
| 0223 | 钩端螺旋体病 | 乙类 |
|
||||
| 0224 | 血吸虫病 | 乙类 |
|
||||
| 0225 | 疟疾 | 乙类 |
|
||||
|
||||
存值示例:乙肝→`0203`;肺结核→`0213`;梅毒→`0222`
|
||||
|
||||
|
||||
|
||||
**丙类传染病(11 种)―― 03xxxx**
|
||||
|
||||
| **disease_code** | **疾病名称** | **国家平台码** |
|
||||
| ---------------- | ---------------------- | -------------- |
|
||||
| 0301 | 流行性感冒 | 丙类 |
|
||||
| 0302 | 流行性腮腺炎 | 丙类 |
|
||||
| 0303 | 风疹 | 丙类 |
|
||||
| 0304 | 急性出血性结膜炎 | 丙类 |
|
||||
| 0305 | 麻风病 | 丙类 |
|
||||
| 0306 | 流行性和地方性斑疹伤寒 | 丙类 |
|
||||
| 0307 | 黑热病 | 丙类 |
|
||||
| 0308 | 包虫病 | 丙类 |
|
||||
| 0309 | 丝虫病 | 丙类 |
|
||||
| 0310 | 其它感染性腹泻病 | 丙类 |
|
||||
| 0311 | 手足口病 | 丙类 |
|
||||
|
||||
存值示例:手足口病→`0311`;流感→`0301`
|
||||
|
||||
|
||||
|
||||
**分型码与名称对照(系统存值用)**
|
||||
|
||||
| **大类疾病** | **disease_code** | **分型中文** | **disease_type** **存值** |
|
||||
| -------------- | ---------------- | ------------ | ------------------------- |
|
||||
| **病毒性肝炎** | 0203 | 甲型 | 020301 |
|
||||
| | | 乙型 | 020302 |
|
||||
| | | 丙型 | 020303 |
|
||||
| | | 戊型 | 020304 |
|
||||
| | | 未分型 | 020305 |
|
||||
| **炭疽** | 0211 | 肺炭疽 | 021101 |
|
||||
| | | 皮肤炭疽 | 021102 |
|
||||
| | | 胃肠炭疽 | 021103 |
|
||||
| | | 未分型 | 021104 |
|
||||
| **肺结核** | 0213 | 涂阳 | 021301 |
|
||||
| | | 仅培阳 | 021302 |
|
||||
| | | 菌阴 | 021303 |
|
||||
| | | 未痰检 | 021304 |
|
||||
| **梅毒** | 0222 | Ⅰ期 | 022201 |
|
||||
| | | Ⅱ期 | 022202 |
|
||||
| | | Ⅲ期 | 022203 |
|
||||
| | | 胎传 | 022204 |
|
||||
| | | 隐性 | 022205 |
|
||||
| **疟疾** | 0225 | 间日疟 | 022501 |
|
||||
| | | 恶性疟 | 022502 |
|
||||
| | | 三日疟 | 022503 |
|
||||
| | | 卵形疟 | 022504 |
|
||||
| | | 未分型 | 022505 |
|
||||
| **血吸虫病** | 0224 | 急性 | 022401 |
|
||||
| | | 慢性 | 022402 |
|
||||
| | | 晚期 | 022403 |
|
||||
| | | 未分型 | 022404 |
|
||||
|
||||
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- 主色调:#3498db(按钮/重要标签)
|
||||
- 错误状态:#e74c3c(边框+文字)
|
||||
- 表单间距:8px垂直间距,16px水平间距
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- 支持Chrome/Firefox/Edge最新版
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 身份证号不需脱敏显示
|
||||
|
||||
|
||||
349
md/需求/103-医生个人报卡管理界面-2026-1-29.md
Normal file
349
md/需求/103-医生个人报卡管理界面-2026-1-29.md
Normal file
@@ -0,0 +1,349 @@
|
||||
## 医生个人报卡管理界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:医生个人报卡管理界面
|
||||
**页面目标**:为医生提供传染病报告卡的管理功能,包括查看、编辑、提交、撤回、导出等操作,并提供数据统计和筛选功能。
|
||||
**适用场景**:医生需要查看/编辑或管理已填写的传染病报告卡,进行批量操作或筛选特定状态的报告卡
|
||||
**页面类型**:列表页(含筛选功能) + 表单页(编辑/查看模态框)。
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 报卡数据统计展示(总报卡数/待处理失败/已成功上报)
|
||||
2. 报卡列表筛选与查询(按日期/状态/关键词)
|
||||
3. 报卡详情查看与编辑
|
||||
4. 批量操作(全选/批量提交/批量删除)
|
||||
5. 报卡导出为Word格式
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 快速掌握个人报卡工作整体情况
|
||||
- 高效管理不同状态的报卡记录
|
||||
- 规范疾病报告卡流程,确保数据及时准确上报
|
||||
**原型图地址**:https://static.pm-ai.cn/prototype/20260129/865d147e5650ff42c054b38244ed8239/index.html
|
||||
**流程图**:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([医生进入个人报卡管理界面]) --> Load[展示数据统计卡片]
|
||||
Load --> Stats[展示统计卡片\n总报卡数待处理已上报]
|
||||
Stats --> Filter[渲染筛选区日期状态名称]
|
||||
Filter --> Table[展示报卡列表]
|
||||
|
||||
Table --> Op{用户操作选择}
|
||||
|
||||
Op -->|点击查看| View1[弹出只读模态框]
|
||||
View1 --> View2[展示完整报卡信息]
|
||||
View2 --> View3[关闭模态框]
|
||||
View3 --> Table
|
||||
|
||||
Op -->|点击编辑| Edit1{状态是待提交?}
|
||||
Edit1 -->|否| Table
|
||||
Edit1 -->|是| Edit2[弹出编辑模态框]
|
||||
Edit2 --> Edit3[修改表单字段]
|
||||
Edit3 --> Edit4[点击保存]
|
||||
Edit4 --> Edit5{验证必填项}
|
||||
Edit5 -->|失败| Edit6[显示错误原因]
|
||||
Edit6 --> Edit2
|
||||
Edit5 -->|通过| Edit7[保存数据]
|
||||
Edit7 --> Edit8[提示成功]
|
||||
Edit8 --> Table
|
||||
|
||||
Op -->|点击提交| Sub1{状态是待提交?}
|
||||
Sub1 -->|是| Sub2[确认对话框]
|
||||
Sub2 --> Sub3[变更为已提交]
|
||||
Sub3 --> Sub4[刷新表格统计]
|
||||
Sub4 --> Table
|
||||
Sub1 -->|否| Table
|
||||
|
||||
Op -->|点击撤回| Back1{状态是已提交?}
|
||||
Back1 -->|是| Back2[变更为待提交]
|
||||
Back2 --> Table
|
||||
Back1 -->|否| Table
|
||||
|
||||
Op -->|点击导出| Exp1{状态是已上报?}
|
||||
Exp1 -->|是| Exp2[报告卡导出预览]
|
||||
Exp2 --> Exp3[导出Word文档]
|
||||
Exp3 --> Table
|
||||
Exp1 -->|否| Table
|
||||
|
||||
Op -->|应用筛选| Filt1[触发筛选条件]
|
||||
Filt1 --> Filt2[重新加载列表]
|
||||
Filt2 --> Table
|
||||
|
||||
Op -->|批量操作| Batch1{已选记录?}
|
||||
Batch1 -->|否| Batch2[提示请选择记录]
|
||||
Batch2 --> Table
|
||||
Batch1 -->|是| Batch3{操作类型}
|
||||
|
||||
Batch3 -->|批量提交| Batch4{全是待提交?}
|
||||
Batch4 -->|否| Batch5[提示只能提交待提交]
|
||||
Batch5 --> Table
|
||||
Batch4 -->|是| Batch6[确认数量]
|
||||
Batch6 --> Batch7[更新为已提交]
|
||||
Batch7 --> Batch8[刷新统计数据]
|
||||
Batch8 --> Table
|
||||
|
||||
Batch3 -->|批量删除| Batch9{全是待提交?}
|
||||
Batch9 -->|否| Batch10[提示只能删除待提交]
|
||||
Batch10 --> Table
|
||||
Batch9 -->|是| Batch11[确认删除]
|
||||
Batch11 --> Batch12[状态变为作废]
|
||||
Batch12 --> Batch13[刷新数据]
|
||||
Batch13 --> Table
|
||||
```
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. **顶部标题区**(5%):页面标题。
|
||||
2. **数据统计区**(15%):展示总报卡数、待处理失败数、已成功上报数。
|
||||
3. **高级筛选区**(15%):提供日期范围、状态、报卡名称等筛选条件。
|
||||
4. **数据表格区**(55%):展示报卡列表,支持多选和操作按钮。
|
||||
5. **底部批量操作区**(10%):全选、批量删除、批量提交功能。
|
||||
|
||||
**布局特点**:上下布局,采用卡片式设计,主内容区为表格展示
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部标题区
|
||||
|
||||
**区域位置**:页面最上方
|
||||
**区域尺寸**:高度60px,宽度100%。
|
||||
**区域功能**:展示页面标题和主要操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **页面标题**:
|
||||
|
||||
- - 元素类型:文本
|
||||
- 显示内容:“我的报卡”
|
||||
- 样式特征:20px/600,深灰色(#1e293b)
|
||||
|
||||
#### 2. 数据统计卡片区
|
||||
|
||||
**区域位置**:标题区下方
|
||||
**区域尺寸**:高度150px,宽度100%。
|
||||
**区域功能**:展示关键统计数据,帮助医生快速了解报卡状态分布。
|
||||
**包含元素**:
|
||||
|
||||
- **总报卡数卡片**:
|
||||
|
||||
- - 元素类型:统计卡片
|
||||
- 显示内容:图标+数值+“总报卡数”
|
||||
- 样式特征:紫色渐变背景,圆角12px
|
||||
|
||||
- **待处理失败卡片**:
|
||||
|
||||
- - 同总报卡数卡片,红色系配色
|
||||
|
||||
- **已成功上报卡片**:
|
||||
|
||||
- - 同总报卡数卡片,绿色系配色
|
||||
|
||||
#### 3. 高级筛选区
|
||||
|
||||
**区域位置**:统计卡片下方
|
||||
**区域尺寸**:高度150px,宽度100%
|
||||
**区域功能**:提供多维筛选条件,支持快速定位目标报卡。
|
||||
**包含元素**:
|
||||
|
||||
- **日期范围选择器**:
|
||||
|
||||
- - 元素类型:表单控件(两个date输入框)
|
||||
- 交互行为:选择日期后触发筛选。
|
||||
- 限制条件:结束日期不能早于开始日期。
|
||||
|
||||
- **状态筛选下拉框**:
|
||||
|
||||
- - 元素类型:下拉选择
|
||||
- 可选值:全部状态/待提交/已提交/已审核/已上报/失败/作废
|
||||
|
||||
- **报卡名称搜索框**:
|
||||
|
||||
- - 元素类型:文本输入框
|
||||
- 占位文本:“输入报卡名称…”
|
||||
|
||||
- **应用筛选按钮**:
|
||||
|
||||
- - 元素类型:主要操作按钮
|
||||
- 交互行为:点击后触发表格数据刷新
|
||||
|
||||
- **重置条件按钮**:
|
||||
|
||||
- - 元素类型:次要操作按钮
|
||||
- 交互行为:清空所有筛选条件
|
||||
|
||||
#### 4. 数据表格区
|
||||
|
||||
**区域位置**:页面中部
|
||||
**区域尺寸**:高度自适应,宽度100%。
|
||||
**区域功能**:展示报卡列表及提供行级操作
|
||||
**包含元素**:
|
||||
|
||||
- **表格头部**:
|
||||
|
||||
**数据主要取值于传染病报卡表(infectious_card)**
|
||||
|
||||
- - 包含字段:选择框、卡片ID、患者姓名、身份证号、联系电话、就诊卡号、报卡名称、提交时间、状态、操作
|
||||
- 样式特征:灰色背景,13px字号,大写字母
|
||||
|
||||
- **表格内容行**:
|
||||
|
||||
- - 展示方式:每行显示一条报卡记录
|
||||
|
||||
- 数据字段:
|
||||
|
||||
- - 卡片ID:文本 - HOSP202601150001 - 不可操作
|
||||
- 患者姓名:文本 - 张三 - 不可操作
|
||||
- 身份证号:不脱敏文本 - 110101199001011234 - 不可操作
|
||||
- 联系电话:文本 - 13800138000 - 不可操作
|
||||
- 就诊卡号:文本 - M12345678 - 不可操作
|
||||
- 报卡名称:文本 - 中华人民共和国传染病报告卡 - 不可操作
|
||||
- 提交时间:时间 - 2026-01-15 14:30 - 不可操作
|
||||
- 状态标签:根据状态值显示不同颜色
|
||||
|
||||
- 操作功能:
|
||||
|
||||
- - 查看按钮:所有状态可见
|
||||
- 编辑按钮:仅"待提交"状态可见
|
||||
- 提交按钮:仅"待提交"状态可见
|
||||
- 撤回按钮:仅"已提交"状态可见
|
||||
- 导出按钮:仅"已上报"状态可见
|
||||
|
||||
#### 5. 底部批量操作区
|
||||
|
||||
**区域位置**:页面底部
|
||||
**区域尺寸**:高度60px,宽度100%。
|
||||
**区域功能**:支持批量操作选中报卡。
|
||||
**包含元素**:
|
||||
|
||||
- **全选复选框**:
|
||||
|
||||
- - 交互行为:勾选后选中当前页所有记录
|
||||
|
||||
- **批量删除按钮**:
|
||||
|
||||
- - 元素类型:文本按钮
|
||||
- 限制条件:仅对"待提交"状态记录有效
|
||||
- 交互行为:提交后状态变为"作废"。
|
||||
|
||||
- **批量提交按钮**:
|
||||
|
||||
- - 元素类型:主要操作按钮
|
||||
- 限制条件:仅对"待提交"状态记录有效
|
||||
- 交互行为:提交后状态变为"已提交"。
|
||||
|
||||
#### 6. 报卡详情弹窗(界面内容功能与需求编号102界面保持一致,建议用同一个界面)
|
||||
|
||||
**区域位置**:页面居中模态框
|
||||
**区域功能**:查看/编辑完整报卡信息
|
||||
**包含元素**:
|
||||
|
||||
- 表单字段(按模块分组):
|
||||
|
||||
- 1. 患者基本信息(姓名/身份证号/联系方式*)
|
||||
2. 临床信息(发病日期/诊断日期*)
|
||||
3. 传染病分类(甲/乙/丙类多选*)
|
||||
4. 报告信息(报告单位/医生*)
|
||||
|
||||
- 操作按钮:
|
||||
|
||||
o 取消:关闭弹窗不保存
|
||||
|
||||
o 保存:验证必填项后保持数据
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 报卡查看功能
|
||||
|
||||
**功能描述**:查看报卡详细信息
|
||||
**触发条件**:点击任意行的"查看"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 弹出模态框展示完整报卡信息
|
||||
2. 所有字段为只读状态
|
||||
3. 点击关闭按钮或蒙层关闭模态框
|
||||
|
||||
#### 2. 报卡编辑功能
|
||||
|
||||
**功能描述**:修改待提交的报卡信息
|
||||
**触发条件**:点击"待提交"状态的"编辑"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 弹出可编辑的报卡表单模态框
|
||||
2. 修改必要字段(带*号为必填项)
|
||||
3. 点击"保存"按钮提交修改
|
||||
4. 成功提示后关闭模态框
|
||||
|
||||
#### 3. 批量提交功能
|
||||
|
||||
**功能描述**:批量提交选中的待提交报卡
|
||||
**触发条件**:勾选记录后点击"批量提交"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 校验是否选中有效记录(状态为待提交)
|
||||
2. 弹出确认对话框显示待提交数量
|
||||
3. 确认后更新记录状态为"已提交"
|
||||
4. 刷新表格数据和统计卡片
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 未选中记录:提示"请选择待提交的记录"
|
||||
- 包含不可提交记录:提示"只能提交待提交状态的记录"
|
||||
|
||||
#### 4. 报卡状态流转
|
||||
|
||||
**触发方式**:点击操作列按钮
|
||||
**执行流程**:
|
||||
|
||||
1. 待提交 → 已提交:点击"提交"按钮 → 弹窗确认 → 状态变更
|
||||
2. 已提交 → 待提交:点击"撤回"按钮 → 状态回滚
|
||||
3. 已上报 → 导出:生成标准疾病报告卡Word文档(含医院红头格式)
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 提交失败:显示具体错误原因(如:必填项未完成)
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
**关键数据字段**:
|
||||
|
||||
**传染病报卡表(infectious_card)**
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:#6366f1(紫色)
|
||||
|
||||
- **状态色**:
|
||||
|
||||
- - 待提交:#f59e0b(橙色)
|
||||
- 已提交:#2563eb(蓝色)
|
||||
- 已上报:#16a34a(绿色)
|
||||
- 失败:#dc2626(红色)
|
||||
|
||||
- **字体规范**:
|
||||
|
||||
- - 标题:20px/600
|
||||
- 正文:14px/400
|
||||
|
||||
- **间距系统**:
|
||||
|
||||
- - 卡片内边距:24px
|
||||
- 元素间距:16px
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- **表格组件**:需支持虚拟滚动(大数据量场景)
|
||||
- **导出功能**:实现Word导出
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 身份证号等敏感信息不需做脱敏处理
|
||||
2. 批量操作需考虑性能优化(分页处理)
|
||||
3. 状态变更需同步更新统计卡片数据
|
||||
4. 移动端需特别处理表格的横向滚动体验
|
||||
5. ‘待提交’状态就是‘暂存’状态
|
||||
6. 只能查询医生本人报卡的数据
|
||||
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. **病种名称**:严格遵循《传染病报告卡》规范用词
|
||||
BIN
md/需求/media/clip_image001.png
Normal file
BIN
md/需求/media/clip_image001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
@@ -0,0 +1,46 @@
|
||||
package com.core.web.controller.common;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* 前端路由 fallback 控制器
|
||||
* 处理 Vue Router History 模式下的路由
|
||||
*
|
||||
* @author
|
||||
*/
|
||||
@Controller
|
||||
public class FrontRouterController {
|
||||
|
||||
/**
|
||||
* 处理前端路由,将所有前端路由请求转发到 index.html
|
||||
*/
|
||||
@RequestMapping(value = {
|
||||
"/ybmanagement/**",
|
||||
"/system/**",
|
||||
"/monitor/**",
|
||||
"/tool/**",
|
||||
"/doctorstation/**",
|
||||
"/features/**",
|
||||
"/todo/**",
|
||||
"/appoinmentmanage/**",
|
||||
"/clinicmanagement/**",
|
||||
"/medicationmanagement/**",
|
||||
"/yb/**",
|
||||
"/patient/**",
|
||||
"/charge/**",
|
||||
"/nurse/**",
|
||||
"/pharmacy/**",
|
||||
"/report/**",
|
||||
"/document/**",
|
||||
"/triage/**",
|
||||
"/check/**",
|
||||
"/lab/**",
|
||||
"/financial/**",
|
||||
"/crosssystem/**",
|
||||
"/workflow/**"
|
||||
})
|
||||
public String index() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ public class SysMenuController extends BaseController {
|
||||
@GetMapping("/list")
|
||||
public AjaxResult list(SysMenu menu) {
|
||||
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
|
||||
return success(menus);
|
||||
// 构建带完整路径的菜单树
|
||||
List<SysMenu> menuTreeWithFullPath = menuService.buildMenuTreeWithFullPath(menus);
|
||||
return success(menuTreeWithFullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,4 +117,47 @@ public class SysMenuController extends BaseController {
|
||||
}
|
||||
return toAjax(menuService.deleteMenuById(menuId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单完整路径
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:menu:query')")
|
||||
@GetMapping("/fullPath/{menuId}")
|
||||
public AjaxResult getFullPath(@PathVariable("menuId") Long menuId) {
|
||||
String fullPath = menuService.getMenuFullPath(menuId);
|
||||
return success(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整路径
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:menu:query')")
|
||||
@PostMapping("/generateFullPath")
|
||||
public AjaxResult generateFullPath(@RequestParam(required = false) Long parentId,
|
||||
@RequestParam String currentPath) {
|
||||
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,34 @@
|
||||
package com.core.common.core.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页结果类
|
||||
*
|
||||
* @author
|
||||
* @date 2026-02-02
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<T> rows;
|
||||
|
||||
/**
|
||||
* 总数
|
||||
*/
|
||||
private long total;
|
||||
}
|
||||
@@ -69,6 +69,9 @@ public class SysMenu extends BaseEntity {
|
||||
/** 子菜单 */
|
||||
private List<SysMenu> children = new ArrayList<SysMenu>();
|
||||
|
||||
/** 完整路径 */
|
||||
private String fullPath;
|
||||
|
||||
public Long getMenuId() {
|
||||
return menuId;
|
||||
}
|
||||
@@ -212,6 +215,14 @@ public class SysMenu extends BaseEntity {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public String getFullPath() {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
public void setFullPath(String fullPath) {
|
||||
this.fullPath = fullPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("menuId", getMenuId())
|
||||
@@ -219,8 +230,8 @@ public class SysMenu extends BaseEntity {
|
||||
.append("path", getPath()).append("component", getComponent()).append("query", getQuery())
|
||||
.append("routeName", getRouteName()).append("isFrame", getIsFrame()).append("IsCache", getIsCache())
|
||||
.append("menuType", getMenuType()).append("visible", getVisible()).append("status ", getStatus())
|
||||
.append("perms", getPerms()).append("icon", getIcon()).append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
|
||||
.append("perms", getPerms()).append("icon", getIcon()).append("fullPath", getFullPath())
|
||||
.append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.core.system.controller;
|
||||
|
||||
import com.core.common.annotation.Log;
|
||||
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.enums.BusinessType;
|
||||
import com.core.system.domain.SysUserConfig;
|
||||
import com.core.system.service.ISysUserConfigService;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户配置Controller
|
||||
*
|
||||
* @author
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Api(tags = "用户配置")
|
||||
@RestController
|
||||
@RequestMapping("/system/userConfig")
|
||||
public class SysUserConfigController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysUserConfigService sysUserConfigService;
|
||||
|
||||
/**
|
||||
* 查询用户配置列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:userConfig:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysUserConfig sysUserConfig)
|
||||
{
|
||||
startPage();
|
||||
List<SysUserConfig> list = sysUserConfigService.selectSysUserConfigList(sysUserConfig);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户配置详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:userConfig:query')")
|
||||
@GetMapping(value = "/{configId}")
|
||||
public AjaxResult getInfo(@PathVariable("configId") Long configId)
|
||||
{
|
||||
return AjaxResult.success(sysUserConfigService.selectSysUserConfigById(configId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户配置
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:userConfig:add')")
|
||||
@Log(title = "用户配置", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody SysUserConfig sysUserConfig)
|
||||
{
|
||||
return AjaxResult.success(sysUserConfigService.insertSysUserConfig(sysUserConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户配置
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:userConfig:edit')")
|
||||
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody SysUserConfig sysUserConfig)
|
||||
{
|
||||
return toAjax(sysUserConfigService.updateSysUserConfig(sysUserConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户配置
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:userConfig:remove')")
|
||||
@Log(title = "用户配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{configIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] configIds)
|
||||
{
|
||||
return toAjax(sysUserConfigService.deleteSysUserConfigByIds(configIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的指定配置
|
||||
*/
|
||||
@ApiOperation("获取当前用户的指定配置")
|
||||
@GetMapping("/currentUserConfig")
|
||||
public AjaxResult getCurrentUserConfig(@RequestParam String configKey)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String configValue = sysUserConfigService.selectConfigValueByUserIdAndKey(userId, configKey);
|
||||
// 返回原始配置值,不需要额外编码,由前端处理
|
||||
return AjaxResult.success(configValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前用户的配置
|
||||
*/
|
||||
@ApiOperation("保存当前用户的配置")
|
||||
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/saveCurrentUserConfig")
|
||||
public AjaxResult saveCurrentUserConfig(@RequestParam String configKey, @RequestParam String configValue)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
int result = sysUserConfigService.saveConfigValueByUserIdAndKey(userId, configKey, configValue);
|
||||
return result > 0 ? AjaxResult.success() : AjaxResult.error("保存失败");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.core.system.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.core.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户配置对象 sys_user_config
|
||||
*
|
||||
* @author
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user_config")
|
||||
public class SysUserConfig extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 配置ID */
|
||||
@TableId
|
||||
private Long configId;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 配置键名 */
|
||||
private String configKey;
|
||||
|
||||
/** 配置值 */
|
||||
private String configValue;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 创建者 - 标记为非数据库字段 */
|
||||
@TableField(exist = false)
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 - 使用自动填充 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 - 标记为非数据库字段 */
|
||||
@TableField(exist = false)
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 - 使用自动填充 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -140,4 +140,11 @@ public interface SysMenuMapper {
|
||||
* @return 结果
|
||||
*/
|
||||
public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
|
||||
|
||||
/**
|
||||
* 查询所有菜单信息
|
||||
*
|
||||
* @return 菜单列表
|
||||
*/
|
||||
public List<SysMenu> selectAllMenus();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.core.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.core.system.domain.SysUserConfig;
|
||||
|
||||
/**
|
||||
* 用户配置Mapper接口
|
||||
*
|
||||
* @author
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
public interface SysUserConfigMapper extends BaseMapper<SysUserConfig>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.core.system.service;
|
||||
import com.core.common.core.domain.TreeSelect;
|
||||
import com.core.common.core.domain.entity.SysMenu;
|
||||
import com.core.system.domain.vo.RouterVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -22,7 +21,7 @@ public interface ISysMenuService {
|
||||
public List<SysMenu> selectMenuList(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户查询系统菜单列表
|
||||
* 查询系统菜单列表
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @param userId 用户ID
|
||||
@@ -50,7 +49,7 @@ public interface ISysMenuService {
|
||||
* 根据用户ID查询菜单树信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 菜单列表
|
||||
* @return 菜单树信息
|
||||
*/
|
||||
public List<SysMenu> selectMenuTreeByUserId(Long userId);
|
||||
|
||||
@@ -72,15 +71,23 @@ public interface ISysMenuService {
|
||||
|
||||
/**
|
||||
* 构建前端所需要树结构
|
||||
*
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 树结构列表
|
||||
*/
|
||||
public List<SysMenu> buildMenuTree(List<SysMenu> menus);
|
||||
|
||||
/**
|
||||
* 构建前端所需要树结构(包含完整路径)
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 树结构列表
|
||||
*/
|
||||
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus);
|
||||
|
||||
/**
|
||||
* 构建前端所需要下拉树结构
|
||||
*
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 下拉树结构列表
|
||||
*/
|
||||
@@ -98,15 +105,15 @@ public interface ISysMenuService {
|
||||
* 是否存在菜单子节点
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 结果 true 存在 false 不存在
|
||||
* @return 结果
|
||||
*/
|
||||
public boolean hasChildByMenuId(Long menuId);
|
||||
|
||||
/**
|
||||
* 查询菜单是否存在角色
|
||||
* 查询菜单使用数量
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 结果 true 存在 false 不存在
|
||||
* @return 结果
|
||||
*/
|
||||
public boolean checkMenuExistRole(Long menuId);
|
||||
|
||||
@@ -141,4 +148,40 @@ public interface ISysMenuService {
|
||||
* @return 结果
|
||||
*/
|
||||
public boolean checkMenuNameUnique(SysMenu menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单ID获取完整路径
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 完整路径
|
||||
*/
|
||||
public String getMenuFullPath(Long menuId);
|
||||
|
||||
/**
|
||||
* 根据路径参数生成完整路径
|
||||
*
|
||||
* @param parentId 父级菜单ID
|
||||
* @param currentPath 当前路径
|
||||
* @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);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.core.system.service;
|
||||
|
||||
import com.core.system.domain.SysUserConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户配置Service接口
|
||||
*
|
||||
* @author
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
public interface ISysUserConfigService
|
||||
{
|
||||
/**
|
||||
* 查询用户配置
|
||||
*
|
||||
* @param configId 用户配置ID
|
||||
* @return 用户配置
|
||||
*/
|
||||
public SysUserConfig selectSysUserConfigById(Long configId);
|
||||
|
||||
/**
|
||||
* 查询用户配置列表
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 用户配置集合
|
||||
*/
|
||||
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig);
|
||||
|
||||
/**
|
||||
* 新增用户配置
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertSysUserConfig(SysUserConfig sysUserConfig);
|
||||
|
||||
/**
|
||||
* 修改用户配置
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateSysUserConfig(SysUserConfig sysUserConfig);
|
||||
|
||||
/**
|
||||
* 批量删除用户配置
|
||||
*
|
||||
* @param configIds 需要删除的用户配置ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteSysUserConfigByIds(Long[] configIds);
|
||||
|
||||
/**
|
||||
* 删除用户配置信息
|
||||
*
|
||||
* @param configId 用户配置ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteSysUserConfigById(Long configId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和配置键获取配置值
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param configKey 配置键
|
||||
* @return 配置值
|
||||
*/
|
||||
public String selectConfigValueByUserIdAndKey(Long userId, String configKey);
|
||||
|
||||
/**
|
||||
* 根据用户ID和配置键保存配置
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param configKey 配置键
|
||||
* @param configValue 配置值
|
||||
* @return 结果
|
||||
*/
|
||||
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue);
|
||||
}
|
||||
@@ -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,11 +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 != 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;
|
||||
// 管理员显示所有菜单信息
|
||||
@@ -109,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)) {
|
||||
@@ -193,7 +196,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 构建前端所需要树结构
|
||||
*
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 树结构列表
|
||||
*/
|
||||
@@ -215,6 +218,146 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
return returnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端所需要树结构(包含完整路径)
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 树结构列表
|
||||
*/
|
||||
@Override
|
||||
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) {
|
||||
List<SysMenu> menuTree = buildMenuTree(menus);
|
||||
// 一次性获取所有菜单信息,避免N+1查询问题
|
||||
List<SysMenu> allMenus = menuMapper.selectAllMenus();
|
||||
Map<Long, SysMenu> menuMap = allMenus.stream()
|
||||
.collect(Collectors.toMap(SysMenu::getMenuId, menu -> menu));
|
||||
|
||||
// 为每个菜单项添加完整路径(优化版本,避免N+1查询问题)
|
||||
addFullPathsToMenuTreeOptimized(menuTree, menuMap);
|
||||
return menuTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为菜单树添加完整路径(优化版本)
|
||||
*
|
||||
* @param menus 菜单树
|
||||
* @param menuMap 菜单映射
|
||||
*/
|
||||
private void addFullPathsToMenuTreeOptimized(List<SysMenu> menus, Map<Long, SysMenu> menuMap) {
|
||||
for (SysMenu menu : menus) {
|
||||
// 使用优化的路径计算方法
|
||||
menu.setFullPath(computeMenuFullPathOptimized(menu, menuMap));
|
||||
|
||||
// 递归处理子菜单
|
||||
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
|
||||
addFullPathsToMenuTreeOptimized(menu.getChildren(), menuMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的计算菜单完整路径方法
|
||||
*
|
||||
* @param menu 菜单
|
||||
* @param menuMap 菜单映射
|
||||
* @return 完整路径
|
||||
*/
|
||||
private String computeMenuFullPathOptimized(SysMenu menu, Map<Long, SysMenu> menuMap) {
|
||||
if (menu == null || menu.getMenuId() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder fullPath = new StringBuilder();
|
||||
buildMenuPathOptimized(menu, fullPath, menuMap);
|
||||
return normalizePath(fullPath.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的递归构建菜单路径
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @param path 路径构建器
|
||||
* @param menuMap 菜单映射
|
||||
*/
|
||||
private void buildMenuPathOptimized(SysMenu menu, StringBuilder path, Map<Long, SysMenu> menuMap) {
|
||||
if (menu == null || menu.getMenuId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果不是根节点,则递归查找父节点
|
||||
if (menu.getParentId() != null && menu.getParentId() > 0) {
|
||||
SysMenu parentMenu = menuMap.get(menu.getParentId());
|
||||
if (parentMenu != null) {
|
||||
buildMenuPathOptimized(parentMenu, path, menuMap);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前菜单的路径,避免双斜杠
|
||||
String currentPath = normalizePathSegment(menu.getPath());
|
||||
if (currentPath != null && !currentPath.isEmpty()) {
|
||||
if (path.length() > 0) {
|
||||
// 确保路径之间只有一个斜杠分隔符
|
||||
if (path.charAt(path.length() - 1) != '/') {
|
||||
path.append("/").append(currentPath);
|
||||
} else {
|
||||
path.append(currentPath);
|
||||
}
|
||||
} else {
|
||||
// 对于第一个路径,直接追加
|
||||
path.append(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集菜单树中的所有菜单ID
|
||||
*
|
||||
* @param menus 菜单树
|
||||
* @return 菜单ID列表
|
||||
*/
|
||||
private List<Long> collectMenuIds(List<SysMenu> menus) {
|
||||
List<Long> menuIds = new ArrayList<>();
|
||||
for (SysMenu menu : menus) {
|
||||
menuIds.add(menu.getMenuId());
|
||||
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
|
||||
menuIds.addAll(collectMenuIds(menu.getChildren()));
|
||||
}
|
||||
}
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取菜单完整路径
|
||||
*
|
||||
* @param menuIds 菜单ID列表
|
||||
* @return 菜单ID到完整路径的映射
|
||||
*/
|
||||
private Map<Long, String> batchGetMenuFullPaths(List<Long> menuIds) {
|
||||
Map<Long, String> fullPathMap = new HashMap<>();
|
||||
for (Long menuId : menuIds) {
|
||||
// 使用缓存的getMenuFullPath方法
|
||||
fullPathMap.put(menuId, getMenuFullPath(menuId));
|
||||
}
|
||||
return fullPathMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为菜单树设置完整路径
|
||||
*
|
||||
* @param menus 菜单树
|
||||
* @param fullPathMap 完整路径映射
|
||||
*/
|
||||
private void setFullPathsToMenuTree(List<SysMenu> menus, Map<Long, String> fullPathMap) {
|
||||
for (SysMenu menu : menus) {
|
||||
// 设置当前菜单的完整路径
|
||||
menu.setFullPath(fullPathMap.get(menu.getMenuId()));
|
||||
// 递归处理子菜单
|
||||
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
|
||||
setFullPathsToMenuTree(menu.getChildren(), fullPathMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端所需要下拉树结构
|
||||
*
|
||||
@@ -264,34 +407,50 @@ 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)
|
||||
})
|
||||
public int insertMenu(SysMenu menu) {
|
||||
//路径Path唯一性判断
|
||||
SysMenu sysMenu = menuMapper.selectMenuByPath(menu.getPath());
|
||||
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 = "'menuTree:' + #menu.updateBy")
|
||||
})
|
||||
public int updateMenu(SysMenu menu) {
|
||||
//路径Path唯一性判断(排除当前菜单本身)
|
||||
String path = menu.getPath();
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
SysMenu sysMenu = menuMapper.selectMenuByPathExcludeId(menu.getPath(), menu.getMenuId());
|
||||
if (sysMenu != null) {
|
||||
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
|
||||
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
|
||||
menu.getMenuId(), menu.getPath(), sysMenu.getMenuId());
|
||||
return -1; // 路由地址已存在
|
||||
}
|
||||
@@ -302,11 +461,14 @@ 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)
|
||||
})
|
||||
public int deleteMenuById(Long menuId) {
|
||||
return menuMapper.deleteMenuById(menuId);
|
||||
}
|
||||
@@ -488,11 +650,186 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
|
||||
/**
|
||||
* 内链域名特殊字符替换
|
||||
*
|
||||
*
|
||||
* @return 替换后的内链域名
|
||||
*/
|
||||
public String innerLinkReplaceEach(String path) {
|
||||
return StringUtils.replaceEach(path, new String[] {Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":"},
|
||||
new String[] {"", "", "", "/", "/"});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单ID获取完整路径
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 完整路径
|
||||
*/
|
||||
@Override
|
||||
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'fullPath:' + #menuId", unless = "#result == null || #result.isEmpty()")
|
||||
public String getMenuFullPath(Long menuId) {
|
||||
SysMenu menu = menuMapper.selectMenuById(menuId);
|
||||
if (menu == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder fullPath = new StringBuilder();
|
||||
buildMenuPath(menu, fullPath);
|
||||
// 标准化完整路径,确保没有多余的斜杠
|
||||
return normalizePath(fullPath.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建菜单路径
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @param path 路径构建器
|
||||
*/
|
||||
private void buildMenuPath(SysMenu menu, StringBuilder path) {
|
||||
if (menu == null || menu.getMenuId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果不是根节点,则递归查找父节点
|
||||
if (menu.getParentId() != null && menu.getParentId() > 0) {
|
||||
SysMenu parentMenu = menuMapper.selectMenuById(menu.getParentId());
|
||||
if (parentMenu != null) {
|
||||
buildMenuPath(parentMenu, path);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前菜单的路径,避免双斜杠
|
||||
String currentPath = normalizePathSegment(menu.getPath());
|
||||
if (currentPath != null && !currentPath.isEmpty()) {
|
||||
if (path.length() > 0) {
|
||||
// 确保路径之间只有一个斜杠分隔符
|
||||
// 如果当前路径不为空,且当前路径不以斜杠结尾,则添加斜杠并追加路径
|
||||
if (path.charAt(path.length() - 1) != '/') {
|
||||
path.append("/").append(currentPath);
|
||||
} else {
|
||||
path.append(currentPath);
|
||||
}
|
||||
} else {
|
||||
// 对于第一个路径,直接追加
|
||||
path.append(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化路径片段,移除开头的斜杠
|
||||
*
|
||||
* @param path 原始路径
|
||||
* @return 标准化后的路径片段
|
||||
*/
|
||||
private String normalizePathSegment(String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 移除开头的斜杠
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化完整路径,移除多余的斜杠
|
||||
*
|
||||
* @param path 原始路径
|
||||
* @return 标准化后的完整路径
|
||||
*/
|
||||
private String normalizePath(String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理多个连续斜杠,将其替换为单个斜杠
|
||||
while (path.contains("//")) {
|
||||
path = path.replace("//", "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径参数生成完整路径
|
||||
*
|
||||
* @param parentId 父级菜单ID
|
||||
* @param currentPath 当前路径
|
||||
* @return 完整路径
|
||||
*/
|
||||
@Override
|
||||
public String generateFullPath(Long parentId, String currentPath) {
|
||||
StringBuilder fullPath = new StringBuilder();
|
||||
|
||||
// 如果有父级菜单,则先获取父级菜单的完整路径
|
||||
if (parentId != null && parentId > 0) {
|
||||
SysMenu parentMenu = menuMapper.selectMenuById(parentId);
|
||||
if (parentMenu != null) {
|
||||
String parentFullPath = getMenuFullPath(parentId);
|
||||
if (StringUtils.isNotEmpty(parentFullPath)) {
|
||||
fullPath.append(parentFullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前路径
|
||||
if (StringUtils.isNotEmpty(currentPath)) {
|
||||
if (fullPath.length() > 0) {
|
||||
fullPath.append("/").append(currentPath);
|
||||
} else {
|
||||
fullPath.append(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.core.system.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户配置Service业务层处理
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 查询用户配置
|
||||
*
|
||||
* @param configId 用户配置ID
|
||||
* @return 用户配置
|
||||
*/
|
||||
@Override
|
||||
public SysUserConfig selectSysUserConfigById(Long configId)
|
||||
{
|
||||
return sysUserConfigMapper.selectById(configId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户配置列表
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 用户配置集合
|
||||
*/
|
||||
@Override
|
||||
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig)
|
||||
{
|
||||
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(StringUtils.isNotEmpty(sysUserConfig.getConfigKey()), SysUserConfig::getConfigKey, sysUserConfig.getConfigKey())
|
||||
.eq(sysUserConfig.getUserId() != null, SysUserConfig::getUserId, sysUserConfig.getUserId());
|
||||
return sysUserConfigMapper.selectList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户配置
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertSysUserConfig(SysUserConfig sysUserConfig)
|
||||
{
|
||||
return sysUserConfigMapper.insert(sysUserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户配置
|
||||
*
|
||||
* @param sysUserConfig 用户配置
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateSysUserConfig(SysUserConfig sysUserConfig)
|
||||
{
|
||||
return sysUserConfigMapper.updateById(sysUserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID和配置键更新配置值
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param configKey 配置键
|
||||
* @param configValue 配置值
|
||||
* @return 结果
|
||||
*/
|
||||
private int updateConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
|
||||
{
|
||||
SysUserConfig config = new SysUserConfig();
|
||||
config.setConfigValue(configValue);
|
||||
|
||||
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(SysUserConfig::getUserId, userId)
|
||||
.eq(SysUserConfig::getConfigKey, configKey);
|
||||
|
||||
return sysUserConfigMapper.update(config, lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除用户配置
|
||||
*
|
||||
* @param configIds 需要删除的用户配置ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteSysUserConfigByIds(Long[] configIds)
|
||||
{
|
||||
return sysUserConfigMapper.deleteBatchIds(java.util.Arrays.asList(configIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户配置信息
|
||||
*
|
||||
* @param configId 用户配置ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteSysUserConfigById(Long configId)
|
||||
{
|
||||
return sysUserConfigMapper.deleteById(configId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID和配置键获取配置值
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param configKey 配置键
|
||||
* @return 配置值
|
||||
*/
|
||||
@Override
|
||||
public String selectConfigValueByUserIdAndKey(Long userId, String configKey)
|
||||
{
|
||||
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(SysUserConfig::getUserId, userId)
|
||||
.eq(SysUserConfig::getConfigKey, configKey);
|
||||
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
|
||||
return config != null ? config.getConfigValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID和配置键保存配置
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param configKey 配置键
|
||||
* @param configValue 配置值
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
|
||||
{
|
||||
// 参数验证
|
||||
if (userId == null || configKey == null) {
|
||||
throw new IllegalArgumentException("用户ID和配置键不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(SysUserConfig::getUserId, userId)
|
||||
.eq(SysUserConfig::getConfigKey, configKey);
|
||||
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
|
||||
|
||||
if (config != null) {
|
||||
// 更新现有配置,只更新配置值,避免更新审计字段
|
||||
return updateConfigValueByUserIdAndKey(userId, configKey, configValue);
|
||||
} else {
|
||||
// 插入新配置
|
||||
SysUserConfig newConfig = new SysUserConfig();
|
||||
newConfig.setUserId(userId);
|
||||
newConfig.setConfigKey(configKey);
|
||||
newConfig.setConfigValue(configValue);
|
||||
return sysUserConfigMapper.insert(newConfig);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录错误日志以便调试
|
||||
log.error("保存用户配置时发生错误, userId: {}, configKey: {}", userId, configKey, e);
|
||||
throw e; // 重新抛出异常让上层处理
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<result property="status" column="status"/>
|
||||
<result property="perms" column="perms"/>
|
||||
<result property="icon" column="icon"/>
|
||||
<result property="fullPath" column="full_path"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
@@ -273,4 +274,27 @@
|
||||
where menu_id = #{menuId}
|
||||
</delete>
|
||||
|
||||
<select id="selectAllMenus" resultMap="SysMenuResult">
|
||||
select menu_id,
|
||||
parent_id,
|
||||
menu_name,
|
||||
path,
|
||||
component,
|
||||
"query",
|
||||
route_name,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
order_num,
|
||||
create_time,
|
||||
update_time,
|
||||
remark
|
||||
from sys_menu
|
||||
order by parent_id, order_num
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.core.system.mapper.SysUserConfigMapper">
|
||||
|
||||
<resultMap type="com.core.system.domain.SysUserConfig" id="SysUserConfigResult">
|
||||
<result property="configId" column="config_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="configKey" column="config_key" />
|
||||
<result property="configValue" column="config_value" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 根据用户ID和配置键名查询配置值 -->
|
||||
<select id="selectConfigValueByUserIdAndKey" resultType="String">
|
||||
SELECT config_value
|
||||
FROM sys_user_config
|
||||
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||
</select>
|
||||
|
||||
<!-- 根据用户ID和配置键名查询完整配置 -->
|
||||
<select id="selectByUserIdAndKey" resultMap="SysUserConfigResult">
|
||||
SELECT config_id, user_id, config_key, config_value, remark, create_time, update_time
|
||||
FROM sys_user_config
|
||||
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||
</select>
|
||||
|
||||
<!-- 根据用户ID和配置键更新配置值 -->
|
||||
<update id="updateConfigValueByUserIdAndKey">
|
||||
UPDATE sys_user_config
|
||||
SET config_value = #{configValue}, update_time = CURRENT_TIMESTAMP
|
||||
WHERE user_id = #{userId} AND config_key = #{configKey}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -112,6 +112,13 @@
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,19 @@ public interface IDoctorScheduleAppService {
|
||||
|
||||
R<?> getDoctorScheduleList();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,35 +2,101 @@ package com.openhis.web.appointmentmanage.appservice.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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.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();
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
public R<?> addDoctorSchedule(DoctorSchedule doctorSchedule) {
|
||||
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());
|
||||
@@ -41,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);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -40,4 +80,22 @@ public class DoctorScheduleController {
|
||||
return R.ok(doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId));
|
||||
}
|
||||
|
||||
/*
|
||||
* 获取今日医生排班List
|
||||
*
|
||||
* */
|
||||
@GetMapping("/today")
|
||||
public R<?> getTodayDoctorScheduleList() {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.core.common.core.domain.R;
|
||||
import com.openhis.web.basedatamanage.dto.OrganizationDto;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Organization 应该服务类
|
||||
@@ -13,18 +14,19 @@ public interface IOrganizationAppService {
|
||||
/**
|
||||
* 查询机构树
|
||||
*
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param classEnum 科室分类
|
||||
* @param sortField 排序字段
|
||||
* @param sortOrder 排序方向
|
||||
* @param request 请求数据
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param classEnumList 科室分类列表(逗号分隔的值)
|
||||
* @param sortField 排序字段
|
||||
* @param sortOrder 排序方向
|
||||
* @param request 请求数据
|
||||
* @return 机构树分页列表
|
||||
*/
|
||||
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, String classEnum,
|
||||
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);
|
||||
|
||||
/**
|
||||
* 机构信息详情
|
||||
@@ -66,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,50 +36,70 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
||||
private AssignSeqUtil assignSeqUtil;
|
||||
|
||||
@Override
|
||||
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, String classEnum,
|
||||
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);
|
||||
|
||||
// 创建查询条件
|
||||
LambdaQueryWrapper<Organization> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Organization::getDeleteFlag, "0"); // 只查询未删除的记录
|
||||
|
||||
// 添加查询条件
|
||||
if (StringUtils.isNotEmpty(name)) {
|
||||
queryWrapper.like(Organization::getName, name);
|
||||
}
|
||||
if (typeEnum != null) {
|
||||
queryWrapper.eq(Organization::getTypeEnum, typeEnum);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(classEnum)) {
|
||||
// 对于多选,需要处理逗号分隔的值
|
||||
if (classEnumList != null && !classEnumList.isEmpty()) {
|
||||
// 使用OR条件来匹配class_enum字段中包含任一值的记录
|
||||
queryWrapper.and(wrapper -> {
|
||||
String[] classEnums = classEnum.split(",");
|
||||
for (String cls : classEnums) {
|
||||
String trimmedCls = cls.trim();
|
||||
// 使用OR连接多个条件来匹配逗号分隔的值
|
||||
wrapper.or().and(subWrapper -> {
|
||||
subWrapper.eq(Organization::getClassEnum, trimmedCls)
|
||||
.or()
|
||||
.likeRight(Organization::getClassEnum, trimmedCls + ",")
|
||||
.or()
|
||||
.likeLeft(Organization::getClassEnum, "," + trimmedCls)
|
||||
.or()
|
||||
.like(Organization::getClassEnum, "," + trimmedCls + ",");
|
||||
});
|
||||
for (int i = 0; i < classEnumList.size(); i++) {
|
||||
String classEnum = classEnumList.get(i);
|
||||
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 + ","); // 在中间,被逗号包围
|
||||
});
|
||||
} 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 + ","); // 在中间,被逗号包围
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建Page对象
|
||||
Page<Organization> page = new Page<>(pageNo, pageSize);
|
||||
|
||||
// 执行分页查询
|
||||
page = organizationService.page(page, queryWrapper);
|
||||
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
|
||||
|
||||
List<Organization> organizationList = page.getRecords();
|
||||
// 将机构列表转为树结构
|
||||
// 将查询结果转为DTO并构建树结构
|
||||
List<Organization> organizationList = resultPage.getRecords();
|
||||
List<OrganizationDto> orgTree = buildTree(organizationList);
|
||||
Page<OrganizationDto> orgQueryDtoPage = new Page<>(pageNo, pageSize, page.getTotal());
|
||||
orgQueryDtoPage.setRecords(orgTree);
|
||||
return orgQueryDtoPage;
|
||||
|
||||
// 创建结果分页对象
|
||||
Page<OrganizationDto> result = new Page<>();
|
||||
result.setRecords(orgTree);
|
||||
result.setTotal(resultPage.getTotal());
|
||||
result.setSize(resultPage.getSize());
|
||||
result.setCurrent(resultPage.getCurrent());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,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<>();
|
||||
@@ -139,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[] { "机构信息查询" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,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);
|
||||
}
|
||||
@@ -185,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[] { "机构信息更新添加" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,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[] { "机构信息" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,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[] { "机构信息启用" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,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[] { "机构信息停用" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,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()))
|
||||
|
||||
@@ -5,6 +5,7 @@ package com.openhis.web.basedatamanage.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.StringUtils;
|
||||
import com.core.common.utils.MessageUtils;
|
||||
import com.openhis.common.constant.PromptMsgConstant;
|
||||
import com.openhis.web.basedatamanage.appservice.IOrganizationAppService;
|
||||
@@ -16,6 +17,8 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 机构管理controller
|
||||
@@ -39,28 +42,35 @@ public class OrganizationController {
|
||||
/**
|
||||
* 机构分页列表
|
||||
*
|
||||
* @param pageNo 当前页码
|
||||
* @param pageSize 查询条数
|
||||
* @param name 科室名称
|
||||
* @param typeEnum 科室类型
|
||||
* @param classEnum 科室分类
|
||||
* @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) {
|
||||
Page<OrganizationDto> organizationTree =
|
||||
iOrganizationAppService.getOrganizationTree(pageNo, pageSize, name, typeEnum, classEnum, sortField, sortOrder, 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;
|
||||
if (StringUtils.isNotBlank(classEnum)) {
|
||||
classEnumList = Arrays.asList(classEnum.split(","));
|
||||
}
|
||||
|
||||
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[] { "机构信息" }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,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);
|
||||
}
|
||||
@@ -10,16 +10,21 @@ import com.core.common.utils.MessageUtils;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.common.utils.StringUtils;
|
||||
import com.core.common.utils.bean.BeanUtils;
|
||||
import com.core.common.core.domain.entity.SysRole;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import com.openhis.administration.domain.*;
|
||||
import com.openhis.administration.mapper.PatientMapper;
|
||||
import com.openhis.administration.service.*;
|
||||
import com.openhis.common.constant.CommonConstants;
|
||||
import com.openhis.common.constant.PromptMsgConstant;
|
||||
import com.openhis.common.enums.*;
|
||||
import com.openhis.common.enums.ybenums.YbPayment;
|
||||
import com.openhis.common.utils.EnumUtils;
|
||||
import com.openhis.common.utils.HisPageUtils;
|
||||
import com.openhis.common.utils.HisQueryUtils;
|
||||
import com.openhis.financial.domain.PaymentReconciliation;
|
||||
import com.openhis.financial.domain.RefundLog;
|
||||
import com.openhis.financial.service.IRefundLogService;
|
||||
import com.openhis.web.basicservice.dto.HealthcareServiceDto;
|
||||
import com.openhis.web.basicservice.mapper.HealthcareServiceBizMapper;
|
||||
import com.openhis.web.chargemanage.appservice.IOutpatientRegistrationAppService;
|
||||
@@ -40,10 +45,14 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 门诊挂号 应用实现类
|
||||
@@ -85,6 +94,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
||||
@Resource
|
||||
TriageCandidateExclusionService triageCandidateExclusionService;
|
||||
|
||||
@Resource
|
||||
IRefundLogService iRefundLogService;
|
||||
|
||||
/**
|
||||
* 门诊挂号 - 查询患者信息
|
||||
*
|
||||
@@ -235,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
|
||||
@@ -260,7 +276,7 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
||||
R<?> result = iPaymentRecService.cancelRegPayment(cancelPaymentDto);
|
||||
|
||||
PaymentReconciliation paymentRecon = null;
|
||||
if (PaymentReconciliation.class.isAssignableFrom(result.getData().getClass())) {
|
||||
if (result.getData() != null && PaymentReconciliation.class.isAssignableFrom(result.getData().getClass())) {
|
||||
paymentRecon = (PaymentReconciliation)result.getData();
|
||||
}
|
||||
if (paymentRecon != null) {
|
||||
@@ -275,6 +291,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
||||
}
|
||||
}
|
||||
|
||||
// 记录退号日志
|
||||
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
|
||||
|
||||
// 2025/05/05 该处保存费用项后,会通过统一收费处理进行收费
|
||||
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
|
||||
}
|
||||
@@ -394,4 +413,124 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
||||
return R.ok(null, "补打挂号成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录退号日志
|
||||
*
|
||||
* @param cancelRegPaymentDto 退号请求对象
|
||||
* @param encounter 就诊信息
|
||||
* @param result 退号结果
|
||||
* @param paymentRecon 支付对账信息
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void recordRefundLog(CancelRegPaymentDto cancelRegPaymentDto,
|
||||
Encounter encounter,
|
||||
R<?> result,
|
||||
PaymentReconciliation paymentRecon) {
|
||||
RefundLog refundLog = new RefundLog();
|
||||
try {
|
||||
// 1. 订单ID(唯一)
|
||||
String orderId = String.valueOf(cancelRegPaymentDto.getEncounterId());
|
||||
refundLog.setOrderId(orderId);
|
||||
|
||||
// 已存在则不重复插入(防止唯一约束异常)
|
||||
long exist = iRefundLogService.lambdaQuery()
|
||||
.eq(RefundLog::getOrderId, orderId)
|
||||
.count();
|
||||
if (exist > 0) {
|
||||
log.warn("退号日志已存在,orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 患者信息
|
||||
if (encounter != null) {
|
||||
refundLog.setPatientId(String.valueOf(encounter.getPatientId()));
|
||||
Patient patient = patientMapper.selectById(encounter.getPatientId());
|
||||
refundLog.setPatientName(patient != null ? patient.getName() : "未知");
|
||||
} else {
|
||||
refundLog.setPatientId("0");
|
||||
refundLog.setPatientName("未知");
|
||||
}
|
||||
|
||||
// 3. 金额
|
||||
// 优先使用paymentRecon中的displayAmount,如果没有则尝试使用cancelRegPaymentDto中的displayAmount
|
||||
BigDecimal refundAmount = BigDecimal.ZERO;
|
||||
if (paymentRecon != null) {
|
||||
// 使用退号后生成的paymentRecon的实际金额(这个金额应该是负数)
|
||||
refundAmount = paymentRecon.getDisplayAmount() != null
|
||||
? paymentRecon.getDisplayAmount().abs() // 取绝对值,因为退费金额通常显示为正数
|
||||
: BigDecimal.ZERO;
|
||||
} else if (cancelRegPaymentDto.getDisplayAmount() != null) {
|
||||
refundAmount = cancelRegPaymentDto.getDisplayAmount();
|
||||
}
|
||||
refundLog.setRefundAmount(refundAmount);
|
||||
|
||||
// 4. 原因 & 类型
|
||||
refundLog.setRefundReason(
|
||||
StringUtils.isNotEmpty(cancelRegPaymentDto.getReason())
|
||||
? cancelRegPaymentDto.getReason()
|
||||
: "诊前退号-已缴费签到未就诊"
|
||||
);
|
||||
refundLog.setRefundType("FULL");
|
||||
|
||||
// 5. 退款方式
|
||||
String refundMethod = "UNKNOWN";
|
||||
if (cancelRegPaymentDto.getPaymentDetails() != null
|
||||
&& !cancelRegPaymentDto.getPaymentDetails().isEmpty()) {
|
||||
Integer payEnum = cancelRegPaymentDto.getPaymentDetails().get(0).getPayEnum();
|
||||
if (payEnum != null) {
|
||||
YbPayment ybPayment = YbPayment.getByValue(payEnum);
|
||||
refundMethod = (ybPayment != null ? ybPayment.getInfo() : payEnum.toString());
|
||||
}
|
||||
}
|
||||
refundLog.setRefundMethod(refundMethod);
|
||||
|
||||
// 6. 原交易号
|
||||
refundLog.setOriginalTradeNo(
|
||||
paymentRecon != null ? paymentRecon.getPaymentNo() : null
|
||||
);
|
||||
|
||||
// 7. 时间
|
||||
refundLog.setRefundTime(LocalDateTime.now());
|
||||
|
||||
// 8. 操作人
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
refundLog.setOpUserId(String.valueOf(loginUser.getUserId()));
|
||||
refundLog.setOpUserName(loginUser.getUsername());
|
||||
List<SysRole> roles = loginUser.getRoleList();
|
||||
refundLog.setOpRole(
|
||||
roles != null && !roles.isEmpty() ? roles.get(0).getRoleName() : "SYSTEM"
|
||||
);
|
||||
} else {
|
||||
refundLog.setOpUserId("0");
|
||||
refundLog.setOpUserName("SYSTEM");
|
||||
refundLog.setOpRole("SYSTEM");
|
||||
}
|
||||
|
||||
// 9. 状态
|
||||
if (result != null && result.getCode() == 200) {
|
||||
refundLog.setState(1);
|
||||
} else {
|
||||
refundLog.setState(0);
|
||||
refundLog.setFailReason(result != null ? result.getMsg() : "未知错误");
|
||||
}
|
||||
|
||||
// 10. 创建时间
|
||||
refundLog.setCreatedAt(LocalDateTime.now());
|
||||
refundLog.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
// 11. 保存
|
||||
boolean saved = iRefundLogService.save(refundLog);
|
||||
if (!saved) {
|
||||
throw new RuntimeException("退号日志保存失败");
|
||||
}
|
||||
|
||||
log.info("退号日志入库成功, orderId={}", orderId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("退号日志入库失败,数据={}", refundLog, e);
|
||||
throw e; // 让事务感知(你也可以只记录不抛)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user