Skip to content

SpringBoot中通用附件的设计

5985字约20分钟

SpringBoot

2025-02-19

该附件设计可以做到业务附件不依赖于业务的数据库,将业务数据库和附件信息隔离开,可以在每个需要保存附件的业务上打上注解,对注解的业务方法进行拦截,用以保存附件信息

缺点:

  1. 业务数据存储和附件存储是分离开的,还无法联动回滚。
  2. controller层放在的common中有点不符合开发规范。(规矩是死的,但是人是活的)

1. 数据库设计

DROP TABLE IF EXISTS sys_attachment;
CREATE TABLE sys_attachment (
    id                     VARCHAR(64)         NOT NULL                   COMMENT '主键',
    file_name              VARCHAR(100)        NOT NULL                   COMMENT '附件名称',
    origin_file_name       VARCHAR(50)         NOT NULL                   COMMENT '源文件名称',
    file_suffix            VARCHAR(10)         NOT NULL                   COMMENT '文件扩展名 不带.',
    file_size_kb           INT                 DEFAULT NULL               COMMENT '文件所占存储的大小,单位KB',
    fileUrl                VARCHAR(100)        NOT NULL                   COMMENT '存储的文件路径',
    file_type              VARCHAR(40)         DEFAULT NULL               COMMENT '文件分类',
    link_id                VARCHAR(64)         NOT NULL                   COMMENT '关联业务id',
    delete_flag            BIT(1)              NOT NULL DEFAULT b'0'      COMMENT '是否删除(0:未删除 1:删除)',
    create_by              VARCHAR(64)         DEFAULT NULL               COMMENT '创建者',
    create_time 	       DATETIME            DEFAULT NULL               COMMENT '创建时间',
    update_user            VARCHAR(64)         DEFAULT NULL               COMMENT '更新者',
    update_time            DATETIME            DEFAULT NULL               COMMENT '更新时间',
    PRIMARY KEY (id)
) COMMENT = '附件信息';

2. 后端设计

目录结构如下:

image-20250219180351819
image-20250219180351819

2.1 @Attachment用以放在需要附件的业务方法上

package com.ruoyi.file.annotation;

import org.springframework.stereotype.Component;
import java.lang.annotation.*;

/**
 * 附件注解,用于设置是否保存附件,用于service方法上
 * @author congpeitong
 * @date 2024-12-04 18:36:25
 */
@Inherited
@Documented
@Component
@Target(ElementType.METHOD) // 用在方法上
@Retention(RetentionPolicy.RUNTIME) // 保留至运行期
public @interface Attachment {

    // 保存:save / 编辑:update 类型区分  新建时 附件为空则不保存,编辑时附件为空则判断上一次是否有附件,有则删除,无则不处理
    String value();

    // 指定 id 字段名 默认为 id
    String idField() default "id" ;

    // 指定文件字段 字段名 默认为 files
    String fileField() default "files" ;

    // 包含附件数组的参数名称,如果不提供,则以第一个参数为准
    String filesParamName() default "";

}

2.2 @EnableAttachment 用于开启附件保存,通常用于SpringBoot的启动类上

package com.ruoyi.file.annotation;

import com.ruoyi.file.aop.AttachmentAop;
import com.ruoyi.file.config.SysAttachmentConfig;
import com.ruoyi.file.controller.SysAttachmentController;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

/**
 * 开启附件服务 注解
 * @author congpeitong
 * @date 2024-12-04 15:36:39
 */
@Documented
@Inherited
@Target(ElementType.TYPE) // 用在类上
@Retention(RetentionPolicy.RUNTIME) // 保留至运行期
@Import({SysAttachmentConfig.class, SysAttachmentController.class, AttachmentAop.class})
public @interface EnableAttachment {
}

2.3 附件保存拦截

package com.ruoyi.file.aop;

import cn.hutool.core.util.ReflectUtil;
import com.ruoyi.file.annotation.Attachment;
import com.ruoyi.file.domain.SysAttachment;
import com.ruoyi.file.service.ISysAttachmentService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Author congpeitong
 * @Date 2024/4/19 14:51
 */
@Aspect
@Slf4j
@Component
public class AttachmentAop {

	@Autowired
	private ISysAttachmentService sysAttachmentService;

	/**
	 * 注解@Attachment方法的拦截
	 * @author congpeitong
	 * @date 2024-12-05 08:30:07
	 * @param point 切入点 及执行的方法(注解标注的方法)
 	 * @param attachment attachment 注解参数
	 * @return java.lang.Object
	 */
    @Around("@annotation(attachment)")
    public Object around(ProceedingJoinPoint point, Attachment attachment) throws Throwable {
		// 执行目标方法(即@Attachment注解标注的方法)
        Object result = point.proceed();

		// 获取参数列表
        Object[] args = point.getArgs();
		//没有参数即也没有附件,直接跳出
		if(args.length==0){
			return result;
		}
		try {
			Object includeFilesParam = null;
			//如果注解未设置方法中包含附件列表的参数名,则取第一个参数
			String filesParamName = attachment.filesParamName();
			if (filesParamName == null || filesParamName.isEmpty()) {
				includeFilesParam = args[0];
			}

			//设置过参数名
			if (includeFilesParam == null && filesParamName != null && !filesParamName.isEmpty()) {
				// 参数名列表   获取目标方法上的注解 MethodSignature methodSignature = (MethodSignature) point.getSignature();
				List<String> argNames = Arrays.asList(((MethodSignature) point.getSignature()).getParameterNames());
				// argNames  args 一一对应
				includeFilesParam = args[argNames.indexOf(filesParamName)];
			}
			//未取到参数对象
			if (includeFilesParam == null) {
				return result;
			}

			Object fieldValueTmp = null;//临时对象
			String bizId = "";//业务id
			List<SysAttachment> files = new ArrayList<>();

			fieldValueTmp = ReflectUtil.getFieldValue(includeFilesParam, attachment.idField());
			if (fieldValueTmp == null) {
				throw new RuntimeException("附件关联Id无效");
			}
			bizId = fieldValueTmp.toString();

			if (!StringUtils.hasLength(bizId)) {
				throw new IllegalArgumentException("参数错误,业务id未找到");
			}

			//文件列表整理
			fieldValueTmp = ReflectUtil.getFieldValue(includeFilesParam, attachment.fileField());
			if (fieldValueTmp == null) {
				return result; //文件列表属性不存在,不予保存
			}
			if (!(fieldValueTmp instanceof List)) {
				throw new RuntimeException("附件列表格式无效");
			}
			List<List<?>> tempFileList = (List<List<?>>) fieldValueTmp;
			for (List<?> fileList : tempFileList) {
				if(fileList!=null) {
					List<SysAttachment> filesTmp = (List<SysAttachment>) fileList;
					files.addAll(filesTmp);
				}
			}

			//保存附件
			if (("save".equals(attachment.value()) && files.size() != 0) || "update".equals(attachment.value())) {
				sysAttachmentService.saveOrUpdateAttachment(bizId,files);
			}
		} catch (Exception e) {
			// 捕获其他潜在异常,进行相应处理。
			e.printStackTrace();
			throw e;
		}
        return result;
    }
}

2.4 附件配置类

package com.ruoyi.file.config;

import com.ruoyi.file.service.ISysAttachmentService;
import com.ruoyi.file.service.impl.SysAttachmentServiceImpl;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 附件配置类,配置 附件的保存/修改和查询
 * @author congpeitong
 * @date 2024-12-04 15:30:48
 */
@Configuration
@MapperScan(basePackages = "com.ruoyi.file.mapper") // 用于可扫描到附件的mapper文件
public class SysAttachmentConfig {

    /**
     * 注入附件服务
     * @author congpeitong
     * @date 2024-12-04 17:42:43
     */
    @Bean
    public ISysAttachmentService sysAttachmentService() {
        return new SysAttachmentServiceImpl();
    }
}

2.5 controller

package com.ruoyi.file.controller;

import com.ruoyi.common.core.domain.R;
import com.ruoyi.file.pojo.AttachmentVo;
import com.ruoyi.file.pojo.QueryAttachmentQo;
import com.ruoyi.file.service.ISysAttachmentService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 附件服务控制器
 * @author congpeitong
 * @date 2024-12-04 15:35:10
 */
@RestController
@RequestMapping("/attachment")
public class SysAttachmentController {

    private ISysAttachmentService iSysAttachmentService;
    public SysAttachmentController(ISysAttachmentService iSysAttachmentService) {
        this.iSysAttachmentService = iSysAttachmentService;
    }

    /**
     * 查询附件信息列表
     */
    @GetMapping("/queryAttachmentList")
    public R<List<AttachmentVo>> queryAttachmentList(QueryAttachmentQo attachmentQo)
    {
        String linkId = attachmentQo.getLinkId();
        if (!StringUtils.hasText(linkId)) {
            throw new IllegalArgumentException("未获取到业务,无法查询");
        }
        List<AttachmentVo> list = iSysAttachmentService.queryAttachmentList(attachmentQo);
        return R.ok(list);
    }
}

2.6实体类

package com.ruoyi.file.domain;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.common.core.web.domain.BaseEntity;

/**
 * 附件信息对象 sys_attachment
 *
 * @author congpeitong
 * @date 2024-12-04
 */
public class SysAttachment extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 主键 */
    private String id;

    /** 附件名称 */
    private String fileName;

    /** 源文件名称 */
    private String originFileName;

    /** 文件扩展名 不带. */
    private String fileSuffix;

    /** 文件所占存储的大小,单位KB */
    private Long fileSizeKb;

    /** 存储的文件路径 */
    private String fileUrl;

    /** 文件分类 */
    private String fileType;

    /** 关联业务id */
    private String linkId;

    /** 是否删除(0:未删除 1:删除) */
    private String deleteFlag;

    /** 更新者 */
    private String updateUser;

    public void setId(String id)
    {
        this.id = id;
    }

    public String getId()
    {
        return id;
    }
    public void setFileName(String fileName)
    {
        this.fileName = fileName;
    }

    public String getFileName()
    {
        return fileName;
    }
    public void setOriginFileName(String originFileName)
    {
        this.originFileName = originFileName;
    }

    public String getOriginFileName()
    {
        return originFileName;
    }
    public void setFileSuffix(String fileSuffix)
    {
        this.fileSuffix = fileSuffix;
    }

    public String getFileSuffix()
    {
        return fileSuffix;
    }
    public void setFileSizeKb(Long fileSizeKb)
    {
        this.fileSizeKb = fileSizeKb;
    }

    public Long getFileSizeKb()
    {
        return fileSizeKb;
    }
    public void setFileUrl(String fileUrl)
    {
        this.fileUrl = fileUrl;
    }

    public String getFileUrl()
    {
        return fileUrl;
    }
    public void setFileType(String fileType)
    {
        this.fileType = fileType;
    }

    public String getFileType()
    {
        return fileType;
    }
    public void setLinkId(String linkId)
    {
        this.linkId = linkId;
    }

    public String getLinkId()
    {
        return linkId;
    }
    public void setDeleteFlag(String deleteFlag)
    {
        this.deleteFlag = deleteFlag;
    }

    public String getDeleteFlag()
    {
        return deleteFlag;
    }
    public void setUpdateUser(String updateUser)
    {
        this.updateUser = updateUser;
    }

    public String getUpdateUser()
    {
        return updateUser;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
                .append("id", getId())
                .append("fileName", getFileName())
                .append("originFileName", getOriginFileName())
                .append("fileSuffix", getFileSuffix())
                .append("fileSizeKb", getFileSizeKb())
                .append("fileUrl", getFileUrl())
                .append("fileType", getFileType())
                .append("linkId", getLinkId())
                .append("deleteFlag", getDeleteFlag())
                .append("createBy", getCreateBy())
                .append("createTime", getCreateTime())
                .append("updateUser", getUpdateUser())
                .append("updateTime", getUpdateTime())
                .toString();
    }
}

2.7 Mapper.java

package com.ruoyi.file.mapper;

import java.util.List;
import com.ruoyi.file.domain.SysAttachment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 附件信息Mapper接口
 *
 * @author congpeitong
 * @date 2024-12-04
 */
@Mapper
public interface SysAttachmentMapper
{
    /**
     * 查询附件信息
     *
     * @param id 附件信息主键
     * @return 附件信息
     */
    public SysAttachment selectSysAttachmentById(String id);

    /**
     * 查询附件信息列表
     *
     * @param sysAttachment 附件信息
     * @return 附件信息集合
     */
    public List<SysAttachment> selectSysAttachmentList(SysAttachment sysAttachment);

    /**
     * 新增附件信息
     *
     * @param entities 附件信息
     * @return 结果
     */
    public int insertSysAttachment(@Param("entities") List<SysAttachment> entities);


    /**
     * 删除附件信息
     *
     * @param id 附件信息主键
     * @return 结果
     */
    public int deleteSysAttachmentById(String id);

    /**
     * 根据业务id查询附件信息
     * @author congpeitong
     * @date 2024-12-09 14:38:42
     * @param linkId 业务id
     * @return int
     */
    int deleteSysAttachmentByLinkId(String linkId);

    /**
     * 批量删除附件信息
     *
     * @param ids 需要删除的数据主键集合
     * @return 结果
     */
    public int deleteSysAttachmentByIds(@Param("ids") String[] ids);
}

2.8 mapper文件

<?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.ruoyi.file.mapper.SysAttachmentMapper">

    <resultMap type="com.ruoyi.file.domain.SysAttachment" id="SysAttachmentResult">
        <result property="id"    column="id"    />
        <result property="fileName"    column="file_name"    />
        <result property="originFileName"    column="origin_file_name"    />
        <result property="fileSuffix"    column="file_suffix"    />
        <result property="fileSizeKb"    column="file_size_kb"    />
        <result property="fileUrl"    column="fileUrl"    />
        <result property="fileType"    column="file_type"    />
        <result property="linkId"    column="link_id"    />
        <result property="deleteFlag"    column="delete_flag"    />
        <result property="createBy"    column="create_by"    />
        <result property="createTime"    column="create_time"    />
        <result property="updateUser"    column="update_user"    />
        <result property="updateTime"    column="update_time"    />
    </resultMap>

    <sql id="selectSysAttachmentVo">
        select id, file_name, origin_file_name, file_suffix, file_size_kb, fileUrl, file_type, link_id, delete_flag, create_by, create_time, update_user, update_time from sys_attachment
    </sql>

    <select id="selectSysAttachmentList" parameterType="com.ruoyi.file.domain.SysAttachment" resultMap="SysAttachmentResult">
        <include refid="selectSysAttachmentVo"/>
        <where>
            <if test="fileName != null  and fileName != ''"> and file_name like concat('%', #{fileName}, '%')</if>
            <if test="originFileName != null  and originFileName != ''"> and origin_file_name like concat('%', #{originFileName}, '%')</if>
            <if test="fileSuffix != null  and fileSuffix != ''"> and file_suffix = #{fileSuffix}</if>
            <if test="fileSizeKb != null "> and file_size_kb = #{fileSizeKb}</if>
            <if test="fileUrl != null  and fileUrl != ''"> and fileUrl = #{fileUrl}</if>
            <if test="fileType != null  and fileType != ''"> and file_type = #{fileType}</if>
            <if test="linkId != null  and linkId != ''"> and link_id = #{linkId}</if>
            <if test="deleteFlag != null  and deleteFlag != ''"> and delete_flag = #{deleteFlag}</if>
            <if test="updateUser != null  and updateUser != ''"> and update_user = #{updateUser}</if>
        </where>
        order by create_time desc
    </select>

    <select id="selectSysAttachmentById" parameterType="String" resultMap="SysAttachmentResult">
        <include refid="selectSysAttachmentVo"/>
        where id = #{id}
    </select>

    <insert id="insertSysAttachment">
        insert into sys_attachment (id,file_name,origin_file_name,file_suffix,file_size_kb,fileUrl,file_type
                                   ,link_id,create_by,create_time)
        values
        <foreach collection="entities" item="entity" separator=",">
            (#{entity.id},#{entity.fileName},#{entity.originFileName},#{entity.fileSuffix},#{entity.fileSizeKb},
                #{entity.fileUrl},#{entity.fileType},#{entity.linkId},#{entity.createBy},
                #{entity.createTime})
        </foreach>
    </insert>

    <delete id="deleteSysAttachmentById" parameterType="String">
        delete from sys_attachment where id = #{id}
    </delete>

    <delete id="deleteSysAttachmentByIds" parameterType="String">
        delete from sys_attachment where id in
        <foreach item="id" collection="ids" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    <delete id="deleteSysAttachmentByLinkId" parameterType="String">
        delete from sys_attachment where link_id = #{linkId}
    </delete>
</mapper>

2.9 pojo 总共由三个类 这儿有点不规范,后续再改吧

package com.ruoyi.file.pojo;

import com.ruoyi.common.core.annotation.Excel;

public class AttachmentDto {

    /** 主键 */
    private String id;

    /** 附件名称 */
    @Excel(name = "附件名称")
    private String fileName;

    /** 源文件名称 */
    @Excel(name = "源文件名称")
    private String originFileName;

    /** 文件扩展名 不带. */
    @Excel(name = "文件扩展名 不带.")
    private String fileSuffix;

    /** 文件所占存储的大小,单位KB */
    @Excel(name = "文件所占存储的大小,单位KB")
    private Long fileSizeKb;

    /** 存储的文件路径 */
    @Excel(name = "存储的文件路径")
    private String fileUrl;

    /** 文件分类 */
    @Excel(name = "文件分类")
    private String fileType;

    /** 关联业务id */
    @Excel(name = "关联业务id")
    private String linkId;
}


package com.ruoyi.file.pojo;

import com.ruoyi.common.core.annotation.Excel;

/**
 * 附件vo
 * @author congpeitong
 * @date 2024-12-04 17:20:03
 */
public class AttachmentVo {

    /** 主键 */
    private String id;

    /** 附件名称 */
    @Excel(name = "附件名称")
    private String fileName;

    /** 源文件名称 */
    @Excel(name = "源文件名称")
    private String originFileName;

    /** 文件扩展名 不带. */
    @Excel(name = "文件扩展名 不带.")
    private String fileSuffix;

    /** 文件所占存储的大小,单位KB */
    @Excel(name = "文件所占存储的大小,单位KB")
    private Long fileSizeKb;

    /** 存储的文件路径 */
    @Excel(name = "存储的文件路径")
    private String fileUrl;

    /** 文件分类 */
    @Excel(name = "文件分类")
    private String fileType;

    /** 关联业务id */
    @Excel(name = "关联业务id")
    private String linkId;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getOriginFileName() {
        return originFileName;
    }

    public void setOriginFileName(String originFileName) {
        this.originFileName = originFileName;
    }

    public String getFileSuffix() {
        return fileSuffix;
    }

    public void setFileSuffix(String fileSuffix) {
        this.fileSuffix = fileSuffix;
    }

    public Long getFileSizeKb() {
        return fileSizeKb;
    }

    public void setFileSizeKb(Long fileSizeKb) {
        this.fileSizeKb = fileSizeKb;
    }

    public String getFileUrl() {
        return fileUrl;
    }

    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    public String getLinkId() {
        return linkId;
    }

    public void setLinkId(String linkId) {
        this.linkId = linkId;
    }

    @Override
    public String toString() {
        return "AttachmentVo{" +
                "id='" + id + '\'' +
                ", fileName='" + fileName + '\'' +
                ", originFileName='" + originFileName + '\'' +
                ", fileSuffix='" + fileSuffix + '\'' +
                ", fileSizeKb=" + fileSizeKb +
                ", fileUrl='" + fileUrl + '\'' +
                ", fileType='" + fileType + '\'' +
                ", linkId='" + linkId + '\'' +
                '}';
    }
}



package com.ruoyi.file.pojo;

import java.util.List;

/**
 * 附件查询对象
 * @author congpeitong
 * @date 2024-12-04 17:15:43
 */
public class QueryAttachmentQo {

    // 关联的业务id
    private String linkId;
    // 文件类型
    private String fileType;

    public String getLinkId() {
        return linkId;
    }

    public void setLinkId(String linkId) {
        this.linkId = linkId;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }


}

2.10 service

package com.ruoyi.file.service;

import java.util.List;
import com.ruoyi.file.domain.SysAttachment;
import com.ruoyi.file.pojo.AttachmentVo;
import com.ruoyi.file.pojo.QueryAttachmentQo;

/**
 * 附件信息Service接口
 *
 * @author congpeitong
 * @date 2024-12-04
 */
public interface ISysAttachmentService {
    /**
     * 查询附件信息
     *
     * @param id 附件信息主键
     * @return 附件信息
     */
    public SysAttachment selectSysAttachmentById(String id);

    /**
     * 查询附件信息列表
     *
     * @param sysAttachment 附件信息
     * @return 附件信息集合
     */
    public List<SysAttachment> selectSysAttachmentList(SysAttachment sysAttachment);

    /**
     * 批量删除附件信息
     *
     * @param ids 需要删除的附件信息主键集合
     * @return 结果
     */
    public int deleteSysAttachmentByIds(String[] ids);

    /**
     * 删除附件信息信息
     *
     * @param id 附件信息主键
     * @return 结果
     */
    public int deleteSysAttachmentById(String id);

    /**
     * 查询附件列表
     * @author congpeitong
     * @date 2024-12-04 17:24:59
     */
    List<AttachmentVo> queryAttachmentList(QueryAttachmentQo attachmentQo);

    /**
     * 保存或更新附件信息
     * @author congpeitong
     * @date 2024-12-04 18:50:08
     * @param attachments 附件列表
     * @return void
     */
    void saveOrUpdateAttachment(String bizId, List<SysAttachment> attachments);
}



package com.ruoyi.file.service.impl;

import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.uuid.IdUtils;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.file.domain.SysAttachment;
import com.ruoyi.file.mapper.SysAttachmentMapper;
import com.ruoyi.file.pojo.AttachmentVo;
import com.ruoyi.file.pojo.QueryAttachmentQo;
import com.ruoyi.file.service.ISysAttachmentService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 附件信息Service业务层处理
 *
 * @author congpeitong
 * @date 2024-12-04
 */
@Service
public class SysAttachmentServiceImpl implements ISysAttachmentService
{
    @Autowired
    private SysAttachmentMapper sysAttachmentMapper;

    /**
     * 查询附件信息
     *
     * @param id 附件信息主键
     * @return 附件信息
     */
    @Override
    public SysAttachment selectSysAttachmentById(String id)
    {
        return sysAttachmentMapper.selectSysAttachmentById(id);
    }

    /**
     * 查询附件信息列表
     *
     * @param sysAttachment 附件信息
     * @return 附件信息
     */
    @Override
    public List<SysAttachment> selectSysAttachmentList(SysAttachment sysAttachment)
    {
        return sysAttachmentMapper.selectSysAttachmentList(sysAttachment);
    }

    /**
     * 批量删除附件信息
     *
     * @param ids 需要删除的附件信息主键
     * @return 结果
     */
    @Override
    public int deleteSysAttachmentByIds(String[] ids)
    {
        return sysAttachmentMapper.deleteSysAttachmentByIds(ids);
    }

    /**
     * 删除附件信息信息
     *
     * @param id 附件信息主键
     * @return 结果
     */
    @Override
    public int deleteSysAttachmentById(String id)
    {
        return sysAttachmentMapper.deleteSysAttachmentById(id);
    }

    /**
     * 查询附件列表 linkId:业务id, fileType:业务类型
     * @author congpeitong
     * @date 2024-12-04 17:25:21
     */
    @Override
    public List<AttachmentVo> queryAttachmentList(QueryAttachmentQo attachmentQo) {
        List<AttachmentVo> resultList = new ArrayList<>();
        SysAttachment sysAttachment = new SysAttachment();
        BeanUtils.copyProperties(attachmentQo, sysAttachment);
        sysAttachment.setDeleteFlag("0");
        List<SysAttachment> sysAttachments = sysAttachmentMapper.selectSysAttachmentList(sysAttachment);

        for (SysAttachment attachment : sysAttachments) {
            AttachmentVo attachmentVo = new AttachmentVo();
            BeanUtils.copyProperties(attachment, attachmentVo);
            resultList.add(attachmentVo);
        }
        return resultList;
    }

    /**
     * 保存附件信息,由于不存在更新所以只进行保存和删除操作
     * @author congpeitong
     * @date 2024-12-04 18:50:36
     * @param bizId 业务id
     * @param attachments 附件信息列表
     */
    @Override
    @Transactional
    public void saveOrUpdateAttachment(String bizId, List<SysAttachment> attachments) {
        if (!StringUtils.hasText(bizId)) {
            throw new IllegalArgumentException("业务id为空,附件未成功保存");
        }
        // 附件列表为空,则根据业务id删除所有的该业务相关的附件
        if (CollectionUtils.isEmpty(attachments)) {
            this.sysAttachmentMapper.deleteSysAttachmentByLinkId(bizId);
            return;
        }
        /* 当保存的附件不为空时,查询旧附件信息,保存的附件信息和旧附件信息作对比 */
        // 查询已存在的附件
        SysAttachment exitFileQuery = new SysAttachment();
        exitFileQuery.setLinkId(bizId);
        List<SysAttachment> exitFiles = this.sysAttachmentMapper.selectSysAttachmentList(exitFileQuery);
        // 数据库中不存在附件直接批量新增
        if (CollectionUtils.isEmpty(exitFiles)) {
            List<SysAttachment> saveList = attachments.stream()
                    .peek(attachment -> setAttachmentInfo(bizId, attachment))
                    .collect(Collectors.toList());
            this.sysAttachmentMapper.insertSysAttachment(saveList);
            return;
        }
        // 对比 接口传过来的附件数据和数据库中存在的数据,该新增新增,该删除删除
        processAttachments(bizId, attachments, exitFiles);
    }

    /**
     * 比对新旧数据保存至数据库,由于只有新增和删除,因此对比比较简单
     * 先根据 附件url对比出 共有的数据
     *      接口中过来的数据中去除共有的数据即为新增的数据
     *      数据库中已存在的数据去除共有数据即为删除的数据
     * @author congpeitong
     * @date 2024-12-05 10:51:12
     * @param bizId 业务id
     * @param newAttachments 接口传过来的数据
     * @param existingAttachments 数据库中已经存在的数据
     */
    private void processAttachments(String bizId,List<SysAttachment> newAttachments, List<SysAttachment> existingAttachments) {
        // 1. 查找共同部分
        Set<String> fileUrls = new HashSet<>();
        for (SysAttachment newAttachment : newAttachments) {
            fileUrls.add(newAttachment.getFileUrl());
        }
        // 共同部分
        List<SysAttachment> commonAttachments = existingAttachments.stream()
                .filter(existingAttachment -> fileUrls.contains(existingAttachment.getFileUrl()))
                .collect(Collectors.toList());
        List<SysAttachment> saveList;
        List<String> deleteIds;
        //  如果有共同部分,接口数据中除去共同部分的数据为新增的数据,数据库中存在的数据除去共同部分为删除的数据
        if (!CollectionUtils.isEmpty(commonAttachments)) {
            List<String> commonUrls = commonAttachments.stream()
                    .map(SysAttachment::getFileUrl)
                    .collect(Collectors.toList());
            saveList = newAttachments.stream()
                    .filter(attachment -> !commonUrls.contains(attachment.getFileUrl()))
                    .peek(attachment -> setAttachmentInfo(bizId, attachment))
                    .collect(Collectors.toList());

            deleteIds = existingAttachments.stream()
                    .filter(attachment -> !commonUrls.contains(attachment.getFileUrl()))
                    .map(SysAttachment::getId)
                    .collect(Collectors.toList());
        } else { // 如果无共同部分,则接口传过来的数据全部新增,数据库中存在的数据全部删除
            saveList = newAttachments.stream()
                    .peek(attachment -> setAttachmentInfo(bizId, attachment))
                    .collect(Collectors.toList());
            deleteIds = existingAttachments.stream().map(SysAttachment::getId).collect(Collectors.toList());
        }

        // 操作数据库数据
        if (!CollectionUtils.isEmpty(saveList)) {
            this.sysAttachmentMapper.insertSysAttachment(saveList);
        }
        if (!CollectionUtils.isEmpty(deleteIds)) {
            this.sysAttachmentMapper.deleteSysAttachmentByIds(deleteIds.toArray(new String[0]));
        }
    }

    /**
     * 设置附件信息
     * @author congpeitong
     * @date 2024-12-05 09:22:13
     * @param linkId 业务id
     * @param sysAttachment 附件信息
     */
    private void setAttachmentInfo(String linkId, SysAttachment sysAttachment) {
        if (sysAttachment == null || !StringUtils.hasText(linkId)) return;
        // 设置id
        sysAttachment.setId(IdUtils.simpleUUID());
        // 设置业务id
        sysAttachment.setLinkId(linkId);
        String fileName = sysAttachment.getFileName();
        if (!StringUtils.hasText(fileName)) return;
        /* 文件保存后的文件名命名规则为 原文件名_一串日期时间编码.扩展名  例如 附件_202412050950.png  即原文件名为 附件.png */
        // 原文件名称,不带扩展名的
        String originFileName;
        // 文件扩展名 不带 . 的
        String fileExtName;
        int _index = fileName.indexOf("_");
        int dotIndex = fileName.lastIndexOf(".");
        if (_index == -1 || dotIndex == -1) return;
        fileExtName = fileName.substring(dotIndex + 1);
        // 原文件名如果前端传值了按照前端传值的保存,没传值则截取上传后的文件名第一个_前面的然后拼接扩展名
        if (!StringUtils.hasText(sysAttachment.getOriginFileName())){
            originFileName = fileName.substring(0, _index);
            sysAttachment.setOriginFileName(originFileName + "." + fileExtName);
        }
        sysAttachment.setFileSuffix(fileExtName);
        // 设置创建时间
        sysAttachment.setCreateTime(DateUtils.getNowDate());
        // 设置创建人
        sysAttachment.setCreateBy(SecurityUtils.getUsername());
    }
}

3. 前端

前端使用的是vue2做了一个组件


<!--文件上传/查看组件-->
<template>
  <div class="upload-file">
    <el-upload
        ref="fileUpload"
        :action="uploadFileUrl"
        :before-upload="handleBeforeUpload"
        :file-list="fileList"
        :headers="headers"
        :on-exceed="handleExceed"
        :on-error="handleUploadError"
        :on-remove="handleRemove"
        :on-preview="handlePreview"
        :auto-upload="autoUpload"
        :on-progress="handleProgress"
        :on-success="handleUploadSuccess"
        class="myupload"
        multiple
        :limit="limit"
        list-type="picture-card"
        :disabled="isDetail"
    >
      <i slot="default" class="el-icon-plus" v-if="!isDetail && !isScreenshot"></i>
      <div slot="default" class="screenshot" v-if="!isDetail && isScreenshot">
        <div class="s-btn"><el-button type="primary" style="padding:5px 10px;">上传文件</el-button></div>
        <div class="s-btn" @click.stop="screenshotUpload"><el-button type="primary" style="padding:5px 10px;">上传截图</el-button></div>
      </div>

      <div slot="file" slot-scope="{file}" style="text-align: center;">
        <img class="el-upload-list__item-thumbnail" :src="file.fileUrl"/>
        <div class="filetext el-upload-list__item-file-name">{{file.originFileName}}</div>
        <div class="el-upload-list__item-actions"
             v-if="['png','jpg','jpeg','gif','PNG','JPG','JPEG','GIF','mp4','MP4'].indexOf(file.fileSuffix) != -1" >
          <span class="el-upload-list__item-preview" @click="preview(file)">
                <i class="el-icon-zoom-in"></i>
          </span>
          <span @click="handlePreview(file)">
                 <i class="el-icon-download"></i>
			    </span>
          <span class="el-upload-list__item-delete" @click="remove(file)" v-if="!isDetail">
                <i class="el-icon-delete"></i>
          </span>
        </div>
        <div class="el-upload-list__item-actions" v-else>
          <span @click="handlePreview(file)">
				         <i class="el-icon-download"></i>
			    </span>
          <span class="el-upload-list__item-delete" @click="remove(file)" v-if="!isDetail">
				         <i class="el-icon-delete"></i>
          </span>
        </div>
      </div>
      <div slot="tip" v-if="!isDetail">
        <div v-if="isShowTip" class="divline">
          <slot></slot>
          <template v-if="fileSize">
            大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b></template>
          <template v-if="fileType">
            格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>的文件
          </template>
          <slot name="afterbody-tip"></slot>
        </div>
      </div>
    </el-upload>
    <!-- 图片预览-->
    <el-dialog :visible.sync="dialogVisible" width="50%" append-to-body>
      <el-image :src="dialogImageUrl" :zoon-rate="1.2" :max-scale="7" :preview-src-list="dialogImageUrlList" style="width:100%;"></el-image>
    </el-dialog>
    <!-- 视频预览-->
    <el-dialog :visible.sync="videoDialogVisible" width="50%" append-to-body>
      <video :src="dialogImageUrl" controls  style="width:100%;height:600px;" autoPlay></video>
    </el-dialog>
    <!-- 粘贴截图-->
    <el-dialog :visible.sync="screenshotDialogVisible" width="50%" title="上传截图" destroy-on-close :before-close="pasteBeforeClose" append-to-body>
      <div v-if="!pasteimgSrc">
        <textarea ref="pasteRef" rows="30" class="area" placeholder="粘贴截图" @paste="pasteInput"></textarea>
      </div>
      <div v-if="pasteimgSrc">
        <el-image :src="pasteimgSrc" :zoon-rate="1.2" :max-scale="7" :preview-src-list="pasteimgSrcList" ></el-image>
      </div>
      <div slot="footer">
        <div class="dialog-footer">
          <el-input v-model="screenshotName" placeholder="输入截图名称" style="width: 240px;margin-right:20px;"></el-input>
          <el-button type="primary" @click="handleHttpUpload(screenshotFile)">
            上传
          </el-button>
          <el-button type="primary" @click="cancelImage">清除截图</el-button>
          <el-button @click="cancel">取消</el-button>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { getToken } from "@/utils/auth";
import { Loading } from 'element-ui';
import request from "@/utils/request";
import {getListByReview,downBlobFile} from "@/api/material/attachment";
export default {
  name: 'upload-file',
  props: {
    // 值
    value: [Array],
    // 数量限制
    limit: {
      type: Number,
      default: 9,
    },
    // 大小限制(MB)
    fileSize: {
      type: Number,
      default: 20,
    },
    fileType: {
      type: Array,
      default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx', 'mp4','zip','rar'],
    },
    // 是否显示提示
    isShowTip: {
      type: Boolean,
      default: true,
    },
    uploadFileUrl: {
      type: String,
      default: process.env.VUE_APP_BASE_API + "/file/upload", // 此处自己的微服务有自己的API上下文/服务路由(process.env.VUE_APP_BASE_API)
    },
    autoUpload: {
      type: Boolean,
      default: true,
    },
    attachmentData:{
      type: Object,
      default: () => {},
    },//外面传过来的附件详情的请求参数
    linkId:{ // 业务id
      type: String,
      default: '',
    },
    isDetail:{
      type: Boolean,
      default: false,
    }, //外面页面如果是查看详情传true,
    isScreenshot: {
      type: Boolean,
      default: false,
    }, //是否显示截图上传按钮
  },
  data() {
    return {
      number: 0,
      uploadList: [],
      headers: {
        认证key: "认证前缀 " + getToken(),
      },
      fileList: this.value,
      dialogImageUrl: '',
      dialogVisible: false,
      videoDialogVisible: false,
      screenshotDialogVisible: false,
      dialogImageUrlList: [],
      loadingInstance:null,
      pasteimgSrc: '',
      pasteimgSrcList: [],
      screenshotFile: null,
      screenshotName: '',
      //静态图片
      word: require('@/assets/upload/docx.png'),
      ppt: require('@/assets/upload/ppt.png'),
      excel: require('@/assets/upload/excel.png'),
      pdf: require('@/assets/upload/pdf.png'),
      img: require('@/assets/upload/img.png'),
      txt: require('@/assets/upload/txt.png'),
      mp4: require('@/assets/upload/mp4.png'),
      file: require('@/assets/upload/file.png'),
      zip: require('@/assets/upload/zip.png'),
    }
  },
  watch: {
    value: {
      handler(newVal) {
        this.fileList = newVal
      },
      deep: true,
      immediate: true
    },
    linkId: {
      handler(newValue, oldValue) {
        this.fileList = this.fileList ? this.fileList : []
        if(newValue !== oldValue ) {
          if(!newValue){
            this.fileList.splice(0,this.fileList.length)
          }
          this.getAttachmentList();
        } else {
          if(this.fileList.length==0){
            this.getAttachmentList();
          }
        }
      },
      deep: true,
      immediate: true
    },
    fileList : {
      handler(val) {
        if(!val){
          this.fileList = []
        }
        this.$nextTick(() => {
          let fileElementList = document.getElementsByClassName('el-upload-list__item is-success');
          if (fileElementList && fileElementList.length > 0) {
            for (let ele of fileElementList) {
              let fileName = ele.innerText;
              //获取文件名后缀
              let fileType = fileName.substring(fileName.lastIndexOf(".") + 1);
              let iconElement = ele.getElementsByClassName('el-upload-list__item-thumbnail')[0];
              if (['doc','docx','DOC','DOCX'].indexOf(fileType) != -1) {
                iconElement.src = this.word // 文档
              } else if (['xls','xlsx','XLS','XLSX'].indexOf(fileType) != -1) {
                iconElement.src = this.excel // 表格
              } else if (['ppt','pptx','PPT','PPTX'].indexOf(fileType) != -1) {
                iconElement.src = this.ppt // PPT
              }  else if (['pdf','PDF'].indexOf(fileType) != -1) {
                iconElement.src = this.pdf // PDF
              } else if (['txt'].indexOf(fileType) != -1) {
                iconElement.src = this.txt // PDF
              } else if (['mp4'].indexOf(fileType) != -1) {
                iconElement.src = this.mp4 // mp4视频
              } else if (['zip','rar'].indexOf(fileType) != -1) {
                iconElement.src = this.zip // 压缩
              } else if (['jpg','JPG', 'png', 'PNG', 'jpeg', 'JPEG'].indexOf(fileType) != -1) {

              }else {
                iconElement.src = this.file
              }
            }
          }
        })
      },
      deep: true,
      immediate: true
    },
    screenshotDialogVisible: {
      handler(newValue, oldValue) {
        if (newValue) {
          this.$nextTick(() => {
            this.$nextTick(() => {
              this.$refs.pasteRef.focus();
            })
          })
        }
      },
      deep: true,
      immediate: true
    },
  },
  methods: {
    // //删除当前文件
    async remove(file) {
      await this.$modal.confirm('您确定要删除该文件?')
      let index = this.fileList.indexOf(file)
      this.fileList.splice(index, 1)
      this.$emit("input",this.fileList);
    },
    // 上传前校检格式和大小
    handleBeforeUpload(file) {
      // 校检文件类型
      if (this.fileType.length) {
        const fileName = file.name.split('.');
        const fileExt = fileName[fileName.length - 1];
        const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
        if (!isTypeOk) {
          this.$modal.msgError(`文件类型错误, 请上传${this.fileType.join("/")}格式文件!`);
          return false;
        }
      }
      // 校检文件大小
      if (this.fileSize) {
        const isLt = file.size / 1024 / 1024 < this.fileSize;
        if (!isLt) {
          this.$modal.msgError(`文件大小不超过${this.fileSize}MB!`);
          return false;
        }
      }
      this.number++;
      return true;
    },
    //文件上传时的回调
    handleProgress(event, file) {
      this.loadingInstance = Loading.service({
        text:'附件正在上传中...'
      })
    },
    // 上传成功回调
    handleUploadSuccess(res, file) {
      try {
        this.loadingInstance.close()
        if (res.code === 200) {
          let fileName = file.name.split('.');
          this.uploadList.push({
            originFileName: file.name,
            fileName: res.data.name,
            fileUrl: res.data.url,
            fileSizeKb: file.size / 1024,
            fileType: this.attachmentData.fileType,
            fileSuffix: fileName[fileName.length - 1]
          });
          this.uploadedSuccessfully();
        } else {
          this.number--;
          this.$modal.msgError(res.msg);
          this.$refs.fileUpload.handleRemove(file);
          this.uploadedSuccessfully();
        }
      } catch(e) {
        this.loadingInstance.close()
      }
    },
    // 上传结束处理
    uploadedSuccessfully(res) {
      if (this.number > 0 && this.uploadList.length === this.number) {
        //this.uploadList.forEach(item => item.dir = props.dir);
        this.fileList = this.fileList.filter((f) => f.fileUrl !== undefined).concat(this.uploadList);
        this.uploadList = [];
        this.number = 0;
        this.$emit("input", this.fileList);
      }
    },
    //文件列表移除文件时的钩子
    handleRemove(file) {
      this.fileList = this.fileList.filter((f) => !(f === file.fileUrl));
    },
    // 文件个数限制提示
    handleExceed() {
      this.$modal.msgError(`只允许上传${this.limit}个文件!`);
    },
    //文件上传失败构子
    handleUploadError() {
      this.$modal.msgError('上传文件失败');
    },
    //文件下载
    async handlePreview(file) {
      console.log('files',file)
      try {
        this.loadingInstance = Loading.service({
          text:'附件下载中...'
        })
        await downBlobFile(file.fileUrl, {}, file.fileName);
        this.loadingInstance.close()
      } catch(e) {
        this.loadingInstance.close()
      }
    },
    //文件预览
    preview(file) {
      this.dialogImageUrlList.splice(0,this.dialogImageUrlList.length)
      //let fileType = file.fileUrl.substring(file.fileUrl.lastIndexOf(".") + 1);
      if(['png','jpg','jpeg',".gif",'PNG','JPG','JPEG',"GIF"].indexOf(file.fileSuffix) != -1){
        this.dialogVisible = true
        this.dialogImageUrl =  file.fileUrl
        this.dialogImageUrlList.push(this.dialogImageUrl)
      } else if(['mp4','MP4'].indexOf(fileType) != -1) {
        this.dialogImageUrl =  file.fileUrl
        this.videoDialogVisible = true
      }
    },
    //截图上传弹框
    screenshotUpload() {
      this.screenshotDialogVisible = true
    },
    //生成A-Z加数字 随机数
    gdCode() {
      let str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
      let arr = str.split("")
      let n = Math.round(Math.random() * (arr.length -1))
      return arr[n] + Math.floor(Math.random() * 10)
    },
    //截图粘贴上传
    pasteInput(event) {
      this.pasteimgSrcList.splice(0,this.pasteimgSrcList.length)
      let item =  event.clipboardData?.items[0];
      if(!item) {
        return
      }
      if(item &&(item.type.indexOf('image') > -1 )){
        let file =  item.getAsFile()
        this.screenshotFile = file
        const reader = new FileReader(); // 创建新的读取文件 FileReader 实例
        reader.onload = (e) => {
          this.pasteimgSrc = e.target.result  // 获取文件内容
          this.pasteimgSrcList.push(e.target.result)
        }
        reader.readAsDataURL(file)  // 读取文件内容二进制数据,并将其编码为 base64 的 data url
      }
    },
    //截图上传请求
    async handleHttpUpload (file) {
      let formData = new FormData();
      formData.append('file', file);
      //formData.append('dir', props.dir);
      try {
        const res = await request({
          url:  "/file/upload",
          method: 'post',
          headers: this.headers,
          data: formData,
        });
        let fileName = file.name.split('.');
        let screenshotFileName = ''
        if(this.screenshotName){
          screenshotFileName = this.screenshotName + '.' + fileName[fileName.length - 1]
        } else {
          screenshotFileName = '截图' + this.gdCode() + '.' + fileName[fileName.length - 1]
        }
        this.fileList.push({
          originFileName: screenshotFileName,
          fileName: res.data.name,
          fileUrl: res.data.url,
          fileSizeKb: file.size / 1024,
          fileType: this.attachmentData.fileType,
          fileSuffix: fileName[fileName.length - 1]
        });
        this.$emit("input", this.fileList);
        this.pasteimgSrc = null
        this.screenshotName = ''
      } catch (error) {
        this.$modal.msgError("上传文件失败");
        this.pasteimgSrc = null
        this.screenshotName = ''
      }
      this.screenshotDialogVisible = false
    },
    //清除粘贴内容和粘贴板
    cancelImage() {
      this.pasteimgSrc = null
      this.screenshotName = ''
      navigator.clipboard.writeText('')
    },
    cancel() {
      this.pasteimgSrc = null
      this.screenshotName = ''
      this.screenshotDialogVisible = false
    },
    pasteBeforeClose(done) {
      this.pasteimgSrc = null
      this.screenshotName = ''
      done()
    },
    //获取附件列表
    getAttachmentList() {
      if(this.attachmentData.linkId) {
        let params = {
          linkId:this.attachmentData.linkId,
          fileType:this.attachmentData.fileType,
        }
        getListByReview(params).then((res) => {
          this.fileList.splice(0,this.fileList.length)
          res.data.forEach(item => {
            this.fileList.push(item)
          })
        })
      }
    },
  }
}
</script>

<style scoped lang="scss">
.upload-file{
  width:100%;
}
.f-position{
  position: relative;
  cursor: pointer;
  .f-icon{
    position: absolute;
    top:0;
    right:0;
  }
}
::v-deep .el-upload-list--picture-card .el-upload-list__item {
  width: 130px;
  height:130px;
}
::v-deep .el-dialog__header {
  padding-bottom: 0 !important;
}
::v-deep .el-dialog__body {
  padding:15px 20px;
}
::v-deep .el-upload-list--picture-card .el-upload-list__item-thumbnail {
  height:105px;
  width:95%;
}
::v-deep .el-upload--picture-card {
  width:130px;
  height:130px;
}

.filetext{
  margin-top:-7px;
}
.divline{
  line-height: 24px;
}
.d-auto{
  width: 80%;
  margin: 0 auto;
  margin-top: 11px;
}
.d-auto1{
  width: 80%;
  margin: 0 auto;
  margin-top: 40px;
}
.screenshot{
  width: 100%;
  display: flex;
  flex-direction: column;
}
.s-btn{
  width: 100%;
  height: 44px;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top:10px;
}
.area {
  width:100%;
  border:1px solid #dcdfe6;
}
</style>

4. 具体使用方式

4.1 前端使用

image-20250219182314018
image-20250219182314018

说明:

  1. form.files[0]:实际上这儿设置的二维数组,存储的是上传的附件信息,如果有多个upload组件引用则是files[1], files[2] ......
  2. linkId:及业务id
  3. fileType: 附件的业务类型,只是语义上的意义,其实没有太大的实际意义。用于为业务附件分类的

4.2 后端的使用

  1. 引入附件 common

    image-20250219183356775
    image-20250219183356775
  2. 开启附件服务

    image-20250219183024359
    image-20250219183024359
  3. 首先入参必须有二维数组,且名字是files,如果不想用files想看注解代码的释义

    image-20250219182808931
    image-20250219182808931
  4. 在保存附件的service方法上打上@Attachment注解,标注 是新增 save 还是 更新 update

    image-20250219182824714
    image-20250219182824714
    image-20250219183106379
    image-20250219183106379

5. 最终的释义说明**(mybatis xml的配置相当重要)**

### 数据库xml配置
```yml
# mybatis配置
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.ruoyi.material
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
    mapperLocations: classpath*:mapper/**/*.xml

```
~注意~:在 `mapperLocation` 中的 `classpath` 后面需要配置 *,否则服务之扫描本服务的xml文件,不会扫描引入的xml文件

正确:classpath*:mapper/**/*.xml
错误:classpath:mapper/**/*.xml

### 开启使用
如果想要使用ruoyi-common-file 需要 在启动类上加上 @EnableAttachment

### 附件保存和更新
需要在相应的server类中添加@Attachment注解,具体使用详见注解文件中的注释