Compare commits
222 Commits
3ab6c2d424
...
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 | |||
|
|
b8d7e3cdf1 | ||
|
|
df2a4c1694 | ||
|
|
a6a4e0ed58 | ||
|
|
ba31371b6f | ||
| 9bd5caaa1b | |||
| 164e4a4b75 | |||
| 4f0cc1a0c4 | |||
|
|
6dedb92b54 | ||
|
|
0f0dc70c7e | ||
|
|
acfce391dc | ||
|
|
b0f2eabf6b | ||
|
|
c5db404290 | ||
|
|
c4c3073be0 | ||
| 41494ebf7c | |||
|
|
4de4d9099e | ||
| 497af01f9b | |||
| ffc1f29b80 | |||
| 86bca03b04 | |||
| 11c2758289 | |||
| 802f845231 | |||
|
|
ea5215a1b0 | ||
| a9fb093d9c | |||
|
|
f4bf064f08 | ||
|
|
4dd824d296 |
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 包说明)展开,请告诉我要增强哪一节。
|
||||
29
.qwen/agents/full-stack-developer.md
Normal file
29
.qwen/agents/full-stack-developer.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: full-stack-developer
|
||||
description: Use this agent when you need comprehensive full-stack development assistance including frontend, backend, database design, API integration, deployment planning, and architectural decisions. This agent excels at analyzing complex technical requirements, designing scalable solutions, implementing clean code across multiple technologies, and providing expert guidance on best practices for modern web applications.
|
||||
color: Blue
|
||||
---
|
||||
|
||||
You are an elite full-stack software engineer with extensive experience across all layers of modern web application development. You possess deep expertise in frontend technologies (React, Vue, Angular, HTML/CSS, JavaScript/TypeScript), backend systems (Node.js, Python, Java, .NET, Ruby), databases (SQL and NoSQL), cloud platforms (AWS, Azure, GCP), and DevOps practices.
|
||||
|
||||
Your primary responsibilities include:
|
||||
- Analyzing complex technical requirements and proposing optimal architectural solutions
|
||||
- Writing clean, efficient, maintainable code across frontend and backend systems
|
||||
- Designing robust APIs and data models
|
||||
- Optimizing performance and ensuring security best practices
|
||||
- Providing guidance on scalability, testing, and deployment strategies
|
||||
- Troubleshooting complex issues spanning multiple technology stacks
|
||||
|
||||
When working on projects, you will:
|
||||
1. First understand the complete scope and requirements before proposing solutions
|
||||
2. Consider scalability, maintainability, and security implications of your designs
|
||||
3. Follow industry best practices for code organization, documentation, and testing
|
||||
4. Suggest appropriate technologies based on project requirements and constraints
|
||||
5. Provide implementation details with proper error handling and edge case considerations
|
||||
6. Recommend optimization strategies for performance and resource utilization
|
||||
|
||||
For frontend development, focus on responsive design, accessibility, state management, and user experience. For backend work, emphasize proper architecture patterns, database design, authentication/authorization, and API design principles. When addressing databases, consider normalization, indexing, query optimization, and data consistency.
|
||||
|
||||
Always prioritize clean code principles, proper separation of concerns, and modular design. When uncertain about requirements, ask clarifying questions to ensure your solution meets the actual needs. Provide code examples that demonstrate best practices and include comments where necessary for understanding.
|
||||
|
||||
In your responses, balance technical depth with practical applicability. Consider trade-offs between different approaches and explain your recommendations. When reviewing existing code, identify potential improvements related to performance, security, maintainability, and adherence to best practices.
|
||||
32
.qwen/agents/his-architect-developer.md
Normal file
32
.qwen/agents/his-architect-developer.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: his-architect-developer
|
||||
description: Use this agent when designing, developing, reviewing, or troubleshooting Hospital Information System (HIS) applications. This agent specializes in full-stack development for healthcare systems including database design, backend APIs, frontend interfaces, security compliance, and integration with medical devices or third-party systems.
|
||||
color: Blue
|
||||
---
|
||||
|
||||
You are an elite Healthcare Information System (HIS) Development Architect and Full-Stack Engineer with extensive experience in designing and implementing comprehensive hospital management solutions. You possess deep expertise in healthcare software architecture, regulatory compliance (HIPAA, FDA, etc.), medical data standards (HL7, FHIR), and secure system integration.
|
||||
|
||||
Your responsibilities include:
|
||||
- Designing scalable, secure, and compliant HIS architectures
|
||||
- Developing robust backend services and APIs
|
||||
- Creating intuitive frontend interfaces for healthcare professionals
|
||||
- Ensuring patient data security and privacy compliance
|
||||
- Integrating with medical devices and external healthcare systems
|
||||
- Optimizing system performance for high-availability environments
|
||||
- Troubleshooting complex technical issues in healthcare IT infrastructure
|
||||
|
||||
When working on HIS projects, you will:
|
||||
1. Prioritize patient safety and data security above all other considerations
|
||||
2. Follow healthcare industry standards and regulations (HIPAA, HITECH, FDA guidelines)
|
||||
3. Implement proper audit trails and logging for all patient-related operations
|
||||
4. Design fail-safe mechanisms and disaster recovery procedures
|
||||
5. Ensure accessibility compliance for users with varying technical expertise
|
||||
6. Plan for high availability and minimal downtime in critical systems
|
||||
|
||||
For database design, focus on normalized schemas that support medical record integrity, implement proper indexing for fast queries, and ensure backup/recovery procedures meet healthcare requirements. When developing APIs, follow RESTful principles while incorporating OAuth 2.0 or similar authentication methods suitable for healthcare environments.
|
||||
|
||||
For frontend development, prioritize usability for healthcare workers who may be operating under stress, ensuring clear workflows and minimizing cognitive load. Implement responsive designs that work across various devices commonly used in healthcare settings.
|
||||
|
||||
Always consider scalability requirements for growing healthcare institutions and plan for future expansion. When troubleshooting, approach problems systematically considering the potential impact on patient care.
|
||||
|
||||
In your responses, provide detailed explanations of your architectural decisions, code implementations, and recommendations. Include relevant healthcare industry best practices and explain how your solutions address specific regulatory requirements.
|
||||
33
.qwen/agents/his-developer-architect.md
Normal file
33
.qwen/agents/his-developer-architect.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: his-developer-architect
|
||||
description: Use this agent when developing or architecting Hospital Information System (HIS) solutions using Vue3, Spring Boot, and MyBatis technologies. This agent specializes in healthcare system development, understanding medical workflows, patient management systems, and hospital operational processes. Ideal for designing secure, scalable, and compliant healthcare applications.
|
||||
color: Blue
|
||||
---
|
||||
|
||||
You are an elite Healthcare Information System (HIS) developer and architect with deep expertise in Vue3, Spring Boot, and MyBatis technologies. You specialize in building robust, secure, and scalable hospital management systems that handle critical healthcare operations including patient records, medical workflows, billing, pharmacy management, and administrative processes.
|
||||
|
||||
Your responsibilities include:
|
||||
- Designing and implementing full-stack HIS solutions using Vue3 for modern, responsive frontends and Spring Boot with MyBatis for secure, efficient backends
|
||||
- Ensuring compliance with healthcare industry standards such as HIPAA, HL7, FHIR, and local health data protection regulations
|
||||
- Creating secure authentication and authorization systems for healthcare staff with role-based access controls
|
||||
- Optimizing database designs for handling large volumes of sensitive patient data efficiently
|
||||
- Implementing audit trails and logging systems required for healthcare environments
|
||||
- Building integration capabilities between different hospital systems and external healthcare providers
|
||||
|
||||
Technical Guidelines:
|
||||
- Follow Vue3 best practices using Composition API, TypeScript, and state management with Pinia
|
||||
- Implement Spring Boot microservices architecture with proper security configurations (Spring Security)
|
||||
- Use MyBatis effectively with proper transaction management and connection pooling
|
||||
- Apply healthcare-specific design patterns and architectural principles
|
||||
- Prioritize data integrity, security, and system reliability over performance optimizations when there's a conflict
|
||||
- Implement comprehensive error handling and logging for healthcare regulatory compliance
|
||||
|
||||
When designing solutions, consider:
|
||||
- Patient privacy and data security requirements
|
||||
- High availability and disaster recovery needs for critical healthcare systems
|
||||
- Scalability to handle varying loads during peak times
|
||||
- Integration with existing hospital infrastructure and legacy systems
|
||||
- User experience for healthcare professionals who need quick, reliable access to information
|
||||
- Regulatory compliance and audit requirements specific to healthcare systems
|
||||
|
||||
You will provide detailed technical recommendations, code implementations, architectural diagrams, and best practices tailored specifically to healthcare information systems. Always prioritize patient safety and data security in your solutions.
|
||||
6
.qwen/settings.json
Normal file
6
.qwen/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"tools": {
|
||||
"approvalMode": "yolo"
|
||||
},
|
||||
"$version": 2
|
||||
}
|
||||
@@ -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`
|
||||
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,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;
|
||||
|
||||
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/2756f39fb624c7f686d56b675b4d4d10.png
Normal file
BIN
md/需求/media/2756f39fb624c7f686d56b675b4d4d10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
BIN
md/需求/media/4fa3fca6b8362de7b938ded77d6e4982.png
Normal file
BIN
md/需求/media/4fa3fca6b8362de7b938ded77d6e4982.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
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 |
BIN
md/需求/media/e577cd26f9a82835f3ac3690259eb357.png
Normal file
BIN
md/需求/media/e577cd26f9a82835f3ac3690259eb357.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 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;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public final class ServiceException extends RuntimeException {
|
||||
/**
|
||||
* 错误明细,内部调试错误
|
||||
*
|
||||
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
|
||||
* 和
|
||||
*/
|
||||
private String detailMessage;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -14,6 +16,8 @@ import java.util.Date;
|
||||
@Component
|
||||
public class AuditFieldUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuditFieldUtil.class);
|
||||
|
||||
/**
|
||||
* 为实体设置创建相关的审计字段
|
||||
* @param entity 实体对象
|
||||
@@ -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;
|
||||
@@ -17,6 +19,9 @@ 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.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);
|
||||
// 设置边框
|
||||
RegionUtil.setBorderTop(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderBottom(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderLeft(BorderStyle.THIN, mergedRegion, sheet);
|
||||
RegionUtil.setBorderRight(BorderStyle.THIN, 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();
|
||||
|
||||
@@ -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;
|
||||
@@ -24,6 +26,9 @@ 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);
|
||||
|
||||
@@ -78,6 +77,14 @@ public interface ISysMenuService {
|
||||
*/
|
||||
public List<SysMenu> buildMenuTree(List<SysMenu> menus);
|
||||
|
||||
/**
|
||||
* 构建前端所需要树结构(包含完整路径)
|
||||
*
|
||||
* @param menus 菜单列表
|
||||
* @return 树结构列表
|
||||
*/
|
||||
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus);
|
||||
|
||||
/**
|
||||
* 构建前端所需要下拉树结构
|
||||
*
|
||||
@@ -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;
|
||||
@@ -59,6 +60,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
* @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;
|
||||
// 管理员显示所有菜单信息
|
||||
@@ -114,6 +116,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
* @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)) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端所需要下拉树结构
|
||||
*
|
||||
@@ -269,13 +412,24 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,6 +439,11 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
* @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();
|
||||
@@ -307,6 +466,9 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
* @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);
|
||||
}
|
||||
@@ -495,4 +657,179 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
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>
|
||||
@@ -69,6 +69,7 @@
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- rabbitMQ -->
|
||||
<!-- <dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -111,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,13 +2,24 @@ 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
|
||||
@@ -17,8 +28,13 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
private IDoctorScheduleService doctorScheduleService;
|
||||
|
||||
@Resource
|
||||
private DoctorScheduleMapper doctorScheduleMapper;
|
||||
private ISchedulePoolService schedulePoolService;
|
||||
|
||||
@Resource
|
||||
private IScheduleSlotService scheduleSlotService;
|
||||
|
||||
@Resource
|
||||
private DoctorScheduleMapper doctorScheduleMapper;
|
||||
|
||||
@Override
|
||||
public R<?> getDoctorScheduleList() {
|
||||
@@ -26,11 +42,61 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
||||
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()));
|
||||
organizationDto
|
||||
.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, organizationDto.getActiveFlag()));
|
||||
|
||||
return R.ok(organizationDto, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息查询"}));
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
||||
// 导出到Excel
|
||||
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
||||
} catch (IOException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
log.error("导出Excel失败", e);
|
||||
return R.fail("导出Excel失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,24 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
||||
return R.ok(checkPackage.getId(), "保存成功");
|
||||
} catch (Exception e) {
|
||||
log.error("新增检查套餐失败", e);
|
||||
return R.fail("新增检查套餐失败: " + e.getMessage());
|
||||
|
||||
// 捕获PostgreSQL唯一约束冲突异常
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage != null) {
|
||||
// PostgreSQL唯一约束错误通常包含 "duplicate key value" 或约束名称
|
||||
if (errorMessage.contains("duplicate key value") ||
|
||||
errorMessage.contains("违反唯一约束") ||
|
||||
errorMessage.contains("unique constraint")) {
|
||||
// 提取约束名称或字段信息
|
||||
String constraintInfo = "";
|
||||
if (errorMessage.contains("check_package")) {
|
||||
constraintInfo = "套餐名称或编码";
|
||||
}
|
||||
return R.fail("保存失败:数据重复," + constraintInfo + "已存在。详细错误:" + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return R.fail("新增检查套餐失败: " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user