Backup local changes before resolving remote repository issue

This commit is contained in:
2026-01-27 10:05:25 +08:00
parent 11c2758289
commit 86bca03b04
29 changed files with 2626 additions and 126 deletions

View 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.

View 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.

View 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
View File

@@ -0,0 +1,6 @@
{
"tools": {
"approvalMode": "yolo"
},
"$version": 2
}

View 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
View 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

View 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
View 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
View 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注解或其他序列化配置问题

View 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
View 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);
// ... 其他代码
}
```
通过以上步骤,应该能够诊断并解决自动填充不工作的问题。

View 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
View 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());
}
}

View File

@@ -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("没有找到数据");
}
}

View File

@@ -6,13 +6,13 @@ spring:
druid:
# 主库数据源
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
password: Jchl1528
password: Jchl1528 # 请替换为实际的数据库密码
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled:
enabled: false
url:
username:
password:
@@ -35,10 +35,12 @@ spring:
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 # FROM DUAL
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnBorrow: true # 改为true以确保连接有效
testOnReturn: false
# 配置监控统计拦截的filters去掉后监控界面sql无法统计'wall'用于防火墙
filters: stat,wall,slf4j
webStatFilter:
enabled: true
statViewServlet:
@@ -59,14 +61,12 @@ spring:
wall:
config:
multi-statement-allow: true
# redis 配置
redis:
# 地址
host: 192.168.110.252
host: 47.116.196.11
# 端口默认为6379
port: 6379
port: 26379
# 数据库索引
database: 1
# 密码
@@ -83,10 +83,8 @@ spring:
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 文言
messages:
basename: i18n/general_message/messages
encoding: utf-8
# 服务器配置
server:
# 服务器的HTTP端口默认为18080
port: 18080

View File

@@ -34,6 +34,14 @@ logging:
level:
com.openhis: debug
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:

View 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
}
}

View 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;

View 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;

View File

@@ -42,6 +42,7 @@ import {defineEmits, ref, unref} from 'vue';
import {deleteRecord, getRecordByEncounterIdList} from '../api';
import {ElMessage} from 'element-plus';
import {patientInfo} from '../../store/patient.js';
import apiRequestManager from '@/utils/apiRequestManager.js';
const emits = defineEmits(['historyClick']);
const props = defineProps({
@@ -67,15 +68,30 @@ const queryParams = ref({
isPage: 0,
});
const historyData = ref([]);
// 防止重复加载的标志
let isLoadingHistory = false;
const queryList = async () => {
// 防止重复加载
if (isLoadingHistory) {
console.log('History data is already loading, skipping duplicate call');
return;
}
isLoadingHistory = true;
try {
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
const res = await getRecordByEncounterIdList({
...queryParams.value,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
});
const res = await apiRequestManager.execute(
getRecordByEncounterIdList,
'/document/record/getRecordByEncounterIdList',
{
isPage: 0, // 确保参数一致,便于去重
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
}
);
historyData.value = res.data || [];
} else {
historyData.value = [];
@@ -83,6 +99,8 @@ const queryList = async () => {
} catch (error) {
// ElMessage.error(' 获取模板树失败 ');
historyData.value = [];
} finally {
isLoadingHistory = false; // 重置加载标志
}
};
const handleNodeClick = (data) => {

View File

@@ -103,6 +103,7 @@ import dayjs from 'dayjs';
// 打印工具
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
import {getEncounterDiagnosis} from '../api';
import apiRequestManager from '@/utils/apiRequestManager.js';
import History from './components/history';
import Template from './components/template';
import TemplateEdit from './components/templateEdit.vue';
@@ -205,7 +206,7 @@ const handleNodeClick = (data, node) => {
// 选择任何病历模板后,都加载该病历类型的最新历史记录
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
loadLatestMedicalRecord();
debouncedLoadLatestMedicalRecord();
}
}, 100);
});
@@ -279,7 +280,7 @@ const handleSubmitOk = async (data) => {
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
setTimeout(() => {
loadLatestMedicalRecord();
debouncedLoadLatestMedicalRecord();
}, 100);
} catch (error) {
ElMessage.error('提交失败');
@@ -410,7 +411,7 @@ const selectOutpatientMedicalRecordTemplate = async () => {
// 等待模板加载完成,然后获取并回显最新病历数据
setTimeout(() => {
historyRef.value?.queryList();
loadLatestMedicalRecord();
debouncedLoadLatestMedicalRecord();
}, 500);
});
} else {
@@ -421,19 +422,36 @@ const selectOutpatientMedicalRecordTemplate = async () => {
// 当前选中的历史病历ID用于在History组件中高亮显示
const selectedHistoryRecordId = ref('');
import { debounce } from 'lodash-es';
// 防止重复加载的标志
let isLoadingLatestRecord = false;
// 加载最新的病历数据并回显
const loadLatestMedicalRecord = async () => {
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;
try {
// 获取患者的历史病历记录
const res = await getRecordByEncounterIdList({
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
});
const res = await apiRequestManager.execute(
getRecordByEncounterIdList,
'/document/record/getRecordByEncounterIdList',
{
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
}
);
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
@@ -519,8 +537,12 @@ const loadLatestMedicalRecord = async () => {
});
} finally {
loading.value = false;
isLoadingLatestRecord = false; // 重置加载标志
}
};
// 防抖版本的加载最新病历数据函数
const debouncedLoadLatestMedicalRecord = debounce(loadLatestMedicalRecord, 300);
const templateRef = ref(null);
const handleTemplateClick = (data) => {
@@ -750,7 +772,7 @@ const selectDefaultTemplate = () => {
// 直接加载最新病历数据不再使用额外的setTimeout延迟
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
loadLatestMedicalRecord();
debouncedLoadLatestMedicalRecord();
});
} else {
console.log('未找到门诊病历模板');

View File

@@ -209,6 +209,7 @@ import useUserStore from '@/store/modules/user';
import { nextTick } from 'vue';
import { updatePatientInfo } from './components/store/patient.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { debounce } from 'lodash-es';
// // 监听路由离开事件
// onBeforeRouteLeave((to, from, next) => {
@@ -487,7 +488,8 @@ function handleOpen() {
patientDrawerRef.value.refreshList();
}
function handleCardClick(item, index) {
// 原始的handleCardClick函数
function handleCardClickOriginal(item, index) {
console.log('handleCardClick 被调用');
console.log('点击的患者项目:', item);
console.log('患者项目中的encounterId:', item.encounterId);
@@ -544,6 +546,9 @@ function handleCardClick(item, index) {
});
}
// 使用防抖的handleCardClick函数防止短时间内多次点击
const handleCardClick = debounce(handleCardClickOriginal, 500);
function handleLeave(encounterId) {
leaveEncounter(encounterId).then((res) => {
if (res.code == 200) {
@@ -589,7 +594,7 @@ function handleHospitalizationClick() {
// 接诊回调
function handleReceive(row) {
handleCardClick(row);
handleCardClickOriginal(row);
currentEncounterId.value = row.encounterId;
drawer.value = false;
getPatientList();
@@ -776,7 +781,7 @@ const markSeen = async () => {
currentCallPatient.value = {};
};
const callThis = (row) => {
handleCardClick(row);
handleCardClickOriginal(row);
currentCallPatient.value = row;
dialogVisible.value = false;
// 刷新患者列表和候诊列表

View File

@@ -525,19 +525,33 @@ function getList() {
function refresh() {
getListInfo(false);
}
// 防止重复请求的标志
let listInfoRequestPromise = null;
// 获取列表信息
function getListInfo(addNewRow) {
// 如果已经有正在进行的请求则返回该请求的Promise
if (listInfoRequestPromise) {
return listInfoRequestPromise;
}
loadingInstance = ElLoading.service({ fullscreen: true });
setTimeout(() => {
loadingInstance.close();
if (loadingInstance) {
loadingInstance.close();
}
}, 180);
isAdding.value = false;
expandOrder.value = [];
getPrescriptionList(patientInfo.value.encounterId).then((res) => {
console.log('getListInfo==========>', JSON.stringify(res.data));
loadingInstance.close();
prescriptionList.value = res.data
// 并行请求两个API并将结果合并处理
listInfoRequestPromise = Promise.all([
getPrescriptionList(patientInfo.value.encounterId),
getContract({ encounterId: patientInfo.value.encounterId })
])
.then(([prescriptionRes, contractRes]) => {
// 处理处方列表
prescriptionList.value = prescriptionRes.data
.map((item) => {
return {
...JSON.parse(item.contentJson),
@@ -549,15 +563,35 @@ function getListInfo(addNewRow) {
.sort((a, b) => {
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) {
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;
});
accountId.value = patientInfo.value.accountId;
return listInfoRequestPromise;
}
// 数据过滤
const filterPrescriptionList = computed(() => {
@@ -571,18 +605,37 @@ const filterPrescriptionList = computed(() => {
return pList;
});
// 防止诊断信息重复请求的标志
let diagnosisInfoRequestPromise = null;
function getDiagnosisInfo() {
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => {
diagnosisList.value = res.data;
let diagnosisInfo = diagnosisList.value.filter((item) => {
return item.maindiseFlag == 1;
// 如果已经有正在进行的请求则返回该请求的Promise
if (diagnosisInfoRequestPromise) {
return diagnosisInfoRequestPromise;
}
diagnosisInfoRequestPromise = getEncounterDiagnosis(patientInfo.value.encounterId)
.then((res) => {
diagnosisList.value = res.data;
let diagnosisInfo = diagnosisList.value.filter((item) => {
return item.maindiseFlag == 1;
});
diagnosisInfo.value = diagnosisInfo[0];
conditionDefinitionId.value = diagnosisInfo[0].definitionId;
conditionId.value = diagnosisInfo[0].conditionId;
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
diagnosisName.value = diagnosisInfo[0].name;
})
.catch(error => {
console.error('获取诊断信息失败:', error);
ElMessage.error('获取诊断信息失败');
})
.finally(() => {
// 请求完成后清除Promise引用
diagnosisInfoRequestPromise = null;
});
diagnosisInfo.value = diagnosisInfo[0];
conditionDefinitionId.value = diagnosisInfo[0].definitionId;
conditionId.value = diagnosisInfo[0].conditionId;
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
diagnosisName.value = diagnosisInfo[0].name;
});
return diagnosisInfoRequestPromise;
}
function getRowDisabled(row) {

View File

@@ -65,22 +65,40 @@ const queryParams = ref({
isPage: 0,
});
const historyData = ref([]);
// 防止重复请求的标志
let queryListPromise = null;
const queryList = async () => {
// 如果已经有正在进行的请求则返回该请求的Promise
if (queryListPromise) {
return queryListPromise;
}
try {
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
const res = await getRecordByEncounterIdList({
queryListPromise = getRecordByEncounterIdList({
...queryParams.value,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: unref(definitionId),
})
.then(res => {
historyData.value = res.data || [];
})
.finally(() => {
// 请求完成后清除Promise引用
queryListPromise = null;
});
historyData.value = res.data || [];
return queryListPromise;
} else {
historyData.value = [];
}
} catch (error) {
// 不显示错误消息,避免干扰用户体验
historyData.value = [];
// 请求完成后清除Promise引用
queryListPromise = null;
}
};

View File

@@ -305,9 +305,10 @@ const handleSubmitOk = async (data) => {
// templateRef.value?.queryList();
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
// 增加延迟时间以确保数据库更新完成
setTimeout(() => {
loadLatestMedicalRecord();
}, 100);
}, 300);
ElMessage.success('保存成功');
} catch (error) {
ElMessage.error('提交失败');
@@ -553,54 +554,94 @@ const selectOutpatientMedicalRecordTemplate = async () => {
selectDefaultTemplate();
};
// 防止重复请求的标志
let loadLatestMedicalRecordPromise = null;
// 加载最新的病历数据并回显
const loadLatestMedicalRecord = async () => {
// 如果已经有正在进行的请求则返回该请求的Promise
if (loadLatestMedicalRecordPromise) {
return loadLatestMedicalRecordPromise;
}
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
editForm.value.id = '';
loading.value = true;
try {
// 获取患者的历史病历记录
const res = await getRecordByEncounterIdList({
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
});
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
// 按时间排序,获取最新的病历记录
historyRecords.sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime));
const latestRecord = historyRecords[0];
// 创建一个新的Promise来处理请求
loadLatestMedicalRecordPromise = new Promise(async (resolve, reject) => {
try {
// 获取患者的历史病历记录
const res = await getRecordByEncounterIdList({
isPage: 0,
encounterId: patientInfo.value.encounterId,
patientId: patientInfo.value.patientId,
definitionId: currentSelectTemplate.value.id,
});
// 保存最新病历ID用于在History组件中高亮显示
selectedHistoryRecordId.value = latestRecord.id;
// 自动回显最新病历数据到模板
editForm.value = latestRecord;
nextTick(() => {
if (emrComponentRef.value && latestRecord.contentJson) {
try {
const parsedData = JSON.parse(latestRecord.contentJson);
emrComponentRef.value.setFormData(parsedData);
} catch (parseError) {
console.error('解析病历数据失败:', parseError);
// 解析失败时仍然尝试设置空数据以清空之前的残留数据
const historyRecords = res.data || [];
if (historyRecords.length > 0) {
// 按时间排序,获取最新病历记录
historyRecords.sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime));
const latestRecord = historyRecords[0];
// 保存最新病历ID用于在History组件中高亮显示
selectedHistoryRecordId.value = latestRecord.id;
// 自动回显最新病历数据到模板
editForm.value = latestRecord;
nextTick(() => {
if (emrComponentRef.value && latestRecord.contentJson) {
try {
const parsedData = JSON.parse(latestRecord.contentJson);
emrComponentRef.value.setFormData(parsedData);
} catch (parseError) {
console.error('解析病历数据失败:', parseError);
// 解析失败时仍然尝试设置空数据以清空之前的残留数据
emrComponentRef.value.setFormData({});
}
} else {
// 如果没有内容数据,也要清空组件中的数据
emrComponentRef.value.setFormData({});
}
} else {
// 如果没有内容数据,也要清空组件中的数据
emrComponentRef.value.setFormData({});
}
// 通知History组件更新选中状态
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
historyRef.value.updateSelectedRecord(latestRecord.id);
}
});
} else {
// 清空选中状态
// 通知History组件更新选中状态
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
historyRef.value.updateSelectedRecord(latestRecord.id);
}
resolve(); // 成功完成
});
} else {
// 清空选中状态
selectedHistoryRecordId.value = '';
// 当没有历史记录时,也要清空当前表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
definitionBusNo: '',
contentJson: '',
statusEnum: 1,
organizationId: 0,
encounterId: '',
patientId: '',
recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
createBy: '',
source: '',
};
nextTick(() => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
resolve(); // 成功完成
});
loading.value = false;
}
} catch (error) {
ElMessage.error('加载最新病历数据失败=====>', error);
// 出错时也清空选中状态
selectedHistoryRecordId.value = '';
// 当没有历史记录时,也要清空当前表单数据,避免显示之前患者的数据
// 出错时也要清空表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
@@ -619,37 +660,17 @@ const loadLatestMedicalRecord = async () => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
reject(error); // 错误完成
});
loading.value = false;
} finally {
loading.value = false;
// 请求完成后清除Promise引用
loadLatestMedicalRecordPromise = null;
}
} catch (error) {
ElMessage.error('加载最新病历数据失败=====>', error);
// 出错时也清空选中状态
selectedHistoryRecordId.value = '';
// 出错时也要清空表单数据,避免显示之前患者的数据
editForm.value = {
id: '',
definitionId: '',
definitionBusNo: '',
contentJson: '',
statusEnum: 1,
organizationId: 0,
encounterId: '',
patientId: '',
recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
createBy: '',
source: '',
};
});
nextTick(() => {
if (emrComponentRef.value) {
emrComponentRef.value.setFormData({});
}
});
loading.value = false;
} finally {
loading.value = false;
}
return loadLatestMedicalRecordPromise;
};
// 选择默认模板 - 获取住院病历分类下的第一个模板
@@ -835,6 +856,41 @@ watch(
{ 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 () => {
// 移除日志
await queryTemplateTree();

View File

@@ -109,16 +109,25 @@ const getList = () => {
});
};
// 标记是否已经手动选择过患者,防止后续自动选择
const hasManuallySelectedPatient = ref(false);
// 添加一个变量来跟踪当前期望的患者ID
let expectedPatientId = null;
watch(
() => filteredCardData.value,
(newData) => {
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
// 只有在从未手动选择过患者的情况下才自动选择
// 并且确保当前没有正在处理的患者切换操作
if (
newData &&
newData.length > 0 &&
!cardId.value &&
isFirstLoad.value &&
!patientInfo.value?.encounterId
!patientInfo.value?.encounterId &&
!hasManuallySelectedPatient.value
) {
const firstPatient = newData[0];
if (firstPatient?.encounterId) {
@@ -130,34 +139,81 @@ watch(
debounceTimer = setTimeout(() => {
handleItemClick(firstPatient);
isFirstLoad.value = false;
hasManuallySelectedPatient.value = true; // 标记已手动选择过
}, 100);
}
} else if (expectedPatientId && cardId.value && cardId.value !== expectedPatientId) {
// 如果当前cardId与期望的不一致且不是初始状态这可能意味着发生了意外的重置
// 这种情况下,我们不希望自动选择第一个患者
console.debug(`期望的患者ID: ${expectedPatientId}, 当前cardId: ${cardId.value}`);
}
},
{ immediate: true }
);
// 防抖函数,防止快速点击导致状态冲突
let debounceTimer = null;
// 更新handleItemClick函数设置期望的患者ID
const handleItemClick = (node) => {
// 设置期望的患者ID
expectedPatientId = node.encounterId;
// 清除之前的计时器
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 取消之前未完成的患者加载操作
if (currentPatientPromise) {
// 注意这里无法真正取消Promise但我们可以标记当前操作已过期
currentPatientPromise.cancelled = true;
}
// 设置新的计时器
debounceTimer = setTimeout(() => {
debounceTimer = setTimeout(async () => {
// 检查是否已被取消
if (currentPatientPromise?.cancelled) {
return;
}
cardId.value = node.encounterId;
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
updatePatientInfo(node);
updateLocalPatientInfo(node);
diagnosisRef.value?.getList();
adviceRef.value?.getListInfo();
adviceRef.value?.getDiagnosisInfo();
// 标记已手动选择患者,防止自动选择第一条
hasManuallySelectedPatient.value = true;
// 创建一个新的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 防抖延迟
};
// 防抖函数,防止快速点击导致状态冲突
const handleSearch = (keyword) => {
searchData.keyword = keyword;
getList();

85
sql/fix_audit_fields.sql Normal file
View 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. 考虑在应用程序启动时进行审计字段的完整性检查

View File

@@ -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 值而导致的违反非空约束错误

View File

@@ -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 值