Compare commits
329 Commits
9b35fec931
...
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 | ||
| 3ab6c2d424 | |||
| 12b2bf255c | |||
|
|
c878dc19d7 | ||
|
|
c1efd84332 | ||
| 1616f66fc4 | |||
|
|
2df1ed645f | ||
|
|
4f7fc1c09a | ||
| bd873f81d2 | |||
| 1975fda73c | |||
| ffce6f81c3 | |||
| ca043de624 | |||
| 054b51c63d | |||
| 5cf2dd165c | |||
| 27b094744c | |||
|
|
55e3533600 | ||
|
|
1522183432 | ||
| 6382741b71 | |||
| 16c854d55f | |||
| 73617e1b0f | |||
| abd5bd9f2f | |||
|
|
9000d66c0c | ||
|
|
61be9ff552 | ||
|
|
9408cf6c2d | ||
|
|
66c70a2b4a | ||
| f6d9321f95 | |||
| ccff9a7246 | |||
| 2884f610f5 | |||
|
|
035738f990 | ||
|
|
d0c6f57f6b | ||
|
|
58c1e02415 | ||
|
|
1e459b8883 | ||
|
|
0d57e984a6 | ||
| 4450e3cc50 | |||
|
|
902ee0587e | ||
| 49550fcc2e | |||
|
|
1dd7ee3428 | ||
|
|
8dff5d466a | ||
|
|
19ada4ace9 | ||
| c92ff38133 | |||
| 1c07108e58 | |||
|
|
34dd969cb4 | ||
| a0b546266d | |||
|
|
fc9ce6241e | ||
|
|
5187ff1ae3 | ||
|
|
73b1d01044 | ||
|
|
b88ad89146 | ||
|
|
de8039c513 | ||
| 3464153d93 | |||
| 6b868e378f | |||
| f6403fa059 | |||
|
|
bc92b9aa62 | ||
|
|
46145ff636 | ||
|
|
3ad32fac9f | ||
| d1223aec07 | |||
| 649f7bcf5b | |||
| a3dce8de60 | |||
| f81dd54f0c | |||
| 803e4d0bb5 | |||
| deebcde41f | |||
| 095c43bbf3 | |||
| aa3beb848b | |||
|
|
ae96bbd0bb | ||
|
|
1a2c444269 | ||
|
|
9cba8fea12 | ||
|
|
f11b7380a4 | ||
| da17b2b89c | |||
| 9e4a010a8d | |||
| 7e76083c37 | |||
| de105adbdc | |||
| f3eeee7405 | |||
| 97f04d0b15 | |||
| 5667e04d12 | |||
| 59157fda56 | |||
| 2fe6d45ad4 | |||
| 982ee316f7 | |||
| 64c7db68e8 | |||
| cb6b6ced67 | |||
|
|
8fcfb481c9 | ||
|
|
be0514bc08 | ||
|
|
2b3add4808 | ||
| b33cb6f9a1 | |||
| 072e71b025 | |||
| 47394de43c | |||
| f0f1dde6b6 | |||
| 1ab1165697 | |||
| a8f1b1fdfa | |||
| 3b94d19199 | |||
| db1139a14f | |||
|
|
bea74aeac2 | ||
|
|
634a1f45f9 | ||
| 8f1ad3307c | |||
| d8080fa22d | |||
|
|
e8783d9f8f | ||
| d8c4348341 | |||
|
|
8e61490005 | ||
| f5f4e3c48e | |||
| 0f013715b8 | |||
| fb9722d328 | |||
| 6f9192d30d | |||
| f2b5b90f34 | |||
| a2cbd5e583 | |||
| d3df46858b | |||
| 47a7a945bc | |||
| 0a56c0dcf0 | |||
| 15d32134e2 | |||
| eff98ea5eb | |||
| a47306825a |
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,179 +0,0 @@
|
||||
# 中医诊断主诊断功能实现说明
|
||||
|
||||
## 问题描述
|
||||
中医诊断在添加时无法设置主诊断标记,导致保存后无法正确标识主诊断。
|
||||
|
||||
## 问题原因
|
||||
在 `addDiagnosisDialog.vue` 中保存中医诊断时,没有传递 `maindiseFlag`(主诊断标记)字段到后端。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 前端修改
|
||||
|
||||
#### 1.1 添加主诊断UI(addDiagnosisDialog.vue)
|
||||
在诊断详情区域为每个中医诊断添加了主诊断复选框:
|
||||
|
||||
```vue
|
||||
<el-checkbox
|
||||
v-model="item.isMain"
|
||||
label="主诊断"
|
||||
:true-label="true"
|
||||
:false-label="false"
|
||||
border
|
||||
size="small"
|
||||
style="margin-right: 10px;"
|
||||
@change="(value) => handleMaindise(value, index)"
|
||||
/>
|
||||
```
|
||||
|
||||
#### 1.2 添加主诊断逻辑处理
|
||||
|
||||
**新增诊断时的默认行为:**
|
||||
- 第一个添加的中医诊断自动设置为主诊断
|
||||
- 后续添加的诊断默认不是主诊断
|
||||
|
||||
**主诊断唯一性校验:**
|
||||
```javascript
|
||||
function handleMaindise(value, index) {
|
||||
if (value) {
|
||||
// 检查是否已有其他主诊断
|
||||
let mainCount = 0;
|
||||
tcmDiagonsisList.value.forEach((item, idx) => {
|
||||
if (item.isMain && idx !== index) {
|
||||
mainCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (mainCount > 0) {
|
||||
// 取消当前选择
|
||||
tcmDiagonsisList.value[index].isMain = false;
|
||||
proxy.$modal.msgWarning('只能有一条主诊断');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新保存列表中的主诊断标记
|
||||
const syndromeGroupNo = tcmDiagonsisList.value[index].syndromeGroupNo;
|
||||
tcmDiagonsisSaveList.value.forEach((item, idx) => {
|
||||
if (item.syndromeGroupNo === syndromeGroupNo) {
|
||||
// 每个证候组有两条记录(病和证),只有第一条(病)设置主诊断标记
|
||||
if (idx % 2 === 0 || tcmDiagonsisSaveList.value[idx - 1]?.syndromeGroupNo !== syndromeGroupNo) {
|
||||
item.maindiseFlag = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 取消主诊断
|
||||
const syndromeGroupNo = tcmDiagonsisList.value[index].syndromeGroupNo;
|
||||
tcmDiagonsisSaveList.value.forEach((item) => {
|
||||
if (item.syndromeGroupNo === syndromeGroupNo) {
|
||||
item.maindiseFlag = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 保存时包含主诊断字段
|
||||
|
||||
**新增诊断时:**
|
||||
```javascript
|
||||
tcmDiagonsisSaveList.value.push({
|
||||
definitionId: row.id,
|
||||
ybNo: row.ybNo,
|
||||
syndromeGroupNo: timestamp.value,
|
||||
verificationStatusEnum: 4,
|
||||
medTypeCode: '11',
|
||||
maindiseFlag: isFirstDiagnosis ? 1 : 0, // 添加主诊断标记
|
||||
});
|
||||
```
|
||||
|
||||
**修改诊断时:**
|
||||
```javascript
|
||||
tcmDiagonsisSaveList.value.push({
|
||||
conditionId: item.conditionId,
|
||||
updateId: updateIds[0],
|
||||
definitionId: item.illnessDefinitionId,
|
||||
ybNo: item.ybNo,
|
||||
syndromeGroupNo: item.syndromeGroupNo,
|
||||
verificationStatusEnum: 4,
|
||||
medTypeCode: '11',
|
||||
diagSrtNo: item.diagSrtNo,
|
||||
maindiseFlag: isMain ? 1 : 0, // 保留原有的主诊断标记
|
||||
});
|
||||
```
|
||||
|
||||
#### 1.4 诊断列表显示(diagnosis.vue)
|
||||
|
||||
在获取中医诊断列表时,正确读取并显示主诊断标记:
|
||||
|
||||
```javascript
|
||||
form.value.diagnosisList.push({
|
||||
name: item.name + '-' + res.data.symptom[index].name,
|
||||
diagSrtNo: item.diagSrtNo,
|
||||
ybNo: item.ybNo,
|
||||
medTypeCode: item.medTypeCode,
|
||||
syndromeGroupNo: item.syndromeGroupNo,
|
||||
typeName: '中医诊断',
|
||||
conditionId: item.conditionId,
|
||||
symptomConditionId: res.data.symptom[index].conditionId,
|
||||
updateId: item.encounterDiagnosisId + '-' + res.data.symptom[index].encounterDiagnosisId,
|
||||
illnessDefinitionId: item.definitionId,
|
||||
symptomDefinitionId: res.data.symptom[index].definitionId,
|
||||
symptomYbNo: res.data.symptom[index].ybNo,
|
||||
maindiseFlag: item.maindiseFlag || 0, // 添加主诊断标记
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 后端支持
|
||||
|
||||
后端已经支持 `maindiseFlag` 字段的保存和读取:
|
||||
|
||||
**SaveDiagnosisChildParam.java:**
|
||||
```java
|
||||
/**
|
||||
* 主诊断标记 (1:是,0:否)
|
||||
*/
|
||||
private Integer maindiseFlag;
|
||||
```
|
||||
|
||||
**DoctorStationChineseMedicalAppServiceImpl.java:**
|
||||
```java
|
||||
encounterDiagnosis.setMaindiseFlag(saveDiagnosisChildParam.getMaindiseFlag());
|
||||
```
|
||||
|
||||
## 业务规则
|
||||
|
||||
1. **主诊断标记位置**:主诊断标记在"病"上(每个病-证组合的第一条记录)
|
||||
2. **主诊断唯一性**:中医诊断只能有一个主诊断
|
||||
3. **与西医诊断的关系**:中医诊断和西医诊断可以各有一个主诊断(互不冲突)
|
||||
4. **默认行为**:第一个添加的中医诊断自动设置为主诊断
|
||||
|
||||
## 修改文件清单
|
||||
|
||||
1. `openhis-ui-vue3/src/views/doctorstation/components/diagnosis/addDiagnosisDialog.vue`
|
||||
- 添加主诊断复选框UI
|
||||
- 添加主诊断逻辑处理函数
|
||||
- 修改保存数据时包含 maindiseFlag 字段
|
||||
|
||||
2. `openhis-ui-vue3/src/views/doctorstation/components/diagnosis/diagnosis.vue`
|
||||
- 修改获取中医诊断列表时读取 maindiseFlag 字段
|
||||
- 修改传递给对话框的数据包含 maindiseFlag 字段
|
||||
|
||||
## 测试要点
|
||||
|
||||
1. ✅ 新增中医诊断时,第一个诊断自动设置为主诊断
|
||||
2. ✅ 可以手动勾选/取消主诊断复选框
|
||||
3. ✅ 只能有一个主诊断(尝试勾选第二个时会提示错误)
|
||||
4. ✅ 保存后主诊断标记正确保存到数据库
|
||||
5. ✅ 刷新页面后主诊断标记正确显示
|
||||
6. ✅ 修改已有诊断时,主诊断标记正确回显
|
||||
7. ✅ 中医诊断和西医诊断的主诊断互不影响
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 中医诊断是"病-证"成对出现的,主诊断标记只设置在"病"上
|
||||
2. 证候记录的 maindiseFlag 始终为 0
|
||||
3. 主诊断唯一性校验只在中医诊断内部进行,不影响西医诊断
|
||||
|
||||
## 完成时间
|
||||
2026年1月9日
|
||||
178
md/公告通知弹窗功能说明.md
178
md/公告通知弹窗功能说明.md
@@ -1,178 +0,0 @@
|
||||
# 公告通知弹窗功能说明
|
||||
|
||||
## 功能概述
|
||||
用户登录后,系统会自动弹出公告通知窗口,显示未读的系统公告和通知。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 自动弹出
|
||||
- 登录后 1 秒自动加载公告列表
|
||||
- 如果有未读公告,自动弹出弹窗显示
|
||||
- 延迟加载避免影响页面初始渲染
|
||||
|
||||
### 2. 优先级显示
|
||||
- **高优先级**(1):红色背景 `#fff1f0`,红色文字 `#ff4d4f`
|
||||
- **中优先级**(2):橙色背景 `#fff7e6`,橙色文字 `#faad14`
|
||||
- **低优先级**(3):灰色背景 `#f0f2f5`,灰色文字 `#909399`
|
||||
|
||||
### 3. 排序规则
|
||||
- 按优先级升序排列(高 -> 中 -> 低)
|
||||
- 相同优先级按创建时间降序排列
|
||||
|
||||
### 4. 已读状态
|
||||
- 未读公告:黄色背景高亮显示
|
||||
- 已读公告:默认白色背景
|
||||
- 点击公告自动标记为已读
|
||||
|
||||
### 5. 分类显示
|
||||
- **通知**(1):蓝色图标
|
||||
- **紧急**(2):红色警告图标
|
||||
- **信息**(3):绿色信息图标
|
||||
- **成功**(4):紫色成功图标
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── NoticePopup/
|
||||
│ │ └── index.vue # 登录后自动弹窗组件
|
||||
│ └── NoticePanel/
|
||||
│ └── index.vue # 手动打开的公告面板
|
||||
├── layout/
|
||||
│ └── index.vue # 主布局,引入 NoticePopup
|
||||
└── views/
|
||||
└── system/
|
||||
└── notice/
|
||||
└── index.vue # 公告管理页面
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 前端 API
|
||||
```javascript
|
||||
// 获取用户公告列表
|
||||
getUserNotices()
|
||||
|
||||
// 标记为已读
|
||||
markAsRead(noticeId)
|
||||
|
||||
// 全部标记为已读
|
||||
markAllAsRead(noticeIds)
|
||||
|
||||
// 获取未读数量
|
||||
getUnreadCount()
|
||||
```
|
||||
|
||||
### 后端接口
|
||||
```java
|
||||
// GET /system/notice/public/notice
|
||||
// 获取当前用户的公告列表(含已读状态)
|
||||
|
||||
// GET /system/notice/public/unread/count
|
||||
// 获取未读公告数量
|
||||
|
||||
// POST /system/notice/public/read/{noticeId}
|
||||
// 标记公告为已读
|
||||
|
||||
// POST /system/notice/public/read/all
|
||||
// 批量标记为已读
|
||||
```
|
||||
|
||||
## 数据库字段
|
||||
|
||||
### sys_notice 表新增字段
|
||||
```sql
|
||||
-- 优先级字段
|
||||
priority VARCHAR(1) DEFAULT '3'
|
||||
|
||||
-- 发布状态字段
|
||||
publish_status VARCHAR(1) DEFAULT '0'
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 数据库迁移
|
||||
执行 SQL 脚本添加必要的字段:
|
||||
```bash
|
||||
# 添加优先级字段
|
||||
ALTER TABLE sys_notice ADD COLUMN priority VARCHAR(1) DEFAULT '3';
|
||||
|
||||
# 添加发布状态字段
|
||||
ALTER TABLE sys_notice ADD COLUMN publish_status VARCHAR(1) DEFAULT '0';
|
||||
```
|
||||
|
||||
### 2. 后台管理
|
||||
在 `系统管理 > 公告管理` 中:
|
||||
1. 点击"新增"创建公告
|
||||
2. 选择优先级:高/中/低
|
||||
3. 选择公告类型:通知/紧急/信息/成功
|
||||
4. 填写标题和内容
|
||||
5. 点击"发布"按钮
|
||||
|
||||
### 3. 用户端显示
|
||||
- 登录后自动弹出未读公告
|
||||
- 点击顶部导航栏铃铛图标可手动打开
|
||||
- 点击公告查看详情
|
||||
- 支持全部标记为已读
|
||||
|
||||
## 样式说明
|
||||
|
||||
### 弹窗样式
|
||||
- 宽度:800px
|
||||
- 高度:最大 600px
|
||||
- 标题栏:渐变紫色背景 `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`
|
||||
- 左右分栏布局
|
||||
|
||||
### 列表样式
|
||||
- 列表宽度:380px
|
||||
- 滚动区域:最大高度 500px
|
||||
- 未读高亮:黄色背景 `#fffbe6`
|
||||
- 激活项:蓝色左边框 `#1890ff`
|
||||
|
||||
### 详情样式
|
||||
- 自适应宽度
|
||||
- 标题字号:18px
|
||||
- 内容字号:14px
|
||||
- 行高:1.8
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**
|
||||
- 只有已发布的公告才会显示
|
||||
- 只有正常状态的公告才会显示
|
||||
|
||||
2. **性能优化**
|
||||
- 使用延迟加载(1秒)
|
||||
- 使用虚拟滚动(el-scrollbar)
|
||||
- 按需加载详情
|
||||
|
||||
3. **用户体验**
|
||||
- 支持点击空白处关闭
|
||||
- 支持ESC键关闭
|
||||
- 未读状态醒目标识
|
||||
- 优先级颜色区分明显
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 弹窗不显示
|
||||
1. 检查后端接口 `/system/notice/public/notice` 是否正常返回
|
||||
2. 检查是否有未读公告
|
||||
3. 检查浏览器控制台是否有错误
|
||||
4. 清除浏览器缓存重试
|
||||
|
||||
### 样式错乱
|
||||
1. 检查 Element Plus 版本是否兼容
|
||||
2. 检查样式是否被其他 CSS 覆盖
|
||||
3. 使用浏览器开发者工具检查样式
|
||||
|
||||
### 数据不更新
|
||||
1. 检查后端返回的数据格式
|
||||
2. 检查 API 调用是否成功
|
||||
3. 检查响应拦截器处理
|
||||
|
||||
## 版本信息
|
||||
- 创建日期:2025-12-30
|
||||
- 功能版本:v1.0
|
||||
- 前端框架:Vue 3 + Element Plus
|
||||
- 后端框架:Spring Boot + MyBatis Plus
|
||||
@@ -1,254 +0,0 @@
|
||||
# 手术人员字段不显示问题解决方案
|
||||
|
||||
## 问题描述
|
||||
主刀医生、麻醉医生、助手1、助手2、执行科这些字段在手术查看页面中没有显示数据。
|
||||
|
||||
## 问题原因
|
||||
这些字段在数据库中可能为 **null 或空值**,虽然保存了 ID(如 `main_surgeon_id`),但没有保存对应的姓名(如 `main_surgeon_name`)。
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 步骤 1:检查数据库中字段的实际值
|
||||
|
||||
执行以下 SQL 查看当前数据:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
surgery_no,
|
||||
main_surgeon_id,
|
||||
main_surgeon_name,
|
||||
anesthetist_id,
|
||||
anesthetist_name,
|
||||
assistant_1_id,
|
||||
assistant_1_name,
|
||||
assistant_2_id,
|
||||
assistant_2_name,
|
||||
operating_room_id,
|
||||
operating_room_name,
|
||||
org_id,
|
||||
org_name
|
||||
FROM public.cli_surgery
|
||||
WHERE delete_flag = '0'
|
||||
ORDER BY create_time DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
**请告诉我结果**:特别是 `main_surgeon_name`、`anesthetist_name`、`assistant_1_name`、`assistant_2_name`、`operating_room_name`、`org_name` 这些字段的值。
|
||||
|
||||
### 步骤 2:检查用户表结构
|
||||
|
||||
执行以下 SQL 查看用户表的结构:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
column_name,
|
||||
data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'sys_user'
|
||||
AND column_name IN ('user_id', 'nick_name', 'user_name', 'practitioner_id')
|
||||
ORDER BY column_name;
|
||||
```
|
||||
|
||||
**目的**:确定人员ID和姓名的对应关系。
|
||||
|
||||
### 步骤 3:填充人员姓名字段(推荐方法)
|
||||
|
||||
使用以下 SQL 脚本填充人员姓名:
|
||||
|
||||
```sql
|
||||
-- 填充主刀医生姓名
|
||||
UPDATE public.cli_surgery s
|
||||
SET main_surgeon_name = u.nick_name
|
||||
FROM public.sys_user u
|
||||
WHERE s.main_surgeon_id = u.user_id
|
||||
AND s.main_surgeon_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
|
||||
-- 填充麻醉医生姓名
|
||||
UPDATE public.cli_surgery s
|
||||
SET anesthetist_name = u.nick_name
|
||||
FROM public.sys_user u
|
||||
WHERE s.anesthetist_id = u.user_id
|
||||
AND s.anesthetist_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
|
||||
-- 填充助手1姓名
|
||||
UPDATE public.cli_surgery s
|
||||
SET assistant_1_name = u.nick_name
|
||||
FROM public.sys_user u
|
||||
WHERE s.assistant_1_id = u.user_id
|
||||
AND s.assistant_1_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
|
||||
-- 填充助手2姓名
|
||||
UPDATE public.cli_surgery s
|
||||
SET assistant_2_name = u.nick_name
|
||||
FROM public.sys_user u
|
||||
WHERE s.assistant_2_id = u.user_id
|
||||
AND s.assistant_2_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
|
||||
-- 填充手术室名称
|
||||
UPDATE public.cli_surgery s
|
||||
SET operating_room_name = r.name
|
||||
FROM public.cli_operating_room r
|
||||
WHERE s.operating_room_id = r.id
|
||||
AND s.operating_room_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
|
||||
-- 填充执行科室名称
|
||||
UPDATE public.cli_surgery s
|
||||
SET org_name = o.name
|
||||
FROM public.adm_organization o
|
||||
WHERE s.org_id = o.id
|
||||
AND s.org_name IS NULL
|
||||
AND s.delete_flag = '0';
|
||||
```
|
||||
|
||||
### 步骤 4:验证更新结果
|
||||
|
||||
执行以下 SQL 验证是否更新成功:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
surgery_no,
|
||||
main_surgeon_id,
|
||||
main_surgeon_name,
|
||||
anesthetist_id,
|
||||
anesthetist_name,
|
||||
assistant_1_id,
|
||||
assistant_1_name,
|
||||
assistant_2_id,
|
||||
assistant_2_name,
|
||||
operating_room_id,
|
||||
operating_room_name,
|
||||
org_id,
|
||||
org_name
|
||||
FROM public.cli_surgery
|
||||
WHERE delete_flag = '0'
|
||||
ORDER BY create_time DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
**预期结果**:所有 `*_name` 字段都应该有值。
|
||||
|
||||
### 步骤 5:刷新前端页面
|
||||
|
||||
1. 刷新手术管理页面
|
||||
2. 点击某个手术记录的"查看"按钮
|
||||
3. 检查详情对话框中是否显示这些字段
|
||||
|
||||
## 前端代码检查
|
||||
|
||||
### 1. 检查详情对话框显示
|
||||
|
||||
打开 `surgerymanage/index.vue` 文件,查看详情对话框部分:
|
||||
|
||||
```vue
|
||||
<el-descriptions-item label="主刀医生">{{ viewData.mainSurgeonName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="麻醉医生">{{ viewData.anesthetistName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="助手1">{{ viewData.assistant1Name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="助手2">{{ viewData.assistant2Name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手术室">{{ viewData.operatingRoomName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行科室">{{ viewData.orgName }}</el-descriptions-item>
|
||||
```
|
||||
|
||||
**确认**:这些字段名是否正确(注意驼峰命名)。
|
||||
|
||||
### 2. 检查浏览器控制台
|
||||
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 Console 标签
|
||||
3. 点击"查看"按钮
|
||||
4. 查看是否有 JavaScript 错误
|
||||
|
||||
### 3. 检查 Network 响应
|
||||
|
||||
1. 切换到 Network 标签
|
||||
2. 点击"查看"按钮
|
||||
3. 找到 `/clinical-manage/surgery/surgery-detail` 请求
|
||||
4. 查看响应内容
|
||||
|
||||
**检查**:响应数据中是否包含这些字段,值是什么。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题 1:UPDATE SQL 执行失败
|
||||
|
||||
**症状**:报错 "relation does not exist" 或 "column does not exist"
|
||||
|
||||
**解决**:
|
||||
1. 检查表名是否正确(sys_user 或 adm_practitioner)
|
||||
2. 检查字段名是否正确(user_id 或 practitioner_id)
|
||||
|
||||
### 问题 2:UPDATE 后字段仍为 null
|
||||
|
||||
**症状**:UPDATE 执行成功,但字段仍为 null
|
||||
|
||||
**原因**:关联表中没有对应的记录
|
||||
|
||||
**解决**:检查人员ID是否存在于人员表中
|
||||
|
||||
```sql
|
||||
-- 检查主刀医生ID是否存在
|
||||
SELECT s.main_surgeon_id, u.nick_name
|
||||
FROM public.cli_surgery s
|
||||
LEFT JOIN public.sys_user u ON s.main_surgeon_id = u.user_id
|
||||
WHERE s.main_surgeon_id IS NOT NULL
|
||||
AND u.user_id IS NULL
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
### 问题 3:前端仍然不显示
|
||||
|
||||
**症状**:数据库中有值,但前端不显示
|
||||
|
||||
**原因**:
|
||||
1. 前端字段名不匹配
|
||||
2. 前端数据绑定有问题
|
||||
|
||||
**解决**:
|
||||
1. 检查 MyBatis XML 映射是否正确
|
||||
2. 检查后端返回的 JSON 数据结构
|
||||
3. 检查前端变量名是否正确
|
||||
|
||||
## 后续改进建议
|
||||
|
||||
### 1. 保存时自动填充姓名
|
||||
|
||||
在前端或后端保存手术信息时,根据选择的医生ID自动查询并填充姓名字段。
|
||||
|
||||
### 2. 提供人员选择功能
|
||||
|
||||
在前端提供医生、科室等选择下拉框,而不是手动输入ID。
|
||||
|
||||
### 3. 添加数据完整性校验
|
||||
|
||||
在保存前检查:如果选择了人员ID,必须填充对应的姓名字段。
|
||||
|
||||
## 相关文件
|
||||
|
||||
1. **检查和填充脚本**:`e:/his/检查和填充手术人员字段.sql`
|
||||
2. **填充脚本**:`e:/his/填充手术人员字段姓名.sql`
|
||||
3. **MyBatis 映射**:`e:/his/openhis-server-new/openhis-application/src/main/resources/mapper/clinicalmanage/SurgeryMapper.xml`
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 数据库查询显示字段为 null
|
||||
- [ ] 执行了填充 SQL 脚本
|
||||
- [ ] 验证更新后字段有值
|
||||
- [ ] 刷新前端页面
|
||||
- [ ] 详情对话框中正确显示
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果以上步骤都无法解决问题,请提供:
|
||||
|
||||
1. **步骤 1 的查询结果**:当前数据库中这些字段的值
|
||||
2. **步骤 2 的查询结果**:sys_user 表的结构
|
||||
3. **UPDATE SQL 执行结果**:是否有错误,更新了多少条记录
|
||||
4. **步骤 4 的验证结果**:更新后的字段值
|
||||
5. **浏览器控制台错误**:是否有 JavaScript 错误
|
||||
6. **Network 响应数据**:后端返回的完整数据
|
||||
@@ -1,120 +0,0 @@
|
||||
# 手术和麻醉信息Redis缓存实现说明
|
||||
|
||||
## 概述
|
||||
为提高手术和麻醉信息的查询性能,已将手术信息缓存到Redis中。接口查询时先从Redis缓存获取,如果没有则从数据库查询并更新到Redis缓存。
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. Redis缓存Key定义
|
||||
在 `openhis-common/src/main/java/com/openhis/common/utils/RedisKeys.java` 中定义了以下缓存Key:
|
||||
|
||||
```java
|
||||
// 单个手术信息缓存
|
||||
public static String getSurgeryKey(Long surgeryId)
|
||||
|
||||
// 按患者ID查询的手术列表缓存
|
||||
public static String getSurgeryListByPatientKey(Long patientId)
|
||||
|
||||
// 按就诊ID查询的手术列表缓存
|
||||
public static String getSurgeryListByEncounterKey(Long encounterId)
|
||||
```
|
||||
|
||||
### 2. 缓存实现
|
||||
|
||||
#### 2.1 SurgeryServiceImpl (Domain层)
|
||||
- **getSurgeryById(Long id)**: 根据手术ID查询单个手术信息
|
||||
- 先从Redis缓存获取
|
||||
- 缓存未命中则从数据库查询
|
||||
- 查询结果存入Redis缓存(30分钟过期)
|
||||
|
||||
- **getSurgeryListByPatientId(Long patientId)**: 根据患者ID查询手术列表
|
||||
- 先从Redis缓存获取
|
||||
- 缓存未命中则从数据库查询
|
||||
- 查询结果存入Redis缓存(30分钟过期)
|
||||
|
||||
- **getSurgeryListByEncounterId(Long encounterId)**: 根据就诊ID查询手术列表
|
||||
- 先从Redis缓存获取
|
||||
- 缓存未命中则从数据库查询
|
||||
- 查询结果存入Redis缓存(30分钟过期)
|
||||
|
||||
- **insertSurgery(Surgery surgery)**: 新增手术信息
|
||||
- 插入成功后清除相关缓存
|
||||
|
||||
- **updateSurgery(Surgery surgery)**: 更新手术信息
|
||||
- 更新成功后清除相关缓存
|
||||
|
||||
- **deleteSurgery(Long id)**: 删除手术信息
|
||||
- 删除成功后清除相关缓存
|
||||
|
||||
- **updateSurgeryStatus(Long id, Integer statusEnum)**: 更新手术状态
|
||||
- 更新成功后清除相关缓存
|
||||
|
||||
#### 2.2 SurgeryAppServiceImpl (Application层)
|
||||
- **getSurgeryDetail(Long id)**: 根据ID查询手术详情
|
||||
- 先从Redis缓存获取
|
||||
- 缓存未命中则从数据库查询
|
||||
- 查询结果存入Redis缓存(30分钟过期)
|
||||
|
||||
- **addSurgery(SurgeryDto surgeryDto)**: 新增手术信息
|
||||
- 插入成功后清除相关缓存
|
||||
|
||||
- **updateSurgery(SurgeryDto surgeryDto)**: 更新手术信息
|
||||
- 更新成功后清除相关缓存
|
||||
|
||||
- **deleteSurgery(Long id)**: 删除手术信息
|
||||
- 删除成功后清除相关缓存
|
||||
|
||||
- **updateSurgeryStatus(Long id, Integer statusEnum)**: 更新手术状态
|
||||
- 更新成功后清除相关缓存
|
||||
|
||||
### 3. 缓存清除策略
|
||||
当手术信息发生变化时(新增、更新、删除),会清除以下相关缓存:
|
||||
1. 单个手术信息缓存
|
||||
2. 患者手术列表缓存
|
||||
3. 就诊手术列表缓存
|
||||
|
||||
### 4. 缓存配置
|
||||
- **缓存时间**: 30分钟
|
||||
- **时间单位**: TimeUnit.MINUTES
|
||||
- **序列化**: 使用RedisTemplate默认序列化方式
|
||||
|
||||
## 关于麻醉信息
|
||||
当前项目中,麻醉信息是手术表(`cli_surgery`)中的字段,包括:
|
||||
- 麻醉医生ID (anesthetistId)
|
||||
- 麻醉医生姓名 (anesthetistName)
|
||||
- 麻醉方式编码 (anesthesiaTypeEnum)
|
||||
- 麻醉费用 (anesthesiaFee)
|
||||
|
||||
这些字段已经包含在手术实体的缓存中,无需单独实现麻醉信息的缓存。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 查询手术信息(自动使用缓存)
|
||||
```java
|
||||
// 自动从缓存获取,未命中则查询数据库
|
||||
Surgery surgery = surgeryService.getSurgeryById(surgeryId);
|
||||
```
|
||||
|
||||
### 更新手术信息(自动清除缓存)
|
||||
```java
|
||||
// 更新数据库,同时清除相关缓存
|
||||
surgeryService.updateSurgery(surgery);
|
||||
```
|
||||
|
||||
### 手动清除缓存(如需要)
|
||||
```java
|
||||
String cacheKey = RedisKeys.getSurgeryKey(surgeryId);
|
||||
redisCache.deleteObject(cacheKey);
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
1. 对于频繁访问的手术信息,缓存命中率高,可显著提升查询性能
|
||||
2. 对于不常访问的手术信息,30分钟缓存时间可避免占用过多Redis内存
|
||||
3. 如需调整缓存时间,可修改代码中的 `30, TimeUnit.MINUTES` 参数
|
||||
4. 如需更精细的缓存控制,可考虑使用不同的缓存时间策略(如根据手术状态设置不同过期时间)
|
||||
|
||||
## 监控建议
|
||||
建议监控以下指标:
|
||||
- Redis缓存命中率
|
||||
- Redis内存使用情况
|
||||
- 查询响应时间对比(缓存命中 vs 缓存未命中)
|
||||
@@ -1,256 +0,0 @@
|
||||
# 手术室管理添加类型和所属科室字段功能说明
|
||||
|
||||
## 概述
|
||||
|
||||
本次更新为手术室管理模块添加了"类型"和"所属科室"字段,优化了手术室信息的分类管理。
|
||||
|
||||
**数据库类型**:PostgreSQL
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 1. 手术室类型
|
||||
|
||||
支持四种手术室类型:
|
||||
- **急诊手术室**:用于急诊手术的手术室
|
||||
- **择期手术室**:用于择期手术的手术室(默认类型)
|
||||
- **日间手术室**:用于日间手术的手术室
|
||||
- **复合手术室**:用于复合手术的手术室
|
||||
|
||||
### 2. 所属科室
|
||||
|
||||
每个手术室可以关联到具体的科室,便于科室级别的资源管理。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 前端修改(Vue3)
|
||||
|
||||
#### 1. 手术室列表页面 (`operatingroom/index.vue`)
|
||||
|
||||
**列表表格新增列**:
|
||||
- 类型列:显示手术室类型(急诊手术室、择期手术室等)
|
||||
- 所属科室列:显示手术室所属的科室名称
|
||||
|
||||
**新增/修改对话框新增字段**:
|
||||
- 类型选择器:下拉选择手术室类型
|
||||
- 所属科室选择器:可搜索的科室下拉框
|
||||
|
||||
**查询表单保持原样**:
|
||||
- 仍支持按手术室名称和状态查询
|
||||
|
||||
**查看对话框新增显示**:
|
||||
- 类型信息
|
||||
- 所属科室信息
|
||||
|
||||
#### 2. 表单数据结构
|
||||
|
||||
```javascript
|
||||
const form = ref({
|
||||
id: undefined,
|
||||
busNo: undefined,
|
||||
name: undefined,
|
||||
roomTypeEnum: undefined, // 新增:手术室类型
|
||||
organizationId: undefined, // 已有:所属科室ID
|
||||
organizationName: undefined, // 已有:所属科室名称
|
||||
locationDescription: undefined,
|
||||
equipmentConfig: undefined,
|
||||
capacity: 1,
|
||||
statusEnum: 1,
|
||||
displayOrder: 0,
|
||||
remark: undefined
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. 类型选项配置
|
||||
|
||||
```javascript
|
||||
const roomTypeOptions = ref([
|
||||
{ value: 1, label: '急诊手术室' },
|
||||
{ value: 2, label: '择期手术室' },
|
||||
{ value: 3, label: '日间手术室' },
|
||||
{ value: 4, label: '复合手术室' }
|
||||
])
|
||||
```
|
||||
|
||||
### 后端修改(Java)
|
||||
|
||||
#### 1. 实体类 (`OperatingRoom.java`)
|
||||
|
||||
**新增字段**:
|
||||
```java
|
||||
/** 手术室类型 */
|
||||
@Dict(dictCode = "operating_room_type")
|
||||
private Integer roomTypeEnum;
|
||||
private String roomTypeEnum_dictText;
|
||||
|
||||
/** 所属机构ID */
|
||||
private Long organizationId;
|
||||
|
||||
/** 所属机构名称 */
|
||||
private String organizationName;
|
||||
```
|
||||
|
||||
#### 2. DTO类 (`OperatingRoomDto.java`)
|
||||
|
||||
**新增字段**:
|
||||
```java
|
||||
/** 手术室类型 */
|
||||
@Dict(dictCode = "operating_room_type")
|
||||
private Integer roomTypeEnum;
|
||||
private String roomTypeEnum_dictText;
|
||||
|
||||
/** 所属机构ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long organizationId;
|
||||
|
||||
/** 机构名称 */
|
||||
private String organizationName;
|
||||
```
|
||||
|
||||
#### 3. Service实现类 (`OperatingRoomAppServiceImpl.java`)
|
||||
|
||||
**查询列表方法优化**:
|
||||
- 添加类型字段的枚举值转换逻辑
|
||||
- 根据类型编码设置对应的中文描述
|
||||
|
||||
**详情查询方法优化**:
|
||||
- 添加类型字段的枚举值转换
|
||||
- 查询所属科室的名称并回显
|
||||
|
||||
### 数据库修改
|
||||
|
||||
#### SQL脚本文件:`add_operating_room_type_fields.sql`(PostgreSQL版本)
|
||||
|
||||
**1. 添加字段**:
|
||||
```sql
|
||||
ALTER TABLE public.adm_operating_room
|
||||
ADD COLUMN room_type_enum INTEGER DEFAULT 2;
|
||||
|
||||
COMMENT ON COLUMN public.adm_operating_room.room_type_enum IS
|
||||
'手术室类型:1-急诊手术室,2-择期手术室,3-日间手术室,4-复合手术室';
|
||||
```
|
||||
|
||||
**2. 更新现有数据**:
|
||||
```sql
|
||||
UPDATE public.adm_operating_room
|
||||
SET room_type_enum = 2
|
||||
WHERE room_type_enum IS NULL;
|
||||
```
|
||||
|
||||
**3. 添加索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_room_type ON public.adm_operating_room(room_type_enum);
|
||||
CREATE INDEX idx_org_id ON public.adm_operating_room(organization_id);
|
||||
```
|
||||
|
||||
**4. 字典数据**:
|
||||
- 新增字典类型:`operating_room_type`(手术室类型)
|
||||
- 新增字典项:
|
||||
- 急诊手术室(1)
|
||||
- 择期手术室(2)
|
||||
- 日间手术室(3)
|
||||
- 复合手术室(4)
|
||||
|
||||
**PostgreSQL特定语法说明**:
|
||||
- 使用 `public.adm_operating_room` 替代 `` `adm_operating_room` ``
|
||||
- 使用 `COMMENT ON COLUMN` 替代 `COMMENT` 在 `ALTER TABLE` 中
|
||||
- 使用 `nextval()` 和序列来生成字典类型ID
|
||||
- 使用 `information_schema.columns` 获取列信息
|
||||
- 使用 `CASE WHEN` 语句进行条件判断
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 数据库部署
|
||||
|
||||
执行SQL脚本(PostgreSQL):
|
||||
```bash
|
||||
psql -U postgres -d his_database -f add_operating_room_type_fields.sql
|
||||
```
|
||||
|
||||
或者使用 pgAdmin 等图形化工具执行SQL脚本。
|
||||
|
||||
### 2. 后端部署
|
||||
|
||||
重启后端服务,使新的代码生效。
|
||||
|
||||
### 3. 前端部署
|
||||
|
||||
重新编译并部署前端代码:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 新增手术室
|
||||
|
||||
1. 进入手术室管理页面
|
||||
2. 点击"新增手术室"按钮
|
||||
3. 填写手术室信息:
|
||||
- 手术室名称(必填)
|
||||
- 类型(可选,默认为择期手术室)
|
||||
- 所属科室(必填)
|
||||
- 位置描述
|
||||
- 设备配置
|
||||
- 容纳人数
|
||||
- 状态(默认为启用)
|
||||
- 显示顺序
|
||||
- 备注
|
||||
4. 点击"确定"保存
|
||||
|
||||
### 修改手术室
|
||||
|
||||
1. 在手术室列表中找到要修改的记录
|
||||
2. 点击"编辑"按钮
|
||||
3. 修改相关信息(包括类型和所属科室)
|
||||
4. 点击"确定"保存
|
||||
|
||||
### 查看手术室详情
|
||||
|
||||
1. 在手术室列表中点击"查看"按钮
|
||||
2. 查看完整的手术室信息,包括类型和所属科室
|
||||
|
||||
### 查询手术室
|
||||
|
||||
- 按手术室名称模糊查询
|
||||
- 按状态筛选(启用/停用)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据迁移**:现有手术室的类型默认设置为"择期手术室"(2),可以根据实际需要调整。
|
||||
|
||||
2. **科室关联**:所属科室是必填字段,需要在科室管理中先配置好科室信息。
|
||||
|
||||
3. **类型字典**:手术室类型字典已自动创建,可以在系统字典管理中进行维护。
|
||||
|
||||
4. **索引优化**:已为类型和科室字段添加索引,提升查询性能。
|
||||
|
||||
5. **兼容性**:此次修改保持了向后兼容性,不影响现有功能。
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 数据库字段添加成功
|
||||
- [ ] 字典数据创建成功
|
||||
- [ ] 前端列表正确显示类型和所属科室
|
||||
- [ ] 新增手术室时可选择类型和所属科室
|
||||
- [ ] 修改手术室时可更新类型和所属科室
|
||||
- [ ] 查看手术室详情时正确显示类型和所属科室
|
||||
- [ ] 类型下拉选项显示正确
|
||||
- [ ] 所属科室选择器可正常搜索和选择
|
||||
- [ ] 查询功能正常工作
|
||||
- [ ] 没有语法错误和运行时错误
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如需撤销本次修改,请执行SQL脚本中的回滚语句(PostgreSQL):
|
||||
|
||||
```sql
|
||||
DROP INDEX idx_room_type ON public.adm_operating_room;
|
||||
DROP INDEX idx_org_id ON public.adm_operating_room;
|
||||
DELETE FROM public.sys_dict_data WHERE dict_type = 'operating_room_type';
|
||||
DELETE FROM public.sys_dict_type WHERE dict_type = 'operating_room_type';
|
||||
ALTER TABLE public.adm_operating_room DROP COLUMN room_type_enum;
|
||||
```
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系技术支持团队。
|
||||
@@ -1,215 +0,0 @@
|
||||
# 手术申请医生科室字段保存问题解决方案
|
||||
|
||||
## 问题确认
|
||||
数据库中只保存了 ID 字段(`apply_doctor_id`、`apply_dept_id`),但没有保存名称字段(`apply_doctor_name`、`apply_dept_name`)。
|
||||
|
||||
## 根本原因
|
||||
**数据库表中缺少 `apply_doctor_name` 和 `apply_dept_name` 这两个字段!**
|
||||
|
||||
虽然 MyBatis 映射文件和实体类都配置了这些字段,但如果数据库表中不存在这些列,MyBatis 在插入时会静默忽略这些字段(不会报错),导致只有 ID 被保存。
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 步骤 1:执行数据库迁移脚本(必须!)
|
||||
|
||||
使用 Navicat Premium 17 执行以下 SQL:
|
||||
|
||||
```sql
|
||||
-- 方法1:使用 IF NOT EXISTS 语法(推荐)
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN IF NOT EXISTS apply_doctor_name VARCHAR(100);
|
||||
COMMENT ON COLUMN public.cli_surgery.apply_doctor_name IS '申请医生姓名';
|
||||
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN IF NOT EXISTS apply_dept_name VARCHAR(100);
|
||||
COMMENT ON COLUMN public.cli_surgery.apply_dept_name IS '申请科室名称';
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
SELECT column_name, data_type, character_maximum_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'cli_surgery'
|
||||
AND column_name IN ('apply_doctor_name', 'apply_dept_name');
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
```
|
||||
apply_doctor_name | character varying | 100
|
||||
apply_dept_name | character varying | 100
|
||||
```
|
||||
|
||||
### 步骤 2:重启后端服务
|
||||
|
||||
执行数据库迁移后,必须重启后端服务以重新加载表结构。
|
||||
|
||||
### 步骤 3:新增手术并查看日志
|
||||
|
||||
1. 打开后端控制台或日志文件
|
||||
2. 在前端新增一条手术记录
|
||||
3. 查看后端日志,应该能看到:
|
||||
|
||||
```
|
||||
设置申请医生信息 - doctorId: 123, doctorName: 张医生, deptId: 456, deptName: 普外科
|
||||
前端提交的数据 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, applyDeptName: 普外科
|
||||
准备插入手术记录 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, deptName: 普外科
|
||||
准备插入手术记录 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, deptName: 普外科
|
||||
插入后查询结果 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, deptName: 普外科
|
||||
手术记录插入成功 - surgeryId: 1234567890123456789, surgeryNo: OP202501051234
|
||||
```
|
||||
|
||||
**关键检查点**:
|
||||
- `准备插入手术记录` 这行日志中,`applyDoctorName` 和 `applyDeptName` 必须有值(不能为 null)
|
||||
- `插入后查询结果` 这行日志中,这两个字段也必须有值
|
||||
|
||||
### 步骤 4:验证数据库
|
||||
|
||||
执行以下 SQL 查询最新插入的记录:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
surgery_no,
|
||||
apply_doctor_id,
|
||||
apply_doctor_name,
|
||||
apply_dept_id,
|
||||
apply_dept_name,
|
||||
surgery_name,
|
||||
create_time
|
||||
FROM public.cli_surgery
|
||||
WHERE delete_flag = '0'
|
||||
ORDER BY create_time DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- `apply_doctor_id`:有值(例如:123)
|
||||
- `apply_doctor_name`:有值(例如:张医生)
|
||||
- `apply_dept_id`:有值(例如:456)
|
||||
- `apply_dept_name`:有值(例如:普外科)
|
||||
|
||||
### 步骤 5:测试前端显示
|
||||
|
||||
1. 刷新手术管理页面
|
||||
2. 查看列表中是否显示申请医生和申请科室列
|
||||
3. 点击"查看"或"编辑"按钮,检查详情对话框是否显示这些信息
|
||||
|
||||
## 常见问题和解决
|
||||
|
||||
### 问题 1:执行 SQL 后报错 "column does not exist"
|
||||
|
||||
**原因**:数据库表结构可能不同,或者表名不是 `cli_surgery`
|
||||
|
||||
**解决**:先执行以下 SQL 检查表名:
|
||||
|
||||
```sql
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name LIKE '%surgery%'
|
||||
AND table_schema = 'public';
|
||||
```
|
||||
|
||||
### 问题 2:执行 SQL 后字段仍然不存在
|
||||
|
||||
**原因**:可能是权限问题或 SQL 语法问题
|
||||
|
||||
**解决**:尝试使用更简单的方式:
|
||||
|
||||
```sql
|
||||
-- 先检查表结构
|
||||
\d public.cli_surgery
|
||||
|
||||
-- 手动添加字段(如果不存在)
|
||||
-- 注意:如果字段已存在,这个语句会报错,这是正常的
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN apply_doctor_name VARCHAR(100);
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN apply_dept_name VARCHAR(100);
|
||||
```
|
||||
|
||||
### 问题 3:字段添加成功,但插入时仍然为空
|
||||
|
||||
**原因**:MyBatis 或 MyBatis-Plus 配置问题
|
||||
|
||||
**解决**:
|
||||
1. 检查实体类字段是否有 `@TableField` 注解
|
||||
2. 检查字段名是否与数据库列名一致
|
||||
3. 查看后端日志中的 `插入后查询结果`
|
||||
|
||||
### 问题 4:后端日志显示字段为 null
|
||||
|
||||
**原因**:后端代码中 `applyDoctorName` 或 `applyDeptName` 被设置为 null
|
||||
|
||||
**解决**:
|
||||
1. 检查 `SecurityUtils.getLoginUser().getUser().getNickName()` 是否返回 null
|
||||
2. 检查 `SecurityUtils.getLoginUser().getOrgId()` 是否返回 null
|
||||
3. 检查 `organizationService.getById(orgId)` 是否返回 null
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 数据库迁移脚本已执行
|
||||
- [ ] 数据库字段已添加(步骤 1 验证 SQL 有结果)
|
||||
- [ ] 后端服务已重启
|
||||
- [ ] 后端日志显示 `准备插入手术记录` 且字段有值
|
||||
- [ ] 后端日志显示 `插入后查询结果` 且字段有值
|
||||
- [ ] 数据库查询显示字段有值(步骤 4)
|
||||
- [ ] 前端列表正确显示
|
||||
- [ ] 前端详情正确显示
|
||||
|
||||
## 调试 SQL 脚本
|
||||
|
||||
如果需要手动测试插入功能,可以执行:
|
||||
|
||||
```sql
|
||||
-- 测试插入(确保字段存在)
|
||||
INSERT INTO public.cli_surgery (
|
||||
surgery_no,
|
||||
patient_id,
|
||||
encounter_id,
|
||||
apply_doctor_id,
|
||||
apply_doctor_name,
|
||||
apply_dept_id,
|
||||
apply_dept_name,
|
||||
surgery_name,
|
||||
status_enum,
|
||||
delete_flag,
|
||||
create_time,
|
||||
update_time
|
||||
) VALUES (
|
||||
'TEST202501050002',
|
||||
(SELECT id FROM public.adm_patient WHERE delete_flag = '0' LIMIT 1),
|
||||
(SELECT id FROM public.adm_encounter WHERE delete_flag = '0' LIMIT 1),
|
||||
999,
|
||||
'手动测试医生',
|
||||
999,
|
||||
'手动测试科室',
|
||||
'手动测试手术',
|
||||
0,
|
||||
'0',
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
-- 查询刚才插入的数据
|
||||
SELECT
|
||||
surgery_no,
|
||||
apply_doctor_id,
|
||||
apply_doctor_name,
|
||||
apply_dept_id,
|
||||
apply_dept_name,
|
||||
surgery_name
|
||||
FROM public.cli_surgery
|
||||
WHERE surgery_no = 'TEST202501050002';
|
||||
|
||||
-- 清理测试数据
|
||||
-- DELETE FROM public.cli_surgery WHERE surgery_no = 'TEST202501050002';
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果以上步骤都无法解决问题,请提供:
|
||||
|
||||
1. **数据库表结构查询结果**:
|
||||
```sql
|
||||
\d public.cli_surgery
|
||||
```
|
||||
|
||||
2. **后端日志**:特别是 `准备插入手术记录` 和 `插入后查询结果` 这两行
|
||||
|
||||
3. **数据库查询结果**:执行步骤 4 中的 SQL,告诉我结果
|
||||
|
||||
4. **错误信息**:如果有任何错误提示
|
||||
@@ -1,194 +0,0 @@
|
||||
# 手术申请医生科室数据保存问题排查指南
|
||||
|
||||
## 问题现象
|
||||
新增手术后,列表页面和编辑查看页面没有显示申请医生名称和科室名称。
|
||||
|
||||
## 排查步骤
|
||||
|
||||
### 步骤1:检查数据库字段是否存在
|
||||
执行以下 SQL 检查字段是否已添加:
|
||||
|
||||
```sql
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'cli_surgery'
|
||||
AND column_name IN ('apply_doctor_name', 'apply_dept_name');
|
||||
```
|
||||
|
||||
**预期结果**:应该返回两条记录
|
||||
```
|
||||
apply_doctor_name | character varying
|
||||
apply_dept_name | character varying
|
||||
```
|
||||
|
||||
**如果结果为空**:说明字段未添加,需要执行迁移脚本。
|
||||
|
||||
### 步骤2:检查数据库迁移脚本是否执行
|
||||
执行迁移脚本(如果未执行):
|
||||
|
||||
```sql
|
||||
-- 检查并添加申请医生姓名字段
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'cli_surgery'
|
||||
AND column_name = 'apply_doctor_name'
|
||||
) THEN
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN apply_doctor_name VARCHAR(100);
|
||||
COMMENT ON COLUMN public.cli_surgery.apply_doctor_name IS '申请医生姓名';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 检查并添加申请科室名称字段
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'cli_surgery'
|
||||
AND column_name = 'apply_dept_name'
|
||||
) THEN
|
||||
ALTER TABLE public.cli_surgery ADD COLUMN apply_dept_name VARCHAR(100);
|
||||
COMMENT ON COLUMN public.cli_surgery.apply_dept_name IS '申请科室名称';
|
||||
END IF;
|
||||
END $$;
|
||||
```
|
||||
|
||||
### 步骤3:重启后端服务
|
||||
执行数据库迁移后,必须重启后端服务。
|
||||
|
||||
### 步骤4:新增手术并查看后端日志
|
||||
1. 打开后端控制台或日志文件
|
||||
2. 在前端新增一条手术记录
|
||||
3. 查看后端日志,应该能看到以下信息:
|
||||
|
||||
```
|
||||
设置申请医生信息 - doctorId: 123, doctorName: 张医生, deptId: 456, deptName: 普外科
|
||||
前端提交的数据 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, applyDeptName: 普外科
|
||||
准备插入手术记录 - applyDoctorId: 123, applyDoctorName: 张医生, applyDeptId: 456, deptName: 普外科
|
||||
手术记录插入成功 - surgeryId: 1234567890123456789, surgeryNo: OP202501051234
|
||||
```
|
||||
|
||||
**如果看不到这些日志**:说明代码没有执行到这里,检查是否有异常抛出。
|
||||
|
||||
**如果看到 "前端提交的数据 - applyDoctorName: null"**:说明前端提交的数据为空,需要检查前端代码。
|
||||
|
||||
### 步骤5:检查数据库中是否保存成功
|
||||
执行以下 SQL 查询最新插入的记录:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
surgery_no,
|
||||
patient_id,
|
||||
apply_doctor_id,
|
||||
apply_doctor_name,
|
||||
apply_dept_id,
|
||||
apply_dept_name,
|
||||
surgery_name,
|
||||
status_enum,
|
||||
create_time
|
||||
FROM public.cli_surgery
|
||||
WHERE delete_flag = '0'
|
||||
ORDER BY create_time DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
**如果 apply_doctor_name 和 apply_dept_name 字段为空**:说明数据没有保存成功。
|
||||
|
||||
**如果字段有值**:说明保存成功,问题出在前端显示。
|
||||
|
||||
### 步骤6:检查前端 API 响应
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 Network 标签
|
||||
3. 新增手术
|
||||
4. 找到 `/clinical-manage/surgery/surgery-page` 请求
|
||||
5. 点击查看响应内容
|
||||
|
||||
检查响应数据中是否包含 `applyDoctorName` 和 `applyDeptName` 字段。
|
||||
|
||||
**如果响应中没有这些字段**:说明 MyBatis 映射有问题,检查 XML 配置。
|
||||
|
||||
**如果响应中有这些字段但值为 null**:说明数据库中为空,回到步骤5。
|
||||
|
||||
### 步骤7:检查前端表格显示
|
||||
查看前端代码中的表格列配置:
|
||||
|
||||
```vue
|
||||
<el-table-column label="申请医生" align="center" prop="applyDoctorName" width="100" />
|
||||
<el-table-column label="申请科室" align="center" prop="applyDeptName" width="120" show-overflow-tooltip />
|
||||
```
|
||||
|
||||
确保 `prop` 属性与后端返回的字段名一致(注意大小写)。
|
||||
|
||||
## 常见问题和解决方案
|
||||
|
||||
### 问题1:数据库字段未添加
|
||||
**症状**:后端报错 "column apply_doctor_name does not exist"
|
||||
**解决**:执行数据库迁移脚本
|
||||
|
||||
### 问题2:后端日志显示 applyDoctorName 为 null
|
||||
**症状**:日志中 "前端提交的数据 - applyDoctorName: null"
|
||||
**原因**:前端提交数据时,disabled 字段没有被包含
|
||||
**解决**:检查前端 submitForm 函数,确保手动设置了这些字段
|
||||
|
||||
### 问题3:数据库中有值,但前端不显示
|
||||
**症状**:数据库查询有值,前端响应也有值,但表格不显示
|
||||
**原因**:
|
||||
1. 前端 prop 属性名与后端字段名不一致(大小写问题)
|
||||
2. 前端数据未正确绑定
|
||||
**解决**:
|
||||
1. 检查 prop 属性名,确保与后端返回的 JSON 字段名一致
|
||||
2. 检查浏览器控制台是否有 JavaScript 错误
|
||||
|
||||
### 问题4:MyBatis 映射未生效
|
||||
**症状**:后端保存成功,但查询时字段为 null
|
||||
**原因**:XML 映射文件未正确配置
|
||||
**解决**:
|
||||
1. 检查 SurgeryMapper.xml 中的 resultMap 配置
|
||||
2. 检查 SQL 查询中是否包含这些字段
|
||||
3. 重启后端服务
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 数据库迁移脚本已执行
|
||||
- [ ] 数据库字段已添加(步骤1)
|
||||
- [ ] 后端服务已重启
|
||||
- [ ] 后端日志显示申请医生信息(步骤4)
|
||||
- [ ] 数据库中已保存数据(步骤5)
|
||||
- [ ] 前端 API 响应包含这些字段(步骤6)
|
||||
- [ ] 前端表格正确显示(步骤7)
|
||||
|
||||
## 附加 SQL 脚本
|
||||
|
||||
### 查看统计信息
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
COUNT(apply_doctor_name) as has_doctor_name_count,
|
||||
COUNT(apply_dept_name) as has_dept_name_count
|
||||
FROM public.cli_surgery
|
||||
WHERE delete_flag = '0';
|
||||
```
|
||||
|
||||
### 手动更新测试数据
|
||||
如果需要手动更新已有的测试数据:
|
||||
|
||||
```sql
|
||||
UPDATE public.cli_surgery
|
||||
SET apply_doctor_name = '测试医生',
|
||||
apply_dept_name = '测试科室'
|
||||
WHERE apply_doctor_name IS NULL
|
||||
AND delete_flag = '0';
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果以上步骤都无法解决问题,请提供以下信息:
|
||||
1. 数据库字段查询结果(步骤1)
|
||||
2. 后端日志截图(步骤4)
|
||||
3. 数据库查询结果(步骤5)
|
||||
4. 浏览器 Network 响应截图(步骤6)
|
||||
5. 浏览器 Console 错误信息
|
||||
351
md/手术管理模块开发说明.md
351
md/手术管理模块开发说明.md
@@ -1,351 +0,0 @@
|
||||
# 手术管理模块开发说明
|
||||
|
||||
## 模块概述
|
||||
|
||||
手术管理模块是一个完整的医疗手术管理系统,涵盖从手术排期、执行到记录的全流程管理。本模块基于经典的Spring Boot + Vue3前后端分离架构开发。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 手术信息管理
|
||||
- 手术基本信息录入(手术名称、编码、类型、等级)
|
||||
- 患者信息关联
|
||||
- 就诊信息关联
|
||||
- 手术部位描述
|
||||
|
||||
### 2. 手术团队管理
|
||||
- 主刀医生选择
|
||||
- 麻醉医生选择
|
||||
- 助手1/助手2选择
|
||||
- 巡回护士选择
|
||||
- 麻醉方式选择
|
||||
|
||||
### 3. 手术状态管理
|
||||
- 待排期
|
||||
- 已排期
|
||||
- 手术中
|
||||
- 已完成
|
||||
- 已取消
|
||||
- 暂停
|
||||
|
||||
### 4. 手术时间管理
|
||||
- 计划手术时间
|
||||
- 实际开始时间
|
||||
- 实际结束时间
|
||||
|
||||
### 5. 诊断信息管理
|
||||
- 术前诊断
|
||||
- 术后诊断
|
||||
- 手术经过描述
|
||||
- 术后医嘱
|
||||
- 并发症描述
|
||||
|
||||
### 6. 手术费用管理
|
||||
- 手术费用
|
||||
- 麻醉费用
|
||||
- 总费用自动计算
|
||||
|
||||
### 7. 手术切口管理
|
||||
- 切口等级(I级、II级、III级、IV级)
|
||||
- 愈合等级(甲级、乙级、丙级)
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 后端技术栈
|
||||
- Spring Boot 2.x
|
||||
- MyBatis Plus
|
||||
- PostgreSQL 12+
|
||||
- JDK 1.8+
|
||||
|
||||
### 前端技术栈
|
||||
- Vue 3.x
|
||||
- Element Plus
|
||||
- Axios
|
||||
- Vite
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
openh-is/
|
||||
├── openhis-server-new/ # 后端项目
|
||||
│ ├── openhis-domain/ # 领域层
|
||||
│ │ └── src/main/java/com/openhis/
|
||||
│ │ ├── clinical/
|
||||
│ │ │ ├── domain/
|
||||
│ │ │ │ └── Surgery.java # 手术实体类
|
||||
│ │ │ ├── mapper/
|
||||
│ │ │ │ └── SurgeryMapper.java # 手术Mapper接口
|
||||
│ │ │ └── service/
|
||||
│ │ │ ├── ISurgeryService.java # 手术Service接口
|
||||
│ │ │ └── impl/
|
||||
│ │ │ └── SurgeryServiceImpl.java # 手术Service实现
|
||||
│ │ └── common/ # 公共模块
|
||||
│ │ └── src/main/java/com/openhis/common/
|
||||
│ │ └── enums/ # 枚举类
|
||||
│ │ ├── SurgeryTypeEnum.java # 手术类型枚举
|
||||
│ │ ├── SurgeryStatusEnum.java # 手术状态枚举
|
||||
│ │ ├── SurgeryLevelEnum.java # 手术等级枚举
|
||||
│ │ ├── AnesthesiaTypeEnum.java # 麻醉方式枚举
|
||||
│ │ ├── IncisionLevelEnum.java # 切口等级枚举
|
||||
│ │ └── HealingLevelEnum.java # 愈合等级枚举
|
||||
│ │
|
||||
│ ├── openhis-application/ # 应用层
|
||||
│ │ └── src/main/java/com/openhis/web/clinicalmanage/
|
||||
│ │ ├── controller/
|
||||
│ │ │ └── SurgeryController.java # 手术控制器
|
||||
│ │ ├── dto/
|
||||
│ │ │ └── SurgeryDto.java # 手术数据传输对象
|
||||
│ │ ├── appservice/
|
||||
│ │ │ ├── ISurgeryAppService.java # 手术应用服务接口
|
||||
│ │ │ └── impl/
|
||||
│ │ │ └── SurgeryAppServiceImpl.java # 手术应用服务实现
|
||||
│ │ └── mapper/
|
||||
│ │ └── SurgeryAppMapper.java # 手术应用Mapper
|
||||
│ │
|
||||
│ └── src/main/resources/mapper/
|
||||
│ ├── clinical/
|
||||
│ │ └── SurgeryMapper.xml # 手术Mapper XML
|
||||
│ └── clinicalmanage/
|
||||
│ └── SurgeryMapper.xml # 手术应用Mapper XML
|
||||
│
|
||||
├── openhis-ui-vue3/ # 前端项目
|
||||
│ └── src/
|
||||
│ ├── api/
|
||||
│ │ └── surgerymanage.js # 手术API接口
|
||||
│ └── views/
|
||||
│ └── surgerymanage/
|
||||
│ └── index.vue # 手术管理页面
|
||||
│
|
||||
└── surgery_manage_init.sql # 数据库初始化脚本
|
||||
```
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 主表:cli_surgery(手术管理表)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | bigint | 主键ID |
|
||||
| surgery_no | varchar(50) | 手术编号(唯一) |
|
||||
| patient_id | bigint | 患者ID |
|
||||
| patient_name | varchar(100) | 患者姓名 |
|
||||
| encounter_id | bigint | 就诊ID |
|
||||
| surgery_name | varchar(200) | 手术名称 |
|
||||
| surgery_code | varchar(100) | 手术编码 |
|
||||
| surgery_type_enum | int2 | 手术类型 |
|
||||
| surgery_level | int2 | 手术等级 |
|
||||
| status_enum | int2 | 手术状态 |
|
||||
| planned_time | timestamp | 计划手术时间 |
|
||||
| actual_start_time | timestamp | 实际开始时间 |
|
||||
| actual_end_time | timestamp | 实际结束时间 |
|
||||
| main_surgeon_id | bigint | 主刀医生ID |
|
||||
| main_surgeon_name | varchar(100) | 主刀医生姓名 |
|
||||
| anesthetist_id | bigint | 麻醉医生ID |
|
||||
| anesthetist_name | varchar(100) | 麻醉医生姓名 |
|
||||
| anesthesia_type_enum | int2 | 麻醉方式 |
|
||||
| body_site | varchar(200) | 手术部位 |
|
||||
| preoperative_diagnosis | text | 术前诊断 |
|
||||
| postoperative_diagnosis | text | 术后诊断 |
|
||||
| surgery_fee | numeric(10,2) | 手术费用 |
|
||||
| anesthesia_fee | numeric(10,2) | 麻醉费用 |
|
||||
| total_fee | numeric(10,2) | 总费用 |
|
||||
|
||||
### 字典表
|
||||
|
||||
手术管理模块包含以下字典类型:
|
||||
- surgery_status(手术状态)
|
||||
- surgery_type(手术类型)
|
||||
- surgery_level(手术等级)
|
||||
- anesthesia_type(麻醉方式)
|
||||
- incision_level(切口等级)
|
||||
- healing_level(愈合等级)
|
||||
|
||||
## 安装部署
|
||||
|
||||
### 1. 数据库初始化
|
||||
|
||||
执行SQL脚本初始化数据库表和字典数据:
|
||||
|
||||
```bash
|
||||
psql -U postgres -d his_database -f surgery_manage_init.sql
|
||||
```
|
||||
|
||||
或者使用psql客户端执行:
|
||||
|
||||
```sql
|
||||
\i /path/to/surgery_manage_init.sql
|
||||
```
|
||||
|
||||
### 2. 后端配置
|
||||
|
||||
1. 将后端代码复制到对应目录
|
||||
2. 修改数据库连接配置(application.yml)
|
||||
3. 启动Spring Boot应用
|
||||
|
||||
### 3. 前端配置
|
||||
|
||||
1. 将前端代码复制到对应目录
|
||||
2. 配置API接口地址(.env.development)
|
||||
3. 启动前端开发服务器
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## API接口说明
|
||||
|
||||
### 1. 分页查询手术列表
|
||||
|
||||
**接口地址:** `GET /clinical-manage/surgery/surgery-page`
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"pageNo": 1,
|
||||
"pageSize": 10,
|
||||
"surgeryNo": "SS20251230001",
|
||||
"surgeryName": "阑尾切除术",
|
||||
"patientName": "张三",
|
||||
"statusEnum": 1,
|
||||
"surgeryTypeEnum": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 查询手术详情
|
||||
|
||||
**接口地址:** `GET /clinical-manage/surgery/surgery-detail`
|
||||
|
||||
**请求参数:**
|
||||
```
|
||||
id: 手术ID
|
||||
```
|
||||
|
||||
### 3. 新增手术
|
||||
|
||||
**接口地址:** `POST /clinical-manage/surgery/surgery`
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"patientId": 1,
|
||||
"surgeryName": "阑尾切除术",
|
||||
"surgeryCode": "ICD-9-CM:47.09",
|
||||
"surgeryTypeEnum": 2,
|
||||
"surgeryLevel": 2,
|
||||
"plannedTime": "2025-12-31 09:00:00",
|
||||
"mainSurgeonId": 10,
|
||||
"anesthetistId": 11,
|
||||
"anesthesiaTypeEnum": 3,
|
||||
"bodySite": "腹部"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 修改手术
|
||||
|
||||
**接口地址:** `PUT /clinical-manage/surgery/surgery`
|
||||
|
||||
**请求参数:** 同新增手术,需包含id
|
||||
|
||||
### 5. 删除手术
|
||||
|
||||
**接口地址:** `DELETE /clinical-manage/surgery/surgery`
|
||||
|
||||
**请求参数:**
|
||||
```
|
||||
id: 手术ID
|
||||
```
|
||||
|
||||
### 6. 更新手术状态
|
||||
|
||||
**接口地址:** `PUT /clinical-manage/surgery/surgery-status`
|
||||
|
||||
**请求参数:**
|
||||
```
|
||||
id: 手术ID
|
||||
statusEnum: 状态值
|
||||
```
|
||||
|
||||
## 前端页面功能
|
||||
|
||||
### 1. 查询功能
|
||||
- 支持按手术编号、手术名称、患者姓名模糊查询
|
||||
- 支持按手术状态、手术类型精确查询
|
||||
- 支持按计划时间范围查询
|
||||
|
||||
### 2. 新增功能
|
||||
- 完整的手术信息录入表单
|
||||
- 患者下拉选择
|
||||
- 医生/护士下拉选择
|
||||
- 费用自动计算
|
||||
|
||||
### 3. 编辑功能
|
||||
- 仅待排期和已排期状态的手术可编辑
|
||||
- 手术中或已完成的手术不可编辑
|
||||
|
||||
### 4. 状态流转
|
||||
- 已排期 → 手术中
|
||||
- 手术中 → 已完成
|
||||
- 待排期/已排期 → 已取消
|
||||
|
||||
### 5. 删除功能
|
||||
- 仅待排期和已排期状态的手术可删除
|
||||
- 已完成的手术不能删除
|
||||
|
||||
## 扩展开发建议
|
||||
|
||||
### 1. 手术排期管理
|
||||
- 可增加手术排期日历视图
|
||||
- 手术室资源冲突检测
|
||||
- 手术排队优先级管理
|
||||
|
||||
### 2. 手术统计报表
|
||||
- 手术量统计
|
||||
- 手术类型分布
|
||||
- 手术成功率统计
|
||||
- 手术费用统计
|
||||
|
||||
### 3. 手术文档管理
|
||||
- 手术知情同意书
|
||||
- 手术安全核查表
|
||||
- 手术记录单
|
||||
- 麻醉记录单
|
||||
|
||||
### 4. 手术质控管理
|
||||
- 手术质量评估
|
||||
- 并发症统计
|
||||
- 术后恢复跟踪
|
||||
- 手术质量指标管理
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **手术编号生成**:手术编号采用自动生成机制,格式为SS + 10位数字
|
||||
2. **权限控制**:需要配置相应的菜单权限和操作权限
|
||||
3. **数据校验**:新增手术时必须选择患者和主刀医生
|
||||
4. **状态流转**:手术状态的流转需要符合业务逻辑
|
||||
5. **费用计算**:总费用自动计算,不允许手动修改
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 手术编号重复怎么办?
|
||||
A: 手术编号是系统自动生成的唯一编号,不会重复。如果需要自定义编号,需要修改SurgeryServiceImpl中的生成逻辑。
|
||||
|
||||
### Q2: 如何添加新的手术类型?
|
||||
A: 在数据库sys_dict_data表中添加新的surgery_type字典数据即可。
|
||||
|
||||
### Q3: 手术开始后还能修改信息吗?
|
||||
A: 根据业务规则,手术开始后不允许修改基本信息,但可以补充术后诊断等信息。
|
||||
|
||||
### Q4: 如何实现手术室资源管理?
|
||||
A: 可以新增手术室管理模块,建立手术排期与手术室的关联关系,实现资源冲突检测。
|
||||
|
||||
## 版本历史
|
||||
|
||||
- v1.0.0 (2025-12-30)
|
||||
- 初始版本发布
|
||||
- 实现手术基本管理功能
|
||||
- 实现手术状态流转
|
||||
- 实现手术团队管理
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
@@ -1,160 +0,0 @@
|
||||
# 门诊就诊记录SQL查询优化建议
|
||||
|
||||
## 当前查询分析
|
||||
|
||||
### 主要查询表
|
||||
```sql
|
||||
SELECT
|
||||
enc.id as encounterId,
|
||||
pt.name,
|
||||
pt.id_card,
|
||||
pt.bus_no as patientBusNo,
|
||||
enc.bus_no as encounterBusNo,
|
||||
pt.gender_enum,
|
||||
pt.phone,
|
||||
enc.create_time as encounterTime,
|
||||
enc.status_enum as subjectStatusEnum,
|
||||
org.name as organizationName,
|
||||
prac.name as doctorName
|
||||
FROM adm_encounter AS enc
|
||||
LEFT JOIN adm_organization AS org ON enc.organization_id = org.ID AND org.delete_flag = '0'
|
||||
LEFT JOIN adm_encounter_participant AS ep
|
||||
ON enc.ID = ep.encounter_id AND ep.type_code = #{participantType} AND ep.delete_flag = '0'
|
||||
LEFT JOIN adm_practitioner AS prac ON ep.practitioner_id = prac.ID AND prac.delete_flag = '0'
|
||||
LEFT JOIN adm_patient AS pt ON enc.patient_id = pt.ID AND pt.delete_flag = '0'
|
||||
```
|
||||
|
||||
### 常见查询条件
|
||||
1. `enc.delete_flag = '0'`
|
||||
2. `enc.tenant_id = ?`
|
||||
3. `pt.name LIKE ?`
|
||||
4. `pt.id_card LIKE ?`
|
||||
5. `pt.bus_no LIKE ?`
|
||||
6. `enc.bus_no LIKE ?`
|
||||
7. `pt.gender_enum = ?`
|
||||
8. `enc.status_enum = ?`
|
||||
9. `prac.name LIKE ?`
|
||||
10. `pt.phone LIKE ?`
|
||||
11. `enc.create_time BETWEEN ? AND ?`
|
||||
|
||||
## 索引优化建议
|
||||
|
||||
### 1. adm_encounter 表索引
|
||||
```sql
|
||||
-- 复合索引:提高查询性能
|
||||
CREATE INDEX idx_encounter_tenant_delete_status ON adm_encounter(tenant_id, delete_flag, status_enum);
|
||||
|
||||
-- 时间范围查询索引
|
||||
CREATE INDEX idx_encounter_create_time ON adm_encounter(create_time);
|
||||
|
||||
-- 业务编号查询索引
|
||||
CREATE INDEX idx_encounter_bus_no ON adm_encounter(bus_no);
|
||||
|
||||
-- 患者ID关联索引
|
||||
CREATE INDEX idx_encounter_patient_id ON adm_encounter(patient_id);
|
||||
```
|
||||
|
||||
### 2. adm_patient 表索引
|
||||
```sql
|
||||
-- 姓名模糊查询索引
|
||||
CREATE INDEX idx_patient_name ON adm_patient(name);
|
||||
|
||||
-- 身份证号查询索引
|
||||
CREATE INDEX idx_patient_id_card ON adm_patient(id_card);
|
||||
|
||||
-- 业务编号查询索引
|
||||
CREATE INDEX idx_patient_bus_no ON adm_patient(bus_no);
|
||||
|
||||
-- 电话查询索引
|
||||
CREATE INDEX idx_patient_phone ON adm_patient(phone);
|
||||
|
||||
-- 复合索引:常用查询条件
|
||||
CREATE INDEX idx_patient_delete_gender ON adm_patient(delete_flag, gender_enum);
|
||||
```
|
||||
|
||||
### 3. adm_encounter_participant 表索引
|
||||
```sql
|
||||
-- 复合索引:提高连接性能
|
||||
CREATE INDEX idx_ep_encounter_type ON adm_encounter_participant(encounter_id, type_code, delete_flag);
|
||||
|
||||
-- 参与者ID索引
|
||||
CREATE INDEX idx_ep_practitioner ON adm_encounter_participant(practitioner_id);
|
||||
```
|
||||
|
||||
### 4. adm_practitioner 表索引
|
||||
```sql
|
||||
-- 姓名查询索引
|
||||
CREATE INDEX idx_practitioner_name ON adm_practitioner(name);
|
||||
|
||||
-- 复合索引:常用查询条件
|
||||
CREATE INDEX idx_practitioner_delete_tenant ON adm_practitioner(delete_flag, tenant_id);
|
||||
```
|
||||
|
||||
### 5. adm_organization 表索引
|
||||
```sql
|
||||
-- 主键关联索引
|
||||
CREATE INDEX idx_organization_id_delete ON adm_organization(id, delete_flag);
|
||||
```
|
||||
|
||||
## 查询优化建议
|
||||
|
||||
### 1. 添加查询统计信息收集
|
||||
```sql
|
||||
-- 定期分析表统计信息
|
||||
ANALYZE TABLE adm_encounter;
|
||||
ANALYZE TABLE adm_patient;
|
||||
ANALYZE TABLE adm_encounter_participant;
|
||||
ANALYZE TABLE adm_practitioner;
|
||||
ANALYZE TABLE adm_organization;
|
||||
```
|
||||
|
||||
### 2. 考虑分区表(针对大数据量)
|
||||
如果 `adm_encounter` 表数据量超过100万条,考虑按时间分区:
|
||||
```sql
|
||||
-- 按月分区
|
||||
PARTITION BY RANGE (YEAR(create_time) * 100 + MONTH(create_time))
|
||||
(
|
||||
PARTITION p202501 VALUES LESS THAN (202501),
|
||||
PARTITION p202502 VALUES LESS THAN (202502),
|
||||
-- ... 更多分区
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 添加覆盖索引(Covering Index)
|
||||
对于常用查询字段,创建覆盖索引避免回表:
|
||||
```sql
|
||||
CREATE INDEX idx_encounter_cover ON adm_encounter(
|
||||
tenant_id, delete_flag, create_time,
|
||||
status_enum, bus_no, patient_id
|
||||
) INCLUDE (organization_id);
|
||||
```
|
||||
|
||||
## 执行计划检查
|
||||
|
||||
建议定期检查查询执行计划:
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT -- 完整查询语句
|
||||
FROM adm_encounter AS enc
|
||||
-- ... 连接条件
|
||||
WHERE enc.delete_flag = '0'
|
||||
AND enc.tenant_id = 1
|
||||
-- ... 其他条件
|
||||
ORDER BY enc.create_time DESC;
|
||||
```
|
||||
|
||||
## 监控建议
|
||||
|
||||
1. **慢查询监控**:监控执行时间超过1秒的查询
|
||||
2. **索引使用监控**:定期检查未使用的索引
|
||||
3. **表空间监控**:监控表增长和碎片情况
|
||||
4. **连接性能监控**:监控JOIN操作的性能
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在测试环境创建建议的索引
|
||||
2. 执行查询性能测试
|
||||
3. 分析执行计划,确认索引有效性
|
||||
4. 在生产环境非高峰期创建索引
|
||||
5. 监控生产环境性能变化
|
||||
6. 定期维护和优化索引
|
||||
273
md/需求/100-门诊手术中临时医嘱生成界面PRD_2026-1-23.md
Normal file
273
md/需求/100-门诊手术中临时医嘱生成界面PRD_2026-1-23.md
Normal file
@@ -0,0 +1,273 @@
|
||||
## 门诊手术中临时医嘱生成界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:门诊手术中临时医嘱生成界面
|
||||
**页面目标**:帮助麻醉医师在手术过程中快速生成临时医嘱,完成药品计费引用、医嘱预览和电子签名确认的全流程操作
|
||||
**适用场景**:门诊手术过程中需要追加药品医嘱时使用
|
||||
**页面类型**:表单页+数据展示页
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 患者手术信息展示
|
||||
2. 已引用计费药品列表展示与汇总
|
||||
3. 临时医嘱预览与编辑功能
|
||||
4. 医师电子签名确认流程
|
||||
5. 数据刷新与退出操作
|
||||
**用户价值**:简化手术中医嘱生成流程,确保医嘱准确性,实现无纸化操作,提高手术室工作效率
|
||||
原型图地址:https://static.pm-ai.cn/prototype/20260122/e1d7f10b85e9efea543bf47bd6831600/index.html
|
||||
**流程图:**
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Start"]) --> Enter["进入门诊手术中临时医嘱生成界面"]
|
||||
Enter --> ShowBase["展示患者基本信息"]
|
||||
ShowBase --> ShowQuoted["显示已引用计费药品列表"]
|
||||
ShowQuoted --> ShowPreview["显示医嘱预览表格"]
|
||||
ShowPreview --> UserOp{用户操作}
|
||||
|
||||
UserOp -- "引用计费" --> GetLatest{"获取最新计费药品数据\n获取成功?"}
|
||||
GetLatest -- "否" --> ErrTip1["显示错误提示"]
|
||||
GetLatest -- "是" --> UpdateTable["更新药品表格和汇总"]
|
||||
|
||||
UserOp -- "编辑" --> PopEdit["弹出医嘱编辑表单"]
|
||||
PopEdit --> EditVal{"验证通过?"}
|
||||
EditVal -- "否" --> ErrTip2["返回错误提示"]
|
||||
EditVal -- "是" --> SaveClick{"点击保存?"}
|
||||
SaveClick -- "是" --> GenTemp["生成临时药品医嘱"]
|
||||
SaveClick -- "否" --> UserOp
|
||||
|
||||
GenTemp --> UpdatePreview["更新医嘱预览表格"]
|
||||
UpdatePreview --> UpdateRecord["更新手术记录"]
|
||||
UpdateRecord --> ShowResult["显示生成结果"]
|
||||
|
||||
UserOp -- "一键签名并生成医嘱" --> PopPwd["弹出账户密码输入框"]
|
||||
PopPwd --> PopConfirm{"弹出确认对话框"}
|
||||
PopConfirm -- "否" --> UserOp
|
||||
PopConfirm -- "是" --> GenTemp
|
||||
|
||||
UserOp -- "刷新" --> Reload["重新加载界面数据"]
|
||||
Reload --> ShowQuoted
|
||||
|
||||
UserOp -- "退出" --> ExitConfirm{"确认退出?"}
|
||||
ExitConfirm -- "否" --> UserOp
|
||||
ExitConfirm -- "是" --> ReturnUp["返回上级页面"]
|
||||
|
||||
ErrTip1 --> UserOp
|
||||
ErrTip2 --> PopEdit
|
||||
ShowResult --> UserOp
|
||||
ReturnUp --> End([结束])
|
||||
```
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
|
||||
**要区域划分**:
|
||||
|
||||
1. 顶部信息区(15%):患者基本信息+操作按钮区
|
||||
2. 计费药品展示区(35%):已引用计费药品表格+金额汇总
|
||||
3. 医嘱预览区(35%):待生成医嘱的预览表格
|
||||
4. 签名确认区(15%):医师签名信息+操作按钮
|
||||
**布局特点**:上下分块布局,采用卡片式设计,主要区域间有明确分隔线
|
||||
**响应式要求**:768px以下时患者信息改为纵向排列,操作按钮换行显示
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部信息区
|
||||
|
||||
**区域位置**:页面顶部
|
||||
**区域尺寸**:高度180px(包含20px内边距)
|
||||
**区域功能**:展示患者基本信息+提供主要操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **标题栏**:
|
||||
- 元素类型:标题文本
|
||||
- 显示内容:“门诊术中临时医嘱”
|
||||
- 样式特征:白色文字,1.5rem字号,居中显示,渐变蓝色背景
|
||||
- **患者信息卡**:
|
||||
- 元素类型:信息展示区块
|
||||
- 显示内容:患者姓名、就诊卡号、手术单号、科室、医师、角色
|
||||
- 患者:样例值-张三
|
||||
- 就诊卡号:样例值-202507010122
|
||||
- 手术单号:样例值- S202507010135
|
||||
- 科室: 样例值-手术室(OR101)-取值于手术安排的手术间号字段
|
||||
- 医师:样例值-李麻(3015)
|
||||
- 角色:样例值-麻醉医师
|
||||
- 样式特征:半透明白色背景,圆角8px,内部flex布局
|
||||
- **操作按钮组**:
|
||||
- **[刷新按钮]**:
|
||||
- 元素类型:主要操作按钮
|
||||
- 显示内容:↻ 刷新
|
||||
- 交互行为:点击后重新加载当前界面的数据
|
||||
- 样式特征:蓝色渐变背景,悬停有上浮效果
|
||||
- **[引用计费按钮]**:
|
||||
- 元素类型:次要操作按钮
|
||||
- 显示内容:引用计费
|
||||
- 交互行为:点击后拉取当前患者最新计费药品的数据
|
||||
|
||||
#### 2. 计费药品展示区
|
||||
|
||||
**区域位置**:顶部信息区下方
|
||||
**区域尺寸**:高度约420px(包含标题和表格)
|
||||
**区域功能**:展示待生成医嘱的计费药品清单
|
||||
**包含元素**:
|
||||
|
||||
- **表格标题**:
|
||||
- 显示内容:“一、已引用计费药品(待生成医嘱)”
|
||||
- 样式特征:1.2rem字号,底部边框线
|
||||
- **药品数据表格**:
|
||||
|
||||
**取值于门诊术中计费界面生成的药品计费数据(adm_charge_item(费用项管理)、med_medication_request(药品请求管理)等),具体与系统实际业务数据为主。**
|
||||
|
||||
**(参考)关联字段:adm_charge_item. encounter_id = med_medication_request. encounter_id and –就诊ID**
|
||||
|
||||
**adm_charge_item. service_table = 'med_medication_request' and --记录药品数据**
|
||||
|
||||
**adm_charge_item. bus_no = med_medication_request. bus_no -- adm_charge_item. bus_no的值之前多加了‘CI’**
|
||||
|
||||
- 展示方式:斑马纹表格
|
||||
- 数据字段:
|
||||
- 序号:数字 - 自动生成
|
||||
- 药品名称:文本 - 如"罗哌卡因注射液"
|
||||
- 规格:文本 - 如"10ml"
|
||||
- 数量:数字 - 可编辑
|
||||
- 批号:文本 - 如"L240715"
|
||||
- 单价:数字 - 如"38"
|
||||
- 小计:数字 - 自动计算
|
||||
- 医保:标签 - “甲/乙/自费”
|
||||
- 样式特征:表头浅灰色背景,医保类型有颜色区分(蓝色=医保,绿色=自费)
|
||||
- **金额汇总栏**:
|
||||
- 显示内容:
|
||||
- 医保内金额(蓝色强调)
|
||||
- 自费金额(绿色强调)
|
||||
- 总计金额(红色强调)
|
||||
- 位置:表格底部右对齐
|
||||
|
||||
#### 3. 医嘱预览区
|
||||
|
||||
**区域位置**:计费药品展示区下方
|
||||
**区域尺寸**:高度约420px(包含标题和表格)
|
||||
**区域功能**:展示即将生成的药品医嘱
|
||||
**包含元素**:
|
||||
|
||||
\*生成门诊药品医嘱表相关的数据,满足**计费药品明细 ↔ 药品医嘱** 一一对应的要求。
|
||||
|
||||
可以对照参考:需结合门诊医生站开立药品医嘱时生成的药品医嘱表
|
||||
|
||||
- **表格标题**:
|
||||
- 显示内容:“二、临时医嘱预览(已生成)”
|
||||
- **医嘱表格**:
|
||||
- 展示方式:斑马纹表格
|
||||
- 数据字段:
|
||||
- 序号:数字
|
||||
- 医嘱名称:文本(取已引用计费药品的药品名称)
|
||||
- 剂量:数字(自动计算=规格×数量)
|
||||
- 单位:文本(根据药品类型自动判断)
|
||||
- 用法:下拉选择(不可编辑)
|
||||
- 频次:固定"临时"
|
||||
- 执行时间:自动生成当前时间
|
||||
- 操作:编辑/删除按钮
|
||||
- 操作功能:
|
||||
- 编辑:弹出表单修改剂量、用法等字段
|
||||
- 删除:二次确认后移除该条医嘱
|
||||
|
||||
#### 4. 签名确认区
|
||||
|
||||
**区域位置**:页面底部
|
||||
**区域尺寸**:高度约180px
|
||||
**区域功能**:完成医嘱确认和电子签名
|
||||
**包含元素**:
|
||||
|
||||
- **签名信息卡**:
|
||||
- 显示内容:医师姓名工号、签名状态、签名时间
|
||||
- 样式特征:浅灰色背景,圆角边框
|
||||
- **[一键签名按钮]**:
|
||||
- 元素类型:主要操作按钮
|
||||
- 显示内容:“一键签名并生成医嘱”
|
||||
- 交互行为:点击后弹出账户密码输入框
|
||||
- 样式特征:绿色背景,悬停效果
|
||||
- **[取消按钮]**:
|
||||
- 元素类型:次要操作按钮
|
||||
- 显示内容:“取消”
|
||||
- 交互行为:返回上级页面
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 引用计费功能
|
||||
|
||||
**功能描述**:从术中计费药品获取患者当前最新的计费药品数据
|
||||
**触发条件**:点击"引用计费"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 点击按钮获取患者当前最新的计费药品数据
|
||||
2. 成功返回后更新药品表格数据
|
||||
3. 自动计算并更新费用汇总
|
||||
|
||||
**反馈机制**:成功提示弹窗"已成功引用最新计费药品信息!"
|
||||
**异常处理**:请求失败时显示错误提示“获取计费数据失败,请重试”,保留原数据
|
||||
|
||||
#### 2. 医嘱生成功能
|
||||
|
||||
**功能描述**:将计费药品转为正式医嘱
|
||||
**触发条件**:点击"一键签名并生成医嘱"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 自动生成药品医嘱预览(带默认用法和剂量)
|
||||
2. 弹出账户密码输入框
|
||||
3. 验证通过后生成临时药品医嘱数据
|
||||
4. 成功返回后显示生成结果
|
||||
**数据转换规则**:
|
||||
- 剂量 = 规格数值 × 数量(如"10ml"×2 → 20ml)
|
||||
- 单位:根据药品名称自动判断(默认获取当前药品在《药品目录》维护剂量单位的值)
|
||||
- 用法:根据药品名称自动判断(默认获取当前药品在《药品目录》维护用法的值,如果未维护默认空)
|
||||
- 医嘱名称:取值药品名称
|
||||
- 频次:默认ST
|
||||
- 执行时间:默认当前系统时间
|
||||
|
||||
#### 3. 医嘱编辑功能
|
||||
|
||||
**功能描述**:修改已生成的医嘱明细
|
||||
**触发条件**:点击"编辑"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 弹出编辑表单(带当前值医嘱值)
|
||||
2. 修改后点击保存更新表格
|
||||
3. 自动重新计算相关字段得值
|
||||
**字段限制**:
|
||||
- 剂量:必须为数字
|
||||
- 用法:限定下拉选项,取值于字典管理:用药途径(用法)的值
|
||||
- 频次:固定为"ST"不可编辑
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
**关键数据字段**:
|
||||
|
||||
| **字段名** | **说明** | **数据类型** | **示例值** | **是否必填** | **备注** |
|
||||
|---------------|----------|--------------|--------------------|--------------|------------------|
|
||||
| patientId | 患者ID | string | “202507010122” | 是 | 就诊卡号 |
|
||||
| surgeryNo | 手术单号 | string | “S202507010135” | 是 | |
|
||||
| medicineName | 药品名称 | string | “罗哌卡因注射液” | 是 | |
|
||||
| spec | 规格 | string | “10ml” | 是 | 需包含数值和单位 |
|
||||
| batchNo | 批号 | string | “L240715” | 是 | |
|
||||
| insuranceType | 医保类型 | string | “乙” | 是 | 甲/乙/自费 |
|
||||
| usage | 用法 | string | “静脉推注” | 是 | |
|
||||
| execTime | 执行时间 | datetime | “2025-07-01 08:41” | 是 | 精确到分钟 |
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:\#4a90e2(按钮/标题)
|
||||
- **辅助色**:\#5cb85c(成功操作)、\#e74c3c(警告)
|
||||
- **字体规范**:标题1.5rem/正文0.95rem,行高1.6
|
||||
- **间距系统**:区块padding20px,元素间距15px
|
||||
- **表格样式**:斑马纹,行高56px,单元格padding15px 20px
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- **浏览器兼容**:Chrome/Firefox/Edge最新版
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 医嘱生成后需同步更新手术记录
|
||||
2. 所有金额显示保留两位小数
|
||||
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. **病种名称**:严格遵循《传染病报告卡》规范用词
|
||||
287
md/需求/94-手术室维护界面_2026-1-9.md
Normal file
287
md/需求/94-手术室维护界面_2026-1-9.md
Normal file
@@ -0,0 +1,287 @@
|
||||
## 手术室维护界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:手术室维护界面
|
||||
**页面目标**:提供手术室基础数据的维护功能,包括新增、编辑、启用/停用手术室信息,为手术安排提供基础数据支持
|
||||
**适用场景**:医院管理员需要新增、修改、启用/停用手术室信息时使用
|
||||
**页面类型**:列表页+表单页(含模态框)
|
||||
|
||||
**原型图地址:**https://static.pm-ai.cn/prototype/20260104/ee5d222231effefcb39624d1646a2e20/index.html
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 手术室列表展示与查询
|
||||
2. 新增手术室信息
|
||||
3. 编辑现有手术室信息
|
||||
4. 启用/停用手术室状态
|
||||
5. 数据有效性校验
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 管理员可集中管理所有手术室基础信息
|
||||
- 确保手术安排时能获取准确的手术室数据
|
||||
- 通过状态管理控制手术室可用性
|
||||
|
||||
**流程图:**
|
||||
|
||||

|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[手术室维护界面] --> B[手术室列表展示]
|
||||
|
||||
B --> C[新增手术室]
|
||||
|
||||
B --> D[编辑手术室]
|
||||
|
||||
B --> E[启用/停用手术室]
|
||||
|
||||
B --> F[查询手术室]
|
||||
|
||||
C --> G[点击新增按钮]
|
||||
|
||||
G --> H[打开新增模态框]
|
||||
|
||||
H --> I[填写表单字段]
|
||||
|
||||
I --> J{必填字段校验}
|
||||
|
||||
J -->|通过| K[提交数据]
|
||||
|
||||
J -->|不通过| L[提示请填写所有必填项]
|
||||
|
||||
K --> M[表格新增数据行]
|
||||
|
||||
D --> N[点击修改按钮]
|
||||
|
||||
N --> O[打开编辑模态框]
|
||||
|
||||
O --> P[修改表单字段]
|
||||
|
||||
P --> Q{必填字段校验}
|
||||
|
||||
Q -->|通过| R[保存数据]
|
||||
|
||||
Q -->|不通过| S[提示请填写所有必填项]
|
||||
|
||||
R --> T[更新表格对应行]
|
||||
|
||||
E --> U[点击启用/停用按钮]
|
||||
|
||||
U --> V{二次确认}
|
||||
|
||||
V -->|确认| W[切换状态标签]
|
||||
|
||||
V -->|取消| X[取消操作]
|
||||
|
||||
W --> Y[更新按钮状态]
|
||||
|
||||
F --> Z[输入查询条件]
|
||||
|
||||
Z --> AA[筛选表格数据]
|
||||
|
||||
K --> AB{房间号重复校验}
|
||||
|
||||
AB -->|不重复| AC[提示房间号已存在]
|
||||
|
||||
AB -->|重复| AD[更新按钮状态]
|
||||
```
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. 页头区域(15%高度)
|
||||
2. 表格展示区(85%高度)
|
||||
**布局特点**:上下布局,表格采用固定表头+滚动内容区设计
|
||||
**响应式要求**:移动端适配时改为纵向堆叠布局,操作按钮组变为纵向排列
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 页头区域
|
||||
|
||||
**区域位置**:页面顶部
|
||||
**区域尺寸**:高度60px,宽度100%
|
||||
**区域功能**:展示标题和主要操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **标题文本**
|
||||
- 元素类型:H1标题
|
||||
- 显示内容:"手术室列表"
|
||||
- 样式特征:1.75rem/600字重,深灰色(#333)
|
||||
- **新增按钮**
|
||||
- 元素类型:主要操作按钮
|
||||
- 显示内容:"新增"(带+图标)
|
||||
- 交互行为:点击触发新增模态框
|
||||
- 样式特征:蓝色背景(#5a7cff),白色文字,8px圆角,悬停上浮1px
|
||||
|
||||
#### 2. 表格展示区(手术室列表表格)
|
||||
|
||||
**区域位置**:页头下方
|
||||
**区域尺寸**:高度自适应,宽度100%
|
||||
**区域功能**:展示手术室数据并支持行级操作
|
||||
**包含元素**:
|
||||
|
||||
- **数据表格**
|
||||
- 展示方式:固定表头表格
|
||||
- 数据字段:
|
||||
- 房间号:文本 - OR01 - 不可操作
|
||||
- 手术室名称:文本 - 第一手术室 - 不可操作
|
||||
- 类型:文本 - 普通/日间/复合 - 不可操作
|
||||
- 所属科室:文本 - 外科 - 不可操作
|
||||
- 状态:标签 - 有效/无效 - 通过操作按钮切换
|
||||
- 操作功能:每行包含"修改"和"状态切换(停用-黄色/启用-绿色)"按钮
|
||||
- **表格样式**:
|
||||
- 表头:浅灰色背景(#f8f9fa),大写字母,14px字号
|
||||
- 行悬停:浅灰色背景(#f8f9fa)
|
||||
- 状态标签:
|
||||
- 有效:绿色背景+文字(#28a745)
|
||||
- 无效:灰色背景+文字(#6c757d)
|
||||
|
||||
#### 3. 新增手术室弹窗
|
||||
|
||||
**区域位置**:页面居中模态弹窗
|
||||
**区域功能**:收集新增手术室所需信息
|
||||
**包含元素**:
|
||||
|
||||
- 表单字段:
|
||||
1. 房间号输入框
|
||||
2. 类型:文本输入
|
||||
3. 必填:是
|
||||
4. 示例值:OR04
|
||||
5. 校验规则:非空校验
|
||||
6. 手术室名称输入框
|
||||
- 类型:文本输入
|
||||
- 必填:是
|
||||
- 示例值:第四手术室
|
||||
1. 手术室类型下拉框
|
||||
- 类型:单选下拉
|
||||
- 选项:普通/日间/复合/特殊
|
||||
- 默认值:普通
|
||||
1. 所属科室下拉框
|
||||
- 类型:单选下拉
|
||||
- 必填:是
|
||||
- 选项:外科/妇产科等8个科室
|
||||
- 默认提示:"请选择科室"
|
||||
- 操作按钮:
|
||||
- 取消按钮(灰色边框)
|
||||
- 确认按钮(蓝色填充)
|
||||
- 校验逻辑:必填字段非空校验
|
||||
- 成功反馈:提示"手术室添加成功"
|
||||
- 失败反馈:提示"请填写所有必填项"
|
||||
|
||||
#### 4. 编辑手术室弹窗
|
||||
|
||||
**区域位置**:页面居中模态弹窗
|
||||
**区域功能**:修改现有手术室信息
|
||||
**包含元素**:
|
||||
|
||||
- 表单字段(同新增弹窗,带初始值)
|
||||
- 操作按钮:
|
||||
- 取消按钮
|
||||
- 保存按钮
|
||||
- 校验逻辑:同新增弹窗
|
||||
- 成功反馈:提示"手术室信息已更新"
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 新增手术室
|
||||
|
||||
**功能描述**:添加新的手术室记录
|
||||
**触发条件**:点击页头"新增"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 打开新增模态框
|
||||
2. 填写必填字段(房间号、名称、科室)
|
||||
3. 点击确认提交(插入his_or_room表)
|
||||
4. 表格末尾新增数据行
|
||||
**异常处理**:
|
||||
- 必填项为空时弹出"请填写所有必填项"提示
|
||||
- 房间号重复需在后端校验并提示
|
||||
|
||||
#### 2. 编辑手术室
|
||||
|
||||
**功能描述**:修改现有手术室信息
|
||||
**触发条件**:点击行内"修改"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 打开编辑模态框(自动填充当前行数据)
|
||||
2. 用户修改数据
|
||||
3. 点击"保存"时校验并更新对应行数据
|
||||
**状态保持**:记录当前编辑行索引确保数据更新准确
|
||||
|
||||
#### 3. 状态切换
|
||||
|
||||
**功能描述**:启用/停用手术室
|
||||
**触发条件**:点击"停用"或"启用"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 弹出二次确认对话框
|
||||
2. 用户确认后切换状态标签
|
||||
3. 按钮变为相反操作(停用↔启用)
|
||||
4. 、停用手术室
|
||||
- **步骤**:
|
||||
1. 查询需要停用的手术室记录。
|
||||
2. 将 valid_flag 设置为 0(无效)。
|
||||
- **示例**:
|
||||
|
||||
UPDATE his_or_room
|
||||
|
||||
SET valid_flag = '0'
|
||||
|
||||
WHERE room_code = 'OR06';
|
||||
|
||||
5\. 启用手术室
|
||||
|
||||
**步骤**:
|
||||
|
||||
1. 查询需要启用的手术室记录。
|
||||
1. 将 valid_flag 设置为 1(有效)。
|
||||
- **示例**:
|
||||
|
||||
UPDATE his_or_room
|
||||
|
||||
SET valid_flag = '1'
|
||||
|
||||
WHERE room_code = 'OR06';
|
||||
|
||||
**防误操作**:所有状态变更需二次确认
|
||||
|
||||
### 五、数据结构说明(HIS_OR_ROOM手术室字典表)
|
||||
|
||||
| **字段名称** | **数据类型** | **是否为空** | **说明/典型值** | **外键/来源** |
|
||||
|--------------|--------------|--------------|-----------------|-------------------------------|
|
||||
| room_id | VARCHAR(10) | N | 主键 | 自增主键 |
|
||||
| room_code | VARCHAR(10) | N | 手术室房间号 | 自定义编码,如 OR01、OR02 |
|
||||
| room_name | VARCHAR(100) | N | 手术室名称 | 如 "第一手术室"、"第二手术室" |
|
||||
| room_type | VARCHAR(10) | N | 手术室类型 | 普通、日间、复合 |
|
||||
| dept_code | VARCHAR(10) | N | 所属科室 | FK → 科室管理的科室代码 |
|
||||
| valid_flag | CHAR(1) | N | 是否有效 | 1有效,0无效 |
|
||||
| created_time | DATETIME | N | 创建时间 | 默认当前时间 |
|
||||
| updated_time | DATETIME | N | 更新时间 | 默认当前时间,自动更新 |
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- 主色调:#5a7cff(按钮/交互元素)
|
||||
- 辅助色:#7b8a8b(次要文本)
|
||||
- 字体:
|
||||
- 标题:1.75rem/600字重
|
||||
- 正文:0.875rem/400字重
|
||||
- 间距系统:
|
||||
- 卡片内边距:24px
|
||||
- 表单字段间距:16px
|
||||
|
||||
**技术要求**:
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 数据安全:
|
||||
- 所有变更操作需记录操作日志
|
||||
- 停用状态的手术室需在前端标记不可预约
|
||||
2. 性能优化:
|
||||
- 表格数据分页加载
|
||||
- 模态框使用懒加载
|
||||
387
md/需求/95-门诊医生站开立会诊申请单界面PRD_2026-01-15.md
Normal file
387
md/需求/95-门诊医生站开立会诊申请单界面PRD_2026-01-15.md
Normal file
@@ -0,0 +1,387 @@
|
||||
## 门诊医生站开立会诊申请单界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:门诊医生站开立会诊申请单界面**页面目标**:帮助门诊医生完成会诊申请单的创建、编辑、提交和作废操作,实现多科室会诊流程的电子化管理**适用场景**:
|
||||
|
||||
1. 门诊医生需要邀请其他科室专家进行会诊时
|
||||
2. 会诊申请单需要修改或补充信息时
|
||||
3. 会诊流程需要跟踪管理时
|
||||
**页面类型**:表单页+列表页复合型界面
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 会诊申请单的新增、保存、提交、作废功能
|
||||
2. 会诊科室/专家可视化选择
|
||||
3. 申请单数据表格展示与交互
|
||||
4. 表单数据自动填充与校验
|
||||
5. 申请单打印输出
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 规范会诊申请流程,减少纸质单据使用
|
||||
- 通过智能填充减少医生重复录入
|
||||
- 实时查看会诊申请状态(新开/已提交/已确认/已签名/已完成/已取消)
|
||||
|
||||
原型图地址:https://static.pm-ai.cn/prototype/20260115/4eb1bd5367f9d5610b32c0ecc6c793f5/index.html
|
||||
|
||||
流程图:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% ---------- 开始 ----------
|
||||
START(["开始"]) --> A["医生进入会诊申请单界面"]
|
||||
|
||||
%% ---------- 操作选择 ----------
|
||||
A --> B{"操作选择"}
|
||||
B -->|"打印"| C["选择已有申请单"]
|
||||
B -->|"提交/取消提交"| D{"校验状态为“已提交”?"}
|
||||
B -->|"删除"| E["弹出确认对话框"]
|
||||
B -->|"结束"| F{"校验状态为“已提交”?"}
|
||||
B -->|"编辑"| G["修改表单内容"]
|
||||
B -->|"新增"| H["清空表单(保留患者信息)"]
|
||||
|
||||
%% ---------- 打印分支 ----------
|
||||
C --> I["高亮选中行"]
|
||||
I --> J["生成打印视图"]
|
||||
J --> K["输出打印样式"]
|
||||
K --> L(["取消"])
|
||||
|
||||
%% ---------- 提交/取消提交分支 ----------
|
||||
D -->|"不通过"| M["提示“请完善必填信息”"]
|
||||
D -->|"通过"| N["更新状态为“已提交/新开”"]
|
||||
|
||||
%% ---------- 删除分支 ----------
|
||||
E --> O{"确认?"}
|
||||
O -->|"是"| P["标记状态为“已取消”"]
|
||||
O -->|"否"| L
|
||||
|
||||
%% ---------- 结束分支 ----------
|
||||
F -->|"不通过"| Q["提示“请先提交申请”"]
|
||||
F -->|"通过"| R["标记状态为“已完成”"]
|
||||
|
||||
%% ---------- 编辑分支 ----------
|
||||
G --> S{"校验必填字段"}
|
||||
S -->|"不通过"| M
|
||||
S -->|"通过"| T["保存到表格"]
|
||||
|
||||
%% ---------- 新增/保存通用路径 ----------
|
||||
H --> U["填写表单"]
|
||||
U --> V["选择会诊科室/专家"]
|
||||
V --> W["自动填充邀请对象"]
|
||||
W --> X["填写病史及目的"]
|
||||
X --> Y["点击保存"]
|
||||
Y --> Z{"校验必填字段"}
|
||||
Z -->|"不通过"| M
|
||||
Z -->|"通过"| AA["生成会诊申请记录"]
|
||||
AA --> AB["保存到表格"]
|
||||
AB --> AC["新增/更新记录"]
|
||||
|
||||
%% ---------- 循环 ----------
|
||||
AC --> A
|
||||
N --> A
|
||||
P --> A
|
||||
R --> A
|
||||
T --> A
|
||||
M --> A
|
||||
Q --> A
|
||||
L --> A
|
||||
```
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应宽度(主内容区采用7:3比例分割)
|
||||
**主要区域划分**:
|
||||
|
||||
1. 顶部操作栏(48px固定高度)
|
||||
2. 会诊申请单列表区(高度自适应)
|
||||
3. 主内容区(分左右结构,7:3比例)
|
||||
|
||||
- 左侧:会诊申请单表单区
|
||||
- 右侧:会诊科室/专家选择区
|
||||
**布局特点**:响应式上下+左右混合布局,主要对齐方式为左对齐
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部操作栏区域
|
||||
|
||||
**区域位置**:页面顶部固定位置**区域尺寸**:高度48px,宽度100%**区域功能**:提供全局操作功能入口**包含元素**:
|
||||
|
||||
- **打印按钮**
|
||||
- 元素类型:操作按钮
|
||||
- 显示内容:“打印”
|
||||
- 交互行为:点击后生成A4打印视图,自动适配医院抬头格式
|
||||
- 样式特征:绿色背景(\#13C2C2),圆角4px,32px高度
|
||||
- **新增按钮**
|
||||
- 元素类型:操作按钮
|
||||
- 显示内容:“新增”
|
||||
- 交互行为:点击清空表单(保留当前患者基本信息)
|
||||
- 样式特征:蓝色背景(\#1890FF)
|
||||
- **结束按钮**
|
||||
- 元素类型:危险操作按钮
|
||||
- 显示内容:“结束”
|
||||
- 交互行为:点击结束已提交的会诊流程,标记申请单状态为"已结束",禁用后续操作
|
||||
- 样式特征:红色背景(\#FF4D4F)
|
||||
- 限制条件:需先选中已提交的会诊单
|
||||
- **保存按钮**
|
||||
- 元素类型:主要操作按钮
|
||||
- 显示内容:“保存”
|
||||
- 交互行为:点击保存当前表单数据,校验必填字段后保存至表格,自动生成时间戳
|
||||
- 样式特征:绿色背景(\#52C41A)
|
||||
|
||||
#### 2. 会诊申请单列表区
|
||||
|
||||
**区域位置**:顶部操作栏下方**区域尺寸**:高度自适应,宽度100%**区域功能**:展示当前医生的会诊申请记录**包含元素**:
|
||||
|
||||
- **申请单表格**
|
||||
- 展示方式:带边框表格
|
||||
- 数据字段:
|
||||
|
||||
- 序号:文本 - 自增序号 - 不可操作
|
||||
- 急:布尔 - ✓表示紧急 - 不可操作
|
||||
- 申请单号:文本 - CS20260105001 - 不可操作
|
||||
- 会诊时间:日期 - 2026-01-05 15:08 - 不可操作
|
||||
- 邀请对象:文本 - 吴院长 - 不可操作
|
||||
- 申请科室:文本 - 内科 - 不可操作
|
||||
- 申请医师:文本 - 张医生 - 不可操作
|
||||
- 申请时间:日期 - 2026-01-05 15:08 - 不可操作
|
||||
- 提交状态:布尔 - 复选框 - 仅查看
|
||||
- 结束状态:布尔 - 复选框 - 仅查看
|
||||
- 操作功能:
|
||||
|
||||
- - o 提交/取消提交按钮
|
||||
|
||||
```
|
||||
样式要求:蓝色小按钮,禁用状态显示灰色
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
交互行为:切换提交状态,需二次确认
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
o 删除图标
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
样式要求:红色垃圾桶图标,hover时放大10%
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
交互行为:弹出确认对话框后作废该记录
|
||||
```
|
||||
|
||||
|
||||
[删除]**将状态改为“已取消”****
|
||||
|
||||
UPDATE ConsultationRequest
|
||||
SET ConsultationStatus = 50,cancelnatureDate = <作废会诊时间>
|
||||
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus <> 40 ;
|
||||
|
||||
- 交互特性:
|
||||
- 行点击选中效果(蓝色高亮+左侧边框)
|
||||
- 行hover浅灰色背景
|
||||
- 提交按钮状态联动(切换提交状态,需二次确认)
|
||||
|
||||
#### 3. 会诊申请单表单区
|
||||
|
||||
**区域位置**:主内容区左侧**区域尺寸**:占主内容区70%宽度**区域功能**:会诊申请单的详细表单填写**包含元素**:
|
||||
|
||||
- **基础信息区**
|
||||
- 申请单号:只读文本,【保存】时自动生成规则CS+年月日时分秒+4位随机数
|
||||
- 申请时间:只读文本,自动获取系统当前时间
|
||||
- 病人信息:病人姓名/性别/年龄/就诊卡号/申请医师/申请科室(不可编辑),自动获取当前患者档案信息。
|
||||
- **会诊信息区**
|
||||
- 会诊时间:时间控件可编辑
|
||||
- 紧急标识:复选框控件
|
||||
- 申请医师:默认当前登录医生
|
||||
- 申请科室:默认当前医生登录的开单科室
|
||||
- 门诊诊断:自动获取医生开立的门诊诊断(主诊断)
|
||||
- **病史及目的**
|
||||
- 多行文本域,最小高度100px
|
||||
- **会诊邀请**
|
||||
- 会诊邀请对象:支持多选(逗号分隔)-》(可从右侧会诊邀请对象选择)
|
||||
- **会诊记录区**
|
||||
- 会诊意见:只读文本域
|
||||
- 会诊确认参加医师:只读字段
|
||||
- 所属医生、代表科室、签名医生、签名时间:只读字段
|
||||
|
||||
#### 4. 会诊邀请对象选择区(侧边栏)
|
||||
|
||||
**区域位置**:主内容区右侧**区域尺寸**:占主内容区30%宽度**区域功能**:快速选择会诊科室和专家**包含元素**:
|
||||
|
||||
- **会诊科室列表**
|
||||
- 展示方式:带边框可滚动列表
|
||||
- 交互行为:选择科室后动态加载对应专家
|
||||
- **会诊专家列表**
|
||||
- 展示方式:带边框可滚动列表
|
||||
- 交互行为:点击专家自动填入会诊邀请对象字段(防重复:已选专家提示"请勿重复选择")
|
||||
- 特殊逻辑:支持多选(自动用逗号分隔)
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 会诊申请单提交流程
|
||||
|
||||
**功能描述**:完成会诊申请单的提交操作**触发条件**:点击表格行的"提交"按钮**操作流程**:
|
||||
|
||||
1. 医生点击行内"提交"按钮
|
||||
2. 系统校验必填字段(会诊时间、邀请对象)
|
||||
3. 提交状态复选框变为已勾选
|
||||
4. 按钮文字变为"取消提交"
|
||||
5. 禁用该行编辑功能
|
||||
|
||||
【提交】**将状态从“新开”改为“已提交”**
|
||||
|
||||
UPDATE ConsultationRequest
|
||||
SET ConsultationStatus = 10,ConfirmingPhysician = <提交会诊医生姓名> ,ConfirmingPhysicianID = <提交会诊医生ID> ,ConfirmingDate = <提交会诊时间>
|
||||
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 0 ;
|
||||
|
||||
【取消提交】**将状态从“已提交”改为“新开”**
|
||||
|
||||
UPDATE ConsultationRequest
|
||||
SET ConsultationStatus = 0,ConfirmingPhysician = '',ConfirmingPhysicianID = '',ConfirmingDate = ''
|
||||
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 10 ;
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 必填字段缺失:弹出"请完善会诊时间和邀请对象信息"
|
||||
- 重复提交:提示"该申请已提交,请勿重复操作"
|
||||
|
||||
#### 2. 会诊流程结束功能
|
||||
|
||||
**功能描述**:标记会诊流程已结束**触发条件**:选中已提交的申请单后点击顶部"结束"按钮**操作流程**:
|
||||
|
||||
1. 医生选中已提交的申请单(行高亮)
|
||||
2. 点击顶部"结束"按钮
|
||||
3. 系统校验提交状态为已提交
|
||||
4. 结束状态复选框变为已勾选
|
||||
5. 禁用该行的取消提交功能
|
||||
|
||||
【结束】**将状态从“已签名”改为“已完成”**
|
||||
|
||||
UPDATE ConsultationRequest
|
||||
SET ConsultationStatus = 40,Signature = <结束会诊医生姓名> ,SignatureDate=<结束会诊时间>
|
||||
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 30 ;
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 未选中记录:提示"请先选择要结束的会诊申请"
|
||||
- 未提交记录:提示"请先提交该会诊申请"
|
||||
|
||||
#### 3. 申请单保存功能
|
||||
|
||||
**功能描述**:保存会诊申请单数据**触发条件**:点击顶部"保存"按钮**操作流程**:
|
||||
|
||||
1. 系统自动生成申请单号(如为空)
|
||||
2. 保存当前表单所有字段值
|
||||
3. 新增记录插入表格末尾
|
||||
4. 已有记录更新对应行数据
|
||||
|
||||
【保存】
|
||||
|
||||
①、写入门诊医嘱表(医嘱状态为新开,医嘱名称为"门诊会诊")
|
||||
|
||||
②、写入门诊会诊申请单表(ConsultationRequest)
|
||||
|
||||
**数据校验**:
|
||||
|
||||
- 必填字段:病人姓名、会诊时间、申请科室、会诊时间、会诊邀请对象、简要病史及会诊目的
|
||||
- 未选会诊对象:提示"请至少选择1位会诊专家"
|
||||
- 过期时间:提示"会诊时间不能早于当前时间"
|
||||
|
||||
#### 4. 会诊邀请对象选择联动
|
||||
|
||||
**触发方式**:点击科室列表项
|
||||
**数据联动**:
|
||||
|
||||
1. 根据选中会诊科室过滤会诊专家列表
|
||||
2. 记忆已选专家(跨科室切换时不丢失)
|
||||
|
||||
**技术要点**:
|
||||
|
||||
- 使用对象存储会诊科室-会诊专家映射关系
|
||||
- 采用事件委托处理动态生成的列表项
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
门诊会诊申请单表(ConsultationRequest)
|
||||
|
||||
|
||||
| **字段名称** | **数据类型** | **长度** | **描述** | **取值范围** |
|
||||
|-----------------------------| ------------ | -------- |----------------| --------------------------------------------------------- |
|
||||
| **PatientID** | Text | 20 | 患者唯一标识 | 患者就诊卡号 (取值患者档案) |
|
||||
| **ConsultationID** | Text | 20 | 会诊申请单唯一标识 | 系统自动生成的唯一编号,生成规则CS+年月日时分秒+4位随机数 |
|
||||
| **VisitID** | BIGINT | 20 | 门诊就诊流水号(逻辑外键) | 取值于本次门诊就诊记录表的主键 |
|
||||
| **OrderID** | BIGINT | 20 | 门诊医嘱表主键(一对一外键) | 门诊医嘱表 |
|
||||
| **PatientName** | Text | 50 | 患者姓名 | 患者的姓名 (取值患者档案) |
|
||||
| **Gender** | Text | 10 | 患者性别 | 男/女/其他 (取值患者档案) |
|
||||
| **Age** | Integer | - | 患者年龄 | 取值患者档案 |
|
||||
| **Department** | Text | 50 | 申请会诊的科室 | 当前科室名称 |
|
||||
| **RequestingPhysician** | Text | 50 | 申请会诊的医生 | 当前医生姓名 |
|
||||
| **ConsultationrequestDate** | DateTime | - | 会诊申请时间 | YYYY-MM-DD HH:MM:SS
|
||||
| **ConsultationPurpose** | Text | 255 | 简要病史及会诊目的 | 文本描述,自定义编辑 |
|
||||
| **ProvisionalDiagnosis** | Text | 255 | 门诊诊断 | 文本描述,自动获取医生开立的门诊诊断(主诊断) |
|
||||
| **ConsultationDate** | DateTime | - | 会诊时间 | YYYY-MM-DD HH:MM:SS |
|
||||
| **ConsultationStatus** | Text | 20 | 会诊状态 | 新开/已提交/已确认/已签名/已完成/已取消 |
|
||||
| **ConsultationUrgency** | Text | 20 | 是否紧急 | 勾选框:一般/紧急 |
|
||||
| **ConsultationOpinion** | Text | 255 | 会诊意见 | 文本描述 |
|
||||
| **ConfirmingPhysician** | Text | 50 | 提交会诊的医生 | 医生姓名 |
|
||||
| **ConfirmingPhysicianID** | Text | 20 | 提交会诊的医生ID | 医生唯一标识 |
|
||||
| **ConfirmingDate** | DateTime | - | 提交会诊日期 | YYYY-MM-DD HH:MM:SS |
|
||||
| **Signature** | Text | 50 | 结束会诊医生 | 医生姓名 |
|
||||
| **SignatureDate** | DateTime | - | 结束会诊日期 | YYYY-MM-DD HH:MM:SS |
|
||||
| **cancelnatureDate** | DateTime | - | 作废会诊日期 | YYYY-MM-DD HH:MM:SS |
|
||||
| InvitedObject | Text | 50 | 会诊邀请对象 | |
|
||||
|
||||
**诊状态用于记录会诊申请在不同阶段的状态,以下是常见的会诊状态及其说明:**
|
||||
|
||||
|
||||
| **状态名称** | **状态值** | **描述** |
|
||||
| ------------ | ---------- | ---------------------------------------------------------------------- |
|
||||
| **新开** | 0 | 会诊申请单已保存 |
|
||||
| **已提交** | 10 | 会诊申请已提交,但尚未被会诊医生确认。 |
|
||||
| **已确认** | 20 | 会诊医生已确认会诊申请,并准备进行会诊。 |
|
||||
| **已签名** | 30 | 会诊完成后进行签名 |
|
||||
| **已完成** | 40 | 会诊已经完成,会诊意见已记录。 |
|
||||
| **已取消** | 50 | 会诊申请被取消,可能由于患者情况变化或其他原因,申请医生进行作废操作。 |
|
||||
|
||||
**门诊医嘱表在相关会诊操作步骤的相关事务**
|
||||
|
||||
把“门诊会诊申请”当成**一种特殊医嘱**(OrderType = 'Consult')由系统**在同一事务内**自动插入 门诊医嘱表,再挂到 `ConsultationRequest` **注意:按照现有系统的门诊医嘱表进行设置相关字段的值**
|
||||
|
||||
|
||||
| **节点** | **是否自动** | **说明** |
|
||||
| --------------------- | ------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| 医生点击【保存】 | ✅ | 后台事务:先插门诊医嘱表(医嘱状态为“新开”),再插`ConsultationRequest`.Status=0 |
|
||||
| 医生点击【提交】 | ✅ | 仅更新两表状态 → 门诊医嘱表的医嘱状态和`ConsultationRequest.Status=10` (已提交),不重复生成医嘱 |
|
||||
| 医生点击【作废/删除】 | ✅ | 自动将门诊医嘱表的医嘱状态字段置为“作废”,级联`ConsultationRequest.Status=50` |
|
||||
| 医生点击【结束】 | ✅ | 将 门诊医嘱表的医嘱状态字段置为“已完成”,同时写`ConsultationRequest.Status=40` |
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:\#1890FF(操作按钮)
|
||||
- **辅助色**:\#13C2C2(打印)、\#52C41A(保存)、\#FF4D4F(结束)
|
||||
- **字体规范**:14px/1.5,中文字体优先使用"PingFang SC"
|
||||
- **间距系统**:16px基准,表单行间距12px
|
||||
- **组件样式**:
|
||||
- 按钮:4px圆角,32px高度
|
||||
- 输入框:4px圆角,1px \#D9D9D9边框
|
||||
- 表格行:选中状态\#E6F7FF背景+左侧3px蓝色边框
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- **浏览器兼容**:支持Chrome/Firefox/Edge最新版
|
||||
- **性能要求**:表单提交响应时间\<1秒
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 时间字段需统一处理为YYYY-MM-DD HH:mm:ss格式
|
||||
2. 申请单号生成需加锁防止重复
|
||||
3. 移动端需优化表格横向滚动体验
|
||||
4. 打印功能需特殊样式处理(隐藏操作按钮)
|
||||
310
md/需求/96-门诊医生站会诊申请确认界面_2026-01-15.md
Normal file
310
md/需求/96-门诊医生站会诊申请确认界面_2026-01-15.md
Normal file
@@ -0,0 +1,310 @@
|
||||
## 门诊医生站会诊申请确认界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:门诊医生站会诊申请确认界面
|
||||
**页面目标**:帮助医生完成会诊申请的确认、签名和打印操作,展示会诊申请详细信息
|
||||
**适用场景**:医生在收到会诊申请后,查看申请信息并给出会诊意见
|
||||
**页面类型**:表单页+列表页复合型页面
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 会诊申请单列表展示与选择
|
||||
2. 会诊确认与取消确认功能
|
||||
3. 签名功能
|
||||
4. 会诊记录单打印
|
||||
5. 会诊意见编辑与保存
|
||||
|
||||
**用户价值**:
|
||||
|
||||
- 规范会诊申请流程
|
||||
- 电子化确认和签名提高效率
|
||||
- 完整记录会诊意见便于后续诊疗
|
||||
- 打印功能满足纸质存档需求
|
||||
**原型图地址:**https://static.pm-ai.cn/prototype/20260115/7c45e175239257e0f04c9081bf2ca204/index.html
|
||||
**流程图:**
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["医生进入会诊申请确认界面"]) --> LoadList["加载会诊申请列表"]
|
||||
LoadList --> HasUntreated{"是否有未处理申请?"}
|
||||
|
||||
HasUntreated -- "否" --> ShowNoTip["显示无申请提示"]
|
||||
HasUntreated -- "是" --> SelectApp["医生选择会诊申请"]
|
||||
|
||||
SelectApp --> ShowDetail["显示会诊申请详情"]
|
||||
ShowDetail --> EditOpinion["医生编辑会诊意见"]
|
||||
|
||||
EditOpinion --> ConfirmClick{"点击确认按钮?"}
|
||||
ConfirmClick -- "否" --> SignClick{"点击签名按钮?"}
|
||||
ConfirmClick -- "是" --> ValidateConfirm{"校验必填字段"}
|
||||
|
||||
ValidateConfirm -- "不通过" --> TipFill["提示\n请先填写会诊意见"]
|
||||
ValidateConfirm -- "通过" --> CheckConfirmed{"是否已确认?"}
|
||||
|
||||
CheckConfirmed -- "是" --> UpdateConfirmed["更新状态为\n已确认"]
|
||||
UpdateConfirmed --> AutoFill["自动填充医生科室信息"]
|
||||
AutoFill --> DisableCancel["禁用取消确认功能"]
|
||||
|
||||
CheckConfirmed -- "否" --> KeepState["保持当前状态"]
|
||||
|
||||
SignClick -- "否" --> PrintClick{"点击打印按钮?"}
|
||||
SignClick -- "是" --> ValidateSign{"校验通过?"}
|
||||
|
||||
ValidateSign -- "不通过" --> TipConfirmFirst["提示\n请先确认会诊申请"]
|
||||
ValidateSign -- "通过" --> UpdateSigned["更新状态为\n已签名"]
|
||||
UpdateSigned --> RecordSign["记录签名医生和时间"]
|
||||
|
||||
PrintClick -- "否" --> RefreshClick{"点击刷新按钮?"}
|
||||
PrintClick -- "是" --> GenPrintView["生成打印优化视图"]
|
||||
GenPrintView --> BrowserPrint["调用浏览器打印功能"]
|
||||
|
||||
RefreshClick -- "是" --> LoadList
|
||||
RefreshClick -- "否" --> KeepState
|
||||
|
||||
TipFill --> EditOpinion
|
||||
TipConfirmFirst --> EditOpinion
|
||||
KeepState --> End(["结束"])
|
||||
BrowserPrint --> End
|
||||
DisableCancel --> End
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. 顶部标签导航(高度48px)
|
||||
2. 操作按钮区(高度36px+间距)
|
||||
3. 会诊申请列表区(高度自适应)
|
||||
4. 会诊记录单表单区(高度自适应)
|
||||
**布局特点**:上下布局,采用网格系统对齐,左侧对齐为主
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部标签导航区域
|
||||
|
||||
**区域位置**:页面顶部
|
||||
**区域尺寸**:高度48px,宽度100%
|
||||
**区域功能**:页面导航标识
|
||||
**包含元素**:
|
||||
|
||||
- **会诊确认标签**
|
||||
- 元素类型:文本标签
|
||||
- 显示内容:“会诊确认”
|
||||
- 交互行为:无点击交互(当前页面)
|
||||
- 样式特征:蓝色下划线,16px字体,700字重
|
||||
|
||||
#### 2. 操作按钮区域
|
||||
|
||||
**区域位置**:标签导航下方
|
||||
**区域尺寸**:高度36px,宽度100%
|
||||
**区域功能**:提供页面主要操作入口
|
||||
**包含元素**:
|
||||
|
||||
- **打印按钮**
|
||||
- 元素类型:操作按钮
|
||||
- 显示内容:“打印”
|
||||
- 交互行为:点击触发打印会诊记录单
|
||||
- 样式特征:绿色背景,白色文字,圆角6px
|
||||
- **刷新按钮**
|
||||
- 元素类型:操作按钮
|
||||
- 显示内容:“刷新”
|
||||
- 交互行为:点击重新加载页面数据
|
||||
- 样式特征:白色背景,灰色边框,黑色文字
|
||||
- **确认按钮**
|
||||
- 元素类型:状态切换按钮
|
||||
- 显示内容:“确认”/“取消确认”
|
||||
- 交互行为:
|
||||
- 点击后变为"取消确认"状态(红色样式)
|
||||
- 已签名时禁用取消操作
|
||||
- 样式特征:蓝色背景,白色文字
|
||||
- 限制条件:需选中表格行才可操作
|
||||
- **签名按钮**
|
||||
- 元素类型:操作按钮
|
||||
- 显示内容:“签名”
|
||||
- 交互行为:
|
||||
- 需先确认才能签名
|
||||
- 签名后自动记录签名时间和签名医生
|
||||
- 样式特征:蓝色背景,白色文字
|
||||
- 限制条件:需先完成确认操作
|
||||
|
||||
#### 3. 会诊申请列表区域
|
||||
|
||||
**区域位置**:按钮区域下方
|
||||
**区域尺寸**:高度自适应,宽度100%
|
||||
**区域功能**:展示待处理的会诊申请列表
|
||||
**包含元素**:
|
||||
|
||||
- **申请列表表格** (取值于门诊会诊申请单表(ConsultationRequest))
|
||||
- 检索要求:医生登录门诊医生站打开会诊申请确认界面时只能检索出当前登录医生姓名包含在会诊邀请对象内(只能查看自己受会诊邀请对象)
|
||||
- 展示方式:带斑马纹表格
|
||||
- 表头字段:
|
||||
- 序号 \| 紧急 \| 申请单号 \| 病人姓名 \| 会诊时间 \| 邀请对象 \| 申请科室 \| 申请医师 \| 申请时间 \| 确认 \| 签名
|
||||
- 数据字段:
|
||||
- 序号:文本 - 自动编号 - “1” - 不可操作
|
||||
- 紧急:复选框 - 布尔值 - 未勾选 - 可操作
|
||||
- 申请单号:文本 - 字符串 - “CS20250812001” - 不可操作
|
||||
- 病人姓名:文本 - 字符串 - “陈明” - 不可操作
|
||||
- 会诊时间:日期 - 日期时间 - “2025-08-12 17:48” - 不可操作
|
||||
- 邀请对象:文本 - 字符串 - “演示测试” - 不可操作
|
||||
- 申请科室:文本 - 字符串 - “内科” - 不可操作
|
||||
- 申请医师:文本 - 字符串 - “徐斌” - 不可操作
|
||||
- 申请时间:日期 - 日期时间 - “2025-08-12 17:48” - 不可操作
|
||||
- 确认:复选框 - 布尔值 - 勾选框 – 不可操作
|
||||
- 签名:复选框 - 布尔值 - 勾选框 – 不可操作
|
||||
- 操作功能:点击行选中查看会诊申请详情
|
||||
- 样式特征:斑马纹交替背景,悬停高亮
|
||||
|
||||
#### 4. 会诊记录单表单区域
|
||||
|
||||
**区域位置**:列表区域下方
|
||||
**区域尺寸**:高度自适应,宽度100%
|
||||
**区域功能**:展示和编辑会诊详细信息
|
||||
**包含元素**:
|
||||
|
||||
- **基础信息区**
|
||||
- 布局方式:8列网格
|
||||
- 包含字段:
|
||||
- 病人姓名/性别/年龄/就诊卡号
|
||||
- 申请单号/申请科室
|
||||
- 会诊时间/紧急标志
|
||||
- 会诊邀请对象
|
||||
- 提交医生/提交时间
|
||||
- **病史及目的区**
|
||||
- 元素类型:文本区域
|
||||
- 显示内容:患者主诉和会诊目的
|
||||
- 交互行为:只读展示
|
||||
- **会诊确认参加医师**
|
||||
- **会诊意见区**
|
||||
- 元素类型:可编辑文本域
|
||||
- 显示内容:会诊意见文本
|
||||
- 交互行为:支持多行编辑
|
||||
- 样式特征:浅灰色背景,120px最小高度
|
||||
- **确认/签名信息区**
|
||||
- 包含字段:
|
||||
- 所属医生/代表科室(确认后自动填充当前医生和科室)
|
||||
- 签名医生/签名时间(自动填充签名医生和签名时间(系统当前时间))
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 会诊申请选择功能
|
||||
|
||||
**触发方式**:点击表格行
|
||||
**执行流程**:
|
||||
|
||||
1. 高亮选中行(浅蓝色背景)
|
||||
2. 同步该行数据到下方表单
|
||||
3. 根据确认状态更新按钮文字
|
||||
4. 加载存储的会诊意见到文本域
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 无选中行时禁用确认/签名按钮
|
||||
- 已签名行禁止取消确认
|
||||
|
||||
#### 2. 会诊确认功能
|
||||
|
||||
**触发方式**:点击确认按钮
|
||||
|
||||
**执行流程**:
|
||||
|
||||
1. 校验必填字段(会诊意见、会诊确认参加医师)
|
||||
2. 保存会诊意见等相关内容到行数据(写入门诊会诊申请确认表(ConsultationConfirmation))
|
||||
3. 勾选确认复选框
|
||||
4. 更新按钮为"取消确认"状态
|
||||
5. 所属医生和代表科室(自动填充当前医生和科室)
|
||||
|
||||
**异常处理**:
|
||||
|
||||
- 未填写会诊意见时提示"请先填写会诊意见"
|
||||
- 保存失败时保持原状态并提示错误
|
||||
|
||||
#### 2. 电子签名功能
|
||||
|
||||
**功能描述**:医生对确认的会诊进行电子签名
|
||||
**触发条件**:已确认的会诊申请点击"签名"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 医生确认会诊申请
|
||||
2. 点击"签名"按钮
|
||||
3. 校验确认状态
|
||||
4. 表格中"签名"列复选框被勾选
|
||||
5. 自动记录签名医生(当前用户)
|
||||
6. 自动填充签名时间为系统时间
|
||||
7. 禁用取消确认功能
|
||||
**成功反馈**:表单区显示签名信息
|
||||
**失败处理**:提示"请先确认会诊申请"
|
||||
|
||||
#### 3. 打印会诊记录单
|
||||
|
||||
**功能描述**:打印格式化的会诊记录
|
||||
**触发条件**:点击"打印"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 点击"打印"按钮
|
||||
2. 系统生成打印优化视图
|
||||
3. 调用浏览器打印功能
|
||||
**特殊处理**:隐藏交互元素,优化打印布局
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
**门诊会诊申请确认表(**ConsultationConfirmation**)**
|
||||
|
||||
| **字段名称** | **数据类型** | **长度** | **描述** | **约束/说明** |
|
||||
|-----------------------------|--------------|----------|--------------------|------------------------------------------------------------------------------------|
|
||||
| **ConsultationID** | INTEGER | 20 | 会诊申请单唯一标识 | FOREIGN KEY REFERENCES ConsultationRequest(ConsultationID) |
|
||||
| **ConfirmingPhysicianID** | TEXT | -20 | 确认会诊的医生ID | 操作【确认】按钮的当前医生ID |
|
||||
| **ConfirmingPhysicianName** | TEXT | -20 | 确认会诊的医生姓名 | 操作【确认】按钮的当前医生姓名 |
|
||||
| **ConfirmingDeptName** | TEXT | 20 | 代表科室 | 操作【确认】按钮的当前开单科室 |
|
||||
| **ConfirmingDate** | DateTime | - | 确认会诊的日期 | 操作【确认】按钮当前系统时间 |
|
||||
| **ConsultationStatus** | TEXT | 20 | 会诊状态 | CHECK (ConsultationStatus IN ('已确认', '取消确认', '已签名', '已完成')), NOT NULL |
|
||||
| **ConsultationOpinion** | TEXT | 500 | 会诊意见 | |
|
||||
| **ConfirmingPhysician** | TEXT | 100 | 会诊确认参加医师 | |
|
||||
| **Signature** | TEXT | 20 | 签名医生 | |
|
||||
| **SignatureDate** | DateTime | - | 签名时间 | - |
|
||||
|
||||
ConsultationConfirmation.ConsultationStatu会诊状态
|
||||
|
||||
| **状态值** | **状态名** | **描述** |
|
||||
|------------|------------|-----------------------------------|
|
||||
| **0** | 取消确认 | 作废 |
|
||||
| **20** | 已确认 | 会诊医生已查看/同意,可写初步意见 |
|
||||
| **30** | 已签名 | 已电子签名,意见最终生效 |
|
||||
| **40** | 已完成 | 会诊报告已回写,流程关闭 |
|
||||
|
||||
**按钮涉及的事务**
|
||||
|
||||
| **按钮** | **涉及表** | **执行事务** | **锁/并发** | **成功状态** | **失败处理** |
|
||||
|--------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|------------------|--------------------------|
|
||||
| **确认** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =20<br>2、医嘱 状态='已执行'<br>3、写入ConsultationConfirmation表相关的数据 | SELECT ... FOR UPDATE | 已提交 → 已确认 | 任何异常 → 整体 ROLLBACK |
|
||||
| **取消确认** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =10<br>2、医嘱 状态='已提交'<br>3、ConsultationConfirmation. ConsultationStatus = 0 | 同上 | 已确认→ 取消确认 | 同上回滚 |
|
||||
| **签名** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =30<br>2、医嘱 Status='已完成'<br>3、写入ConsultationConfirmation. Signature, SignatureDate,ConsultationStatus =30 | 同上 | 已确认 → 已签名 | 同上回滚 |
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:\#4A89DC(按钮蓝色)
|
||||
- **辅助色**:\#4CAF50(成功绿色)
|
||||
- **字体规范**:14px/1.5 常规,16px 标题
|
||||
- **间距系统**:8px基础间距,24px区块间距
|
||||
- **组件样式**:
|
||||
- 按钮:6px圆角,1px边框
|
||||
- 输入框:4px圆角,1px \#E0E0E0边框
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- **浏览器兼容**:Chrome/Firefox/Edge最新版
|
||||
- **性能要求**:列表加载时间\<1s
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 确认和签名状态需要联动控制
|
||||
2. 打印功能需要特殊样式处理
|
||||
3. 时间字段需统一使用YYYY-MM-DD HH:mm:ss格式
|
||||
4. 移动端需优化表单布局
|
||||
267
md/需求/97-门诊会诊申请管理界面_2026-1-19.md
Normal file
267
md/需求/97-门诊会诊申请管理界面_2026-1-19.md
Normal file
@@ -0,0 +1,267 @@
|
||||
## 门诊会诊申请管理界面PRD文档
|
||||
|
||||
### 一、页面概述
|
||||
|
||||
**页面名称**:门诊会诊申请管理界面
|
||||
**页面目标**:提供会诊申请的全流程管理功能,包括申请记录查询、编辑申请、查看详情、状态变更等核心操作
|
||||
**适用场景**:门诊医生需要查看会诊申请或管理已有申请记录时使用
|
||||
**页面类型**:列表页+表单弹窗复合型页面
|
||||
|
||||
**核心功能**:
|
||||
|
||||
1. 多条件组合筛选会诊申请记录
|
||||
2. 会诊申请表格展示与操作(编辑/查看/删除)
|
||||
3. 会诊申请单的填写与提交
|
||||
4. 会诊状态标记(提交/结束)
|
||||
**用户价值**:规范会诊申请流程,减少纸质单据流转,提高多科室协作效率
|
||||
原型图地址:https://static.pm-ai.cn/prototype/20260116/aed1f102d614677f100c0d1fe3104999/index.html
|
||||
**流程图:**
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Start]) --> A[进入门诊会诊申请管理界面]
|
||||
A --> B{用户操作类型}
|
||||
B -->|筛选查询| C[设置筛选条件]
|
||||
B -->|编辑申请| D[点击编辑按钮]
|
||||
B -->|查看详情| E[点击查看按钮]
|
||||
B -->|删除申请| G[点击删除按钮]
|
||||
|
||||
C --> H{验证筛选条件}
|
||||
H -->|有效| I[展示筛选结果]
|
||||
H -->|无效| J[显示错误提示]
|
||||
J --> C
|
||||
|
||||
I --> K[用户浏览列表]
|
||||
|
||||
D --> L{检查会诊状态}
|
||||
L -->|未结束| M[打开编辑弹窗]
|
||||
L -->|已结束| N[提示不可编辑]
|
||||
N --> O[关闭弹窗]
|
||||
|
||||
M --> P[修改表单内容]
|
||||
P --> Q{表单验证}
|
||||
Q -->|通过| R[保存修改]
|
||||
Q -->|不通过| S[标红错误字段]
|
||||
S --> P
|
||||
|
||||
E --> T{检查会诊状态}
|
||||
T -->|未结束| U[打开只读弹窗]
|
||||
T -->|已结束| U
|
||||
|
||||
G --> Z{删除验证}
|
||||
Z -->|可删除| AA[确认删除]
|
||||
Z -->|不可删除| AB[提示删除失败]
|
||||
AB --> K
|
||||
|
||||
AA --> AC[更新状态为已取消]
|
||||
AC --> AD[更新列表显示]
|
||||
|
||||
R --> AD
|
||||
AD --> K
|
||||
K --> AE([End])
|
||||
```
|
||||
|
||||
### 二、整体布局分析
|
||||
|
||||
**页面宽度**:自适应布局
|
||||
**主要区域划分**:
|
||||
|
||||
1. 顶部筛选区(高度自适应,约80px)
|
||||
2. 表格展示区(高度自适应,占主要空间)
|
||||
3. 底部页码区(固定高度56px)
|
||||
**布局特点**:上下布局+弹性布局,采用左右对齐方式
|
||||
|
||||
### 三、页面区域详细描述
|
||||
|
||||
#### 1. 顶部筛选区
|
||||
|
||||
**区域位置**:页面顶部
|
||||
**区域尺寸**:100%宽度,高度自适应
|
||||
**区域功能**:提供多维度筛选和快速搜索功能
|
||||
**包含元素**:
|
||||
|
||||
- **时间类型选择器**
|
||||
- 元素类型:下拉选择框
|
||||
- 显示内容:默认"会诊时间",可选"申请时间"
|
||||
- 交互行为:点击展开下拉选项
|
||||
- 样式特征:宽度120px,高度32px,圆角4px
|
||||
- **时间范围选择器**(开始/结束时间)
|
||||
- 元素类型:日期时间输入框
|
||||
- 显示内容:placeholder提示"开始时间"/“结束时间”
|
||||
- 交互行为:点击弹出日期选择面板
|
||||
- 样式特征:宽度180px,高度32px
|
||||
- **申请科室/申请医生选择器**
|
||||
- 元素类型:带datalist的输入框
|
||||
- 显示内容:placeholder提示"选择或输入科室/医生"
|
||||
- 交互行为:输入时显示匹配选项
|
||||
- 数据来源:动态生成申请科室/医生候选列表,取值于门诊会诊申请单表(ConsultationRequest.Department/ RequestingPhysician)
|
||||
- **会诊状态筛选器**
|
||||
- 元素类型:下拉选择框
|
||||
- 可选值:全部/未提交/提交/结束
|
||||
- 默认值:全部
|
||||
- **病人姓名搜索框**
|
||||
- 元素类型:文本输入框
|
||||
- 显示内容:placeholder提示"病人姓名"
|
||||
- 交互行为:支持模糊搜索
|
||||
- **操作按钮组**
|
||||
- 查询按钮
|
||||
- 样式:蓝色背景,带搜索图标
|
||||
- 交互:触发筛选条件应用
|
||||
- 重置按钮
|
||||
- 样式:灰色背景,带刷新图标
|
||||
- 交互:清空所有筛选条件
|
||||
- 打印按钮
|
||||
- 样式:深灰色背景,带打印图标
|
||||
- 交互:调起浏览器打印功能
|
||||
|
||||
#### 2. 表格展示区
|
||||
|
||||
**区域位置**:页面中部
|
||||
**区域尺寸**:100%宽度,高度自适应
|
||||
**区域功能**:展示会诊申请列表数据,支持行内操作
|
||||
**包含元素**:
|
||||
|
||||
取值于门诊会诊申请单表(ConsultationRequest)和门诊会诊申请单表(ConsultationRequest)
|
||||
|
||||
- **数据表格**
|
||||
- 展示方式:11列固定表头表格
|
||||
- 数据字段:
|
||||
- ID:文本 - 15 -申请单号
|
||||
- 急:复选框 - 布尔值 - 示例false – 不可编辑
|
||||
- 病人姓名:文本 - 朱某某 - 红色高亮
|
||||
- 会诊时间:日期 - 2026-01-05 15:08
|
||||
- 申请科室:文本 - 内科
|
||||
- 邀请对象:文本 - 吴院长
|
||||
- 申请时间:日期 - 2026-01-05 15:08
|
||||
- 申请医师:文本 - 演示测试
|
||||
- 提交:复选框 - 布尔值 - 示例false
|
||||
- 结束:复选框 - 布尔值 - 示例false
|
||||
- 操作功能:
|
||||
- 编辑按钮(✏️):点击打开编辑弹窗
|
||||
- 查看按钮(👁️):点击打开只读弹窗
|
||||
- 删除按钮(🗑️):点击确认删除
|
||||
- 【删除】将状态改为“已取消”
|
||||
- UPDATE ConsultationRequest
|
||||
- SET ConsultationStatus = 50,cancelnatureDate = \<作废会诊时间\>
|
||||
- WHERE ConsultationID = \<会诊申请单ID\> and ConsultationStatus \<\> 40 ;
|
||||
- 交互行为:
|
||||
- 行悬停效果:浅蓝色背景
|
||||
- 复选框点击:即时更新状态(需防抖处理)
|
||||
|
||||
#### 3. 底部页码区
|
||||
|
||||
**区域位置**:页面底部
|
||||
**区域尺寸**:100%宽度,固定高度56px
|
||||
**区域功能**:分页控制和数据统计
|
||||
**包含元素**:
|
||||
|
||||
- **总数统计**:总数统计文本(如:“总数:15”)
|
||||
- **分页控制器**:
|
||||
- 上一页按钮(\<)
|
||||
- 当前页按钮(1)active状态
|
||||
- 下一页按钮(\>)
|
||||
- 交互反馈:hover时边框变蓝
|
||||
|
||||
#### 4. 会诊申请弹窗(模态框)
|
||||
|
||||
**触发方式**:点击表格行操作列的编辑/查看按钮
|
||||
**区域功能**:展示/编辑会诊申请详细信息
|
||||
**包含元素**:
|
||||
|
||||
- 头部区域
|
||||
- 标题:“会诊申请单”
|
||||
- 关闭按钮(×图标)
|
||||
- 表单区域(分两栏布局)
|
||||
- 基础信息区:
|
||||
- 申请单号(只读)
|
||||
- 申请时间(不可编辑)
|
||||
- 病人姓名(不可编辑)
|
||||
- 性别/年龄(不可编辑)
|
||||
- 就诊卡号(不可编辑)
|
||||
- 会诊信息区:
|
||||
- 会诊时间(日期时间选择器)
|
||||
- 申请医师(不可编辑)
|
||||
- 紧急程度(复选框)
|
||||
- 申请科室(不可编辑)
|
||||
- 门诊诊断(不可编辑)
|
||||
- 会诊邀请对象
|
||||
- 会诊确认参加医师
|
||||
- 所属医生
|
||||
- 代表科室
|
||||
- 签名医生
|
||||
- 签名时间
|
||||
- 文本域:
|
||||
- 病史及会诊目的(多行文本)
|
||||
- 会诊意见(多行文本)
|
||||
- 底部按钮区:
|
||||
- 取消按钮(左对齐)
|
||||
- 保存按钮(右对齐,蓝色)
|
||||
|
||||
### 四、交互功能详细说明
|
||||
|
||||
#### 1. 会诊申请编辑功能
|
||||
|
||||
**功能描述**:修改已有会诊申请信息
|
||||
**触发条件**:点击表格行中的"✏️"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 检查会诊状态是否为"结束",若已结束则提示不可编辑
|
||||
2. 弹出会诊申请编辑弹窗,填充当前行数据的会诊申请和确认相关的数据
|
||||
3. 用户修改表单内容(必填字段校验)
|
||||
4. 点击"保存"按钮提交修改
|
||||
**异常处理**:
|
||||
- 必填字段为空时,标红提示
|
||||
- 保存失败时显示toast提示"保存失败,请重试"
|
||||
|
||||
#### 2. 会诊申请查看功能
|
||||
|
||||
**功能描述**:查看会诊申请详细信息
|
||||
**触发条件**:点击表格行中的"👁️"按钮
|
||||
**操作流程**:
|
||||
|
||||
1. 弹出只读弹窗,显示完整申请信息
|
||||
2. 所有字段禁用编辑
|
||||
3. 仅显示"取消"按钮用于关闭弹窗
|
||||
|
||||
#### 3. 数据筛选功能
|
||||
|
||||
**功能描述**:多条件组合查询会诊记录
|
||||
**触发条件**:点击"查询"按钮
|
||||
**数据过滤逻辑**:
|
||||
|
||||
- 时间范围:根据选择的时间类型(会诊/申请)进行筛选
|
||||
- 申请科室/申请医生:支持模糊匹配
|
||||
- 会诊状态筛选:支持多选逻辑(未提交/提交/结束)
|
||||
1. 收集所有筛选条件值
|
||||
2. 发起异步请求(示例中为前端过滤)
|
||||
3. 更新表格数据展示
|
||||
**异常处理**:
|
||||
- 时间范围不合法:提示"结束时间不能早于开始时间"
|
||||
- 无查询结果:显示空白表格+提示文字
|
||||
**性能优化**:前端本地缓存数据,减少服务器请求
|
||||
|
||||
### 五、数据结构说明
|
||||
|
||||
门诊会诊申请单表(ConsultationRequest)和门诊会诊申请单表(ConsultationRequest)
|
||||
|
||||
### 六、开发实现要点
|
||||
|
||||
**样式规范**:
|
||||
|
||||
- **主色调**:\#5D9CEC(按钮/交互元素)
|
||||
- **辅助色**:\#8E8E8E(次要按钮)
|
||||
- **字体规范**:14px/1.5(主要内容),16px/1.5(标题)
|
||||
- **间距系统**:16px(元素间距),24px(区块间距)
|
||||
- **组件样式**:
|
||||
- 按钮:圆角6px,内边距0 16px
|
||||
- 输入框:1px实线边框\#D9D9D9,圆角4px
|
||||
|
||||
**技术要求**:
|
||||
|
||||
- **浏览器兼容**:支持Chrome/Firefox/Edge最新版
|
||||
- **性能要求**:列表数据筛选响应时间\<200ms
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. 状态变更逻辑:已结束的记录不可编辑
|
||||
2. 时间字段需要做时区转换处理
|
||||
3. 申请科室/申请医生选择器需要支持拼音首字母检索
|
||||
62
md/需求/99-门诊手术中计费界面PRD_2026-1-22.md
Normal file
62
md/需求/99-门诊手术中计费界面PRD_2026-1-22.md
Normal file
@@ -0,0 +1,62 @@
|
||||
**门诊手术中计费PRD文档**
|
||||
|
||||
**目标:**
|
||||
|
||||
支持手术中追加计费(耗材、药品等)、退费等场景使用
|
||||
|
||||
术后一站式结算(发票、清单、医保等)
|
||||
|
||||
**流程图:**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["医生开立手术申请单"] --> B{"系统生成计费包"}
|
||||
B --> C["患者缴费"]
|
||||
C --> D["手术室确认"]
|
||||
D --> E{"术中追加/退费?"}
|
||||
|
||||
E -- "是" --> F{"术中计费"}
|
||||
F -- "耗材" --> F2["护士扫码追加耗材\n实时计价 更新库存"]
|
||||
F -- "药品" --> F3["麻醉师追加药品\n实时计价 更新库存"]
|
||||
F -- "诊疗项目" --> F4["追加麻醉时长/项目\n实时计价"]
|
||||
|
||||
|
||||
F2 --> F6["生成术中追加计费单"]
|
||||
F3 --> F6
|
||||
F4 --> F6
|
||||
|
||||
|
||||
F6 --> G{"患者支付?"}
|
||||
G -- "是" --> P["提示支付成功"]--> J
|
||||
G -- "否" --> H["提示支付失败\n保持待支付"]
|
||||
H --> D
|
||||
|
||||
E -- "否" --> I["手术完成"]
|
||||
I --> J["术后统一结算"]
|
||||
J --> K["发票/清单/分割单"]
|
||||
K --> L["财务对账"]
|
||||
```
|
||||
|
||||
**注意:**待门诊手术安排界面(禅道需求编号:93)完成后再执行
|
||||
|
||||

|
||||
|
||||
图1:门诊手术安排界面(禅道需求编号:93)
|
||||
|
||||

|
||||
|
||||
图2:门诊管理-》门诊划价:手术计费界面复制《门诊划价》界面红色框内容
|
||||
|
||||
1、如上图1、2所示:在门诊手术安排界面增加【计费】按钮,实现对门诊手术中追加的费用进行记账,手术计费界面如图2所示复制《门诊划价》界面红色框内容进行个性化改造,患者信息:取值于手术安排界面选中行的患者信息,计费账号为当前系统登录的账号。
|
||||
|
||||
\*比如:在手术计费界面给患者1计费成功后,重新从手术按钮界面选中患者1点击【计费】打开界面时显示当前患者已计费成功的手术费用。
|
||||
|
||||
写入事务注意:
|
||||
|
||||
adm_charge_item费用项管理表
|
||||
|
||||
①、术中费用仍走“门诊就诊管理”的就诊ID(adm_encounter.id = adm_charge_item.encounter_id)。
|
||||
|
||||
2\. 为了事后能追溯“这些费用是术中发生的”,在费用项管理表明细上加一个 “来源业务单据(SourceBillNo)” 字段(adm_charge_item.generate_source_enum = 2(帐单生成来源为手术计费),SourceBillNo = 手术申请单号)。
|
||||
|
||||
3\. 其他内容按照《门诊划价》的业务数据流程走。
|
||||
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 |
@@ -1,2 +0,0 @@
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">OpenHis v0.0.1</h1>
|
||||
|
||||
@@ -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("当前用户菜单缓存已刷新");
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,15 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
<arg>--add-modules</arg>
|
||||
<arg>java.base</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.core.common.core.domain;
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
@@ -27,6 +28,7 @@ public class HisBaseEntity implements Serializable {
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
@@ -35,6 +37,7 @@ public class HisBaseEntity implements Serializable {
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
/** 租户ID */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.core.common.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.core.common.utils.AuditFieldUtil;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 包含审计字段自动设置功能的基础服务类
|
||||
*
|
||||
* @param <M> Mapper 类型,必须继承 BaseMapper<T>
|
||||
* @param <T> 实体类型
|
||||
*/
|
||||
public abstract class BaseService<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
|
||||
|
||||
/**
|
||||
* 重写保存方法,自动设置创建审计字段
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean save(T entity) {
|
||||
// 在保存前设置创建审计字段
|
||||
AuditFieldUtil.setCreateInfo(entity);
|
||||
return super.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写批量保存方法,自动设置创建审计字段
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean saveBatch(Collection<T> entityList) {
|
||||
// 为每个实体设置创建审计字段
|
||||
entityList.forEach(AuditFieldUtil::setCreateInfo);
|
||||
return super.saveBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写更新方法,自动设置更新审计字段
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateById(T entity) {
|
||||
// 在更新前设置更新审计字段
|
||||
AuditFieldUtil.setUpdateInfo(entity);
|
||||
return super.updateById(entity);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审计字段工具类
|
||||
* 用于手动设置创建人、创建时间、更新人、更新时间等审计字段
|
||||
*/
|
||||
@Component
|
||||
public class AuditFieldUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuditFieldUtil.class);
|
||||
|
||||
/**
|
||||
* 为实体设置创建相关的审计字段
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
public static void setCreateInfo(Object entity) {
|
||||
if (entity == null) return;
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||
Date currentTime = new Date();
|
||||
|
||||
// 使用反射设置字段值
|
||||
Field createByField = getField(entity.getClass(), "createBy");
|
||||
if (createByField != null) {
|
||||
createByField.setAccessible(true);
|
||||
// 只有当字段值为 null 或空字符串时才设置
|
||||
if (createByField.get(entity) == null || "".equals(createByField.get(entity))) {
|
||||
createByField.set(entity, username);
|
||||
}
|
||||
}
|
||||
|
||||
Field createTimeField = getField(entity.getClass(), "createTime");
|
||||
if (createTimeField != null) {
|
||||
createTimeField.setAccessible(true);
|
||||
// 只有当字段值为 null 时才设置
|
||||
if (createTimeField.get(entity) == null) {
|
||||
createTimeField.set(entity, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下划线命名的字段
|
||||
Field createByFieldUnderscore = getField(entity.getClass(), "create_by");
|
||||
if (createByFieldUnderscore != null) {
|
||||
createByFieldUnderscore.setAccessible(true);
|
||||
// 只有当字段值为 null 或空字符串时才设置
|
||||
if (createByFieldUnderscore.get(entity) == null || "".equals(createByFieldUnderscore.get(entity))) {
|
||||
createByFieldUnderscore.set(entity, username);
|
||||
}
|
||||
}
|
||||
|
||||
Field createTimeFieldUnderscore = getField(entity.getClass(), "create_time");
|
||||
if (createTimeFieldUnderscore != null) {
|
||||
createTimeFieldUnderscore.setAccessible(true);
|
||||
// 只有当字段值为 null 时才设置
|
||||
if (createTimeFieldUnderscore.get(entity) == null) {
|
||||
createTimeFieldUnderscore.set(entity, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("设置创建审计字段时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为实体设置更新相关的审计字段
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
public static void setUpdateInfo(Object entity) {
|
||||
if (entity == null) return;
|
||||
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||
Date currentTime = new Date();
|
||||
|
||||
// 设置更新人字段
|
||||
Field updateByField = getField(entity.getClass(), "updateBy");
|
||||
if (updateByField != null) {
|
||||
updateByField.setAccessible(true);
|
||||
updateByField.set(entity, username);
|
||||
}
|
||||
|
||||
// 设置更新时间字段
|
||||
Field updateTimeField = getField(entity.getClass(), "updateTime");
|
||||
if (updateTimeField != null) {
|
||||
updateTimeField.setAccessible(true);
|
||||
updateTimeField.set(entity, currentTime);
|
||||
}
|
||||
|
||||
// 处理下划线命名的字段
|
||||
Field updateByFieldUnderscore = getField(entity.getClass(), "update_by");
|
||||
if (updateByFieldUnderscore != null) {
|
||||
updateByFieldUnderscore.setAccessible(true);
|
||||
updateByFieldUnderscore.set(entity, username);
|
||||
}
|
||||
|
||||
Field updateTimeFieldUnderscore = getField(entity.getClass(), "update_time");
|
||||
if (updateTimeFieldUnderscore != null) {
|
||||
updateTimeFieldUnderscore.setAccessible(true);
|
||||
updateTimeFieldUnderscore.set(entity, currentTime);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("设置更新审计字段时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用反射获取字段,支持父类字段
|
||||
* @param clazz 类
|
||||
* @param fieldName 字段名
|
||||
* @return 字段对象
|
||||
*/
|
||||
private static Field getField(Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
return clazz.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// 如果在当前类中找不到,尝试在父类中查找
|
||||
if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
|
||||
return getField(clazz.getSuperclass(), fieldName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("流程启动错误");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@
|
||||
<artifactId>oshi-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- spring security 安全认证 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.core</groupId>
|
||||
@@ -65,6 +71,12 @@
|
||||
<artifactId>core-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus 支持 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSQLParser - 用于MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
package com.core.framework.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.core.framework.handler.MybastisColumnsHandler;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -149,4 +159,55 @@ public class MybatisPlusConfig {
|
||||
|
||||
return result != null ? result : 1; // 默认租户ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 SqlSessionFactory
|
||||
* 由于排除了 DataSourceAutoConfiguration,需要手动配置
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public SqlSessionFactory sqlSessionFactory(
|
||||
@Qualifier("dynamicDataSource") DataSource dataSource,
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor,
|
||||
MetaObjectHandler metaObjectHandler) throws Exception { // 注入 MetaObjectHandler
|
||||
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
// 设置 mapper 文件位置
|
||||
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
|
||||
.getResources("classpath*:mapper/**/*Mapper.xml"));
|
||||
// 设置 typeAliases 包路径
|
||||
sessionFactory.setTypeAliasesPackage("com.core.**.domain,com.openhis.**.domain");
|
||||
|
||||
// 配置 MyBatis-Plus
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
// 使用驼峰命名法转换字段
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
// 开启缓存
|
||||
configuration.setCacheEnabled(true);
|
||||
// 允许JDBC支持自动生成主键
|
||||
configuration.setUseGeneratedKeys(true);
|
||||
// 配置默认的执行器
|
||||
configuration.setDefaultExecutorType(org.apache.ibatis.session.ExecutorType.SIMPLE);
|
||||
// 配置日志实现
|
||||
configuration.setLogImpl(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
|
||||
sessionFactory.setConfiguration(configuration);
|
||||
|
||||
// 设置 MyBatis-Plus 的全局配置
|
||||
GlobalConfig globalConfig = new GlobalConfig();
|
||||
globalConfig.setMetaObjectHandler(metaObjectHandler);
|
||||
sessionFactory.setGlobalConfig(globalConfig);
|
||||
|
||||
// 设置拦截器(通过参数注入避免循环依赖)
|
||||
sessionFactory.setPlugins(mybatisPlusInterceptor);
|
||||
|
||||
return sessionFactory.getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自动填充处理器
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new MybastisColumnsHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "tenantId", Integer.class, getCurrentTenantId());
|
||||
// 使用 fillStrategy 而不是 strictInsertFill,确保即使字段已设置也能填充(如果为null)
|
||||
this.fillStrategy(metaObject, "createBy", username != null ? username : "system");
|
||||
this.fillStrategy(metaObject, "tenantId", getCurrentTenantId());
|
||||
}
|
||||
|
||||
// 设置数据修改update时候的,字段自动赋值规则
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- MyBatis-Plus 支持 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- velocity代码生成使用模板 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
@@ -36,6 +42,24 @@
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok 支持 -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON工具类 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson 注解支持 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- MyBatis-Plus 支持 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.core</groupId>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -28,6 +28,11 @@ public class MetaVo {
|
||||
*/
|
||||
private String link;
|
||||
|
||||
/**
|
||||
* 菜单是否可见(用于前端侧边栏显示控制)
|
||||
*/
|
||||
private String visible;
|
||||
|
||||
public MetaVo() {}
|
||||
|
||||
public MetaVo(String title, String icon) {
|
||||
@@ -56,6 +61,16 @@ public class MetaVo {
|
||||
}
|
||||
}
|
||||
|
||||
public MetaVo(String title, String icon, boolean noCache, String link, String visible) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
this.noCache = noCache;
|
||||
if (StringUtils.ishttp(link)) {
|
||||
this.link = link;
|
||||
}
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public boolean isNoCache() {
|
||||
return noCache;
|
||||
}
|
||||
@@ -87,4 +102,12 @@ public class MetaVo {
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public String getVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(String visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
@@ -147,13 +150,15 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
List<RouterVo> routers = new LinkedList<RouterVo>();
|
||||
for (SysMenu menu : menus) {
|
||||
RouterVo router = new RouterVo();
|
||||
router.setHidden("1".equals(menu.getVisible()));
|
||||
// 不再根据 visible 字段设置 hidden,确保所有有权限的路由都可用
|
||||
// router.setHidden("1".equals(menu.getVisible()));
|
||||
router.setHidden(false);
|
||||
router.setName(getRouteName(menu));
|
||||
router.setPath(getRouterPath(menu));
|
||||
router.setComponent(getComponent(menu));
|
||||
router.setQuery(menu.getQuery());
|
||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()),
|
||||
menu.getPath()));
|
||||
menu.getPath(), menu.getVisible()));
|
||||
List<SysMenu> cMenus = menu.getChildren();
|
||||
if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
|
||||
router.setAlwaysShow(true);
|
||||
@@ -167,12 +172,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
children.setComponent(menu.getComponent());
|
||||
children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
|
||||
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(),
|
||||
StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
|
||||
StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getVisible()));
|
||||
children.setQuery(menu.getQuery());
|
||||
childrenList.add(children);
|
||||
router.setChildren(childrenList);
|
||||
} else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
|
||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
|
||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible()));
|
||||
router.setPath("/");
|
||||
List<RouterVo> childrenList = new ArrayList<RouterVo>();
|
||||
RouterVo children = new RouterVo();
|
||||
@@ -180,7 +185,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
children.setPath(routerPath);
|
||||
children.setComponent(UserConstants.INNER_LINK);
|
||||
children.setName(getRouteName(menu.getRouteName(), routerPath));
|
||||
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
|
||||
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, menu.getPath(), menu.getVisible()));
|
||||
childrenList.add(children);
|
||||
router.setChildren(childrenList);
|
||||
}
|
||||
@@ -213,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端所需要下拉树结构
|
||||
*
|
||||
@@ -267,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,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();
|
||||
@@ -305,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);
|
||||
}
|
||||
@@ -493,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; // 重新抛出异常让上层处理
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.core.common.utils.SecurityUtils;
|
||||
import com.core.common.utils.StringUtils;
|
||||
import com.core.common.utils.bean.BeanValidators;
|
||||
import com.core.common.utils.spring.SpringUtils;
|
||||
import com.core.common.utils.AuditFieldUtil; // 引入我们创建的工具类
|
||||
import com.core.system.domain.SysPost;
|
||||
import com.core.system.domain.SysUserPost;
|
||||
import com.core.system.domain.SysUserRole;
|
||||
@@ -55,6 +56,132 @@ public class SysUserServiceImpl implements ISysUserService {
|
||||
@Autowired
|
||||
private ISysDeptService deptService;
|
||||
|
||||
/**
|
||||
* 新增保存用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertUser(SysUser user) {
|
||||
// 在保存前设置审计字段
|
||||
AuditFieldUtil.setCreateInfo(user);
|
||||
|
||||
// 新增用户信息
|
||||
int rows = userMapper.insertUser(user);
|
||||
// 新增用户岗位关联
|
||||
insertUserPost(user);
|
||||
// 新增用户与角色管理
|
||||
insertUserRole(user);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改保存用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateUser(SysUser user) {
|
||||
// 在更新前设置审计字段
|
||||
AuditFieldUtil.setUpdateInfo(user);
|
||||
|
||||
Long userId = user.getUserId();
|
||||
// 删除用户与角色关联
|
||||
userRoleMapper.deleteUserRoleByUserId(userId);
|
||||
// 新增用户与角色管理
|
||||
insertUserRole(user);
|
||||
// 删除用户与岗位关联
|
||||
userPostMapper.deleteUserPostByUserId(userId);
|
||||
// 新增用户与岗位管理
|
||||
insertUserPost(user);
|
||||
return userMapper.updateUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean registerUser(SysUser user) {
|
||||
// 在保存前设置审计字段
|
||||
AuditFieldUtil.setCreateInfo(user);
|
||||
|
||||
return userMapper.insertUser(user) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户数据
|
||||
*
|
||||
* @param userList 用户数据列表
|
||||
* @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
|
||||
* @param operName 操作用户
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
|
||||
if (StringUtils.isNull(userList) || userList.size() == 0) {
|
||||
throw new ServiceException("导入用户数据不能为空!");
|
||||
}
|
||||
int successNum = 0;
|
||||
int failureNum = 0;
|
||||
StringBuilder successMsg = new StringBuilder();
|
||||
StringBuilder failureMsg = new StringBuilder();
|
||||
for (SysUser user : userList) {
|
||||
try {
|
||||
// 验证是否存在这个用户
|
||||
SysUser u = userMapper.selectUserByUserName(user.getUserName());
|
||||
if (StringUtils.isNull(u)) {
|
||||
BeanValidators.validateWithException(validator, user);
|
||||
deptService.checkDeptDataScope(user.getDeptId());
|
||||
String password = configService.selectConfigByKey("sys.user.initPassword");
|
||||
user.setPassword(SecurityUtils.encryptPassword(password));
|
||||
|
||||
// 在导入用户时设置审计字段
|
||||
AuditFieldUtil.setCreateInfo(user);
|
||||
|
||||
userMapper.insertUser(user);
|
||||
successNum++;
|
||||
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
|
||||
} else if (isUpdateSupport) {
|
||||
BeanValidators.validateWithException(validator, user);
|
||||
checkUserAllowed(u);
|
||||
checkUserDataScope(u.getUserId());
|
||||
deptService.checkDeptDataScope(user.getDeptId());
|
||||
user.setUserId(u.getUserId());
|
||||
|
||||
// 在更新用户时设置审计字段
|
||||
AuditFieldUtil.setUpdateInfo(user);
|
||||
|
||||
userMapper.updateUser(user);
|
||||
successNum++;
|
||||
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
|
||||
} else {
|
||||
failureNum++;
|
||||
failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failureNum++;
|
||||
String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
|
||||
failureMsg.append(msg + e.getMessage());
|
||||
log.error(msg, e);
|
||||
}
|
||||
}
|
||||
if (failureNum > 0) {
|
||||
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
|
||||
throw new ServiceException(failureMsg.toString());
|
||||
} else {
|
||||
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
|
||||
}
|
||||
return successMsg.toString();
|
||||
}
|
||||
|
||||
// 以下是原有方法,保持不变
|
||||
/**
|
||||
* 根据条件分页查询用户列表
|
||||
*
|
||||
@@ -220,56 +347,6 @@ public class SysUserServiceImpl implements ISysUserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增保存用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertUser(SysUser user) {
|
||||
// 新增用户信息
|
||||
int rows = userMapper.insertUser(user);
|
||||
// 新增用户岗位关联
|
||||
insertUserPost(user);
|
||||
// 新增用户与角色管理
|
||||
insertUserRole(user);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean registerUser(SysUser user) {
|
||||
return userMapper.insertUser(user) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改保存用户信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateUser(SysUser user) {
|
||||
Long userId = user.getUserId();
|
||||
// 删除用户与角色关联
|
||||
userRoleMapper.deleteUserRoleByUserId(userId);
|
||||
// 新增用户与角色管理
|
||||
insertUserRole(user);
|
||||
// 删除用户与岗位关联
|
||||
userPostMapper.deleteUserPostByUserId(userId);
|
||||
// 新增用户与岗位管理
|
||||
insertUserPost(user);
|
||||
return userMapper.updateUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户授权角色
|
||||
*
|
||||
@@ -425,66 +502,6 @@ public class SysUserServiceImpl implements ISysUserService {
|
||||
return userMapper.deleteUserByIds(userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户数据
|
||||
*
|
||||
* @param userList 用户数据列表
|
||||
* @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
|
||||
* @param operName 操作用户
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
|
||||
if (StringUtils.isNull(userList) || userList.size() == 0) {
|
||||
throw new ServiceException("导入用户数据不能为空!");
|
||||
}
|
||||
int successNum = 0;
|
||||
int failureNum = 0;
|
||||
StringBuilder successMsg = new StringBuilder();
|
||||
StringBuilder failureMsg = new StringBuilder();
|
||||
for (SysUser user : userList) {
|
||||
try {
|
||||
// 验证是否存在这个用户
|
||||
SysUser u = userMapper.selectUserByUserName(user.getUserName());
|
||||
if (StringUtils.isNull(u)) {
|
||||
BeanValidators.validateWithException(validator, user);
|
||||
deptService.checkDeptDataScope(user.getDeptId());
|
||||
String password = configService.selectConfigByKey("sys.user.initPassword");
|
||||
user.setPassword(SecurityUtils.encryptPassword(password));
|
||||
user.setCreateBy(operName);
|
||||
userMapper.insertUser(user);
|
||||
successNum++;
|
||||
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
|
||||
} else if (isUpdateSupport) {
|
||||
BeanValidators.validateWithException(validator, user);
|
||||
checkUserAllowed(u);
|
||||
checkUserDataScope(u.getUserId());
|
||||
deptService.checkDeptDataScope(user.getDeptId());
|
||||
user.setUserId(u.getUserId());
|
||||
user.setUpdateBy(operName);
|
||||
userMapper.updateUser(user);
|
||||
successNum++;
|
||||
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
|
||||
} else {
|
||||
failureNum++;
|
||||
failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failureNum++;
|
||||
String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
|
||||
failureMsg.append(msg + e.getMessage());
|
||||
log.error(msg, e);
|
||||
}
|
||||
}
|
||||
if (failureNum > 0) {
|
||||
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
|
||||
throw new ServiceException(failureMsg.toString());
|
||||
} else {
|
||||
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
|
||||
}
|
||||
return successMsg.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展属性
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
@@ -195,6 +195,7 @@
|
||||
<if test="status != null and status != ''">status,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
tenant_id,
|
||||
create_time
|
||||
)values(
|
||||
<if test="userId != null and userId != ''">#{userId},</if>
|
||||
@@ -209,6 +210,7 @@
|
||||
<if test="status != null and status != ''">#{status},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
#{tenantId},
|
||||
now()
|
||||
)
|
||||
</insert>
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- rabbitMQ -->
|
||||
<!-- <dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -106,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
|
||||
@@ -28,7 +28,10 @@ import com.openhis.web.datadictionary.dto.DiagnosisTreatmentDto;
|
||||
import com.openhis.web.datadictionary.dto.DiagnosisTreatmentSelParam;
|
||||
import com.openhis.web.datadictionary.mapper.ActivityDefinitionManageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -45,6 +48,8 @@ import java.util.List;
|
||||
@Service
|
||||
public class LisConfigManageAppServiceImpl implements ILisConfigManageAppService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LisConfigManageAppServiceImpl.class);
|
||||
|
||||
@Resource
|
||||
private ActivityDefinitionManageMapper activityDefinitionManageMapper;
|
||||
@Resource
|
||||
@@ -120,31 +125,73 @@ public class LisConfigManageAppServiceImpl implements ILisConfigManageAppServic
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public R<?> saveAll(LisConfigManageDto manageDto) {
|
||||
//先全部删除项目下详情
|
||||
activityDefDeviceDefMapper.delete(new QueryWrapper<ActivityDefDeviceDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
activityDefObservationDefMapper.delete(new QueryWrapper<ActivityDefObservationDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
activityDefSpecimenDefMapper.delete(new QueryWrapper<ActivityDefSpecimenDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
// 根据ID查询【诊疗目录】详情
|
||||
DiagnosisTreatmentDto diseaseTreatmentOne = activityDefinitionManageMapper.getDiseaseTreatmentOne(manageDto.getId(), tenantId);
|
||||
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
|
||||
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefDeviceDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
|
||||
});
|
||||
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
|
||||
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefObservationDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefObservationDefMapper.insert(activityDefObservationDef);
|
||||
});
|
||||
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
|
||||
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefSpecimenDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
|
||||
});
|
||||
try {
|
||||
// 先全部删除项目下详情
|
||||
activityDefDeviceDefMapper.delete(new QueryWrapper<ActivityDefDeviceDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
activityDefObservationDefMapper.delete(new QueryWrapper<ActivityDefObservationDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
activityDefSpecimenDefMapper.delete(new QueryWrapper<ActivityDefSpecimenDef>().eq("activity_definition_id", manageDto.getId()));
|
||||
|
||||
return R.ok();
|
||||
// 获取租户ID并验证
|
||||
Integer tenantId = null;
|
||||
try {
|
||||
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
} catch (Exception e) {
|
||||
log.warn("获取租户ID失败,使用默认值", e);
|
||||
}
|
||||
|
||||
// 根据ID查询【诊疗目录】详情
|
||||
DiagnosisTreatmentDto diseaseTreatmentOne = activityDefinitionManageMapper.getDiseaseTreatmentOne(manageDto.getId(), tenantId);
|
||||
if (diseaseTreatmentOne == null) {
|
||||
log.warn("未找到诊疗目录:id={}, tenantId={}", manageDto.getId(), tenantId);
|
||||
// 即使未找到诊疗目录,也继续保存,使用ID作为名称
|
||||
String activityDefinitionName = String.valueOf(manageDto.getId());
|
||||
|
||||
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
|
||||
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefDeviceDef.setActivityDefinitionName(activityDefinitionName);
|
||||
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
|
||||
});
|
||||
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
|
||||
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefObservationDef.setActivityDefinitionName(activityDefinitionName);
|
||||
activityDefObservationDefMapper.insert(activityDefObservationDef);
|
||||
});
|
||||
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
|
||||
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefSpecimenDef.setActivityDefinitionName(activityDefinitionName);
|
||||
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
|
||||
});
|
||||
} else {
|
||||
// 正常保存
|
||||
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
|
||||
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefDeviceDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
|
||||
});
|
||||
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
|
||||
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefObservationDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefObservationDefMapper.insert(activityDefObservationDef);
|
||||
});
|
||||
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
|
||||
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
|
||||
activityDefSpecimenDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
|
||||
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
|
||||
});
|
||||
}
|
||||
|
||||
log.info("保存检验项目设置成功:id={}, deviceCount={}, observationCount={}, specimenCount={}",
|
||||
manageDto.getId(),
|
||||
manageDto.getActivityDefDeviceDefs().size(),
|
||||
manageDto.getActivityDefObservationDefs().size(),
|
||||
manageDto.getActivityDefSpecimenDefs().size());
|
||||
return R.ok("保存成功");
|
||||
} catch (Exception e) {
|
||||
log.error("保存检验项目设置失败:id={}, error={}", manageDto.getId(), e.getMessage(), e);
|
||||
return R.fail("保存失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,6 +22,8 @@ import com.openhis.web.Inspection.dto.ObservationDefManageDto;
|
||||
import com.openhis.web.Inspection.dto.ObservationDefManageInitDto;
|
||||
import com.openhis.web.Inspection.dto.ObservationDefSelParam;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -42,6 +44,8 @@ import java.util.stream.Stream;
|
||||
@RequiredArgsConstructor
|
||||
public class ObservationManageAppServiceImpl implements IObservationManageAppService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(ObservationManageAppServiceImpl.class);
|
||||
|
||||
private final ObservationDefinitionMapper observationDefinitionMapper;
|
||||
|
||||
private final IObservationDefinitionService observationDefinitionService;
|
||||
@@ -88,9 +92,23 @@ public class ObservationManageAppServiceImpl implements IObservationManageAppSer
|
||||
|
||||
@Override
|
||||
public R<?> updateOrAddObservationDef(ObservationDefinition Observation) {
|
||||
Observation.setDeleteFlag(DelFlag.NO.getCode());
|
||||
observationDefinitionService.saveOrUpdate(Observation);
|
||||
return R.ok(" 添加成功");
|
||||
try {
|
||||
Observation.setDeleteFlag(DelFlag.NO.getCode());
|
||||
boolean result = observationDefinitionService.saveOrUpdate(Observation);
|
||||
if (result) {
|
||||
log.info("保存检验项目成功:name={}, code={}, id={}",
|
||||
Observation.getName(), Observation.getCode(), Observation.getId());
|
||||
return R.ok("添加成功");
|
||||
} else {
|
||||
log.warn("保存检验项目失败:name={}, code={}",
|
||||
Observation.getName(), Observation.getCode());
|
||||
return R.fail("添加失败:保存操作未成功");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存检验项目异常:name={}, code={}, error={}",
|
||||
Observation.getName(), Observation.getCode(), e.getMessage(), e);
|
||||
return R.fail("添加失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -13,14 +13,6 @@ import java.util.Map;
|
||||
*/
|
||||
public interface ITicketAppService {
|
||||
|
||||
/**
|
||||
* 查询号源列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 号源列表
|
||||
*/
|
||||
R<?> listTicket(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 预约号源
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user