Backup local changes before resolving remote repository issue
This commit is contained in:
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
|
||||||
|
}
|
||||||
223
MybastisColumnsHandler_optimization_guide.md
Normal file
223
MybastisColumnsHandler_optimization_guide.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# MyBatis-Plus 自动填充处理器优化指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
本文档说明如何优化 `MybastisColumnsHandler` 以确保所有实体的审计字段(create_by、create_time、update_by、update_time)能够正确自动填充。
|
||||||
|
|
||||||
|
## 问题背景
|
||||||
|
在 OpenHIS 系统中,当保存实体时可能会遇到以下错误:
|
||||||
|
```
|
||||||
|
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||||
|
```
|
||||||
|
|
||||||
|
这是因为数据库表中的审计字段设置了 NOT NULL 约束,但在某些情况下自动填充机制未能正确设置这些字段。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
通过优化 `MybastisColumnsHandler` 来确保总是使用当前登录用户的用户名填充 `create_by` 字段,使用当前时间填充 `create_time` 字段。
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
### 1. 替换现有处理器
|
||||||
|
将 `D:\his\openhis-server-new\core-framework\src\main\java\com\core\framework\handler\MybastisColumnsHandler.java` 文件替换为以下内容:
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.core.framework.handler;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import com.core.common.core.domain.model.LoginUser;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.core.framework.config.TenantContext;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyBatis-Plus 自动填充处理器
|
||||||
|
* 用于自动填充创建时间和更新时间,以及创建人和更新人
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||||
|
|
||||||
|
// 设置数据新增时的字段自动赋值规则
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
// 填充创建时间
|
||||||
|
Date currentTime = new Date();
|
||||||
|
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||||
|
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||||
|
|
||||||
|
// 获取当前登录用户名
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
|
||||||
|
// 填充创建人
|
||||||
|
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||||
|
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||||
|
|
||||||
|
// 确保tenantId被设置
|
||||||
|
Integer tenantId = getCurrentTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new RuntimeException("无法获取当前租户ID,请确保用户已登录或正确设置租户上下文");
|
||||||
|
}
|
||||||
|
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
|
||||||
|
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置数据修改时的字段自动赋值规则
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
// 填充更新时间
|
||||||
|
Date currentTime = new Date();
|
||||||
|
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
|
||||||
|
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
|
||||||
|
|
||||||
|
// 填充更新人
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||||
|
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户名
|
||||||
|
* @return 当前登录用户名,如果无法获取则返回 "system"
|
||||||
|
*/
|
||||||
|
private String getCurrentUsername() {
|
||||||
|
String username = "system"; // 默认值
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
username = loginUser.getUsername();
|
||||||
|
} else {
|
||||||
|
// 尝试从请求中获取用户信息
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
// 可以在这里添加额外的逻辑来从请求中获取用户信息
|
||||||
|
// 例如从请求头、session等获取用户信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录异常但不中断处理流程
|
||||||
|
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
|
||||||
|
// 可以考虑记录日志
|
||||||
|
}
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前租户 ID
|
||||||
|
*/
|
||||||
|
private Integer getCurrentTenantId() {
|
||||||
|
Integer result = null;
|
||||||
|
|
||||||
|
// 首先尝试从线程局部变量中获取租户ID(适用于定时任务等场景)
|
||||||
|
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
|
||||||
|
if (threadLocalTenantId != null) {
|
||||||
|
result = threadLocalTenantId;
|
||||||
|
} else {
|
||||||
|
// 获取当前登录用户的租户ID(优先使用SecurityUtils中储存的LoginUser的租户ID)
|
||||||
|
try {
|
||||||
|
if (SecurityUtils.getAuthentication() != null) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
result = loginUser.getTenantId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录异常但不中断处理
|
||||||
|
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
// 尝试从请求头中获取租户ID
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
if (request != null) {
|
||||||
|
// 从请求头获取租户ID,假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
|
||||||
|
String tenantIdHeader = request.getHeader("X-Tenant-ID");
|
||||||
|
String requestMethodName = request.getHeader("Request-Method-Name");
|
||||||
|
// 登录
|
||||||
|
if ("login".equals(requestMethodName)) {
|
||||||
|
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
|
||||||
|
try {
|
||||||
|
result = Integer.parseInt(tenantIdHeader);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍然没有获取到租户ID,返回默认值
|
||||||
|
if (result == null) {
|
||||||
|
System.out.println("警告: 未能获取当前租户ID,将使用默认租户ID 1");
|
||||||
|
result = 1; // 默认租户ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证处理器是否被正确扫描
|
||||||
|
确保在主应用类或配置类中启用了自动填充功能:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootApplication
|
||||||
|
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
|
||||||
|
@EnableTransactionManagement // 启用事务管理
|
||||||
|
public class OpenHisApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(OpenHisApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试验证
|
||||||
|
创建一个简单的测试来验证自动填充是否正常工作:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class AuditFieldTest {
|
||||||
|
@Autowired
|
||||||
|
private PractitionerMapper practitionerMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuditFieldsAutoFill() {
|
||||||
|
Practitioner practitioner = new Practitioner();
|
||||||
|
practitioner.setName("Test Practitioner");
|
||||||
|
|
||||||
|
// 保存实体
|
||||||
|
practitionerMapper.insert(practitioner);
|
||||||
|
|
||||||
|
// 验证审计字段是否被正确填充
|
||||||
|
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||||
|
assertThat(practitioner.getCreateBy()).isNotEqualTo("");
|
||||||
|
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||||
|
|
||||||
|
// 清理测试数据
|
||||||
|
practitionerMapper.deleteById(practitioner.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **安全上下文**:确保在调用保存方法时用户已登录,这样 `SecurityUtils.getLoginUser()` 才能返回有效的用户对象。
|
||||||
|
|
||||||
|
2. **异常处理**:处理器中包含了异常处理,如果无法获取当前用户,将使用 "system" 作为默认值。
|
||||||
|
|
||||||
|
3. **租户ID**:处理器也处理租户ID的自动填充,这对于多租户系统很重要。
|
||||||
|
|
||||||
|
4. **兼容性**:处理器同时支持驼峰命名(createBy)和下划线命名(create_by)的字段,以兼容不同的配置。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
通过优化 `MybastisColumnsHandler`,我们可以确保所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误,同时保持数据完整性和审计跟踪功能。
|
||||||
184
QWEN.md
Normal file
184
QWEN.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Qwen Code Context for HIS (Hospital Information System)
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a comprehensive Hospital Information System (HIS) called OpenHIS, built with a Java Spring Boot backend and Vue 3 frontend. The system is designed to manage hospital operations including patient management, appointments, clinical workflows, billing, and administrative tasks.
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Java 17
|
||||||
|
- Spring Boot 2.5.15
|
||||||
|
- PostgreSQL (recommended v16.2)
|
||||||
|
- Redis
|
||||||
|
- MyBatis-Plus 3.5.5 for ORM
|
||||||
|
- Druid 1.2.27 for database connection pooling
|
||||||
|
- Flowable 6.8.0 for workflow management
|
||||||
|
- LiteFlow 2.12.4.1 for business rule orchestration
|
||||||
|
- Swagger 3.0.0 for API documentation
|
||||||
|
- JWT 0.9.1 for authentication
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Vue 3 with Composition API
|
||||||
|
- Vite 5.0.4 as build tool
|
||||||
|
- Element Plus 2.12.0 as UI component library
|
||||||
|
- Pinia 2.2.0 for state management
|
||||||
|
- Axios 0.27.2 for HTTP requests
|
||||||
|
- Sass for styling
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── openhis-server-new/ # Backend multi-module Maven project
|
||||||
|
│ ├── openhis-application/ # Main application module with startup class
|
||||||
|
│ ├── openhis-domain/ # Business domain modules (administration, clinical, financial, etc.)
|
||||||
|
│ ├── openhis-common/ # Shared utilities and common code
|
||||||
|
│ ├── core-admin/ # Core administration module
|
||||||
|
│ ├── core-framework/ # Framework configuration and security
|
||||||
|
│ ├── core-system/ # System management module
|
||||||
|
│ ├── core-quartz/ # Scheduled tasks
|
||||||
|
│ ├── core-generator/ # Code generation utilities
|
||||||
|
│ ├── core-common/ # Core utilities
|
||||||
|
│ └── core-flowable/ # Workflow engine integration
|
||||||
|
├── openhis-ui-vue3/ # Vue 3 frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API service layer
|
||||||
|
│ │ ├── components/ # Reusable components
|
||||||
|
│ │ ├── router/ # Vue Router configuration
|
||||||
|
│ │ ├── store/ # Pinia state management
|
||||||
|
│ │ ├── utils/ # Utility functions
|
||||||
|
│ │ └── views/ # Page components
|
||||||
|
│ └── vite/ # Vite plugins configuration
|
||||||
|
├── sql/ # Database scripts
|
||||||
|
├── 发版记录/ # Release records
|
||||||
|
└── 迁移记录-DB变更记录/ # Database migration records
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Backend Setup
|
||||||
|
|
||||||
|
1. **Prerequisites:**
|
||||||
|
- JDK 17 (required)
|
||||||
|
- PostgreSQL v16.2 (required)
|
||||||
|
- Redis (stable version)
|
||||||
|
|
||||||
|
2. **Database Setup:**
|
||||||
|
- Import the database initialization script using Navicat 16 or later
|
||||||
|
- Script location: `sql/20251224init脚本(使用Navicat Premium 17导入).sql`
|
||||||
|
- Configure database connection in `application.yml` or `application-dev.yml`
|
||||||
|
|
||||||
|
3. **Build and Run:**
|
||||||
|
```bash
|
||||||
|
cd openhis-server-new
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
cd openhis-application
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run directly from IDE by executing `OpenHisApplication.java`
|
||||||
|
|
||||||
|
### Frontend Setup
|
||||||
|
|
||||||
|
1. **Prerequisites:**
|
||||||
|
- Node.js v16.15 (recommended)
|
||||||
|
|
||||||
|
2. **Installation and Run:**
|
||||||
|
```bash
|
||||||
|
cd openhis-ui-vue3
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access the application:**
|
||||||
|
- Frontend: http://localhost:81
|
||||||
|
- Backend API: http://localhost:18080/openhis
|
||||||
|
- Swagger UI: http://localhost:18080/openhis/swagger-ui/index.html
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
### Backend Architecture
|
||||||
|
|
||||||
|
The backend follows a multi-module Maven architecture with clear separation of concerns:
|
||||||
|
|
||||||
|
1. **openhis-application**: Entry point with `OpenHisApplication.java`
|
||||||
|
- Scans `com.core` and `com.openhis` packages
|
||||||
|
- Configured to run on port 18080 with context path `/openhis`
|
||||||
|
|
||||||
|
2. **openhis-domain**: Business domain modules organized by medical functionality:
|
||||||
|
- `administration`: Administrative functions
|
||||||
|
- `appointmentmanage`: Appointment management
|
||||||
|
- `check`: Medical examination/checkup
|
||||||
|
- `clinical`: Clinical workflows
|
||||||
|
- `crosssystem`: Cross-system integration
|
||||||
|
- `document`: Document management
|
||||||
|
- `financial`: Financial/billing
|
||||||
|
- `lab`: Laboratory operations
|
||||||
|
- `medication`: Medication management
|
||||||
|
- `triageandqueuemanage`: Patient triage and queue management
|
||||||
|
- `yb`, `ybcatalog`, `ybelep`: Insurance (Yi Bao) integration
|
||||||
|
- `workflow`: Workflow management
|
||||||
|
|
||||||
|
3. **Core Modules** (com.core package):
|
||||||
|
- `core-system`: User, role, menu, and permission management
|
||||||
|
- `core-framework`: Security, exception handling, and framework configurations
|
||||||
|
- `core-common`: Shared utilities and base classes
|
||||||
|
- `core-quartz`: Scheduled task management
|
||||||
|
- `core-generator`: Code generation tools
|
||||||
|
- `core-flowable`: Workflow engine integration
|
||||||
|
- `core-admin`: Administrative functions
|
||||||
|
|
||||||
|
### Frontend Architecture
|
||||||
|
|
||||||
|
The frontend uses Vue 3 with composition API and modern tooling:
|
||||||
|
|
||||||
|
1. **State Management:** Pinia for global state with modules for app, dict, permission, settings, tagsView, and user
|
||||||
|
|
||||||
|
2. **Routing:** Vue Router 4.3.0 with public routes and dynamic permission-based routes
|
||||||
|
|
||||||
|
3. **API Integration:** Axios with request/response interceptors and API services organized by module
|
||||||
|
|
||||||
|
4. **Component Architecture:** Element Plus as UI framework with custom components in `src/components/`
|
||||||
|
|
||||||
|
## Key Configuration Files
|
||||||
|
|
||||||
|
### Backend Configuration
|
||||||
|
|
||||||
|
- Main config: `openhis-server-new/openhis-application/src/main/resources/application.yml`
|
||||||
|
- Environment-specific: `application-dev.yml`, `application-test.yml`, `application-prd.yml`
|
||||||
|
- Database connection settings, Redis configuration, server settings, and MyBatis-Plus configuration
|
||||||
|
|
||||||
|
### Frontend Configuration
|
||||||
|
|
||||||
|
- Environment files: `.env.*` in `openhis-ui-vue3/`
|
||||||
|
- Vite configuration: `vite.config.js`
|
||||||
|
- Main entry: `src/main.js`
|
||||||
|
- Router: `src/router/index.js`
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding a New Backend Feature
|
||||||
|
|
||||||
|
1. Create domain entity in appropriate module under `openhis-domain/[module]/domain/`
|
||||||
|
2. Create mapper interface in `openhis-domain/[module]/mapper/`
|
||||||
|
3. Create service interface and implementation in `openhis-domain/[module]/service/`
|
||||||
|
4. Create controller in `openhis-application/src/main/java/com/openhis/web/[module]/`
|
||||||
|
5. Test endpoints via Swagger UI
|
||||||
|
|
||||||
|
### Adding a New Frontend Page
|
||||||
|
|
||||||
|
1. Create Vue component in `openhis-ui-vue3/src/views/[module]/`
|
||||||
|
2. Add API service methods in `openhis-ui-vue3/src/api/`
|
||||||
|
3. Add route to `openhis-ui-vue3/src/router/index.js`
|
||||||
|
4. Add Pinia store module if state management needed
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The system uses logical deletion with a `validFlag` field (1 = active, 0 = deleted)
|
||||||
|
- JWT tokens are stored in the `Authorization` header
|
||||||
|
- The system supports WebView environments with C# accessor integration
|
||||||
|
- File uploads are configured with max 10MB per file and 20MB total request size
|
||||||
|
- Password lockout occurs after 5 failed attempts with a 10-minute lock time
|
||||||
|
- The system includes a code generator accessible via `/tool/gen` route
|
||||||
|
- Printing functionality is implemented using the hiprint plugin
|
||||||
202
audit_field_best_practices.md
Normal file
202
audit_field_best_practices.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# OpenHIS 系统审计字段填充最佳实践
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
本文档介绍如何在 OpenHIS 系统中确保所有实体的审计字段(create_by、create_time、update_by、update_time)能够正确自动填充。
|
||||||
|
|
||||||
|
## 自动填充机制
|
||||||
|
|
||||||
|
### 1. 基础实体类
|
||||||
|
所有需要审计字段的实体类都应该继承自 `HisBaseEntity`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import com.core.common.core.domain.HisBaseEntity;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("adm_practitioner")
|
||||||
|
public class Practitioner extends HisBaseEntity {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
// 其他业务字段...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 自动填充处理器
|
||||||
|
系统使用 `MybastisColumnsHandler` 来自动填充审计字段:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
// 填充创建时间和创建人
|
||||||
|
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||||
|
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
|
||||||
|
|
||||||
|
String username = getCurrentUsername(); // 获取当前用户名
|
||||||
|
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||||
|
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
// 填充更新时间和更新人
|
||||||
|
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||||
|
this.strictUpdateFill(metaObject, "update_time", Date.class, new Date());
|
||||||
|
|
||||||
|
String username = getCurrentUsername(); // 获取当前用户名
|
||||||
|
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||||
|
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCurrentUsername() {
|
||||||
|
String username = "system";
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
username = loginUser.getUsername();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 确保自动填充正常工作的要点
|
||||||
|
|
||||||
|
### 1. 检查实体类继承关系
|
||||||
|
确保所有实体类都正确继承了 `HisBaseEntity`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 正确的做法
|
||||||
|
public class Practitioner extends HisBaseEntity { ... }
|
||||||
|
|
||||||
|
// 如果不能继承 HisBaseEntity,则需要手动添加审计字段
|
||||||
|
public class CustomEntity {
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证安全上下文
|
||||||
|
确保在执行数据库操作时有有效的安全上下文:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class PractitionerService {
|
||||||
|
public void savePractitioner(Practitioner practitioner) {
|
||||||
|
// 确保调用此方法时用户已登录
|
||||||
|
// SecurityUtils.getLoginUser() 应该能返回有效的 LoginUser 对象
|
||||||
|
|
||||||
|
// MyBatis-Plus 会在保存时自动调用 MybastisColumnsHandler
|
||||||
|
practitionerMapper.insert(practitioner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查配置
|
||||||
|
确保自动填充处理器被正确配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application.yml
|
||||||
|
mybatis-plus:
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
# 其他配置...
|
||||||
|
configuration:
|
||||||
|
# 其他配置...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 手动填充(特殊情况)
|
||||||
|
在某些特殊情况下,如果自动填充不工作,可以手动设置:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class PractitionerService {
|
||||||
|
public void savePractitionerManually(Practitioner practitioner) {
|
||||||
|
// 手动设置审计字段
|
||||||
|
Date now = new Date();
|
||||||
|
String currentUser = getCurrentUsername();
|
||||||
|
|
||||||
|
practitioner.setCreateTime(now);
|
||||||
|
practitioner.setCreateBy(currentUser);
|
||||||
|
practitioner.setUpdateTime(now);
|
||||||
|
practitioner.setUpdateBy(currentUser);
|
||||||
|
|
||||||
|
practitionerMapper.insert(practitioner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题及解决方案
|
||||||
|
|
||||||
|
### 问题1:自动填充不生效
|
||||||
|
**原因:**
|
||||||
|
- 实体类没有继承 `HisBaseEntity`
|
||||||
|
- `MybastisColumnsHandler` 没有被Spring管理(缺少@Component注解)
|
||||||
|
- 没有有效的安全上下文
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 确保实体类继承 `HisBaseEntity`
|
||||||
|
- 检查 `MybastisColumnsHandler` 是否有 `@Component` 注解
|
||||||
|
- 确保在调用保存方法时用户已登录
|
||||||
|
|
||||||
|
### 问题2:获取不到当前用户
|
||||||
|
**原因:**
|
||||||
|
- 用户未登录
|
||||||
|
- 安全上下文配置错误
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 在调用保存方法前确保用户已登录
|
||||||
|
- 检查安全配置是否正确
|
||||||
|
|
||||||
|
### 问题3:批量操作时审计字段未填充
|
||||||
|
**原因:**
|
||||||
|
- 批量操作可能绕过了自动填充机制
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 对于批量操作,手动设置审计字段
|
||||||
|
- 或者使用 MyBatis-Plus 的批量操作方法,确保它们支持自动填充
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
创建一个简单的测试来验证自动填充是否正常工作:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class AuditFieldTest {
|
||||||
|
@Autowired
|
||||||
|
private PractitionerMapper practitionerMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuditFieldsAutoFill() {
|
||||||
|
Practitioner practitioner = new Practitioner();
|
||||||
|
practitioner.setName("Test Practitioner");
|
||||||
|
|
||||||
|
// 保存实体
|
||||||
|
practitionerMapper.insert(practitioner);
|
||||||
|
|
||||||
|
// 验证审计字段是否被正确填充
|
||||||
|
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||||
|
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||||
|
|
||||||
|
// 清理测试数据
|
||||||
|
practitionerMapper.deleteById(practitioner.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
通过遵循以上最佳实践,可以确保 OpenHIS 系统中的所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误。
|
||||||
113
audit_field_solution.md
Normal file
113
audit_field_solution.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# 关于数据库审计字段(create_by, create_time等)的处理方案
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
在使用OpenHIS系统时,可能会遇到如下错误:
|
||||||
|
```
|
||||||
|
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
1. 数据库表中的审计字段(如create_by, create_time)设置了NOT NULL约束
|
||||||
|
2. 应用程序层面使用了MyBatis-Plus的自动填充功能来设置这些字段
|
||||||
|
3. 当自动填充机制失效时,就会出现违反非空约束的错误
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案一:修复自动填充机制(推荐)
|
||||||
|
系统已经实现了自动填充机制,位于 `MybastisColumnsHandler.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 设置数据新增时候的,字段自动赋值规则
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
// 同时填充驼峰和下划线命名的字段,以兼容不同的配置
|
||||||
|
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||||
|
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
|
||||||
|
|
||||||
|
String username = "system";
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
username = loginUser.getUsername();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
// 使用 fillStrategy 确保即使字段为 null 也会被填充
|
||||||
|
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||||
|
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||||
|
// 如果 strictInsertFill 没有生效,使用 setFieldValByName 强制设置
|
||||||
|
if (metaObject.hasGetter("createBy") && metaObject.getValue("createBy") == null) {
|
||||||
|
this.setFieldValByName("createBy", username, metaObject);
|
||||||
|
}
|
||||||
|
if (metaObject.hasGetter("create_by") && metaObject.getValue("create_by") == null) {
|
||||||
|
this.setFieldValByName("create_by", username, metaObject);
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
确保所有实体类都继承自 `HisBaseEntity` 或 `BaseEntity`,这样就能自动获得审计字段。
|
||||||
|
|
||||||
|
### 方案二:移除数据库约束(谨慎使用)
|
||||||
|
如果确实需要允许审计字段为NULL,可以移除数据库约束:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 移除 adm_practitioner 表中 create_by 列的 NOT NULL 约束
|
||||||
|
ALTER TABLE "public"."adm_practitioner"
|
||||||
|
ALTER COLUMN "create_by" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- 同样处理 create_time 列(如果需要)
|
||||||
|
ALTER TABLE "public"."adm_practitioner"
|
||||||
|
ALTER COLUMN "create_time" DROP NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案三:批量修复所有表的约束
|
||||||
|
如果多个表都存在这个问题,可以使用以下脚本:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 为所有表的审计字段移除NOT NULL约束
|
||||||
|
-- 注意:执行前请备份数据库!
|
||||||
|
|
||||||
|
-- 1. 检查所有包含审计字段的表
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
column_name,
|
||||||
|
is_nullable
|
||||||
|
FROM
|
||||||
|
information_schema.columns
|
||||||
|
WHERE
|
||||||
|
column_name IN ('create_by', 'create_time', 'update_by', 'update_time')
|
||||||
|
AND table_schema = 'public'
|
||||||
|
AND is_nullable = 'NO'; -- NO 表示 NOT NULL 约束
|
||||||
|
|
||||||
|
-- 2. 根据需要移除特定表的约束
|
||||||
|
-- 示例:移除多个表的create_by约束
|
||||||
|
ALTER TABLE "public"."adm_practitioner" ALTER COLUMN "create_by" DROP NOT NULL;
|
||||||
|
ALTER TABLE "public"."adm_patient" ALTER COLUMN "create_by" DROP NOT NULL;
|
||||||
|
-- 添加更多表的处理...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 确保实体类继承基础类
|
||||||
|
所有实体类应继承 `HisBaseEntity` 或 `BaseEntity`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
@TableName("adm_practitioner")
|
||||||
|
public class Practitioner extends HisBaseEntity {
|
||||||
|
// 其他字段...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查安全上下文
|
||||||
|
确保在保存数据时有有效的安全上下文,这样自动填充处理器才能获取到当前用户信息。
|
||||||
|
|
||||||
|
### 3. 验证自动填充配置
|
||||||
|
确保 `MybastisColumnsHandler` 在Spring容器中被正确注册(使用@Component注解)。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
- 推荐保持数据库中的NOT NULL约束,确保数据完整性
|
||||||
|
- 依赖MyBatis-Plus的自动填充机制来设置审计字段
|
||||||
|
- 确保所有实体类继承基础实体类
|
||||||
|
- 在必要时才考虑移除数据库约束
|
||||||
38
debug_api_return.md
Normal file
38
debug_api_return.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 检查后端API返回数据结构
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
尽管我们更新了DTO和SQL查询,前端仍然没有显示创建时间,可能的原因:
|
||||||
|
1. API响应中没有包含createTime字段
|
||||||
|
2. SQL查询没有正确返回createTime字段
|
||||||
|
3. 数据库中createTime字段本身为null
|
||||||
|
4. JSON序列化问题
|
||||||
|
|
||||||
|
## 检查步骤
|
||||||
|
|
||||||
|
### 1. 检查数据库中数据
|
||||||
|
首先检查数据库中sys_user表的createTime字段是否正确填充:
|
||||||
|
```sql
|
||||||
|
SELECT user_id, user_name, nick_name, create_time
|
||||||
|
FROM sys_user
|
||||||
|
WHERE create_time IS NOT NULL
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查API端点
|
||||||
|
API端点是:GET /base-data-manage/practitioner/user-practitioner-page
|
||||||
|
这个端点在PractitionerController中定义,调用practitionerAppService.getUserPractitionerPage()
|
||||||
|
|
||||||
|
### 3. 检查SQL查询
|
||||||
|
在PractitionerAppMapper.xml中,我们已经添加了createTime字段:
|
||||||
|
```xml
|
||||||
|
T2.create_time
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 验证DTO映射
|
||||||
|
UserAndPractitionerDto中已添加createTime字段:
|
||||||
|
```java
|
||||||
|
private Date createTime;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 检查JSON序列化
|
||||||
|
检查是否有@JsonFormat注解或其他序列化配置问题
|
||||||
290
deep_dive_autofill_solution.md
Normal file
290
deep_dive_autofill_solution.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# 深度排查 MyBatis-Plus 自动填充不生效问题
|
||||||
|
|
||||||
|
## 问题概述
|
||||||
|
尽管对 MyBatis-Plus 的自动填充处理器进行了多次优化和配置,但 `create_by` 和 `create_time` 字段仍然没有被自动填充。
|
||||||
|
|
||||||
|
## 深度排查步骤
|
||||||
|
|
||||||
|
### 1. 检查 AOP 代理是否生效
|
||||||
|
MyBatis-Plus 的自动填充功能依赖于 AOP 代理。如果实体类的方法被直接调用而非通过代理调用,自动填充可能不会生效。
|
||||||
|
|
||||||
|
### 2. 验证 Service 层实现
|
||||||
|
确保使用的是 MyBatis-Plus 提供的通用 Service 方法,而不是自定义的 SQL。
|
||||||
|
|
||||||
|
### 3. 检查 @TableField 注解配置
|
||||||
|
确认实体类中的字段注解配置正确。
|
||||||
|
|
||||||
|
### 4. 检查事务配置
|
||||||
|
某些事务配置可能会影响 AOP 代理的生效。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案一:在 Service 层手动设置审计字段
|
||||||
|
|
||||||
|
创建一个工具类来统一处理审计字段的设置:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class AuditFieldUtil {
|
||||||
|
|
||||||
|
public static void setCreateInfo(Object entity) {
|
||||||
|
if (entity == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||||
|
Date currentTime = new Date();
|
||||||
|
|
||||||
|
// 使用反射设置字段值
|
||||||
|
Field createByField = getField(entity.getClass(), "createBy");
|
||||||
|
if (createByField != null) {
|
||||||
|
createByField.setAccessible(true);
|
||||||
|
if (createByField.get(entity) == null || "".equals(createByField.get(entity))) {
|
||||||
|
createByField.set(entity, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Field createTimeField = getField(entity.getClass(), "createTime");
|
||||||
|
if (createTimeField != null) {
|
||||||
|
createTimeField.setAccessible(true);
|
||||||
|
if (createTimeField.get(entity) == null) {
|
||||||
|
createTimeField.set(entity, currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理下划线命名的字段
|
||||||
|
Field createByFieldUnderscore = getField(entity.getClass(), "create_by");
|
||||||
|
if (createByFieldUnderscore != null) {
|
||||||
|
createByFieldUnderscore.setAccessible(true);
|
||||||
|
if (createByFieldUnderscore.get(entity) == null || "".equals(createByFieldUnderscore.get(entity))) {
|
||||||
|
createByFieldUnderscore.set(entity, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Field createTimeFieldUnderscore = getField(entity.getClass(), "create_time");
|
||||||
|
if (createTimeFieldUnderscore != null) {
|
||||||
|
createTimeFieldUnderscore.setAccessible(true);
|
||||||
|
if (createTimeFieldUnderscore.get(entity) == null) {
|
||||||
|
createTimeFieldUnderscore.set(entity, currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("设置审计字段时发生异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field getField(Class<?> clazz, String fieldName) {
|
||||||
|
try {
|
||||||
|
return clazz.getDeclaredField(fieldName);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
if (clazz.getSuperclass() != null) {
|
||||||
|
return getField(clazz.getSuperclass(), fieldName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 Service 实现中使用:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
|
||||||
|
implements IPractitionerService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuditFieldUtil auditFieldUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public boolean save(Practitioner entity) {
|
||||||
|
// 在保存前手动设置审计字段
|
||||||
|
auditFieldUtil.setCreateInfo(entity);
|
||||||
|
return super.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public boolean saveBatch(Collection<Practitioner> entityList) {
|
||||||
|
entityList.forEach(auditFieldUtil::setCreateInfo);
|
||||||
|
return super.saveBatch(entityList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:重写 BaseMapper 方法
|
||||||
|
|
||||||
|
如果 Service 层的方法不起作用,可以直接在 Mapper 层处理:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mapper
|
||||||
|
public interface PractitionerMapper extends BaseMapper<Practitioner> {
|
||||||
|
|
||||||
|
@Insert({
|
||||||
|
"<script>",
|
||||||
|
"INSERT INTO adm_practitioner (",
|
||||||
|
"id, active_flag, name, name_json, gender_enum, birth_date, deceased_date,",
|
||||||
|
"phone, address, address_province, address_city, address_district, address_street,",
|
||||||
|
"address_json, py_str, wb_str, bus_no, yb_no, user_id, tenant_id, delete_flag,",
|
||||||
|
"create_by, create_time, update_by, update_time, org_id,",
|
||||||
|
"phar_prac_cert_no, prsc_dr_cert_code, dr_profttl_code, kpd_code, signature, pos_no",
|
||||||
|
") VALUES (",
|
||||||
|
"#{id}, #{activeFlag}, #{name}, #{nameJson}, #{genderEnum}, #{birthDate}, #{deceasedDate},",
|
||||||
|
"#{phone}, #{address}, #{addressProvince}, #{addressCity}, #{addressDistrict}, #{addressStreet},",
|
||||||
|
"#{addressJson}, #{pyStr}, #{wbStr}, #{busNo}, #{ybNo}, #{userId}, #{tenantId}, #{deleteFlag},",
|
||||||
|
"#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{orgId},",
|
||||||
|
"#{pharPracCertNo}, #{prscDrCertCode}, #{drProfttlCode}, #{kpdCode}, #{signature}, #{posNo}",
|
||||||
|
")",
|
||||||
|
"</script>"
|
||||||
|
})
|
||||||
|
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||||
|
int insertWithAuditFields(Practitioner record);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案三:使用 MyBatis 拦截器
|
||||||
|
|
||||||
|
创建一个 MyBatis 拦截器来自动填充字段:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Intercepts({
|
||||||
|
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
|
||||||
|
})
|
||||||
|
@Component
|
||||||
|
public class AuditFieldInterceptor implements Interceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object intercept(Invocation invocation) throws Throwable {
|
||||||
|
Object[] args = invocation.getArgs();
|
||||||
|
MappedStatement ms = (MappedStatement) args[0];
|
||||||
|
Object parameter = args[1];
|
||||||
|
|
||||||
|
String sqlCommandType = ms.getSqlCommandType().toString();
|
||||||
|
|
||||||
|
if ("INSERT".equals(sqlCommandType)) {
|
||||||
|
setCreateAuditFields(parameter);
|
||||||
|
} else if ("UPDATE".equals(sqlCommandType)) {
|
||||||
|
setUpdateAuditFields(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invocation.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCreateAuditFields(Object parameter) {
|
||||||
|
if (parameter == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||||
|
Date currentTime = new Date();
|
||||||
|
|
||||||
|
// 设置 createBy 和 createTime
|
||||||
|
setFieldValue(parameter, "createBy", username);
|
||||||
|
setFieldValue(parameter, "create_time", username);
|
||||||
|
setFieldValue(parameter, "createTime", currentTime);
|
||||||
|
setFieldValue(parameter, "create_time", currentTime);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpdateAuditFields(Object parameter) {
|
||||||
|
if (parameter == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
String username = loginUser != null ? loginUser.getUsername() : "system";
|
||||||
|
Date currentTime = new Date();
|
||||||
|
|
||||||
|
// 设置 updateBy 和 updateTime
|
||||||
|
setFieldValue(parameter, "updateBy", username);
|
||||||
|
setFieldValue(parameter, "update_by", username);
|
||||||
|
setFieldValue(parameter, "updateTime", currentTime);
|
||||||
|
setFieldValue(parameter, "update_time", currentTime);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFieldValue(Object obj, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field field = getField(obj.getClass(), fieldName);
|
||||||
|
if (field != null) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
if (field.get(obj) == null) { // 只在原值为 null 时设置
|
||||||
|
field.set(obj, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略无法设置的字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field getField(Class<?> clazz, String fieldName) {
|
||||||
|
try {
|
||||||
|
return clazz.getDeclaredField(fieldName);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
if (clazz.getSuperclass() != null) {
|
||||||
|
return getField(clazz.getSuperclass(), fieldName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object plugin(Object target) {
|
||||||
|
if (target instanceof Executor) {
|
||||||
|
return Plugin.wrap(target, this);
|
||||||
|
} else {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperties(Properties properties) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐实施顺序
|
||||||
|
|
||||||
|
1. 首先尝试方案一(Service 层手动设置),这是最简单且可控的方式
|
||||||
|
2. 如果方案一不行,尝试方案三(MyBatis 拦截器),它在更底层起作用
|
||||||
|
3. 方案二是最后的选择,需要重写具体的插入逻辑
|
||||||
|
|
||||||
|
## 验证方法
|
||||||
|
|
||||||
|
创建一个测试来验证自动填充是否生效:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class AuditFieldTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPractitionerService practitionerService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuditFieldFill() {
|
||||||
|
Practitioner practitioner = new Practitioner();
|
||||||
|
practitioner.setName("Test Practitioner");
|
||||||
|
|
||||||
|
// 记录保存前的值
|
||||||
|
System.out.println("保存前 - createBy: " + practitioner.getCreateBy());
|
||||||
|
System.out.println("保存前 - createTime: " + practitioner.getCreateTime());
|
||||||
|
|
||||||
|
boolean success = practitionerService.save(practitioner);
|
||||||
|
|
||||||
|
// 从数据库重新查询以验证
|
||||||
|
Practitioner saved = practitionerService.getById(practitioner.getId());
|
||||||
|
System.out.println("保存后 - createBy: " + saved.getCreateBy());
|
||||||
|
System.out.println("保存后 - createTime: " + saved.getCreateTime());
|
||||||
|
|
||||||
|
Assertions.assertTrue(success);
|
||||||
|
Assertions.assertNotNull(saved.getCreateBy());
|
||||||
|
Assertions.assertNotNull(saved.getCreateTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
通过这些方案,应该能够解决自动填充不生效的问题。
|
||||||
143
diagnose_autofill_issue.md
Normal file
143
diagnose_autofill_issue.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# 诊断 MyBatis-Plus 自动填充问题
|
||||||
|
|
||||||
|
## 问题现象
|
||||||
|
尽管 `MybastisColumnsHandler` 已经实现并配置了自动填充功能,但 `create_by` 和 `create_time` 字段仍然没有被正确填充。
|
||||||
|
|
||||||
|
## 可能的原因及解决方案
|
||||||
|
|
||||||
|
### 1. 检查组件扫描配置
|
||||||
|
确保 `MybastisColumnsHandler` 类被Spring容器正确管理:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component // 确保这个注解存在
|
||||||
|
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查包扫描路径
|
||||||
|
在主应用类中确保扫描到了处理器所在的包:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootApplication
|
||||||
|
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
|
||||||
|
@ComponentScan(basePackages = {"com.core", "com.openhis"}) // 确保扫描到处理器
|
||||||
|
public class OpenHisApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(OpenHisApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证实体类配置
|
||||||
|
确保实体类正确继承了 `HisBaseEntity` 并且字段上有正确的注解:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
@TableName("adm_practitioner")
|
||||||
|
public class Practitioner extends HisBaseEntity {
|
||||||
|
// 不需要在子类中重复定义 createBy, createTime 等字段
|
||||||
|
// 因为它们已在 HisBaseEntity 中定义并带有 @TableField(fill = FieldFill.INSERT)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 检查安全上下文
|
||||||
|
自动填充处理器依赖于安全上下文来获取当前用户。确保在执行保存操作时用户已登录:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 在保存之前,确保用户已登录
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
// 用户未登录,可能需要手动设置审计字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 手动测试自动填充
|
||||||
|
创建一个简单的测试来验证自动填充是否正常工作:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class AutoFillTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PractitionerMapper practitionerMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoFill() {
|
||||||
|
Practitioner practitioner = new Practitioner();
|
||||||
|
practitioner.setName("Test Practitioner");
|
||||||
|
|
||||||
|
// 检查在保存前字段是否为空
|
||||||
|
System.out.println("Before insert - createBy: " + practitioner.getCreateBy());
|
||||||
|
System.out.println("Before insert - createTime: " + practitioner.getCreateTime());
|
||||||
|
|
||||||
|
// 执行插入操作
|
||||||
|
int result = practitionerMapper.insert(practitioner);
|
||||||
|
|
||||||
|
// 检查保存后字段是否被填充
|
||||||
|
System.out.println("After insert - createBy: " + practitioner.getCreateBy());
|
||||||
|
System.out.println("After insert - createTime: " + practitioner.getCreateTime());
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(1);
|
||||||
|
assertThat(practitioner.getCreateBy()).isNotNull();
|
||||||
|
assertThat(practitioner.getCreateTime()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 临时解决方案
|
||||||
|
如果自动填充仍然不工作,可以在服务层手动设置这些字段:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
|
||||||
|
implements IPractitionerService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void savePractitioner(Practitioner practitioner) {
|
||||||
|
// 手动设置审计字段
|
||||||
|
if (practitioner.getCreateBy() == null || practitioner.getCreateBy().isEmpty()) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
practitioner.setCreateBy(loginUser.getUsername());
|
||||||
|
} else {
|
||||||
|
practitioner.setCreateBy("system"); // 默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (practitioner.getCreateTime() == null) {
|
||||||
|
practitioner.setCreateTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行保存操作
|
||||||
|
this.save(practitioner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 检查 MyBatis-Plus 版本兼容性
|
||||||
|
确保使用的 MyBatis-Plus 版本与自动填充功能兼容。当前项目使用的是 3.5.5 版本,应该支持自动填充功能。
|
||||||
|
|
||||||
|
### 8. 调试自动填充处理器
|
||||||
|
在 `MybastisColumnsHandler` 中添加日志来调试是否被调用:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
System.out.println("MybastisColumnsHandler.insertFill() called"); // 调试日志
|
||||||
|
|
||||||
|
Date currentTime = new Date();
|
||||||
|
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||||
|
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||||
|
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
System.out.println("Setting createBy to: " + username); // 调试日志
|
||||||
|
|
||||||
|
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||||
|
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||||
|
|
||||||
|
// ... 其他代码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
通过以上步骤,应该能够诊断并解决自动填充不工作的问题。
|
||||||
145
enhanced_MybastisColumnsHandler.java
Normal file
145
enhanced_MybastisColumnsHandler.java
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package com.core.framework.handler;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import com.core.common.core.domain.model.LoginUser;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.core.framework.config.TenantContext;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyBatis-Plus 自动填充处理器
|
||||||
|
* 用于自动填充创建时间和更新时间,以及创建人和更新人
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MybastisColumnsHandler implements MetaObjectHandler {
|
||||||
|
|
||||||
|
// 设置数据新增时的字段自动赋值规则
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
// 填充创建时间
|
||||||
|
Date currentTime = new Date();
|
||||||
|
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
|
||||||
|
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
|
||||||
|
|
||||||
|
// 获取当前登录用户名
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
|
||||||
|
// 填充创建人
|
||||||
|
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||||
|
this.strictInsertFill(metaObject, "create_by", String.class, username);
|
||||||
|
|
||||||
|
// 确保tenantId被设置
|
||||||
|
Integer tenantId = getCurrentTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new RuntimeException("无法获取当前租户ID,请确保用户已登录或正确设置租户上下文");
|
||||||
|
}
|
||||||
|
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
|
||||||
|
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置数据修改时的字段自动赋值规则
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
// 填充更新时间
|
||||||
|
Date currentTime = new Date();
|
||||||
|
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
|
||||||
|
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
|
||||||
|
|
||||||
|
// 填充更新人
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||||
|
this.strictUpdateFill(metaObject, "update_by", String.class, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户名
|
||||||
|
* @return 当前登录用户名,如果无法获取则返回 "system"
|
||||||
|
*/
|
||||||
|
private String getCurrentUsername() {
|
||||||
|
String username = "system"; // 默认值
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
username = loginUser.getUsername();
|
||||||
|
} else {
|
||||||
|
// 尝试从请求中获取用户信息
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
// 可以在这里添加额外的逻辑来从请求中获取用户信息
|
||||||
|
// 例如从请求头、session等获取用户信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录异常但不中断处理流程
|
||||||
|
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
|
||||||
|
// 可以考虑记录日志
|
||||||
|
}
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前租户 ID
|
||||||
|
*/
|
||||||
|
private Integer getCurrentTenantId() {
|
||||||
|
Integer result = null;
|
||||||
|
|
||||||
|
// 首先尝试从线程局部变量中获取租户ID(适用于定时任务等场景)
|
||||||
|
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
|
||||||
|
if (threadLocalTenantId != null) {
|
||||||
|
result = threadLocalTenantId;
|
||||||
|
} else {
|
||||||
|
// 获取当前登录用户的租户ID(优先使用SecurityUtils中储存的LoginUser的租户ID)
|
||||||
|
try {
|
||||||
|
if (SecurityUtils.getAuthentication() != null) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
result = loginUser.getTenantId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录异常但不中断处理
|
||||||
|
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
// 尝试从请求头中获取租户ID
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
if (request != null) {
|
||||||
|
// 从请求头获取租户ID,假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
|
||||||
|
String tenantIdHeader = request.getHeader("X-Tenant-ID");
|
||||||
|
String requestMethodName = request.getHeader("Request-Method-Name");
|
||||||
|
// 登录
|
||||||
|
if ("login".equals(requestMethodName)) {
|
||||||
|
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
|
||||||
|
try {
|
||||||
|
result = Integer.parseInt(tenantIdHeader);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍然没有获取到租户ID,返回默认值
|
||||||
|
if (result == null) {
|
||||||
|
System.out.println("警告: 未能获取当前租户ID,将使用默认租户ID 1");
|
||||||
|
result = 1; // 默认租户ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
fragment.java
Normal file
13
fragment.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.openhis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例类 - 引用 OpenHisApplication
|
||||||
|
*/
|
||||||
|
public class Fragment {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 引用 OpenHisApplication
|
||||||
|
Class<?> applicationClass = com.openhis.OpenHisApplication.class;
|
||||||
|
System.out.println("Application class: " + applicationClass.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.openhis.web.debug;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.web.basedatamanage.appservice.IPractitionerAppService;
|
||||||
|
import com.openhis.web.basedatamanage.dto.UserAndPractitionerDto;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试控制器 - 用于检查API返回数据
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/debug")
|
||||||
|
public class DebugController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPractitionerAppService practitionerAppService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户及参与者数据用于调试
|
||||||
|
*/
|
||||||
|
@GetMapping("/user-practitioner-debug")
|
||||||
|
public R<UserAndPractitionerDto> getUserPractitionerDebug() {
|
||||||
|
// 获取第一页第一条数据用于调试
|
||||||
|
var page = practitionerAppService.getUserPractitionerPage(new UserAndPractitionerDto(), "", 1, 1);
|
||||||
|
if (page.getRecords() != null && !page.getRecords().isEmpty()) {
|
||||||
|
return R.ok(page.getRecords().get(0));
|
||||||
|
}
|
||||||
|
return R.fail("没有找到数据");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8
|
url: jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8
|
||||||
username: postgresql
|
username: postgresql
|
||||||
password: Jchl1528
|
password: Jchl1528 # 请替换为实际的数据库密码
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
slave:
|
slave:
|
||||||
# 从数据源开关/默认关闭
|
# 从数据源开关/默认关闭
|
||||||
enabled:
|
enabled: false
|
||||||
url:
|
url:
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
@@ -35,10 +35,12 @@ spring:
|
|||||||
# 配置一个连接在池中最大生存的时间,单位是毫秒
|
# 配置一个连接在池中最大生存的时间,单位是毫秒
|
||||||
maxEvictableIdleTimeMillis: 900000
|
maxEvictableIdleTimeMillis: 900000
|
||||||
# 配置检测连接是否有效
|
# 配置检测连接是否有效
|
||||||
validationQuery: SELECT 1 # FROM DUAL
|
validationQuery: SELECT 1
|
||||||
testWhileIdle: true
|
testWhileIdle: true
|
||||||
testOnBorrow: false
|
testOnBorrow: true # 改为true以确保连接有效
|
||||||
testOnReturn: false
|
testOnReturn: false
|
||||||
|
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
|
||||||
|
filters: stat,wall,slf4j
|
||||||
webStatFilter:
|
webStatFilter:
|
||||||
enabled: true
|
enabled: true
|
||||||
statViewServlet:
|
statViewServlet:
|
||||||
@@ -59,14 +61,12 @@ spring:
|
|||||||
wall:
|
wall:
|
||||||
config:
|
config:
|
||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
|
|
||||||
|
|
||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: 192.168.110.252
|
host: 47.116.196.11
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 26379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 1
|
database: 1
|
||||||
# 密码
|
# 密码
|
||||||
@@ -83,10 +83,8 @@ spring:
|
|||||||
max-active: 8
|
max-active: 8
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||||
max-wait: -1ms
|
max-wait: -1ms
|
||||||
# 文言
|
|
||||||
messages:
|
# 服务器配置
|
||||||
basename: i18n/general_message/messages
|
|
||||||
encoding: utf-8
|
|
||||||
server:
|
server:
|
||||||
# 服务器的HTTP端口,默认为18080
|
# 服务器的HTTP端口,默认为18080
|
||||||
port: 18080
|
port: 18080
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ logging:
|
|||||||
level:
|
level:
|
||||||
com.openhis: debug
|
com.openhis: debug
|
||||||
org.springframework: warn
|
org.springframework: warn
|
||||||
|
# MyBatis和MyBatis-Plus日志
|
||||||
|
com.baomidou.mybatisplus: debug
|
||||||
|
com.openhis.web.regdoctorstation.mapper: debug
|
||||||
|
# JDBC日志
|
||||||
|
org.springframework.jdbc.core: debug
|
||||||
|
# Druid SQL日志
|
||||||
|
com.alibaba.druid: debug
|
||||||
|
com.alibaba.druid.sql: debug
|
||||||
|
|
||||||
# 用户配置
|
# 用户配置
|
||||||
user:
|
user:
|
||||||
|
|||||||
312
openhis-ui-vue3/src/router/json
Normal file
312
openhis-ui-vue3/src/router/json
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"encounterId": "1992766613237190657",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202511240001",
|
||||||
|
"inHospitalTime": "2025-11-26 13:28:14",
|
||||||
|
"outHospitalTime": "2026-01-19 07:00:41",
|
||||||
|
"patientId": "1979081512436203522",
|
||||||
|
"patientName": "随子赫",
|
||||||
|
"genderEnum": 0,
|
||||||
|
"genderEnum_enumText": "男性",
|
||||||
|
"birthDate": "2013-06-23 00:00:00",
|
||||||
|
"age": "13岁",
|
||||||
|
"wardName": null,
|
||||||
|
"houseName": null,
|
||||||
|
"bedName": null,
|
||||||
|
"inOrgTime": null,
|
||||||
|
"inHospitalDays": 55,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "持久的心境[情感]障碍,其他的",
|
||||||
|
"accountId": "1993552505086300162",
|
||||||
|
"advanceAmount": null,
|
||||||
|
"totalAmount": null,
|
||||||
|
"balanceAmount": null,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2012842506781417473",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601180004",
|
||||||
|
"inHospitalTime": "2026-01-19 15:30:37",
|
||||||
|
"outHospitalTime": "2026-01-20 03:43:47",
|
||||||
|
"patientId": "1980816965970288641",
|
||||||
|
"patientName": "刘潇凡",
|
||||||
|
"genderEnum": 0,
|
||||||
|
"genderEnum_enumText": "男性",
|
||||||
|
"birthDate": "2007-04-29 00:00:00",
|
||||||
|
"age": "19岁",
|
||||||
|
"wardName": null,
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": null,
|
||||||
|
"inOrgTime": null,
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "7",
|
||||||
|
"contractName": "城乡居民医疗保险",
|
||||||
|
"regDiagnosisName": "童年情绪障碍",
|
||||||
|
"accountId": "2013131047918845954",
|
||||||
|
"advanceAmount": 500.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 500.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2012842506781417473",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601180004",
|
||||||
|
"inHospitalTime": "2026-01-19 15:30:37",
|
||||||
|
"outHospitalTime": "2026-01-20 03:43:47",
|
||||||
|
"patientId": "1980816965970288641",
|
||||||
|
"patientName": "刘潇凡",
|
||||||
|
"genderEnum": 0,
|
||||||
|
"genderEnum_enumText": "男性",
|
||||||
|
"birthDate": "2007-04-29 00:00:00",
|
||||||
|
"age": "19岁",
|
||||||
|
"wardName": null,
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": null,
|
||||||
|
"inOrgTime": null,
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "7",
|
||||||
|
"contractName": "城乡居民医疗保险",
|
||||||
|
"regDiagnosisName": "童年情绪障碍",
|
||||||
|
"accountId": "2013131047918845954",
|
||||||
|
"advanceAmount": 500.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 500.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013142728942239745",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190002",
|
||||||
|
"inHospitalTime": "2026-01-19 14:54:09",
|
||||||
|
"outHospitalTime": "2026-01-19 00:00:00",
|
||||||
|
"patientId": "1980816965970288641",
|
||||||
|
"patientName": "刘潇凡",
|
||||||
|
"genderEnum": 0,
|
||||||
|
"genderEnum_enumText": "男性",
|
||||||
|
"birthDate": "2007-04-29 00:00:00",
|
||||||
|
"age": "19岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": null,
|
||||||
|
"inOrgTime": null,
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013143088062742529",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encounterId": "2013219287040495617",
|
||||||
|
"statusEnum": 5,
|
||||||
|
"statusEnum_enumText": "已入院",
|
||||||
|
"busNo": "ZY202601190003",
|
||||||
|
"inHospitalTime": "2026-01-19 19:57:30",
|
||||||
|
"outHospitalTime": "2026-01-20 07:12:47",
|
||||||
|
"patientId": "1989707705648041985",
|
||||||
|
"patientName": "豆包",
|
||||||
|
"genderEnum": 1,
|
||||||
|
"genderEnum_enumText": "女性",
|
||||||
|
"birthDate": "2006-07-30 08:00:00",
|
||||||
|
"age": "20岁",
|
||||||
|
"wardName": "儿童青少年心理病区",
|
||||||
|
"houseName": "101号房",
|
||||||
|
"bedName": "02号床",
|
||||||
|
"inOrgTime": "2026-01-20 03:58:24",
|
||||||
|
"inHospitalDays": 1,
|
||||||
|
"inHospitalOrgId": "1989706423340257282",
|
||||||
|
"inHospitalOrgName": "临床心理科",
|
||||||
|
"contractNo": "2",
|
||||||
|
"contractName": "居民基本医疗保险",
|
||||||
|
"regDiagnosisName": "抑郁状态",
|
||||||
|
"accountId": "2013219416401219585",
|
||||||
|
"advanceAmount": 1000.000000,
|
||||||
|
"totalAmount": 0,
|
||||||
|
"balanceAmount": 1000.000000,
|
||||||
|
"insutype": null,
|
||||||
|
"insutype_dictText": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 10,
|
||||||
|
"size": 10,
|
||||||
|
"current": 1,
|
||||||
|
"pages": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
197
openhis-ui-vue3/src/utils/apiRequestManager.js
Normal file
197
openhis-ui-vue3/src/utils/apiRequestManager.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import globalRequestController from './globalRequestController.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API请求管理器 - 用于合并相同参数的请求,避免重复请求
|
||||||
|
* 在医院信息系统中特别重要,因为医疗数据的频繁请求可能影响系统性能
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ApiRequestManager {
|
||||||
|
constructor() {
|
||||||
|
// 存储正在进行的请求
|
||||||
|
this.pendingRequests = new Map();
|
||||||
|
// 缓存成功的响应结果
|
||||||
|
this.responseCache = new Map();
|
||||||
|
// 缓存过期时间(毫秒)
|
||||||
|
this.cacheTimeout = 10000; // 10秒,医疗系统中数据更新可能较频繁
|
||||||
|
|
||||||
|
// 添加调试模式
|
||||||
|
this.debugMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求的唯一键值
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} params - 请求参数
|
||||||
|
* @returns {string} 唯一键值
|
||||||
|
*/
|
||||||
|
generateRequestKey(url, params = {}) {
|
||||||
|
// 对参数进行排序以确保相同参数产生相同键值
|
||||||
|
const sortedParams = Object.keys(params)
|
||||||
|
.sort()
|
||||||
|
.reduce((acc, key) => {
|
||||||
|
acc[key] = params[key];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const key = `${url}?${JSON.stringify(sortedParams)}`;
|
||||||
|
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.log(`Generated request key: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否存在相同的进行中请求
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
* @returns {Promise|undefined} 如果存在则返回Promise,否则返回undefined
|
||||||
|
*/
|
||||||
|
getPendingRequest(requestKey) {
|
||||||
|
return this.pendingRequests.get(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加进行中的请求
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
* @param {Promise} promise - 请求Promise
|
||||||
|
*/
|
||||||
|
addPendingRequest(requestKey, promise) {
|
||||||
|
this.pendingRequests.set(requestKey, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除进行中的请求
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
*/
|
||||||
|
removePendingRequest(requestKey) {
|
||||||
|
this.pendingRequests.delete(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否存在缓存的响应
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
* @returns {object|undefined} 如果存在则返回缓存的响应,否则返回undefined
|
||||||
|
*/
|
||||||
|
getCachedResponse(requestKey) {
|
||||||
|
const cached = this.responseCache.get(requestKey);
|
||||||
|
if (cached && Date.now() < cached.expiry) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
// 如果缓存已过期,删除它
|
||||||
|
if (cached) {
|
||||||
|
this.responseCache.delete(requestKey);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加缓存的响应
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
* @param {object} data - 响应数据
|
||||||
|
*/
|
||||||
|
addCachedResponse(requestKey, data) {
|
||||||
|
this.responseCache.set(requestKey, {
|
||||||
|
data,
|
||||||
|
expiry: Date.now() + this.cacheTimeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指定的缓存
|
||||||
|
* @param {string} requestKey - 请求键值
|
||||||
|
*/
|
||||||
|
clearCache(requestKey) {
|
||||||
|
this.responseCache.delete(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
clearAllCache() {
|
||||||
|
this.responseCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有进行中的请求
|
||||||
|
*/
|
||||||
|
clearAllPending() {
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行API请求(带去重和缓存)
|
||||||
|
* @param {Function} apiFunction - API函数
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} params - 请求参数
|
||||||
|
* @returns {Promise} API响应Promise
|
||||||
|
*/
|
||||||
|
async execute(apiFunction, url, params = {}) {
|
||||||
|
const requestKey = this.generateRequestKey(url, params);
|
||||||
|
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.log(`Executing request with key: ${requestKey}`);
|
||||||
|
console.log(`Pending requests count: ${this.pendingRequests.size}`);
|
||||||
|
console.log(`Cached responses count: ${this.responseCache.size}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有缓存的响应
|
||||||
|
const cachedResponse = this.getCachedResponse(requestKey);
|
||||||
|
if (cachedResponse) {
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.log(`Returning cached response for: ${requestKey}`);
|
||||||
|
}
|
||||||
|
return Promise.resolve(cachedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用全局请求控制器来确保唯一性
|
||||||
|
const requestPromise = globalRequestController.execute(apiFunction, url, params)
|
||||||
|
.then(response => {
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.log(`Request completed for: ${requestKey}`, response);
|
||||||
|
}
|
||||||
|
// 请求成功后,添加到缓存
|
||||||
|
this.addCachedResponse(requestKey, response);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (this.debugMode) {
|
||||||
|
console.error(`Request failed for: ${requestKey}`, error);
|
||||||
|
}
|
||||||
|
throw error; // 不在这里处理错误,让调用方处理
|
||||||
|
});
|
||||||
|
|
||||||
|
return requestPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行POST请求(带去重和缓存)
|
||||||
|
* @param {Function} apiFunction - API函数
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} data - 请求数据
|
||||||
|
* @returns {Promise} API响应Promise
|
||||||
|
*/
|
||||||
|
async executePost(apiFunction, url, data = {}) {
|
||||||
|
const requestKey = this.generateRequestKey(url, data);
|
||||||
|
|
||||||
|
// POST请求通常不缓存,但仍然可以去重
|
||||||
|
const pendingRequest = this.getPendingRequest(requestKey);
|
||||||
|
if (pendingRequest) {
|
||||||
|
return pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPromise = apiFunction(data)
|
||||||
|
.finally(() => {
|
||||||
|
this.removePendingRequest(requestKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addPendingRequest(requestKey, requestPromise);
|
||||||
|
|
||||||
|
return requestPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局实例
|
||||||
|
const apiRequestManager = new ApiRequestManager();
|
||||||
|
|
||||||
|
export default apiRequestManager;
|
||||||
94
openhis-ui-vue3/src/utils/globalRequestController.js
Normal file
94
openhis-ui-vue3/src/utils/globalRequestController.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 全局请求控制器 - 用于在整个应用范围内控制重复请求
|
||||||
|
* 特别针对医院信息系统中的病历数据访问进行优化
|
||||||
|
*/
|
||||||
|
class GlobalRequestController {
|
||||||
|
constructor() {
|
||||||
|
// 存储正在进行的请求
|
||||||
|
this.activeRequests = new Map();
|
||||||
|
// 请求计数器,用于调试
|
||||||
|
this.requestCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求的唯一标识符
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} params - 请求参数
|
||||||
|
* @returns {string} 唯一标识符
|
||||||
|
*/
|
||||||
|
generateRequestId(url, params = {}) {
|
||||||
|
// 标准化参数以确保一致性
|
||||||
|
const normalizedParams = this.normalizeParams(params);
|
||||||
|
const paramString = JSON.stringify(normalizedParams);
|
||||||
|
return `${url}|${paramString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化参数对象
|
||||||
|
* @param {object} params - 原始参数
|
||||||
|
* @returns {object} 标准化后的参数
|
||||||
|
*/
|
||||||
|
normalizeParams(params) {
|
||||||
|
const normalized = {};
|
||||||
|
// 按字母顺序排序参数键
|
||||||
|
Object.keys(params).sort().forEach(key => {
|
||||||
|
normalized[key] = params[key];
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已有相同请求在进行中
|
||||||
|
* @param {string} requestId - 请求ID
|
||||||
|
* @returns {Promise|undefined} 如果存在则返回Promise,否则返回undefined
|
||||||
|
*/
|
||||||
|
hasActiveRequest(requestId) {
|
||||||
|
return this.activeRequests.get(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个新请求
|
||||||
|
* @param {string} requestId - 请求ID
|
||||||
|
* @param {Promise} requestPromise - 请求Promise
|
||||||
|
*/
|
||||||
|
registerRequest(requestId, requestPromise) {
|
||||||
|
this.requestCounter++;
|
||||||
|
console.log(`[GlobalRequestController] Registering request #${this.requestCounter}: ${requestId}`);
|
||||||
|
this.activeRequests.set(requestId, requestPromise);
|
||||||
|
|
||||||
|
// 当请求完成时,从活动请求中移除
|
||||||
|
requestPromise.finally(() => {
|
||||||
|
console.log(`[GlobalRequestController] Removing completed request: ${requestId}`);
|
||||||
|
this.activeRequests.delete(requestId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行请求(确保相同参数的请求只执行一次)
|
||||||
|
* @param {Function} apiFunction - API函数
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} params - 请求参数
|
||||||
|
* @returns {Promise} 请求结果Promise
|
||||||
|
*/
|
||||||
|
async execute(apiFunction, url, params = {}) {
|
||||||
|
const requestId = this.generateRequestId(url, params);
|
||||||
|
|
||||||
|
// 检查是否已有相同请求在进行中
|
||||||
|
const existingRequest = this.hasActiveRequest(requestId);
|
||||||
|
if (existingRequest) {
|
||||||
|
console.log(`[GlobalRequestController] Returning existing request for: ${requestId}`);
|
||||||
|
return existingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新请求
|
||||||
|
const requestPromise = apiFunction(params);
|
||||||
|
this.registerRequest(requestId, requestPromise);
|
||||||
|
|
||||||
|
return requestPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局实例
|
||||||
|
const globalRequestController = new GlobalRequestController();
|
||||||
|
|
||||||
|
export default globalRequestController;
|
||||||
@@ -42,6 +42,7 @@ import {defineEmits, ref, unref} from 'vue';
|
|||||||
import {deleteRecord, getRecordByEncounterIdList} from '../api';
|
import {deleteRecord, getRecordByEncounterIdList} from '../api';
|
||||||
import {ElMessage} from 'element-plus';
|
import {ElMessage} from 'element-plus';
|
||||||
import {patientInfo} from '../../store/patient.js';
|
import {patientInfo} from '../../store/patient.js';
|
||||||
|
import apiRequestManager from '@/utils/apiRequestManager.js';
|
||||||
|
|
||||||
const emits = defineEmits(['historyClick']);
|
const emits = defineEmits(['historyClick']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -67,15 +68,30 @@ const queryParams = ref({
|
|||||||
isPage: 0,
|
isPage: 0,
|
||||||
});
|
});
|
||||||
const historyData = ref([]);
|
const historyData = ref([]);
|
||||||
|
// 防止重复加载的标志
|
||||||
|
let isLoadingHistory = false;
|
||||||
|
|
||||||
const queryList = async () => {
|
const queryList = async () => {
|
||||||
|
// 防止重复加载
|
||||||
|
if (isLoadingHistory) {
|
||||||
|
console.log('History data is already loading, skipping duplicate call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingHistory = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
||||||
const res = await getRecordByEncounterIdList({
|
const res = await apiRequestManager.execute(
|
||||||
...queryParams.value,
|
getRecordByEncounterIdList,
|
||||||
|
'/document/record/getRecordByEncounterIdList',
|
||||||
|
{
|
||||||
|
isPage: 0, // 确保参数一致,便于去重
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: unref(definitionId),
|
definitionId: unref(definitionId),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
historyData.value = res.data || [];
|
historyData.value = res.data || [];
|
||||||
} else {
|
} else {
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
@@ -83,6 +99,8 @@ const queryList = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ElMessage.error(' 获取模板树失败 ');
|
// ElMessage.error(' 获取模板树失败 ');
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoadingHistory = false; // 重置加载标志
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleNodeClick = (data) => {
|
const handleNodeClick = (data) => {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ import dayjs from 'dayjs';
|
|||||||
// 打印工具
|
// 打印工具
|
||||||
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
|
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
|
||||||
import {getEncounterDiagnosis} from '../api';
|
import {getEncounterDiagnosis} from '../api';
|
||||||
|
import apiRequestManager from '@/utils/apiRequestManager.js';
|
||||||
import History from './components/history';
|
import History from './components/history';
|
||||||
import Template from './components/template';
|
import Template from './components/template';
|
||||||
import TemplateEdit from './components/templateEdit.vue';
|
import TemplateEdit from './components/templateEdit.vue';
|
||||||
@@ -205,7 +206,7 @@ const handleNodeClick = (data, node) => {
|
|||||||
|
|
||||||
// 选择任何病历模板后,都加载该病历类型的最新历史记录
|
// 选择任何病历模板后,都加载该病历类型的最新历史记录
|
||||||
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
|
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
|
||||||
loadLatestMedicalRecord();
|
debouncedLoadLatestMedicalRecord();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
@@ -279,7 +280,7 @@ const handleSubmitOk = async (data) => {
|
|||||||
|
|
||||||
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadLatestMedicalRecord();
|
debouncedLoadLatestMedicalRecord();
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('提交失败');
|
ElMessage.error('提交失败');
|
||||||
@@ -410,7 +411,7 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
// 等待模板加载完成,然后获取并回显最新病历数据
|
// 等待模板加载完成,然后获取并回显最新病历数据
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
historyRef.value?.queryList();
|
historyRef.value?.queryList();
|
||||||
loadLatestMedicalRecord();
|
debouncedLoadLatestMedicalRecord();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -421,19 +422,36 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
// 当前选中的历史病历ID,用于在History组件中高亮显示
|
// 当前选中的历史病历ID,用于在History组件中高亮显示
|
||||||
const selectedHistoryRecordId = ref('');
|
const selectedHistoryRecordId = ref('');
|
||||||
|
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
|
// 防止重复加载的标志
|
||||||
|
let isLoadingLatestRecord = false;
|
||||||
|
|
||||||
// 加载最新的病历数据并回显
|
// 加载最新的病历数据并回显
|
||||||
const loadLatestMedicalRecord = async () => {
|
const loadLatestMedicalRecord = async () => {
|
||||||
if (!patientInfo.value.encounterId || !currentSelectTemplate.value.id) return;
|
if (!patientInfo.value.encounterId || !currentSelectTemplate.value.id) return;
|
||||||
|
|
||||||
|
// 防止重复加载
|
||||||
|
if (isLoadingLatestRecord) {
|
||||||
|
console.log('Latest medical record is already loading, skipping duplicate call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingLatestRecord = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取患者的历史病历记录
|
// 获取患者的历史病历记录
|
||||||
const res = await getRecordByEncounterIdList({
|
const res = await apiRequestManager.execute(
|
||||||
|
getRecordByEncounterIdList,
|
||||||
|
'/document/record/getRecordByEncounterIdList',
|
||||||
|
{
|
||||||
isPage: 0,
|
isPage: 0,
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: currentSelectTemplate.value.id,
|
definitionId: currentSelectTemplate.value.id,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const historyRecords = res.data || [];
|
const historyRecords = res.data || [];
|
||||||
if (historyRecords.length > 0) {
|
if (historyRecords.length > 0) {
|
||||||
@@ -519,8 +537,12 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
isLoadingLatestRecord = false; // 重置加载标志
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 防抖版本的加载最新病历数据函数
|
||||||
|
const debouncedLoadLatestMedicalRecord = debounce(loadLatestMedicalRecord, 300);
|
||||||
const templateRef = ref(null);
|
const templateRef = ref(null);
|
||||||
|
|
||||||
const handleTemplateClick = (data) => {
|
const handleTemplateClick = (data) => {
|
||||||
@@ -750,7 +772,7 @@ const selectDefaultTemplate = () => {
|
|||||||
|
|
||||||
// 直接加载最新病历数据,不再使用额外的setTimeout延迟
|
// 直接加载最新病历数据,不再使用额外的setTimeout延迟
|
||||||
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
|
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
|
||||||
loadLatestMedicalRecord();
|
debouncedLoadLatestMedicalRecord();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('未找到门诊病历模板');
|
console.log('未找到门诊病历模板');
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ import useUserStore from '@/store/modules/user';
|
|||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { updatePatientInfo } from './components/store/patient.js';
|
import { updatePatientInfo } from './components/store/patient.js';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
// // 监听路由离开事件
|
// // 监听路由离开事件
|
||||||
// onBeforeRouteLeave((to, from, next) => {
|
// onBeforeRouteLeave((to, from, next) => {
|
||||||
@@ -487,7 +488,8 @@ function handleOpen() {
|
|||||||
patientDrawerRef.value.refreshList();
|
patientDrawerRef.value.refreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCardClick(item, index) {
|
// 原始的handleCardClick函数
|
||||||
|
function handleCardClickOriginal(item, index) {
|
||||||
console.log('handleCardClick 被调用');
|
console.log('handleCardClick 被调用');
|
||||||
console.log('点击的患者项目:', item);
|
console.log('点击的患者项目:', item);
|
||||||
console.log('患者项目中的encounterId:', item.encounterId);
|
console.log('患者项目中的encounterId:', item.encounterId);
|
||||||
@@ -544,6 +546,9 @@ function handleCardClick(item, index) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用防抖的handleCardClick函数,防止短时间内多次点击
|
||||||
|
const handleCardClick = debounce(handleCardClickOriginal, 500);
|
||||||
|
|
||||||
function handleLeave(encounterId) {
|
function handleLeave(encounterId) {
|
||||||
leaveEncounter(encounterId).then((res) => {
|
leaveEncounter(encounterId).then((res) => {
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
@@ -589,7 +594,7 @@ function handleHospitalizationClick() {
|
|||||||
|
|
||||||
// 接诊回调
|
// 接诊回调
|
||||||
function handleReceive(row) {
|
function handleReceive(row) {
|
||||||
handleCardClick(row);
|
handleCardClickOriginal(row);
|
||||||
currentEncounterId.value = row.encounterId;
|
currentEncounterId.value = row.encounterId;
|
||||||
drawer.value = false;
|
drawer.value = false;
|
||||||
getPatientList();
|
getPatientList();
|
||||||
@@ -776,7 +781,7 @@ const markSeen = async () => {
|
|||||||
currentCallPatient.value = {};
|
currentCallPatient.value = {};
|
||||||
};
|
};
|
||||||
const callThis = (row) => {
|
const callThis = (row) => {
|
||||||
handleCardClick(row);
|
handleCardClickOriginal(row);
|
||||||
currentCallPatient.value = row;
|
currentCallPatient.value = row;
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
// 刷新患者列表和候诊列表
|
// 刷新患者列表和候诊列表
|
||||||
|
|||||||
@@ -525,19 +525,33 @@ function getList() {
|
|||||||
function refresh() {
|
function refresh() {
|
||||||
getListInfo(false);
|
getListInfo(false);
|
||||||
}
|
}
|
||||||
|
// 防止重复请求的标志
|
||||||
|
let listInfoRequestPromise = null;
|
||||||
|
|
||||||
// 获取列表信息
|
// 获取列表信息
|
||||||
function getListInfo(addNewRow) {
|
function getListInfo(addNewRow) {
|
||||||
|
// 如果已经有正在进行的请求,则返回该请求的Promise
|
||||||
|
if (listInfoRequestPromise) {
|
||||||
|
return listInfoRequestPromise;
|
||||||
|
}
|
||||||
|
|
||||||
loadingInstance = ElLoading.service({ fullscreen: true });
|
loadingInstance = ElLoading.service({ fullscreen: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (loadingInstance) {
|
||||||
loadingInstance.close();
|
loadingInstance.close();
|
||||||
|
}
|
||||||
}, 180);
|
}, 180);
|
||||||
isAdding.value = false;
|
isAdding.value = false;
|
||||||
expandOrder.value = [];
|
expandOrder.value = [];
|
||||||
getPrescriptionList(patientInfo.value.encounterId).then((res) => {
|
|
||||||
console.log('getListInfo==========>', JSON.stringify(res.data));
|
|
||||||
|
|
||||||
loadingInstance.close();
|
// 并行请求两个API并将结果合并处理
|
||||||
prescriptionList.value = res.data
|
listInfoRequestPromise = Promise.all([
|
||||||
|
getPrescriptionList(patientInfo.value.encounterId),
|
||||||
|
getContract({ encounterId: patientInfo.value.encounterId })
|
||||||
|
])
|
||||||
|
.then(([prescriptionRes, contractRes]) => {
|
||||||
|
// 处理处方列表
|
||||||
|
prescriptionList.value = prescriptionRes.data
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
return {
|
return {
|
||||||
...JSON.parse(item.contentJson),
|
...JSON.parse(item.contentJson),
|
||||||
@@ -549,15 +563,35 @@ function getListInfo(addNewRow) {
|
|||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return new Date(b.requestTime) - new Date(a.requestTime);
|
return new Date(b.requestTime) - new Date(a.requestTime);
|
||||||
});
|
});
|
||||||
getGroupMarkers(); // 更新标记
|
|
||||||
|
// 处理合同列表
|
||||||
|
contractList.value = contractRes.data;
|
||||||
|
|
||||||
|
// 更新账户ID
|
||||||
|
accountId.value = patientInfo.value.accountId;
|
||||||
|
|
||||||
|
// 更新标记
|
||||||
|
getGroupMarkers();
|
||||||
|
|
||||||
if (props.activeTab == 'prescription' && addNewRow) {
|
if (props.activeTab == 'prescription' && addNewRow) {
|
||||||
handleAddPrescription();
|
handleAddPrescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('getListInfo==========>', JSON.stringify(prescriptionRes.data));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取列表信息失败:', error);
|
||||||
|
ElMessage.error('获取列表信息失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (loadingInstance) {
|
||||||
|
loadingInstance.close();
|
||||||
|
}
|
||||||
|
// 请求完成后清除Promise引用
|
||||||
|
listInfoRequestPromise = null;
|
||||||
});
|
});
|
||||||
getContract({ encounterId: patientInfo.value.encounterId }).then((res) => {
|
|
||||||
contractList.value = res.data;
|
return listInfoRequestPromise;
|
||||||
});
|
|
||||||
accountId.value = patientInfo.value.accountId;
|
|
||||||
}
|
}
|
||||||
// 数据过滤
|
// 数据过滤
|
||||||
const filterPrescriptionList = computed(() => {
|
const filterPrescriptionList = computed(() => {
|
||||||
@@ -571,8 +605,17 @@ const filterPrescriptionList = computed(() => {
|
|||||||
return pList;
|
return pList;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 防止诊断信息重复请求的标志
|
||||||
|
let diagnosisInfoRequestPromise = null;
|
||||||
|
|
||||||
function getDiagnosisInfo() {
|
function getDiagnosisInfo() {
|
||||||
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => {
|
// 如果已经有正在进行的请求,则返回该请求的Promise
|
||||||
|
if (diagnosisInfoRequestPromise) {
|
||||||
|
return diagnosisInfoRequestPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnosisInfoRequestPromise = getEncounterDiagnosis(patientInfo.value.encounterId)
|
||||||
|
.then((res) => {
|
||||||
diagnosisList.value = res.data;
|
diagnosisList.value = res.data;
|
||||||
let diagnosisInfo = diagnosisList.value.filter((item) => {
|
let diagnosisInfo = diagnosisList.value.filter((item) => {
|
||||||
return item.maindiseFlag == 1;
|
return item.maindiseFlag == 1;
|
||||||
@@ -582,7 +625,17 @@ function getDiagnosisInfo() {
|
|||||||
conditionId.value = diagnosisInfo[0].conditionId;
|
conditionId.value = diagnosisInfo[0].conditionId;
|
||||||
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
|
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
|
||||||
diagnosisName.value = diagnosisInfo[0].name;
|
diagnosisName.value = diagnosisInfo[0].name;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取诊断信息失败:', error);
|
||||||
|
ElMessage.error('获取诊断信息失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// 请求完成后清除Promise引用
|
||||||
|
diagnosisInfoRequestPromise = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return diagnosisInfoRequestPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowDisabled(row) {
|
function getRowDisabled(row) {
|
||||||
|
|||||||
@@ -65,22 +65,40 @@ const queryParams = ref({
|
|||||||
isPage: 0,
|
isPage: 0,
|
||||||
});
|
});
|
||||||
const historyData = ref([]);
|
const historyData = ref([]);
|
||||||
|
// 防止重复请求的标志
|
||||||
|
let queryListPromise = null;
|
||||||
|
|
||||||
const queryList = async () => {
|
const queryList = async () => {
|
||||||
|
// 如果已经有正在进行的请求,则返回该请求的Promise
|
||||||
|
if (queryListPromise) {
|
||||||
|
return queryListPromise;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
||||||
const res = await getRecordByEncounterIdList({
|
queryListPromise = getRecordByEncounterIdList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: unref(definitionId),
|
definitionId: unref(definitionId),
|
||||||
});
|
})
|
||||||
|
.then(res => {
|
||||||
historyData.value = res.data || [];
|
historyData.value = res.data || [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// 请求完成后清除Promise引用
|
||||||
|
queryListPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryListPromise;
|
||||||
} else {
|
} else {
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 不显示错误消息,避免干扰用户体验
|
// 不显示错误消息,避免干扰用户体验
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
|
// 请求完成后清除Promise引用
|
||||||
|
queryListPromise = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -305,9 +305,10 @@ const handleSubmitOk = async (data) => {
|
|||||||
// templateRef.value?.queryList();
|
// templateRef.value?.queryList();
|
||||||
|
|
||||||
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
||||||
|
// 增加延迟时间以确保数据库更新完成
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
}, 100);
|
}, 300);
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('保存成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('提交失败');
|
ElMessage.error('提交失败');
|
||||||
@@ -553,11 +554,22 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
selectDefaultTemplate();
|
selectDefaultTemplate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 防止重复请求的标志
|
||||||
|
let loadLatestMedicalRecordPromise = null;
|
||||||
|
|
||||||
// 加载最新的病历数据并回显
|
// 加载最新的病历数据并回显
|
||||||
const loadLatestMedicalRecord = async () => {
|
const loadLatestMedicalRecord = async () => {
|
||||||
|
// 如果已经有正在进行的请求,则返回该请求的Promise
|
||||||
|
if (loadLatestMedicalRecordPromise) {
|
||||||
|
return loadLatestMedicalRecordPromise;
|
||||||
|
}
|
||||||
|
|
||||||
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
|
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
|
||||||
editForm.value.id = '';
|
editForm.value.id = '';
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
|
// 创建一个新的Promise来处理请求
|
||||||
|
loadLatestMedicalRecordPromise = new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// 获取患者的历史病历记录
|
// 获取患者的历史病历记录
|
||||||
const res = await getRecordByEncounterIdList({
|
const res = await getRecordByEncounterIdList({
|
||||||
@@ -596,6 +608,8 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
|
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
|
||||||
historyRef.value.updateSelectedRecord(latestRecord.id);
|
historyRef.value.updateSelectedRecord(latestRecord.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve(); // 成功完成
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 清空选中状态
|
// 清空选中状态
|
||||||
@@ -619,6 +633,7 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (emrComponentRef.value) {
|
if (emrComponentRef.value) {
|
||||||
emrComponentRef.value.setFormData({});
|
emrComponentRef.value.setFormData({});
|
||||||
}
|
}
|
||||||
|
resolve(); // 成功完成
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -645,11 +660,17 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (emrComponentRef.value) {
|
if (emrComponentRef.value) {
|
||||||
emrComponentRef.value.setFormData({});
|
emrComponentRef.value.setFormData({});
|
||||||
}
|
}
|
||||||
|
reject(error); // 错误完成
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
// 请求完成后清除Promise引用
|
||||||
|
loadLatestMedicalRecordPromise = null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadLatestMedicalRecordPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择默认模板 - 获取住院病历分类下的第一个模板
|
// 选择默认模板 - 获取住院病历分类下的第一个模板
|
||||||
@@ -835,6 +856,41 @@ watch(
|
|||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 合并两个监听器,避免重复触发
|
||||||
|
let patientChangeProcessing = false; // 防止重复处理
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [patientInfo.value?.encounterId, currentSelectTemplate.value?.id],
|
||||||
|
([newEncounterId, newTemplateId]) => {
|
||||||
|
// 当患者就诊ID或模板ID变化时,加载最新病历数据
|
||||||
|
if (newEncounterId && newTemplateId && !patientChangeProcessing) {
|
||||||
|
patientChangeProcessing = true;
|
||||||
|
|
||||||
|
// 添加延迟以确保模板数据已更新
|
||||||
|
nextTick(() => {
|
||||||
|
loadLatestMedicalRecord().finally(() => {
|
||||||
|
// 重置处理标志
|
||||||
|
patientChangeProcessing = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听模板选择变化,当模板选择变化时加载最新病历数据
|
||||||
|
watch(
|
||||||
|
() => currentSelectTemplate.value.id,
|
||||||
|
(newTemplateId) => {
|
||||||
|
// 当模板选择变化时,加载该模板的最新病历数据
|
||||||
|
if (newTemplateId) {
|
||||||
|
// 只要有模板ID,就尝试加载数据,不管之前是否有患者信息
|
||||||
|
// 因为可能是在切换患者后才选择模板
|
||||||
|
loadLatestMedicalRecord();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 移除日志
|
// 移除日志
|
||||||
await queryTemplateTree();
|
await queryTemplateTree();
|
||||||
|
|||||||
@@ -109,16 +109,25 @@ const getList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 标记是否已经手动选择过患者,防止后续自动选择
|
||||||
|
const hasManuallySelectedPatient = ref(false);
|
||||||
|
|
||||||
|
// 添加一个变量来跟踪当前期望的患者ID
|
||||||
|
let expectedPatientId = null;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filteredCardData.value,
|
() => filteredCardData.value,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
|
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
|
||||||
|
// 只有在从未手动选择过患者的情况下才自动选择
|
||||||
|
// 并且确保当前没有正在处理的患者切换操作
|
||||||
if (
|
if (
|
||||||
newData &&
|
newData &&
|
||||||
newData.length > 0 &&
|
newData.length > 0 &&
|
||||||
!cardId.value &&
|
!cardId.value &&
|
||||||
isFirstLoad.value &&
|
isFirstLoad.value &&
|
||||||
!patientInfo.value?.encounterId
|
!patientInfo.value?.encounterId &&
|
||||||
|
!hasManuallySelectedPatient.value
|
||||||
) {
|
) {
|
||||||
const firstPatient = newData[0];
|
const firstPatient = newData[0];
|
||||||
if (firstPatient?.encounterId) {
|
if (firstPatient?.encounterId) {
|
||||||
@@ -130,34 +139,81 @@ watch(
|
|||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
handleItemClick(firstPatient);
|
handleItemClick(firstPatient);
|
||||||
isFirstLoad.value = false;
|
isFirstLoad.value = false;
|
||||||
|
hasManuallySelectedPatient.value = true; // 标记已手动选择过
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
} else if (expectedPatientId && cardId.value && cardId.value !== expectedPatientId) {
|
||||||
|
// 如果当前cardId与期望的不一致,且不是初始状态,这可能意味着发生了意外的重置
|
||||||
|
// 这种情况下,我们不希望自动选择第一个患者
|
||||||
|
console.debug(`期望的患者ID: ${expectedPatientId}, 当前cardId: ${cardId.value}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 防抖函数,防止快速点击导致状态冲突
|
// 更新handleItemClick函数,设置期望的患者ID
|
||||||
let debounceTimer = null;
|
|
||||||
const handleItemClick = (node) => {
|
const handleItemClick = (node) => {
|
||||||
|
// 设置期望的患者ID
|
||||||
|
expectedPatientId = node.encounterId;
|
||||||
|
|
||||||
// 清除之前的计时器
|
// 清除之前的计时器
|
||||||
if (debounceTimer) {
|
if (debounceTimer) {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消之前未完成的患者加载操作
|
||||||
|
if (currentPatientPromise) {
|
||||||
|
// 注意:这里无法真正取消Promise,但我们可以标记当前操作已过期
|
||||||
|
currentPatientPromise.cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 设置新的计时器
|
// 设置新的计时器
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(async () => {
|
||||||
|
// 检查是否已被取消
|
||||||
|
if (currentPatientPromise?.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cardId.value = node.encounterId;
|
cardId.value = node.encounterId;
|
||||||
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
|
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
|
||||||
updatePatientInfo(node);
|
updatePatientInfo(node);
|
||||||
updateLocalPatientInfo(node);
|
updateLocalPatientInfo(node);
|
||||||
|
|
||||||
diagnosisRef.value?.getList();
|
// 标记已手动选择患者,防止自动选择第一条
|
||||||
adviceRef.value?.getListInfo();
|
hasManuallySelectedPatient.value = true;
|
||||||
adviceRef.value?.getDiagnosisInfo();
|
|
||||||
|
// 创建一个新的Promise来追踪这次加载操作
|
||||||
|
currentPatientPromise = Promise.all([
|
||||||
|
// 并行调用医嘱相关的API,避免重复请求
|
||||||
|
adviceRef.value?.getListInfo().catch(error => {
|
||||||
|
console.error('获取医嘱信息失败:', error);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
adviceRef.value?.getDiagnosisInfo().catch(error => {
|
||||||
|
console.error('获取诊断信息失败:', error);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
// 获取诊断信息
|
||||||
|
diagnosisRef.value?.getList?.().catch(error => {
|
||||||
|
console.error('获取诊断信息失败:', error);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await currentPatientPromise;
|
||||||
|
// 检查在此期间是否选择了其他患者
|
||||||
|
if (currentPatientPromise?.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载患者信息时出错:', error);
|
||||||
|
}
|
||||||
}, 100); // 100ms 防抖延迟
|
}, 100); // 100ms 防抖延迟
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 防抖函数,防止快速点击导致状态冲突
|
||||||
|
|
||||||
const handleSearch = (keyword) => {
|
const handleSearch = (keyword) => {
|
||||||
searchData.keyword = keyword;
|
searchData.keyword = keyword;
|
||||||
getList();
|
getList();
|
||||||
|
|||||||
85
sql/fix_audit_fields.sql
Normal file
85
sql/fix_audit_fields.sql
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
-- 数据库审计字段修复脚本
|
||||||
|
-- 用于确保所有表都能正确处理 create_by 和 create_time 字段
|
||||||
|
|
||||||
|
-- 步骤 1: 检查哪些表有审计字段但没有正确设置
|
||||||
|
-- 查询所有包含审计字段的表及其约束状态
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
column_name,
|
||||||
|
is_nullable,
|
||||||
|
column_default
|
||||||
|
FROM
|
||||||
|
information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema = 'public'
|
||||||
|
AND column_name IN ('create_by', 'create_time', 'update_by', 'update_time')
|
||||||
|
AND is_nullable = 'NO' -- 表示有NOT NULL约束
|
||||||
|
ORDER BY
|
||||||
|
table_name, column_name;
|
||||||
|
|
||||||
|
-- 步骤 2: 为所有审计字段设置合理的默认值(可选)
|
||||||
|
-- 这样即使自动填充失败,也不会违反约束
|
||||||
|
ALTER TABLE "public"."adm_practitioner" ALTER COLUMN "create_by" SET DEFAULT 'system';
|
||||||
|
ALTER TABLE "public"."adm_practitioner" ALTER COLUMN "update_by" SET DEFAULT 'system';
|
||||||
|
|
||||||
|
-- 步骤 3: 检查并更新现有数据,确保没有NULL值
|
||||||
|
-- 对于create_by字段
|
||||||
|
UPDATE "public"."adm_practitioner"
|
||||||
|
SET "create_by" = 'system'
|
||||||
|
WHERE "create_by" IS NULL OR "create_by" = '';
|
||||||
|
|
||||||
|
-- 对于update_by字段
|
||||||
|
UPDATE "public"."adm_practitioner"
|
||||||
|
SET "update_by" = 'system'
|
||||||
|
WHERE "update_by" IS NULL;
|
||||||
|
|
||||||
|
-- 步骤 4: 为其他可能存在相同问题的表执行类似操作
|
||||||
|
-- 以下是根据数据库结构推测的一些表名,您可能需要根据实际情况调整
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
table_name text;
|
||||||
|
tables_with_audit_fields CURSOR FOR
|
||||||
|
SELECT DISTINCT t.table_name
|
||||||
|
FROM information_schema.tables t
|
||||||
|
JOIN information_schema.columns c ON t.table_name = c.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND c.column_name IN ('create_by', 'create_time', 'update_by', 'update_time')
|
||||||
|
AND c.is_nullable = 'NO';
|
||||||
|
BEGIN
|
||||||
|
FOR rec IN tables_with_audit_fields LOOP
|
||||||
|
BEGIN
|
||||||
|
-- 为每个表设置审计字段的默认值
|
||||||
|
EXECUTE format('ALTER TABLE "public"."%I" ALTER COLUMN "create_by" SET DEFAULT ''system''', rec.table_name);
|
||||||
|
EXECUTE format('ALTER TABLE "public"."%I" ALTER COLUMN "update_by" SET DEFAULT ''system''', rec.table_name);
|
||||||
|
|
||||||
|
-- 更新现有的NULL值
|
||||||
|
EXECUTE format('UPDATE "public"."%I" SET "create_by" = ''system'' WHERE "create_by" IS NULL OR "create_by" = ''''', rec.table_name);
|
||||||
|
EXECUTE format('UPDATE "public"."%I" SET "update_by" = ''system'' WHERE "update_by" IS NULL', rec.table_name);
|
||||||
|
|
||||||
|
RAISE NOTICE 'Processed table: %', rec.table_name;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'Could not process table: %, Error: %', rec.table_name, SQLERRM;
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 步骤 5: 验证修复结果
|
||||||
|
-- 检查是否还有表存在审计字段的NULL值
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
column_name,
|
||||||
|
COUNT(*) as null_count
|
||||||
|
FROM (
|
||||||
|
SELECT 'adm_practitioner' as table_name, 'create_by' as column_name, create_by FROM "public"."adm_practitioner" WHERE create_by IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'adm_practitioner' as table_name, 'update_by' as column_name, update_by FROM "public"."adm_practitioner" WHERE update_by IS NULL
|
||||||
|
-- 在这里添加其他表的检查
|
||||||
|
) t
|
||||||
|
GROUP BY table_name, column_name;
|
||||||
|
|
||||||
|
-- 注意事项:
|
||||||
|
-- 1. 在生产环境执行前务必备份数据库
|
||||||
|
-- 2. 根据实际业务需求调整默认值(例如使用实际的用户名而非'system')
|
||||||
|
-- 3. 确保应用程序层面的自动填充机制正常工作
|
||||||
|
-- 4. 考虑在应用程序启动时进行审计字段的完整性检查
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- 移除 adm_practitioner 表中 create_by 列的 NOT NULL 约束
|
||||||
|
-- 用于解决 org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||||
|
|
||||||
|
-- 在PostgreSQL中,NOT NULL约束实际上是列的一个属性,而不是命名约束
|
||||||
|
-- 因此我们使用 ALTER COLUMN ... DROP NOT NULL 来移除它
|
||||||
|
|
||||||
|
-- 可选:先查看当前表结构及约束信息
|
||||||
|
-- 注意:\d 命令仅在 psql 中有效,在脚本中不能使用
|
||||||
|
/*
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
c.is_nullable,
|
||||||
|
c.data_type,
|
||||||
|
tc.constraint_type
|
||||||
|
FROM
|
||||||
|
information_schema.columns c
|
||||||
|
LEFT JOIN
|
||||||
|
information_schema.constraint_column_usage ccu ON c.column_name = ccu.column_name AND c.table_name = ccu.table_name
|
||||||
|
LEFT JOIN
|
||||||
|
information_schema.table_constraints tc ON ccu.constraint_name = tc.constraint_name
|
||||||
|
WHERE
|
||||||
|
c.table_name = 'adm_practitioner'
|
||||||
|
AND c.column_name = 'create_by';
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- 移除 create_by 列的 NOT NULL 约束
|
||||||
|
ALTER TABLE "public"."adm_practitioner"
|
||||||
|
ALTER COLUMN "create_by" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- 可选:如果需要,可以同时移除默认值
|
||||||
|
-- ALTER TABLE "public"."adm_practitioner"
|
||||||
|
-- ALTER COLUMN "create_by" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- 提示:执行此脚本后,create_by 列将允许 NULL 值
|
||||||
|
-- 这将解决插入数据时因缺少 create_by 值而导致的违反非空约束错误
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- 安全移除 adm_practitioner 表中 create_by 列的 NOT NULL 约束
|
||||||
|
-- 用于解决 org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
|
||||||
|
|
||||||
|
-- 步骤 1: 检查当前表结构
|
||||||
|
-- 查看 create_by 列的当前约束情况
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
is_nullable,
|
||||||
|
data_type,
|
||||||
|
column_default
|
||||||
|
FROM
|
||||||
|
information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema = 'public'
|
||||||
|
AND table_name = 'adm_practitioner'
|
||||||
|
AND column_name = 'create_by';
|
||||||
|
|
||||||
|
-- 步骤 2: 备份重要数据(可选但推荐)
|
||||||
|
-- 创建一个临时表来保存当前数据概览
|
||||||
|
CREATE TEMPORARY TABLE adm_practitioner_backup_check AS
|
||||||
|
SELECT id, name, create_by, create_time
|
||||||
|
FROM public.adm_practitioner
|
||||||
|
LIMIT 10; -- 只取前10条记录作为样本检查
|
||||||
|
|
||||||
|
-- 显示备份样本以供参考
|
||||||
|
SELECT * FROM adm_practitioner_backup_check;
|
||||||
|
|
||||||
|
-- 步骤 3: 执行约束移除操作
|
||||||
|
-- 在PostgreSQL中,NOT NULL约束实际上是列的一个属性,而不是命名约束
|
||||||
|
-- 使用 ALTER COLUMN ... DROP NOT NULL 来移除它
|
||||||
|
ALTER TABLE "public"."adm_practitioner"
|
||||||
|
ALTER COLUMN "create_by" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- 步骤 4: 验证更改
|
||||||
|
-- 再次检查列属性,确认NOT NULL约束已被移除
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
is_nullable,
|
||||||
|
data_type,
|
||||||
|
column_default
|
||||||
|
FROM
|
||||||
|
information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema = 'public'
|
||||||
|
AND table_name = 'adm_practitioner'
|
||||||
|
AND column_name = 'create_by';
|
||||||
|
|
||||||
|
-- 步骤 5: 测试插入操作(可选)
|
||||||
|
-- 尝试插入一条不带create_by值的数据来验证约束是否已移除
|
||||||
|
-- 注意:实际执行时请根据需要调整其他必需字段
|
||||||
|
-- INSERT INTO "public"."adm_practitioner" (name, user_id, tenant_id, delete_flag, create_time)
|
||||||
|
-- VALUES ('Test Practitioner', 0, 1, '0', NOW());
|
||||||
|
|
||||||
|
-- 提示:
|
||||||
|
-- 1. 执行此脚本前建议先备份整个表或数据库
|
||||||
|
-- 2. 执行后 create_by 列将允许 NULL 值
|
||||||
|
-- 3. 这将解决插入数据时因缺少 create_by 值而导致的违反非空约束错误
|
||||||
|
-- 4. 考虑在应用程序层面处理可能的 NULL 值
|
||||||
Reference in New Issue
Block a user