gushoubang 2 mesi fa
parent
commit
3ff55c3426

+ 163 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/controller/NodeAttachmentController.java

@@ -0,0 +1,163 @@
+package com.siwei.apply.controller;
+
+import com.siwei.apply.domain.NodeAttachment;
+import com.siwei.apply.service.NodeAttachmentService;
+import com.siwei.common.core.domain.R;
+import com.siwei.common.core.web.controller.BaseController;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 节点附件控制器
+ */
+@RestController
+@RequestMapping("/nodeattachment")
+public class NodeAttachmentController extends BaseController {
+
+    @Autowired
+    private NodeAttachmentService nodeAttachmentService;
+
+    /**
+     * 处理文件并保存附件信息
+     *
+     * @param requestBody 包含filePath的请求体
+     * @return 数据库存储后的id
+     */
+    @PostMapping("/process")
+    public R<Map<String, String>> processFile(@RequestBody Map<String, String> requestBody) {
+        try {
+            String filePath = requestBody.get("filePath");
+
+            if (filePath == null || filePath.trim().isEmpty()) {
+                return R.fail("filePath不能为空");
+            }
+
+            String id = nodeAttachmentService.processFileAndSave(filePath);
+
+            if (id != null) {
+                Map<String, String> responseData = new HashMap<>();
+                responseData.put("nodeAttachmentId", id);
+                return R.ok(responseData);
+            } else {
+                return R.fail("处理文件失败");
+            }
+
+        } catch (Exception e) {
+            logger.error("处理文件异常", e);
+            return R.fail("处理文件异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据ID查询附件信息
+     *
+     * @param nodeAttachmentId 附件ID
+     * @return 附件信息
+     */
+    @GetMapping("/nodeAttachmentId/{nodeAttachmentId}")
+    public R<NodeAttachment> getById(@PathVariable String nodeAttachmentId) {
+        try {
+            if (nodeAttachmentId == null || nodeAttachmentId.trim().isEmpty()) {
+                return R.fail("id不能为空");
+            }
+
+            NodeAttachment nodeAttachment = nodeAttachmentService.getById(nodeAttachmentId);
+            return R.ok(nodeAttachment);
+
+        } catch (Exception e) {
+            logger.error("查询附件信息异常", e);
+            return R.fail("查询附件信息异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据节点ID查询附件信息
+     *
+     * @param nodeId 节点ID
+     * @return 附件信息
+     */
+    @GetMapping("/node/{nodeId}")
+    public R<NodeAttachment> getByNodeId(@PathVariable String nodeId) {
+        try {
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                return R.fail("nodeId不能为空");
+            }
+
+            NodeAttachment nodeAttachment = nodeAttachmentService.getByNodeId(nodeId);
+            return R.ok(nodeAttachment);
+
+        } catch (Exception e) {
+            logger.error("查询节点附件信息异常", e);
+            return R.fail("查询节点附件信息异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据节点ID删除附件信息
+     *
+     * @param nodeId 节点ID
+     * @return 删除结果
+     */
+    @DeleteMapping("/{nodeId}")
+    public R<Map<String, Object>> deleteByNodeId(@PathVariable String nodeId) {
+        try {
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                return R.fail("nodeId不能为空");
+            }
+
+            nodeAttachmentService.deleteByNodeId(nodeId);
+
+            Map<String, Object> responseData = new java.util.HashMap<>();
+            responseData.put("nodeId", nodeId);
+            responseData.put("success", true);
+
+            return R.ok(responseData);
+
+        } catch (Exception e) {
+            logger.error("删除节点附件信息异常", e);
+            return R.fail("删除节点附件信息异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 更新附件记录的nodeId
+     *
+     * @param requestBody 包含nodeAttachmentId和nodeId的请求体
+     * @return 更新结果
+     */
+    @PutMapping("/updateNodeId")
+    public R<Map<String, Object>> updateNodeId(@RequestBody Map<String, String> requestBody) {
+        try {
+            String nodeAttachmentId = requestBody.get("nodeAttachmentId");
+            String nodeId = requestBody.get("nodeId");
+
+            if (nodeAttachmentId == null || nodeAttachmentId.trim().isEmpty()) {
+                return R.fail("nodeAttachmentId不能为空");
+            }
+
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                return R.fail("nodeId不能为空");
+            }
+
+            boolean success = nodeAttachmentService.updateNodeId(nodeAttachmentId, nodeId);
+
+            Map<String, Object> responseData = new java.util.HashMap<>();
+            responseData.put("nodeAttachmentId", nodeAttachmentId);
+            responseData.put("nodeId", nodeId);
+            responseData.put("success", success);
+
+            if (success) {
+                return R.ok(responseData);
+            } else {
+                return R.fail("更新nodeId失败");
+            }
+
+        } catch (Exception e) {
+            logger.error("更新nodeId异常", e);
+            return R.fail("更新nodeId异常:" + e.getMessage());
+        }
+    }
+}

+ 30 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/domain/NodeAttachment.java

@@ -0,0 +1,30 @@
+package com.siwei.apply.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 记录流程对应的附件材料
+ */
+@Data
+public class NodeAttachment implements Serializable {
+    
+    /** 主键ID */
+    private String id;
+    
+    /** 节点id */
+    private String nodeId;
+    
+    /** 附件目录 */
+    private Map<String, Object> attachment;
+    
+    /**
+     * 生成主键ID
+     */
+    public void generateId() {
+        this.id = UUID.randomUUID().toString();
+    }
+}

+ 55 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/mapper/NodeAttachmentMapper.java

@@ -0,0 +1,55 @@
+package com.siwei.apply.mapper;
+
+import com.siwei.apply.domain.NodeAttachment;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 记录流程对应的附件材料 Mapper 接口
+ */
+@Mapper
+public interface NodeAttachmentMapper {
+    
+    /**
+     * 根据ID查询附件信息
+     *
+     * @param id 主键ID
+     * @return NodeAttachment
+     */
+    NodeAttachment selectById(String id);
+
+    /**
+     * 根据节点ID查询附件信息
+     *
+     * @param nodeId 节点ID
+     * @return NodeAttachment
+     */
+    NodeAttachment selectByNodeId(String nodeId);
+    
+    /**
+     * 插入附件记录
+     *
+     * @param nodeAttachment 附件对象
+     */
+    void insert(NodeAttachment nodeAttachment);
+    
+    /**
+     * 根据节点ID更新附件记录
+     *
+     * @param nodeAttachment 附件对象
+     */
+    void update(NodeAttachment nodeAttachment);
+
+    /**
+     * 根据ID更新附件记录
+     *
+     * @param nodeAttachment 附件对象
+     */
+    void updateById(NodeAttachment nodeAttachment);
+    
+    /**
+     * 根据节点ID删除附件记录
+     *
+     * @param nodeId 节点ID
+     */
+    void deleteByNodeId(String nodeId);
+}

+ 51 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/service/NodeAttachmentService.java

@@ -0,0 +1,51 @@
+package com.siwei.apply.service;
+
+import com.siwei.apply.domain.NodeAttachment;
+
+import java.util.Map;
+
+/**
+ * 记录流程对应的附件材料 服务接口
+ */
+public interface NodeAttachmentService {
+    
+    /**
+     * 根据ID查询附件信息
+     *
+     * @param id 附件ID
+     * @return NodeAttachment
+     */
+    NodeAttachment getById(String id);
+
+    /**
+     * 根据节点ID查询附件信息
+     *
+     * @param nodeId 节点ID
+     * @return NodeAttachment
+     */
+    NodeAttachment getByNodeId(String nodeId);
+    
+    /**
+     * 处理文件并保存附件信息
+     *
+     * @param filePath 文件路径
+     * @return 数据库存储后的id
+     */
+    String processFileAndSave(String filePath);
+    
+    /**
+     * 删除节点附件信息
+     *
+     * @param nodeId 节点ID
+     */
+    void deleteByNodeId(String nodeId);
+
+    /**
+     * 更新附件记录的nodeId
+     *
+     * @param nodeAttachmentId 附件记录ID
+     * @param nodeId 节点ID
+     * @return 是否更新成功
+     */
+    boolean updateNodeId(String nodeAttachmentId, String nodeId);
+}

+ 176 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/service/impl/NodeAttachmentServiceImpl.java

@@ -0,0 +1,176 @@
+package com.siwei.apply.service.impl;
+
+import com.siwei.apply.domain.NodeAttachment;
+import com.siwei.apply.mapper.NodeAttachmentMapper;
+import com.siwei.apply.service.NodeAttachmentService;
+import com.siwei.apply.utils.FileExtractUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.nio.file.*;
+import java.util.*;
+
+/**
+ * 记录流程对应的附件材料 服务实现类
+ */
+@Service
+public class NodeAttachmentServiceImpl implements NodeAttachmentService {
+    
+    private static final Logger logger = LoggerFactory.getLogger(NodeAttachmentServiceImpl.class);
+    
+    @Autowired
+    private NodeAttachmentMapper nodeAttachmentMapper;
+    
+    @Override
+    public NodeAttachment getById(String id) {
+        try {
+            if (id == null || id.trim().isEmpty()) {
+                logger.warn("查询附件信息失败:id不能为空");
+                return null;
+            }
+
+            NodeAttachment nodeAttachment = nodeAttachmentMapper.selectById(id);
+            logger.info("查询附件信息,id: {}", id);
+            return nodeAttachment;
+
+        } catch (Exception e) {
+            logger.error("查询附件信息异常,id: {}", id, e);
+            return null;
+        }
+    }
+
+    @Override
+    public NodeAttachment getByNodeId(String nodeId) {
+        try {
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                logger.warn("查询附件信息失败:nodeId不能为空");
+                return null;
+            }
+
+            NodeAttachment nodeAttachment = nodeAttachmentMapper.selectByNodeId(nodeId);
+            logger.info("查询节点附件信息,nodeId: {}", nodeId);
+            return nodeAttachment;
+
+        } catch (Exception e) {
+            logger.error("查询节点附件信息异常,nodeId: {}", nodeId, e);
+            return null;
+        }
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String processFileAndSave(String filePath) {
+        try {
+            if (filePath == null || filePath.trim().isEmpty()) {
+                logger.warn("处理文件失败:filePath不能为空");
+                return null;
+            }
+
+            Path file = Paths.get(filePath);
+            if (!Files.exists(file)) {
+                logger.warn("处理文件失败:文件不存在: {}", filePath);
+                return null;
+            }
+
+            Map<String, Object> directoryStructure;
+
+            // 判断是否为压缩文件
+            if (FileExtractUtil.isCompressedFile(filePath)) {
+                // 解压文件到同名文件夹并获取目录结构
+                String extractDir = FileExtractUtil.extractToSameNameFolder(filePath);
+                if (extractDir != null) {
+                    directoryStructure = FileExtractUtil.getDirectoryStructure(Paths.get(extractDir));
+                } else {
+                    logger.error("解压文件失败: {}", filePath);
+                    return null;
+                }
+            } else {
+                // 直接获取文件信息
+                directoryStructure = FileExtractUtil.getFileStructure(file);
+            }
+
+            // 保存附件信息并返回ID
+            String id = saveAttachment(directoryStructure);
+
+            logger.info("成功处理文件并保存附件信息,id: {}, filePath: {}", id, filePath);
+            return id;
+
+        } catch (Exception e) {
+            logger.error("处理文件并保存附件信息异常,filePath: {}", filePath, e);
+            return null;
+        }
+    }
+    
+    @Override
+    public void deleteByNodeId(String nodeId) {
+        try {
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                throw new IllegalArgumentException("nodeId不能为空");
+            }
+
+            nodeAttachmentMapper.deleteByNodeId(nodeId);
+            logger.info("删除节点附件信息,nodeId: {}", nodeId);
+
+        } catch (Exception e) {
+            logger.error("删除节点附件信息异常,nodeId: {}", nodeId, e);
+            throw e;
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateNodeId(String nodeAttachmentId, String nodeId) {
+        try {
+            if (nodeAttachmentId == null || nodeAttachmentId.trim().isEmpty()) {
+                logger.warn("更新nodeId失败:nodeAttachmentId不能为空");
+                return false;
+            }
+
+            if (nodeId == null || nodeId.trim().isEmpty()) {
+                logger.warn("更新nodeId失败:nodeId不能为空");
+                return false;
+            }
+
+            // 先根据nodeId删除其他的附件记录
+            nodeAttachmentMapper.deleteByNodeId(nodeId);
+            logger.info("删除nodeId对应的其他附件记录,nodeId: {}", nodeId);
+
+            // 再查询要更新的记录是否存在
+            NodeAttachment existing = nodeAttachmentMapper.selectById(nodeAttachmentId);
+            if (existing == null) {
+                logger.warn("更新nodeId失败:附件记录不存在,nodeAttachmentId: {}", nodeAttachmentId);
+                return false;
+            }
+
+            // 更新nodeId
+            existing.setNodeId(nodeId);
+            nodeAttachmentMapper.updateById(existing);
+
+            logger.info("成功更新nodeId,nodeAttachmentId: {}, nodeId: {}", nodeAttachmentId, nodeId);
+            return true;
+
+        } catch (Exception e) {
+            logger.error("更新nodeId异常,nodeAttachmentId: {}, nodeId: {}", nodeAttachmentId, nodeId, e);
+            return false;
+        }
+    }
+    
+
+    
+    /**
+     * 保存附件信息
+     */
+    private String saveAttachment(Map<String, Object> directoryStructure) {
+        // 创建新记录
+        NodeAttachment nodeAttachment = new NodeAttachment();
+        nodeAttachment.generateId();
+        nodeAttachment.setNodeId(null); // 不设置nodeId
+        nodeAttachment.setAttachment(directoryStructure);
+        nodeAttachmentMapper.insert(nodeAttachment);
+
+        return nodeAttachment.getId();
+    }
+}

+ 266 - 0
siwei-modules/siwei-apply/src/main/java/com/siwei/apply/utils/FileExtractUtil.java

@@ -0,0 +1,266 @@
+package com.siwei.apply.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.file.*;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+/**
+ * 文件解压工具类
+ */
+public class FileExtractUtil {
+    
+    private static final Logger logger = LoggerFactory.getLogger(FileExtractUtil.class);
+    
+    /**
+     * 判断是否为压缩文件
+     *
+     * @param filePath 文件路径
+     * @return 是否为压缩文件
+     */
+    public static boolean isCompressedFile(String filePath) {
+        if (filePath == null || filePath.trim().isEmpty()) {
+            return false;
+        }
+        
+        String lowerCasePath = filePath.toLowerCase();
+        return lowerCasePath.endsWith(".zip") || 
+               lowerCasePath.endsWith(".rar") ||
+               lowerCasePath.endsWith(".7z");
+    }
+    
+    /**
+     * 解压文件到指定目录
+     *
+     * @param compressedFilePath 压缩文件路径
+     * @param extractDir 解压目录路径
+     * @return 是否解压成功
+     */
+    public static boolean extractFile(String compressedFilePath, String extractDir) {
+        try {
+            if (compressedFilePath == null || extractDir == null) {
+                logger.error("解压文件失败:文件路径或解压目录不能为空");
+                return false;
+            }
+            
+            Path compressedFile = Paths.get(compressedFilePath);
+            if (!Files.exists(compressedFile)) {
+                logger.error("解压文件失败:压缩文件不存在: {}", compressedFilePath);
+                return false;
+            }
+            
+            // 创建解压目录
+            Path extractPath = Paths.get(extractDir);
+            Files.createDirectories(extractPath);
+            
+            String lowerCasePath = compressedFilePath.toLowerCase();
+            if (lowerCasePath.endsWith(".zip")) {
+                return extractZipFile(compressedFilePath, extractDir);
+            } else if (lowerCasePath.endsWith(".rar")) {
+                logger.warn("暂不支持RAR格式解压: {}", compressedFilePath);
+                return false;
+            } else if (lowerCasePath.endsWith(".7z")) {
+                logger.warn("暂不支持7Z格式解压: {}", compressedFilePath);
+                return false;
+            } else {
+                logger.error("不支持的压缩格式: {}", compressedFilePath);
+                return false;
+            }
+            
+        } catch (Exception e) {
+            logger.error("解压文件异常,压缩文件: {}, 解压目录: {}", compressedFilePath, extractDir, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 解压文件到同名文件夹
+     *
+     * @param compressedFilePath 压缩文件路径
+     * @return 解压目录路径,失败返回null
+     */
+    public static String extractToSameNameFolder(String compressedFilePath) {
+        try {
+            if (compressedFilePath == null || compressedFilePath.trim().isEmpty()) {
+                logger.error("解压文件失败:压缩文件路径不能为空");
+                return null;
+            }
+            
+            Path compressedFile = Paths.get(compressedFilePath);
+            if (!Files.exists(compressedFile)) {
+                logger.error("解压文件失败:压缩文件不存在: {}", compressedFilePath);
+                return null;
+            }
+            
+            // 生成同名文件夹路径
+            String fileName = compressedFile.getFileName().toString();
+            String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
+            Path extractDir = compressedFile.getParent().resolve(baseName);
+            
+            boolean success = extractFile(compressedFilePath, extractDir.toString());
+            if (success) {
+                logger.info("成功解压文件到同名文件夹,压缩文件: {}, 解压目录: {}", compressedFilePath, extractDir);
+                return extractDir.toString();
+            } else {
+                logger.error("解压文件到同名文件夹失败,压缩文件: {}", compressedFilePath);
+                return null;
+            }
+            
+        } catch (Exception e) {
+            logger.error("解压文件到同名文件夹异常,压缩文件: {}", compressedFilePath, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 解压ZIP文件
+     *
+     * @param zipFilePath ZIP文件路径
+     * @param destDir 目标目录
+     * @return 是否解压成功
+     */
+    private static boolean extractZipFile(String zipFilePath, String destDir) {
+        // 尝试多种编码方式解压ZIP文件
+        Charset[] charsets = {
+            Charset.forName("GBK"),     // 中文Windows系统常用编码
+            Charset.forName("GB2312"),  // 简体中文编码
+            Charset.forName("UTF-8"),   // 标准UTF-8编码
+            Charset.defaultCharset()    // 系统默认编码
+        };
+
+        for (Charset charset : charsets) {
+            try {
+                if (extractZipFileWithCharset(zipFilePath, destDir, charset)) {
+                    logger.info("成功解压ZIP文件: {} 到目录: {},使用编码: {}", zipFilePath, destDir, charset.name());
+                    return true;
+                }
+            } catch (Exception e) {
+                logger.debug("使用编码 {} 解压失败,尝试下一种编码: {}", charset.name(), e.getMessage());
+            }
+        }
+
+        logger.error("所有编码方式都无法解压ZIP文件: {}", zipFilePath);
+        return false;
+    }
+
+    /**
+     * 使用指定编码解压ZIP文件
+     *
+     * @param zipFilePath ZIP文件路径
+     * @param destDir 目标目录
+     * @param charset 字符编码
+     * @return 是否解压成功
+     */
+    private static boolean extractZipFileWithCharset(String zipFilePath, String destDir, Charset charset) throws IOException {
+        try (ZipFile zipFile = new ZipFile(zipFilePath, charset)) {
+            java.util.Enumeration<? extends ZipEntry> entries = zipFile.entries();
+
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
+                Path filePath = Paths.get(destDir, entry.getName());
+
+                // 安全检查:防止目录遍历攻击
+                if (!filePath.normalize().startsWith(Paths.get(destDir).normalize())) {
+                    logger.warn("检测到潜在的目录遍历攻击,跳过文件: {}", entry.getName());
+                    continue;
+                }
+
+                if (entry.isDirectory()) {
+                    Files.createDirectories(filePath);
+                } else {
+                    // 确保父目录存在
+                    Files.createDirectories(filePath.getParent());
+
+                    // 写入文件
+                    try (InputStream is = zipFile.getInputStream(entry);
+                         OutputStream os = Files.newOutputStream(filePath)) {
+                        byte[] buffer = new byte[8192];
+                        int length;
+                        while ((length = is.read(buffer)) > 0) {
+                            os.write(buffer, 0, length);
+                        }
+                    }
+                }
+            }
+
+            return true;
+        }
+    }
+    
+    /**
+     * 获取目录结构
+     *
+     * @param directory 目录路径
+     * @return 目录结构Map
+     */
+    public static Map<String, Object> getDirectoryStructure(Path directory) {
+        try {
+            if (!Files.exists(directory)) {
+                logger.warn("目录不存在: {}", directory);
+                return null;
+            }
+            
+            Map<String, Object> structure = new HashMap<>();
+            structure.put("name", directory.getFileName().toString());
+            structure.put("type", Files.isDirectory(directory) ? "directory" : "file");
+            structure.put("path", directory.toString());
+            
+            if (Files.isDirectory(directory)) {
+                List<Map<String, Object>> children = new ArrayList<>();
+                
+                try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
+                    for (Path entry : stream) {
+                        if (Files.isDirectory(entry)) {
+                            children.add(getDirectoryStructure(entry));
+                        } else {
+                            children.add(getFileStructure(entry));
+                        }
+                    }
+                }
+                
+                structure.put("children", children);
+            } else {
+                // 如果是文件,添加文件大小
+                try {
+                    structure.put("size", Files.size(directory));
+                } catch (IOException e) {
+                    structure.put("size", 0);
+                }
+            }
+            
+            return structure;
+            
+        } catch (IOException e) {
+            logger.error("获取目录结构异常,目录: {}", directory, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 获取文件结构
+     *
+     * @param file 文件路径
+     * @return 文件结构Map
+     */
+    public static Map<String, Object> getFileStructure(Path file) {
+        Map<String, Object> structure = new HashMap<>();
+        structure.put("name", file.getFileName().toString());
+        structure.put("type", "file");
+        structure.put("path", file.toString());
+        
+        try {
+            structure.put("size", Files.size(file));
+        } catch (IOException e) {
+            logger.warn("获取文件大小失败: {}", file, e);
+            structure.put("size", 0);
+        }
+        
+        return structure;
+    }
+}

+ 71 - 0
siwei-modules/siwei-apply/src/main/resources/mapper/NodeAttachmentMapper.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.siwei.apply.mapper.NodeAttachmentMapper">
+
+    <!-- 结果映射 -->
+    <resultMap id="BaseResultMap" type="com.siwei.apply.domain.NodeAttachment">
+        <id property="id" column="id"/>
+        <result property="nodeId" column="node_id"/>
+        <result property="attachment" column="attachment" typeHandler="com.siwei.apply.handler.JsonbTypeHandler"/>
+    </resultMap>
+
+    <!-- 基础查询字段 -->
+    <sql id="Base_Column_List">
+        id, node_id, attachment
+    </sql>
+
+    <!-- 根据ID查询附件信息 -->
+    <select id="selectById" resultMap="BaseResultMap" parameterType="String">
+        SELECT
+        <include refid="Base_Column_List"/>
+        FROM t_node_attachment
+        WHERE id = #{id}
+    </select>
+
+    <!-- 根据节点ID查询附件信息 -->
+    <select id="selectByNodeId" resultMap="BaseResultMap" parameterType="String">
+        SELECT
+        <include refid="Base_Column_List"/>
+        FROM t_node_attachment
+        WHERE node_id = #{nodeId}
+    </select>
+
+    <!-- 插入附件记录 -->
+    <insert id="insert" parameterType="com.siwei.apply.domain.NodeAttachment">
+        INSERT INTO t_node_attachment (
+            id,
+            node_id,
+            attachment
+        ) VALUES (
+            #{id},
+            #{nodeId},
+            #{attachment, typeHandler=com.siwei.apply.handler.JsonbTypeHandler}
+        )
+    </insert>
+
+    <!-- 根据节点ID更新附件记录 -->
+    <update id="update" parameterType="com.siwei.apply.domain.NodeAttachment">
+        UPDATE t_node_attachment
+        SET attachment = #{attachment, typeHandler=com.siwei.apply.handler.JsonbTypeHandler}
+        WHERE node_id = #{nodeId}
+    </update>
+
+    <!-- 根据ID更新附件记录 -->
+    <update id="updateById" parameterType="com.siwei.apply.domain.NodeAttachment">
+        UPDATE t_node_attachment
+        <set>
+            <if test="nodeId != null">node_id = #{nodeId},</if>
+            <if test="attachment != null">attachment = #{attachment, typeHandler=com.siwei.apply.handler.JsonbTypeHandler}</if>
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <!-- 根据节点ID删除附件记录 -->
+    <delete id="deleteByNodeId" parameterType="String">
+        DELETE FROM t_node_attachment WHERE node_id = #{nodeId}
+    </delete>
+
+</mapper>