260601-后端各功能接口实现方式与代码规范分析.md 42 KB

CRUD与导入导出代码实现模式分析

分析日期:2026-06-01\ 项目版本:JeecgBoot 3.9.2


一、概述

本文档深入分析湛江人社项目中增删改查(CRUD)数据导入导出下载导入模板等功能的代码实现方式,并提炼出这些功能的最大规范公约数——即所有实现中共享的最核心、最通用的代码模式。


二、增删改查(CRUD)实现模式

2.1 标准单表 CRUD(最大公约数模式)

这是项目中最常见、最规范的实现方式,以 JeecgDemoController 为代表:

@Slf4j
@RestController
@RequestMapping("/test/jeecgDemo")
public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoService> {

    // ========== 查询(R) ==========
    
    // 分页列表查询
    @GetMapping(value = "/list")
    @PermissionData(pageComponent = "system/examples/demo/index")
    public Result<?> list(JeecgDemo jeecgDemo,
                          @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
                          @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
                          HttpServletRequest req) {
        QueryWrapper<JeecgDemo> queryWrapper = QueryGenerator.initQueryWrapper(jeecgDemo, req.getParameterMap());
        Page<JeecgDemo> page = new Page<>(pageNo, pageSize);
        IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
        return Result.OK(pageList);
    }

    // 通过ID查询
    @GetMapping(value = "/queryById")
    public Result<?> queryById(@RequestParam(name="id", required=true) String id) {
        JeecgDemo obj = jeecgDemoService.getById(id);
        return Result.OK(obj);
    }

    // ========== 新增(C) ==========
    
    @PostMapping(value = "/add")
    @AutoLog(value = "添加测试DEMO")
    public Result<?> add(@RequestBody JeecgDemo jeecgDemo) {
        jeecgDemoService.save(jeecgDemo);
        return Result.OK("添加成功!");
    }

    // ========== 修改(U) ==========
    
    @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
    @AutoLog(value = "编辑DEMO", operateType = CommonConstant.OPERATE_TYPE_3)
    public Result<?> edit(@RequestBody JeecgDemo jeecgDemo) {
        jeecgDemoService.updateById(jeecgDemo);
        return Result.OK("更新成功!");
    }

    // ========== 删除(D) ==========
    
    // 单个删除
    @DeleteMapping(value = "/delete")
    public Result<?> delete(@RequestParam(name="id", required=true) String id) {
        jeecgDemoService.removeById(id);
        return Result.OK("删除成功!");
    }

    // 批量删除
    @DeleteMapping(value = "/deleteBatch")
    public Result<?> deleteBatch(@RequestParam(name="ids", required=true) String ids) {
        this.jeecgDemoService.removeByIds(Arrays.asList(ids.split(",")));
        return Result.OK("批量删除成功!");
    }
}

2.2 CRUD 各操作的实现公约

操作 核心方法 数据访问方式 关键注解
分页查询 QueryGenerator.initQueryWrapper() + service.page() MyBatis-Plus Page + QueryWrapper @PermissionData
ID查询 service.getById() MyBatis-Plus 内置方法 @RequiresPermissions
新增 service.save() MyBatis-Plus 内置方法 @AutoLog
修改 service.updateById() MyBatis-Plus 内置方法 @AutoLog
单个删除 service.removeById() 逻辑删除(del_flag @AutoLog
批量删除 service.removeByIds() 逻辑删除 -

2.3 复杂场景的 CRUD 变体

变体一:多租户隔离查询

// SysUserController 中的多租户隔离
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
    String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "-1");
    List<String> userIds = userTenantService.getUserIdsByTenantId(Integer.valueOf(tenantId));
    if (oConvertUtils.listIsNotEmpty(userIds)) {
        queryWrapper.in("id", userIds);
    }
}

变体二:复杂新增(关联表保存)

// SysUserController 中的用户新增
// 前端传 JSONObject,包含角色、部门等关联信息
SysUser user = JSON.parseObject(jsonObject.toJSONString(), SysUser.class);
String salt = oConvertUtils.randomGen(8);
user.setPassword(PasswordUtil.encrypt(user.getUsername(), user.getPassword(), salt));
sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, false);

变体三:安全修改(防空值覆盖)

// SysUserController 中的用户修改
SysUser sysUser = sysUserService.getById(jsonObject.getString("id"));  // 先查旧数据
SysUser user = JSON.parseObject(jsonObject.toJSONString(), SysUser.class);
oConvertUtils.copyNonNullFields(user, sysUser);  // 非空字段覆盖,避免空值覆盖
sysUserService.editUser(sysUser, roles, departs, relTenantIds, updateFromPage);

变体四:缓存清理删除

// SysDictController 中的字典删除
@CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true)
public Result<SysDict> delete(@RequestParam(name="id",required=true) String id) {
    sysDictService.removeById(id);
    return Result.ok("删除成功!");
}

变体五:主子表 CRUD

// JeecgOrderMainController 中的一对多新增
@PostMapping(value = "/add")
public Result<?> add(@RequestBody JeecgOrderMainPage vo) {
    JeecgOrderMain main = new JeecgOrderMain();
    BeanUtils.copyProperties(vo, main);
    jeecgOrderMainService.saveMain(main, vo.getJeecgOrderCustomerList(), vo.getJeecgOrderTicketList());
    return Result.ok("添加成功!");
}

三、QueryGenerator 查询条件自动生成

这是整个 CRUD 体系的核心引擎,实现了前端零配置即可完成复杂查询条件自动构建

3.1 核心入口

QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());

3.2 自动推断查询规则

QueryGenerator 通过分析请求参数的值特征,自动推断查询规则:

值特征 推断规则 示例
>= 值 大于等于 >= 100
<= 值 小于等于 <= 100
> 值 大于 > 100
< 值 小于 < 100
*值* 全模糊 LIKE *张*%张%
*值 左模糊 LIKE *张%张
值* 右模糊 LIKE 张*张%
值1,值2 IN 查询 1,2,3 → IN (1,2,3)
!值 不等于 !admin
普通值 等于 EQ admin

3.3 区间查询

通过 字段_begin字段_end 参数实现:

?createTime_begin=2026-01-01&createTime_end=2026-12-31

3.4 高级查询

通过 superQueryParams 参数传入 JSON 格式的复杂查询条件,支持 AND/OR 组合。

3.5 数据权限注入

通过 @PermissionData 注解 + PermissionDataAspect 切面,自动将数据权限规则注入到查询条件中。

3.6 排序支持

支持三种排序方式(按优先级):

  1. sortInfoString — 多字段排序(优先级最高)
  2. defSortString — 默认排序
  3. column + order — 单字段排序

四、数据导出(Export)实现模式

4.1 模式一:调用父类 exportXls(标准模式 — 最大公约数)

@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysUgroup sysUgroup) {
    return super.exportXls(request, sysUgroup, SysUgroup.class, "用户组表");
}

父类 JeecgController.exportXls 的核心流程

Step 1: QueryGenerator.initQueryWrapper() 组装查询条件
Step 2: selections 参数过滤选中数据
Step 3: service.list(queryWrapper) 获取导出数据
Step 4: 构建 ModelAndView(JeecgEntityExcelView)
        ├── FILE_NAME → 文件名
        ├── CLASS → 实体类(含 @Excel 注解定义列结构)
        ├── PARAMS → ExportParams(标题、Sheet名、格式)
        ├── DATA_LIST → 导出数据列表
        └── EXPORT_FIELDS → 可选,指定导出列

4.2 模式二:分 Sheet 导出

@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, JeecgDemo jeecgDemo) {
    String exportFields = jeecgDemoService.getExportFields();
    return super.exportXlsSheet(request, jeecgDemo, JeecgDemo.class, "单表模型", exportFields, 500);
}

4.3 模式三:大数据量导出

return super.exportXlsForBigData(request, entity, EntityClass.class, "文件名", 1000);

使用 IExcelExportServer 流式分页查询,避免 OOM。

4.4 模式四:主子表导出(手动实现)

@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysDict sysDict) {
    QueryWrapper<SysDict> queryWrapper = QueryGenerator.initQueryWrapper(sysDict, request.getParameterMap());
    List<SysDictPage> pageList = new ArrayList<>();
    List<SysDict> sysDictList = sysDictService.list(queryWrapper);
    for (SysDict dictMain : sysDictList) {
        SysDictPage vo = new SysDictPage();
        BeanUtils.copyProperties(dictMain, vo);
        // 查询子表数据
        vo.setSysDictItemList(sysDictItemService.selectItemsByMainId(dictMain.getId()));
        pageList.add(vo);
    }
    ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
    mv.addObject(NormalExcelConstants.FILE_NAME, "数据字典");
    mv.addObject(NormalExcelConstants.CLASS, SysDictPage.class);
    mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("数据字典列表", "导出人:" + user.getRealname(), "数据字典", ExcelType.XSSF));
    mv.addObject(NormalExcelConstants.DATA_LIST, pageList);
    return mv;
}

4.5 导出核心组件关系

@Excel 注解 (实体类/VO)
    ↓ 定义列结构
JeecgEntityExcelView (Spring MVC View)
    ↓ 渲染 Excel
ExportParams (导出参数)
    ↓ 标题/Sheet/格式/样式
NormalExcelConstants (常量)
    ↓ ModelAndView key
AutoPoiDictConfig (字典翻译)
    ↓ 导出时自动翻译字典值
最终输出 .xlsx 文件

五、数据导入(Import)实现模式

5.1 模式一:调用父类 importExcel(标准模式 — 最大公约数)

@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    return super.importExcel(request, response, JeecgDemo.class);
}

父类 JeecgController.importExcel 的核心流程

Step 1: 从 request 获取上传文件 (MultipartFile)
Step 2: 创建 ImportParams(titleRows=2, headRows=1, needSave=true)
Step 3: ExcelImportUtil.importExcel() 解析 Excel 为 List<T>
Step 4: service.saveBatch(list) 批量保存
Step 5: 返回 Result(成功行数 / 错误信息)

5.2 模式二:手动实现导入(主子表场景)

@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
        MultipartFile file = entity.getValue();
        ImportParams params = new ImportParams();
        params.setTitleRows(2);
        params.setHeadRows(2);  // 主子表时表头为2行
        params.setNeedSave(true);
        try {
            List<JeecgOrderMainPage> list = ExcelImportUtil.importExcel(file.getInputStream(), JeecgOrderMainPage.class, params);
            for (JeecgOrderMainPage page : list) {
                JeecgOrderMain po = new JeecgOrderMain();
                BeanUtils.copyProperties(page, po);
                jeecgOrderMainService.saveMain(po, page.getJeecgOrderCustomerList(), page.getJeecgOrderTicketList());
            }
            return Result.ok("文件导入成功!");
        } catch (Exception e) {
            return Result.error("文件导入失败:" + e.getMessage());
        }
    }
    return Result.error("文件导入失败!");
}

5.3 模式三:带校验和错误汇总的导入

// SysDictController 中的导入实现
List<SysDictPage> list = ExcelImportUtil.importExcel(file.getInputStream(), SysDictPage.class, params);
List<String> errorMessage = new ArrayList<>();
int successLines = 0, errorLines = 0;
for (int i = 0; i < list.size(); i++) {
    SysDict po = new SysDict();
    BeanUtils.copyProperties(list.get(i), po);
    try {
        Integer result = sysDictService.saveMain(po, list.get(i).getSysDictItemList());
        if (result > 0) {
            successLines++;
        } else if (result == -1) {
            errorLines++;
            errorMessage.add("字典项值不能为空,忽略导入。");
        } else {
            errorLines++;
            errorMessage.add("字典编码已存在,忽略导入。");
        }
    } catch (Exception e) {
        errorLines++;
        errorMessage.add("第 " + (i+1) + " 行:" + e.getMessage());
    }
}
return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage);

ImportExcelUtil.imporReturnRes 返回格式

{
    "success": true,
    "code": 201,
    "message": "文件导入成功,但有错误。",
    "result": {
        "totalCount": 100,
        "errorCount": 5,
        "successCount": 95,
        "msg": "总上传行数:100,已导入行数:95,错误行数:5",
        "fileUrl": "/sys/common/static/logs/20260601/userImportExcelErrorLogxxx.txt",
        "fileName": "userImportExcelErrorLogxxx.txt"
    }
}

5.4 导入核心组件关系

上传文件 (MultipartFile)
    ↓
ExcelImportUtil.importExcel()
    ↓ 解析为 List<T>
ImportParams (titleRows, headRows)
    ↓ 控制跳过行数
@Excel 注解 (实体类/VO)
    ↓ 列与字段的映射规则
AutoPoiDictConfig (字典翻译)
    ↓ 导入时文本自动转编码值
service.saveBatch() / 逐条 save
    ↓ 批量或逐条保存
ImportExcelUtil.imporReturnRes()
    ↓ 返回导入结果

六、下载导入模板的实现模式

6.1 核心结论:没有独立的"下载模板"接口

JeecgBoot 框架中,下载导入模板和导出数据共用同一个 exportXls 接口。当请求不带查询条件时,查询结果为空列表,AutoPoi 仍生成包含标题行和表头行的 Excel 文件——这就是"导入模板"。

6.2 模板 Excel 结构

┌──────────────────────────────────────────────────┐
│ 第1行: 大标题(如 "单表模型报表")                    │  ← titleRows=2 的第1行
├──────────────────────────────────────────────────┤
│ 第2行: 副标题(如 "导出人:管理员" / 导入规则说明)      │  ← titleRows=2 的第2行
├──────┬──────┬──────┬──────┬──────────────────────┤
│ 姓名 │关键词│打卡时间│ 工资 │ 性别 │ ...          │  ← headRows=1
├──────┼──────┼──────┼──────┼──────────────────────┤
│ (空) │ (空) │ (空) │ (空) │ (空) │ ...          │  ← 数据行(模板时为空)
└──────┴──────┴──────┴──────┴──────────────────────┘

6.3 导入参数与导出结构的对应关系

导出参数 导入参数 说明
ExportParams(title, secondTitle, sheetName) ImportParams.titleRows=2 标题占2行
@Excel 注解定义的列 ImportParams.headRows=1 表头占1行
主子表时 @ExcelCollection 增加的列 ImportParams.headRows=2 主子表表头占2行

6.4 用户管理导出的导入规则说明

SysUserController.exportXls 中,导出参数包含了详细的导入规则作为标题行:

ExportParams exportParams = new ExportParams(
    "导入规则:\n" +
    "1. 用户名为必填项,仅支持新增数据导入;\n" +
    "2. 多个部门、角色或负责部门请用英文分号 ; 分隔;\n" +
    "3. 部门层级请用英文斜杠 / 分隔;\n" +
    "4. 部门类型需与部门层级一致;\n" +
    "5. 部门根据用户名匹配,不存在时自动新增;\n" +
    "6. 负责部门与所属部门导入规则一致;\n" +
    "7. 用户主岗位导入时会在部门下自动创建新岗位。",
    "导出人:" + user.getRealname(),
    "导出信息"
);
exportParams.setTitleHeight((short)70);

七、@Excel 注解使用模式

7.1 注解属性速查表

属性 说明 示例
name 列标题(必填) @Excel(name = "姓名")
width 列宽 @Excel(width = 15)
format 日期格式 @Excel(format = "yyyy-MM-dd HH:mm:ss")
dicCode 字典编码 @Excel(dicCode = "sex")
dictTable 字典表名 @Excel(dictTable = "sys_depart")
dicText 字典文本字段 @Excel(dicText = "depart_name")
type 字段类型 1=文本(默认), 2=图片, 4=数值

7.2 八种使用模式

模式 注解写法 适用场景
基础文本 @Excel(name="名称", width=15) 普通字符串字段
日期格式 @Excel(name="时间", width=20, format="yyyy-MM-dd HH:mm:ss") Date 类型字段
字典翻译 @Excel(name="性别", width=15, dicCode="sex") 字典编码字段
表字典翻译 @Excel(name="部门", dictTable="sys_depart", dicText="depart_name", dicCode="id") 外键关联字段
图片导出 @Excel(name="头像", width=15, type=2) 图片URL字段
数值类型 @Excel(name="排序", width=15, type=4) 数值型字段
一对多 @ExcelCollection(name="子表") + 子实体 @Excel 主子表导出
不导出 不加 @Excel 注解 不需要导入导出的字段

八、最大规范公约数

8.1 定义

最大规范公约数 = 所有 CRUD + 导入导出实现中,最核心、最通用、最不可省略的代码模式和组件依赖。

8.2 公约数清单

第一层:框架基础设施(不可省略)

组件 作用 位置
JeecgController<T, S> Controller 基类,提供 exportXls/importExcel jeecg-boot-base-core
QueryGenerator 查询条件自动生成 jeecg-boot-base-core
Result<T> 统一返回结果封装 jeecg-boot-base-core
MybatisInterceptor 公共字段自动填充(createBy/createTime等) jeecg-boot-base-core
JeecgEntity 实体基类(id/createBy/createTime/updateBy/updateTime) jeecg-boot-base-core

第二层:CRUD 公约数(6 个方法)

// 最简 CRUD Controller 模板
@RestController
@RequestMapping("/模块/实体")
public class XxxController extends JeecgController<Xxx, IXxxService> {

    // 1. 分页列表
    @GetMapping("/list")
    public Result<?> list(Xxx entity, @RequestParam(defaultValue="1") Integer pageNo,
                          @RequestParam(defaultValue="10") Integer pageSize, HttpServletRequest req) {
        QueryWrapper<Xxx> qw = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
        return Result.OK(service.page(new Page<>(pageNo, pageSize), qw));
    }

    // 2. 新增
    @PostMapping("/add")
    public Result<?> add(@RequestBody Xxx entity) {
        service.save(entity);
        return Result.OK("添加成功!");
    }

    // 3. 修改
    @PutMapping("/edit")
    public Result<?> edit(@RequestBody Xxx entity) {
        service.updateById(entity);
        return Result.OK("更新成功!");
    }

    // 4. 删除
    @DeleteMapping("/delete")
    public Result<?> delete(@RequestParam String id) {
        service.removeById(id);
        return Result.OK("删除成功!");
    }

    // 5. 批量删除
    @DeleteMapping("/deleteBatch")
    public Result<?> deleteBatch(@RequestParam String ids) {
        service.removeByIds(Arrays.asList(ids.split(",")));
        return Result.OK("批量删除成功!");
    }

    // 6. ID查询
    @GetMapping("/queryById")
    public Result<?> queryById(@RequestParam String id) {
        return Result.OK(service.getById(id));
    }
}

第三层:导入导出公约数(2 个方法)

// 导出(同时作为下载模板接口)
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, Xxx entity) {
    return super.exportXls(request, entity, Xxx.class, "文件名");
}

// 导入
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    return super.importExcel(request, response, Xxx.class);
}

第四层:实体注解公约数

@Data
@TableName("表名")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Xxx extends JeecgEntity {
    
    @Excel(name = "字段名", width = 15)          // 需要导出/导入的字段
    private String field;
    
    @Excel(name = "字典字段", width = 15, dicCode = "dict_code")  // 字典翻译字段
    private Integer dictField;
    
    @Excel(name = "日期字段", width = 20, format = "yyyy-MM-dd HH:mm:ss")  // 日期字段
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date dateField;
    
    // 不需要导出的字段不加 @Excel 注解
    private String internalField;
}

8.3 公约数架构图

┌─────────────────────────────────────────────────────────────┐
│                    最大规范公约数                              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              实体层公约数                              │   │
│  │  • 继承 JeecgEntity(公共字段 + @Excel 注解)         │   │
│  │  • @TableName + @TableId(type=ASSIGN_ID)             │   │
│  │  • @Excel(name, width) 定义导出列                     │   │
│  │  • @Excel(dicCode) 字典翻译                           │   │
│  │  • @Excel(format) 日期格式                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Controller 层公约数                       │   │
│  │  • 继承 JeecgController<T, S>                        │   │
│  │  • list:   QueryGenerator + Page + service.page()    │   │
│  │  • add:    service.save()                            │   │
│  │  • edit:   service.updateById()                      │   │
│  │  • delete: service.removeById()                      │   │
│  │  • exportXls:  super.exportXls()                     │   │
│  │  • importExcel: super.importExcel()                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Service 层公约数                          │   │
│  │  • 接口: IXxxService extends IService<Xxx>           │   │
│  │  • 实现: XxxServiceImpl extends ServiceImpl<Mapper,  │   │
│  │          Xxx> implements IXxxService                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Mapper 层公约数                           │   │
│  │  • XxxMapper extends BaseMapper<Xxx>                 │   │
│  │  • XML: resources/xml/XxxMapper.xml(可选)           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              框架自动能力公约数                         │   │
│  │  • QueryGenerator: 查询条件自动生成                    │   │
│  │  • MybatisInterceptor: 公共字段自动填充                │   │
│  │  • DictAspect: 字典值自动翻译(@Dict)                 │   │
│  │  • AutoLogAspect: 操作日志自动记录(@AutoLog)         │   │
│  │  • AutoPoiDictConfig: Excel 字典自动翻译              │   │
│  │  • JeecgEntityExcelView: Excel 视图渲染               │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

8.4 开发一个新模块的最小步骤

基于最大公约数,开发一个带完整 CRUD + 导入导出的新模块,只需以下步骤:

  1. 创建实体类:继承 JeecgEntity,加 @TableName@Excel 注解
  2. 创建 Mapper:继承 BaseMapper<T>
  3. 创建 Service 接口:继承 IService<T>
  4. 创建 Service 实现:继承 ServiceImpl<Mapper, T>
  5. 创建 Controller:继承 JeecgController<T, IService<T>>,实现 6 个 CRUD 方法 + 2 个导入导出方法

无需额外开发的

  • 查询条件自动构建 → QueryGenerator 已实现
  • 公共字段自动填充 → MybatisInterceptor 已实现
  • 字典值自动翻译 → DictAspect + AutoPoiDictConfig 已实现
  • 操作日志自动记录 → AutoLogAspect 已实现
  • Excel 导出渲染 → JeecgEntityExcelView 已实现
  • Excel 导入解析 → ExcelImportUtil 已实现
  • 导入模板下载 → 复用 exportXls 接口

九、涉及的文件清单

| 文件 | 操作 | 原因 | | -------------------------------------- | -- | ------ | | AIWork/260601-湛江人社项目CRUD与导入导出模式分析.md | 新增 | 本次分析文档 |


十、关键源码索引

组件 文件路径
Controller 基类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java
实体基类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/entity/JeecgEntity.java
查询生成器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/query/QueryGenerator.java
导入工具 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/ImportExcelUtil.java
AutoPoi 配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/AutoPoiConfig.java
字典翻译配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/AutoPoiDictConfig.java
错误日志工具 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/PmsUtil.java
单表CRUD示例 jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgDemoController.java
主子表CRUD示例 jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgOrderMainController.java
用户管理Controller jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
字典管理Controller jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java

十一、文件上传实现模式

追加日期:2026-06-01

JeecgBoot 框架内置了完整的文件上传体系,支持本地存储、MinIO、阿里云 OSS 三种存储方式,通过配置切换。

11.1 核心上传控制器:CommonController

CommonController 是框架的通用文件上传控制器,路径前缀 /sys/common

接口 方法 HTTP方法 功能说明
/sys/common/upload upload() POST 文件上传统一方法,支持 local/minio/alioss 三种存储方式
/sys/common/static/** view() GET 预览图片 & 下载文件(仅本地存储模式生效),Shiro 配置为 anon 免认证
/sys/common/uploadImgByHttp uploadImgByHttp() POST 根据网络图片地址上传到服务器

上传接口核心逻辑:

@PostMapping(value = "/upload")
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String bizPath = request.getParameter("biz");
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    MultipartFile file = multipartRequest.getFile("file");
    SsrfFileTypeFilter.checkUploadFileType(file, bizPath);  // 文件安全校验
    if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
        savePath = this.uploadLocal(file,bizPath);  // 本地上传
    }else{
        savePath = CommonUtils.upload(file, bizPath, uploadType);  // 云存储上传
    }
}

静态文件服务核心逻辑:

@GetMapping(value = "/static/**")
public void view(HttpServletRequest request, HttpServletResponse response) {
    // 提取路径中 /** 匹配的部分
    String imgPath = extractPathFromPattern(request);
    imgPath = imgPath.replace("..", "");  // 路径遍历防护
    SsrfFileTypeFilter.checkDownloadFileType(imgPath);  // 下载类型校验
    // 读取本地文件 -> 流式输出
}

返回格式:

{
    "success": true,
    "message": "temp/20260601/xxxx.jpg",
    "code": 200
}

message 字段为文件的相对路径,前端据此拼接完整访问 URL。

11.2 其他上传控制器

控制器 路径前缀 功能
SysUploadController /sys/upload MinIO 专用上传,上传后保存文件信息到 oss_file
OssFileController /sys/oss/file OSS 文件管理(上传/列表/删除),需要 system:ossFile:upload 权限

11.3 上传工具类(三层架构)

上传路由层:CommonUtils

CommonUtils 根据 uploadType 自动选择存储方式:

方法签名 功能
upload(MultipartFile, String bizPath, String uploadType) 根据 uploadType 自动选择 MinIO 或阿里云 OSS
uploadLocal(MultipartFile, String bizPath, String uploadpath) 本地文件上传(含路径遍历防护)
getFileName(String fileName) 文件名安全处理(去盘符、特殊字符、空格等)

存储实现层:MinioUtil / OssBootUtil

工具类 文件路径 功能
MinioUtil jeecg-boot-base-core/.../util/MinioUtil.java MinIO 上传/下载/删除/预签名 URL
OssBootUtil jeecg-boot-base-core/.../util/oss/OssBootUtil.java 阿里云 OSS 上传/下载/删除/预签名 URL

11.4 安全校验:SsrfFileTypeFilter

SsrfFileTypeFilter 提供上传/下载安全校验:

方法 功能
checkUploadFileType(MultipartFile, String customPath) 上传文件类型过滤(后缀白名单 + 文件头校验 + 路径安全校验)
checkDownloadFileType(String filePath) 下载文件类型过滤
validatePathSecurity(String customPath) 路径安全校验(防 ..、限制深度5层、限制字符集)

文件类型白名单: jpg, jpeg, png, gif, bmp, svg, ico, heic, txt, doc, docx, pdf, csv, md, mp4, avi, mov, wmv, mp3, wav, xls, xlsx, zip, rar, 7z, tar, apk, wgt, ppt, pptx

文件头黑名单检测: jsp, php, class, sql

11.5 上传配置参数

YAML 配置application-dev.yml):

jeecg:
  uploadType: local          # 本地:local、Minio:minio、阿里云:alioss
  path:
    upload: /opt/upFiles     # 文件上传根目录(本地存储模式)
    webapp: /opt/webapp      # webapp文件路径

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

上传类型常量(CommonConstant.java):

String UPLOAD_TYPE_LOCAL = "local";
String UPLOAD_TYPE_MINIO = "minio";
String UPLOAD_TYPE_OSS = "alioss";

11.6 静态资源服务配置

WebMvcConfiguration.java 的 addResourceHandlers() 方法配置了静态资源映射,使上传目录中的文件可通过 Spring 静态资源机制直接访问:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
        .addResourceLocations("file:" + jeecgBaseConfig.getPath().getUpload() + "//")
        .addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
}

11.7 Shiro 认证配置

文件上传接口 /sys/common/upload 需要 Token 认证。文件预览/下载接口 /sys/common/static/** 在 ShiroConfig.java 中配置为 anon(匿名访问):

filterChainDefinitionMap.put("/sys/common/static/**", "anon");  // 图片预览 & 下载文件不限制token

此外,框架还支持 @IgnoreAuth 注解实现方法级别的免认证控制,标注该注解的接口方法会自动免 Token 认证。

11.8 整体上传流程架构

上传请求流程:
  前端 POST /sys/common/upload (file + biz)
      |
      v
  CommonController.upload()
      |
      +-- SsrfFileTypeFilter.checkUploadFileType()  // 安全校验
      |
      +-- uploadType == "local" ?
      |     YES --> CommonController.uploadLocal()   // 本地存储 → /opt/upFiles
      |     NO  --> CommonUtils.upload()             // 云存储路由
      |               |
      |               +-- uploadType == "minio" ?
      |               |     YES --> MinioUtil.upload()
      |               |     NO  --> OssBootUtil.upload()  // 阿里云OSS
      |
      v
  返回 Result(success=true, message=相对路径)

文件访问流程(本地存储模式):
  GET /sys/common/static/{相对路径}
      |
      v
  CommonController.view()
      |
      +-- extractPathFromPattern()   // 提取路径
      +-- 路径遍历防护 (replace "..")
      +-- SsrfFileTypeFilter.checkDownloadFileType()  // 下载类型校验
      +-- 读取本地文件 -> 流式输出

11.9 自定义上传控制器的场景

当业务需要独立的文件存储路径(如 D:\sitefile\zjrs 而非框架默认的 /opt/upFiles)时,需要自定义上传控制器。此时需要注意:

  1. 上传接口:参照 CommonController.upload() 的模式,接收 MultipartFile,保存到自定义目录,返回 Result 格式(message 为相对路径)
  2. 静态文件服务接口:参照 CommonController.view() 的模式,提供文件流输出
  3. 认证配置:使用 @IgnoreAuth 注解让静态文件服务接口免认证,或通过 jeecg.shiro.excludeUrls 配置
  4. 安全防护:必须包含路径遍历防护(过滤 .. 字符)

11.10 关键源码索引(文件上传)

组件 文件路径
通用上传控制器 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java
MinIO上传控制器 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java
OSS文件管理控制器 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/oss/controller/OssFileController.java
上传路由工具 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java
MinIO工具 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java
OSS工具 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java
安全校验过滤器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java
静态资源配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java
Shiro配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
免认证注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/IgnoreAuth.java
上传类型常量 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java
文件上传DTO jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/api/dto/FileUploadDTO.java