Skip to content

v1.3.4-beta(20260503) ​

1. 资源模块(sz-common-resource)全新引入 ​

本次升级引入了全新的 sz-common-resource 资源模块,用于替代原来直接调用 OssClient 的上传方式。新模块以「场景(Scene)」为核心概念,支持为不同业务场景独立配置存储类型(本地磁盘 / OSS 公有 / OSS 私有)、命名规则、路径策略和访问安全策略,并通过统一接口 POST /resource/upload 对前端暴露上传能力。

IMPORTANT

本次升级涉及配置文件结构调整。原 oss.yml 顶级 oss: 配置迁移为 sz.oss: 和 sz.resource: 两段,请务必同步更新配置文件。

1.1 两个模块的职责划分 ​

本次升级涉及两个相互配合的模块,职责明确分离:

模块职责配置前缀
sz-common-oss厂商适配层。封装 MinIO / 阿里云 / 腾讯云 / 七牛云等底层 S3 SDK,负责 bucket 接入凭据管理。sz.oss.*
sz-common-resource业务抽象层。以「场景(sceneCode)」为核心,屏蔽底层存储细节,统一管理文件上传、路径规则、命名策略、访问模式和安全白名单。sz.resource.*

业务代码只需调用 ResourceService,不再直接依赖 OssClient;底层存储介质(本地/OSS)和访问方式(直链/预签名/代理)均通过 yml 配置切换,代码无需改动。

NOTE

sz-common-oss 同步修复了部分云厂商配置问题,包括:腾讯云 / 七牛云因 region 填写错误导致 Presigned URL 返回 403、阿里云 2025 年 3 月后中国内地地域需配置 CNAME domain、MinIO URL 协议拼接重复等问题。

各厂商的详细接入说明和排查指引,请阅读项目源码中的 sz-common-oss/docs/oss-provider-guide.md。

1.2 配置文件结构说明 ​

新版配置分为两段写在 oss.yml 中:

  • sz.oss.*:S3 协议接入凭据(对接 MinIO / 阿里云 / 腾讯云等)。
  • sz.resource.*:业务资源体系,按场景(sceneCode)定义存储类型、命名规则、路径策略、访问模式和安全白名单。

以下为实际项目配置示例(config/local/oss.yml),完整展示了三种典型场景配置方式:

yml
sz:
  # oss 存储配置(厂商接入凭据)
  oss:
    # 支持 S3 协议的厂商: MINIO / ALIYUN / TENCENT / QINIU
    provider: MINIO
    endpoint: 192.168.100.176:9000
    accessKey: your-access-key
    secretKey: your-secret-key
    # 全局存储桶(场景未指定 bucket 时回退使用)
    bucketName: test
    domain: http://192.168.100.176:9000
    scheme: http

  # 场景资源配置(业务资源体系)
  resource:
    # 本地存储根目录(LOCAL 类型场景使用),支持相对路径或绝对路径
    root: ./data
    # 全局默认存储类型(场景未单独配置 type 时生效)
    default-storage-type: LOCAL
    # 全局安全白名单(场景未单独配置 exts 时生效)
    security:
      allowed-exts: [jpg, jpeg, png, gif, bmp, webp, svg, ico, pdf, doc, docx, xls, xlsx, ppt, pptx, rtf, txt, csv, zip, rar, 7z, tar, gz, mp3, wav, ogg, mp4, mov, avi, wmv]
    max-size: 50MB

    scenes:

      # ========= 场景示例:账户头像 =========
      # 同一个 sceneCode 可注释切换三种模式,根据环境选择一种激活

      # 【模式一:本地存储 + Java 服务代理访问】(当前激活)
      # 适合:开发/测试环境,无需额外 OSS 服务
      - code: admin.user.logo
        type: LOCAL
        serve-mode: DIRECT
        base-url: http://127.0.0.1:9991/api/admin/resource/file/logo  # Java 服务代理地址
        # base-url: http://127.0.0.1:7980/providers  # 或使用 Nginx 代理地址
        path: logo
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [svg, png, jpg, jpeg, webp, gif]
        max-size: 3

      # 【模式二:OSS 私有桶 + 预签名 URL 访问】(注释状态)
      # 适合:需要访问控制的生产环境,URL 有时效性,安全性高
      # - code: admin.user.logo
      #   type: OSS
      #   serve-mode: PRESIGNED
      #   bucket: client-logos
      #   base-url: http://192.168.100.176:9000/client-logos
      #   naming: ORIGINAL
      #   path-strategy: BIZ_DATE
      #   exts: [svg, png, jpg, jpeg, webp, gif]
      #   max-size: 3

      # 【模式三:OSS 公有桶 + 直链访问】(注释状态)
      # 适合:公开资源,直链访问,无访问控制需求
      # - code: admin.user.logo
      #   type: OSS
      #   serve-mode: DIRECT
      #   bucket: client-logos-public
      #   base-url: http://192.168.100.176:9000/client-logos-public
      #   naming: UUID
      #   path-strategy: BIZ_DATE
      #   exts: [svg, png, jpg, jpeg, webp, gif]
      #   max-size: 3

      # ========= 其他业务场景示例 =========

      - code: teacher.attachment
        type: LOCAL
        serve-mode: DIRECT
        path: teacher-attachments
        base-url: http://127.0.0.1:9991/api/admin/resource/file/teacher-attachments
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 10

      - code: teacher.richtext
        type: LOCAL
        serve-mode: DIRECT
        path: teacher-richtext
        base-url: http://127.0.0.1:9991/api/admin/resource/file/teacher-richtext
        naming: UUID
        path-strategy: DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 5

1.3 三种访问模式对照说明 ​

admin.user.logo 场景在配置文件中同时提供了三种模式的写法(注释切换),按需取消注释、注释掉其他两种即可:

模式typeserve-mode适用场景注意事项
本地 + Java 代理LOCALDIRECT开发/测试环境,无需 OSSbase-url 需配置为 Java 服务的资源代理地址或 Nginx 代理地址
OSS 私有 + 预签名OSSPRESIGNED生产环境,需要访问控制生成的 URL 有时效性(默认 3600 秒),适合合同、证件等敏感资源
OSS 公有 + 直链OSSDIRECT公开资源,无访问控制需求bucket 需设为公读,base-url 填写 OSS 公网访问地址

1.4 场景核心配置字段说明 ​

字段说明可选值
code场景唯一标识(即前端 sceneCode),全局不可重复自定义字符串,建议 模块.用途 格式
type存储类型LOCAL(本地磁盘)/ OSS(对象存储)
pathLOCAL 类型必填,相对于 root 的存储子目录字符串路径
bucketOSS 类型必填,对应的 bucket 名称字符串
naming文件命名规则UUID(随机)/ ORIGINAL(原文件名,冲突时补时间戳)
path-strategy路径生成策略FLAT(扁平)/ DATE(按日期)/ BIZ_DATE(bizKey + 日期)
serve-mode访问模式DIRECT(直链)/ PRESIGNED(预签名,仅 OSS)/ TOKEN(token 鉴权)
base-urlDIRECT 模式必填,文件访问根地址URL 字符串
exts场景级文件扩展名白名单(覆盖全局 security.allowed-exts)字符串列表
max-size场景级最大文件大小,支持 MB 单位如 3(MB)或 3MB

TIP

完整配置说明、所有场景案例(Nginx 前置、OSS 公有/私有、TOKEN 鉴权等)及 ResourceService API 参考,请阅读项目源码中的 sz-common-resource/docs/resource-user-guide.md。

1.5 存储设计:数据库只存 objectKey ​

理解这一设计原则,是正确使用资源模块的关键。

数据库存储的是 objectKey,而不是完整的访问 URL。

objectKey 是文件在存储介质中的相对路径(对象键),例如:

# 本地存储场景下的 objectKey 示例
admin/20260430/avatar.jpg

# OSS 存储场景下的 objectKey 示例
logo/admin/20260430/a3f9c1.jpg

完整的访问 URL 是在运行时由 base-url(场景配置) + objectKey 动态拼装得到的,并不持久化到数据库。


为什么这样设计?

如果将完整 URL 存入数据库,一旦发生以下任何变化,历史数据全部失效,必须批量更新数据库:

  • 服务器 IP 或端口变更(如 localhost:19991 → 生产域名)
  • 存储类型切换(本地磁盘 → OSS)
  • OSS Bucket 或域名变更(换 CDN、换厂商)
  • 访问模式变更(公有直链 → 私有预签名)

而存储 objectKey 后,只需修改 oss.yml 中对应场景的 base-url 或 serve-mode 配置,所有历史记录在下一次查询时即可自动适配新地址,配置一处,全局生效,数据库无需改动。


完整数据流示意:

【上传时】
前端 → POST /resource/upload(携带 sceneCode)
     → 返回 { accessUrl, objectKey, resourceId, ... }
     → 业务代码:入库保存 objectKey(不保存 accessUrl)

【查询时】
数据库读取 objectKey
     → @OssUrlFill 切面根据 sceneCode 的当前配置(base-url / serve-mode)
       动态拼装或生成 accessUrl
     → 接口返回完整可访问的 accessUrl 给前端

核心收益总结:

场景存储完整 URL存储 objectKey(当前方案)
迁移存储环境需批量更新数据库只改 yml 配置,数据库不动
切换 LOCAL → OSS历史数据全部失效历史数据自动适配
切换公有 → 私有访问静态 URL 无法签名@OssUrlFill 自动改用预签名策略
多环境(开发/测试/生产)各环境数据不互通各环境独立配置 base-url,数据结构一致

TIP

上传接口返回的 accessUrl 用于当次前端展示(如上传成功后立即预览),入库时应保存 objectKey。后续查询时通过 @OssUrlFill 自动回填 accessUrl 返回给前端,前端无感知。


1.6 @OssUrlFill 注解 ​

正因为入库保存的是 objectKey,查询接口需要一种机制在返回时自动将其转换为可访问的 accessUrl,这就是 @OssUrlFill 注解的职责。

标注在 VO 的字段上,框架会在接口返回前自动按场景配置完成转换,无需在业务代码中手动拼接 URL。

java
public class UserVO {
    /**
     * 头像字段,数据库存储的是 objectKey(如 admin/20260430/avatar.jpg)
     * @OssUrlFill 会在返回时自动按 admin.user.logo 场景配置转换为完整 accessUrl
     */
    @OssUrlFill(sceneCode = "admin.user.logo")
    private String avatar;
}

2. 前端上传组件 Props 变更(Breaking Change) ​

本次升级统一了前端上传接口,所有上传组件的 dir 属性均已变更为 sceneCode(对应后端配置的场景码),并新增了可选的 bizKey 和 pathSegments 属性。同时,上传接口响应字段也发生了变化。

WARNING

这是一个破坏性变更。使用了以下组件的页面必须同步修改,否则上传功能将无法正常工作。

2.1 涉及组件汇总 ​

组件变更内容
UploadFiles.vuedir → sceneCode,响应字段 url/filename/fileId → accessUrl/originName/resourceId
SimplifyUpload/index.vuedir → sceneCode,新增可选 bizKey、pathSegments
Img.vuedir → sceneCode,新增可选 bizKey、pathSegments,新增 crop 裁剪属性
Imgs.vuedir → sceneCode,新增可选 bizKey、pathSegments
JoditEditor/index.vueuploadDir → sceneCode,默认值变为 system.richtext

2.2 迁移示例 ​

vue
<!-- UploadFiles -->
<UploadFiles :dir="'upload/avatar'" v-model="form.fileList" />

<!-- Img 单图上传 -->
<Img :dir="'upload/avatar'" v-model="form.avatar" />

<!-- Imgs 多图上传 -->
<Imgs :dir="'upload/images'" v-model="form.images" />

<!-- JoditEditor 富文本 -->
<JoditEditor :upload-dir="'teacher/aabb'" v-model="form.content" />
vue
<!-- UploadFiles:dir → sceneCode,值改为后端配置的场景码 -->
<UploadFiles :sceneCode="'admin.user.logo'" v-model="form.fileList" />

<!-- Img 单图上传:dir → sceneCode -->
<Img :sceneCode="'admin.user.logo'" v-model="form.avatar" />

<!-- Imgs 多图上传:dir → sceneCode -->
<Imgs :sceneCode="'teacher.attachment'" v-model="form.images" />

<!-- JoditEditor 富文本:uploadDir → sceneCode,使用富文本场景码 -->
<JoditEditor scene-code="system.richtext" v-model="form.content" />

2.3 响应字段变更(UploadFiles) ​

如果你在业务代码中直接使用了上传接口的响应字段,请参照以下映射关系进行迁移:

旧字段(IUploadResult)新字段(IResourceUploadResult)说明
urlaccessUrl文件访问地址
filenameoriginName原始文件名
fileIdresourceId文件唯一标识
objectNameobjectKey存储对象键(入库字段)
dirTag—已废弃,由 sceneCode 替代

2.4 getOssPreviewUrl 已移除 ​

旧版上传组件在回显时需调用 getOssPreviewUrl() 将 objectKey / objectName 转换为可访问 URL(触发 OSS 预签名)。

新版统一改为由后端在 accessUrl 字段直接返回可访问 URL:LOCAL 模式返回直链,OSS 私有模式返回预签名 URL,OSS 公有模式返回直链。前端直接使用 accessUrl,无需任何转换。

NOTE

如有业务代码中存在 getOssPreviewUrl() 调用,可直接移除,改为使用接口返回的 accessUrl 字段。

2.5 多附件展示 — FileDownloadList 组件 ​

FileDownloadList 组件用于在列表页或详情页展示多个文件附件,统一适配 resource 体系,与 UploadFiles 上传组件配套使用。

组件能力:

  • 展示文件名 + 下载操作
  • 图片类型自动识别,支持 el-image-viewer 全屏预览,切换时显示文件名
  • 超出 maxRows 条时自动折叠,点击展开/收起
  • 兼容 ResourceRef[](后端回填 accessUrl)和 string[](纯 URL)两种数据格式

Props 说明

Prop类型默认值说明
filesResourceRef[] | string[][]文件列表,来自后端回填 accessUrl 的 ResourceRef[] 或纯 URL 数组
align'left' | 'center' | 'right''left'列表对齐方式
maxRowsnumber0折叠阈值,0 表示不折叠

数据流

【上传时】前端 UploadFiles 获得 IResourceUploadResult[]
  → 提交时 map 为 ResourceRef[](只取 objectKey / originName / contentType / sceneCode)
  → 入库保存 ResourceRef JSON 数组(不存 accessUrl)

【查询时】后端读取 ResourceRef[]
  → resolveUrl 按 sceneCode 回填 accessUrl 字段
  → 接口返回含 accessUrl 的 ResourceRef[] 给前端

【展示时】FileDownloadList :files="row.url"
  → 读取 f.accessUrl 作为下载/预览 URL
  → 读取 f.originName 显示文件名
  → 读取 f.contentType 判断是否为图片

后端入库字段(ResourceRef 结构)

字段是否入库说明
objectKey✅存储对象键,唯一标识文件
originName✅原始文件名,展示用
contentType✅MIME 类型,用于判断图片/文件
sceneCode✅场景编码,后端 resolveUrl 回填时使用
accessUrl❌后端动态回填,不入库

完整使用示例(以教师统计模块为例)

vue
<!-- 列表页:ProTable column template 中展示附件 -->
<template #url="{ row }">
  <file-download-list :files="row?.url" align="left" :max-rows="3" />
</template>
vue
<!-- 表单页:UploadFiles 上传附件 -->
<el-form-item label="附件" prop="url">
  <upload-files
    v-model="fileUrls"
    :limit="5"
    :file-size="3"
    scene-code="teacher.attachment"
    path-segments="template,teacher"
    @all-success="handleAllSuccess"
  />
</el-form-item>
ts
// 提交时:将上传结果转换为 ResourceRef[] 入库(只保留 objectKey,不保存 accessUrl)
form.url = (fileUrls.value as IResourceUploadResult[])
  .filter(Boolean)
  .map(({ objectKey, originName, contentType }): ResourceRef => ({
    objectKey,
    originName,
    contentType,
    sceneCode: 'teacher.attachment'
  }))

NOTE

accessUrl 由后端根据场景配置(sceneCode)动态回填,前端不入库、不手动拼接,确保切换存储环境或访问模式时历史数据自动适配。


3. @LogicDeleteFill 注解 ​

本次升级引入 @LogicDeleteFill 注解,用于优化逻辑删除时附加字段(delete_id、delete_time)的自动填充机制。

旧版行为:框架在所有实体执行逻辑删除时,无差别地尝试查找并填充 delete_time、delete_id 字段,但字段名规则固定,不支持自定义,灵活性不足。

新版行为:只有标注了 @LogicDeleteFill 的实体类,才会在逻辑删除时自动填充附加字段;同时支持通过注解属性自定义字段名。

NOTE

@LogicDeleteFill 只控制 delete_id、delete_time 等附加字段的自动填充行为,与逻辑删除本身无关。未标注该注解的实体,逻辑删除(@Column(isLogicDelete = true))依然正常执行,仅不再自动填充附加字段。

3.1 变更说明 ​

版本行为
v1.3.3-beta 及以前所有实体逻辑删除时,框架无差别尝试填充 delete_time、delete_id,字段名固定不可自定义
v1.3.4-beta 起仅标注了 @LogicDeleteFill 的实体才触发附加字段自动填充,字段名可通过注解属性自定义

框架内置的 SysDict、SysDictType、SysMenu 已默认添加该注解,无需额外处理。

3.2 使用说明 ​

对于需要自动填充 delete_id / delete_time 的自定义 PO 类,在类上添加 @LogicDeleteFill 即可:

java
// 使用默认字段名(delete_time / delete_id)
@LogicDeleteFill
public class BizOrderPO {

    @Column(isLogicDelete = true)
    private Integer deleted;

    private LocalDateTime deleteTime;  // 自动填充

    private Long deleteId;             // 自动填充
}

如果实体中的删除时间或删除人字段名与默认值不同,可通过注解属性显式指定(空字符串表示不填充该字段):

java
// 自定义字段名
@LogicDeleteFill(deleteTimeColumn = "deleted_at", deleteByColumn = "deleted_by")
public class BizContractPO {

    @Column(isLogicDelete = true)
    private Integer deleted;

    private LocalDateTime deletedAt;   // 自动填充

    private Long deletedBy;            // 自动填充
}

// 只填充删除时间,不填充删除人
@LogicDeleteFill(deleteByColumn = "")
public class BizLogPO {

    @Column(isLogicDelete = true)
    private Integer deleted;

    private LocalDateTime deleteTime;  // 自动填充

    // 无 deleteId 字段,传空字符串跳过填充
}

4. Excel 导入功能增强 ​

本次升级新增三个注解并引入统一导入框架(AbstractExcelImportTemplate),向后兼容,无需迁移。

4.1 新增注解说明 ​

@ExcelTemplate ​

标注在导入 DTO 类上,将该类注册为可下载的导入模板。alias 作为注册唯一 key 和下载时的文件名,需配合 @EnableExcelTemplateScan 启用扫描。

属性说明默认值
alias模板唯一标识,同时作为下载文件名(如 "教师统计导入模板.xlsx")必填
validRowsExcel 数据验证覆盖行数(从第 2 行起)1000
java
@ExcelTemplate(alias = "教师统计导入模板.xlsx")
public class TeacherStatisticsImportDTO {

    @ImportColumn(required = true, columnWidth = 100)
    @ExcelProperty(value = "统计年份")
    private String year;

    @ImportColumn(required = true)
    @ExcelProperty(value = "统计月份")
    private String month;

    @ExcelProperty(value = "讲师区分类型")
    @DictFormat(dictType = "account_status", isSelected = true)  // 字典下拉
    private String teacherCommonType;

    @ExcelEnumFormat(preset = ExcelEnumPreset.YES_NO)            // 枚举下拉
    @ExcelProperty(value = "是否无效")
    private YesNoEnum hasInvalid;

    @ExcelIgnore  // 不出现在模板中
    private String checkStatus;
}

@ImportColumn ​

字段级注解,控制该列在导入模板中的必填校验和列宽。

属性说明默认值
required是否必填。true 时表头自动加红色 * 前缀,并在 Excel 中生成非空数据验证弹框false
columnWidth指定列宽(字符单位)。-1 表示自动计算-1

@EnableExcelTemplateScan ​

标注在 Spring Boot 启动类或配置类上,启动时扫描指定包路径,将所有带 @ExcelTemplate 的 DTO 类注册到模板中心。basePackages 支持 * 通配符。

java
@SpringBootApplication
@EnableExcelTemplateScan(basePackages = "com.sz.sso.*.pojo.dto")
public class AdminApplication { ... }

4.2 导入模板自动生成 ​

配置好注解后,前端通过模板下载接口即可获得自动生成的空白导入模板,无需手动维护 Excel 文件。

三级查找策略(按优先级):

  1. classpath:/templates/{templateName} — 静态模板文件(优先)
  2. sys_temp_file 表 — 手动上传到系统的模板文件
  3. ExcelTemplateScanRegistry 按 alias 动态生成 — 兜底策略,根据 DTO 注解实时生成

必填列效果

@ImportColumn(required = true) 的字段在生成的模板中有两个可见效果:

  • 表头文字前自动追加红色 * 前缀
  • 选中该列数据格时弹出"此列为必填字段"的输入提示;填写空值提交时触发 Excel STOP 级别拦截弹框

Excel 导入模板表头,必填列红色 * 标注

模板下载后填入数据的效果(字典/枚举列自动生成下拉选项):

填写数据的模板示例


4.3 AbstractExcelImportTemplate — 统一导入框架 ​

框架提供抽象基类 AbstractExcelImportTemplate<T>,业务侧继承后只需实现核心业务逻辑,框架自动处理:批次创建、分片执行、失败记录落库、批次状态更新。

必须实现的方法

方法说明
importDtoClass()返回导入 DTO 的 Class
bizType()业务类型字符串,用于失败记录归类(如 "teacher_statistics")
bizName()业务名称,写入批次记录(如 "教师统计导入")
doImport(batchId, rows)执行实际业务写入,返回 ExcelImportBizResult(含成功数和失败明细)
convertExcelFailItems(failRows)将 FastExcel 解析失败行转换为统一的 ExcelImportFailItem

可选钩子

方法说明默认行为
chunkSize()分片大小,0 表示不分片0
beforeImport(batchId, rows)导入前回调空实现
afterChunk(batchId, i, total, result)每片执行后回调空实现
afterImport(batchId, aggregated)全部执行完毕后回调空实现
onFailBatch(batchId, ex)批次异常时回调空实现
currentOperatorId()返回当前操作人 IDnull

完整示例

java
// ① 导入 DTO(带注解)
@ExcelTemplate(alias = "教师统计导入模板.xlsx")
public class TeacherStatisticsImportDTO {
    @ImportColumn(required = true)
    @ExcelProperty("统计年份")
    private String year;

    @ImportColumn(required = true)
    @ExcelProperty("统计月份")
    private String month;

    @ExcelProperty("教师id")
    private String teacherId;
    // ... 其他字段
}

// ② 业务导入器
@Component
public class TeacherStatisticsExcelImporter
        extends AbstractExcelImportTemplate<TeacherStatisticsImportDTO> {

    @Autowired
    private TeacherStatisticsMapper mapper;

    @Override
    protected Class<TeacherStatisticsImportDTO> importDtoClass() {
        return TeacherStatisticsImportDTO.class;
    }

    @Override
    protected String bizType() { return "teacher_statistics"; }

    @Override
    protected String bizName() { return "教师统计导入"; }

    @Override
    protected ExcelImportBizResult doImport(String batchId,
            List<TeacherStatisticsImportDTO> rows) {
        List<ExcelImportFailItem> failItems = new ArrayList<>();
        List<TeacherStatisticsPO> successList = new ArrayList<>();

        for (int i = 0; i < rows.size(); i++) {
            TeacherStatisticsImportDTO dto = rows.get(i);
            // 业务校验
            if (someValidationFail(dto)) {
                failItems.add(ExcelImportFailItem.builder()
                    .rowNo(i + 2)                    // Excel 行号(首行为表头,从第2行起)
                    .bizKey(dto.getTeacherId())
                    .bizKeyLabel("教师ID")
                    .errorMsg("校验失败原因描述")
                    .build());
                continue;
            }
            successList.add(convert(dto));
        }
        mapper.insertBatch(successList);
        return ExcelImportBizResult.of(successList.size(), failItems);
    }

    @Override
    protected List<ExcelImportFailItem> convertExcelFailItems(
            List<ExcelFailRow<TeacherStatisticsImportDTO>> failRows) {
        // 使用框架工具方法,指定业务主识别字段
        return failRows.stream()
            .map(row -> buildExcelFailItem(row,
                TeacherStatisticsImportDTO::getTeacherId, "教师ID"))
            .toList();
    }
}

// ③ Controller 接口
@PostMapping("/import")
@SaCheckPermission("teacher.statistics.import")
public ApiResult<ExcelImportResultVO> importExcel(@ModelAttribute ImportExcelDTO dto) {
    return ApiResult.success(teacherStatisticsService.importExcel(dto));
}

4.4 错误处理方案 ​

导入过程中的错误分为两类,框架统一收口处理:

解析失败(FastExcel 阶段)

表头不匹配、数据格式错误等在 Excel 解析阶段产生,由 convertExcelFailItems() 转换为统一结构。

业务校验失败

数据通过 Excel 解析,但不满足业务规则(重复数据、关联不存在等),在 doImport() 中自行判断后加入失败列表。

统一失败项结构 ExcelImportFailItem

字段类型说明
rowNoIntegerExcel 行号(1-based,第 1 行为表头,数据从第 2 行起)
bizKeyString业务主识别值(如教师 ID "3"),用于定位问题行
bizKeyLabelString主识别值标签(如 "教师ID")
errorCodeString错误码(可选)
errorMsgString错误信息描述
rowDataMap当前行原始数据快照(DTO 序列化后的 Map,供追溯)

接口返回结构 ExcelImportResultVO

字段说明
batchId批次 ID(UUID),唯一标识本次导入任务
success成功条数
fail失败条数
failDetails失败明细列表(框架预留字段,供后续扩展直接内联返回)

持久化

框架自动将批次信息和失败记录落库,无需业务代码干预。

sys_import_batch — 批次记录:

字段示例值说明
batch_id8649352b-976e-42d8-9e40-21aaaddd50b4批次唯一 ID
biz_typeteacher_statistics业务类型
biz_name教师统计导入业务名称
file_name教师统计导入模板 (11).xlsx原始上传文件名
total_count8总行数
success_count6成功条数
fail_count2失败条数
statusFINISHED批次状态:PROCESSING / FINISHED / FAILED
create_time2026-04-30 22:08:12导入开始时间
finish_time2026-04-30 22:08:12导入完成时间

sys_import_fail_record — 失败行记录:

字段示例值说明
batch_id8649352b-976e-42d8-9e40-21aaaddd50b4关联批次 ID
biz_typeteacher_statistics业务类型
row_no3Excel 行号
biz_key3业务主识别值(教师 ID)
biz_key_label教师ID主识别值标签
error_msg错误编号xxx2错误信息
handle_statusPENDING处理状态(默认 PENDING,预留人工处理流程扩展)
row_data{"year":"2013","month":"03","teacherId":"3",...}原始行数据 JSON 快照

NOTE

失败记录已完整落库(sys_import_batch / sys_import_fail_record),包含行号、原始数据快照、错误信息等字段,可结合自身业务自行实现失败记录查询、按批次筛查或人工处理标记等功能。


4.5 前端使用方式 ​

框架提供通用导入弹窗组件 ImportExcel,封装了模板下载、文件上传、进度反馈、结果展示的完整流程。

导入弹窗

导入弹窗界面

弹窗包含:模板标识信息、下载模板按钮(调用三级查找策略生成模板文件)、文件拖拽/点击上传区、数据覆盖开关(将用户的选择以 isCover 参数透传给后端;具体覆盖逻辑需在 doImport() 中自行根据该参数实现)。

组件 Props(acceptParams 参数)

参数类型必填说明
titlestring✅弹窗标题(显示为"导入:{title}")
aliasstring✅对应 @ExcelTemplate.alias,用于模板下载查找
fileNamestring✅下载的模板文件名
templateNamestring模板展示名(不传则与 fileName 相同)
importApiFunction✅调用导入接口的方法
tempApiFunction下载模板接口方法(不传则使用框架默认)
getTableListFunction导入成功后刷新表格的回调
paramobject透传给导入接口的额外参数
fileSizenumber最大上传文件大小(MB,默认 5)
resultTipstring有失败条数时显示的引导提示文案

调用示例

typescript
// 声明组件引用
const ImportExcelRef = ref<InstanceType<typeof ImportExcel>>()

// 打开导入弹窗
ImportExcelRef.value?.acceptParams({
  title: '教师统计',
  alias: '教师统计导入模板.xlsx',
  fileName: '教师统计导入模板.xlsx',
  tempApi: downloadTemplate,
  importApi: importTeacherStatisticsExcelApi,
  getTableList: proTableRef.value?.getTableList,
  resultTip: '失败结果可到【Excel失败记录 - 教师统计导入】页面查看'
})

导入结果弹窗

上传完成后自动弹出导入结果:

导入结果弹窗,成功 6 条失败 2 条

结果弹窗展示:

  • 批次 ID:可一键复制,后续在"Excel 失败记录"页面按此 ID 精确筛查失败明细
  • 成功 / 失败条数:绿色成功数、红色失败数
  • 引导提示(仅 fail > 0 且传入 resultTip 时显示):橙色提示区,告知用户到哪个页面查看失败详情

5. 用户 Store 变更 ​

用户信息的 Store 结构发生调整,userInfo 已重命名为 profile,并调整了持久化策略(不再持久化用户信息,仅持久化 token,用户信息在每次路由初始化时由接口重新拉取)。

NOTE

若项目中有自定义代码直接引用了 userStore.userInfo,请同步修改为 userStore.profile。

ts
// 迁移前
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
console.log(userStore.userInfo.username)
console.log(userStore.userInfo.logo)

// 迁移后
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
console.log(userStore.profile?.username)
console.log(userStore.profile?.avatar)  // 字段名 logo → avatar