Files
his/MybastisColumnsHandler_optimization_guide.md

9.0 KiB
Raw Blame History

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 文件替换为以下内容:

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. 验证处理器是否被正确扫描

确保在主应用类或配置类中启用了自动填充功能:

@SpringBootApplication
@MapperScan("com.openhis.*.mapper")  // 确保扫描到你的mapper
@EnableTransactionManagement  // 启用事务管理
public class OpenHisApplication {
    public static void main(String[] args) {
        SpringApplication.run(OpenHisApplication.class, args);
    }
}

3. 测试验证

创建一个简单的测试来验证自动填充是否正常工作:

@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,我们可以确保所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误,同时保持数据完整性和审计跟踪功能。