分析日期:2026-06-01\ 项目版本:JeecgBoot 3.9.2
本文档深入分析湛江人社项目中增删改查(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("批量删除成功!");
}
}
| 操作 | 核心方法 | 数据访问方式 | 关键注解 |
|---|---|---|---|
| 分页查询 | 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() |
逻辑删除 | - |
// 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("删除成功!");
}
// 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("添加成功!");
}
这是整个 CRUD 体系的核心引擎,实现了前端零配置即可完成复杂查询条件自动构建。
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());
QueryGenerator 通过分析请求参数的值特征,自动推断查询规则:
| 值特征 | 推断规则 | 示例 |
|---|---|---|
>= 值 |
大于等于 | >= 100 |
<= 值 |
小于等于 | <= 100 |
> 值 |
大于 | > 100 |
< 值 |
小于 | < 100 |
*值* |
全模糊 LIKE | *张* → %张% |
*值 |
左模糊 LIKE | *张 → %张 |
值* |
右模糊 LIKE | 张* → 张% |
值1,值2 |
IN 查询 | 1,2,3 → IN (1,2,3) |
!值 |
不等于 | !admin |
| 普通值 | 等于 EQ | admin |
通过 字段_begin 和 字段_end 参数实现:
?createTime_begin=2026-01-01&createTime_end=2026-12-31
通过 superQueryParams 参数传入 JSON 格式的复杂查询条件,支持 AND/OR 组合。
通过 @PermissionData 注解 + PermissionDataAspect 切面,自动将数据权限规则注入到查询条件中。
支持三种排序方式(按优先级):
sortInfoString — 多字段排序(优先级最高)defSortString — 默认排序column + order — 单字段排序@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 → 可选,指定导出列
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, JeecgDemo jeecgDemo) {
String exportFields = jeecgDemoService.getExportFields();
return super.exportXlsSheet(request, jeecgDemo, JeecgDemo.class, "单表模型", exportFields, 500);
}
return super.exportXlsForBigData(request, entity, EntityClass.class, "文件名", 1000);
使用 IExcelExportServer 流式分页查询,避免 OOM。
@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;
}
@Excel 注解 (实体类/VO)
↓ 定义列结构
JeecgEntityExcelView (Spring MVC View)
↓ 渲染 Excel
ExportParams (导出参数)
↓ 标题/Sheet/格式/样式
NormalExcelConstants (常量)
↓ ModelAndView key
AutoPoiDictConfig (字典翻译)
↓ 导出时自动翻译字典值
最终输出 .xlsx 文件
@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(成功行数 / 错误信息)
@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("文件导入失败!");
}
// 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"
}
}
上传文件 (MultipartFile)
↓
ExcelImportUtil.importExcel()
↓ 解析为 List<T>
ImportParams (titleRows, headRows)
↓ 控制跳过行数
@Excel 注解 (实体类/VO)
↓ 列与字段的映射规则
AutoPoiDictConfig (字典翻译)
↓ 导入时文本自动转编码值
service.saveBatch() / 逐条 save
↓ 批量或逐条保存
ImportExcelUtil.imporReturnRes()
↓ 返回导入结果
JeecgBoot 框架中,下载导入模板和导出数据共用同一个 exportXls 接口。当请求不带查询条件时,查询结果为空列表,AutoPoi 仍生成包含标题行和表头行的 Excel 文件——这就是"导入模板"。
┌──────────────────────────────────────────────────┐
│ 第1行: 大标题(如 "单表模型报表") │ ← titleRows=2 的第1行
├──────────────────────────────────────────────────┤
│ 第2行: 副标题(如 "导出人:管理员" / 导入规则说明) │ ← titleRows=2 的第2行
├──────┬──────┬──────┬──────┬──────────────────────┤
│ 姓名 │关键词│打卡时间│ 工资 │ 性别 │ ... │ ← headRows=1
├──────┼──────┼──────┼──────┼──────────────────────┤
│ (空) │ (空) │ (空) │ (空) │ (空) │ ... │ ← 数据行(模板时为空)
└──────┴──────┴──────┴──────┴──────────────────────┘
| 导出参数 | 导入参数 | 说明 |
|---|---|---|
ExportParams(title, secondTitle, sheetName) |
ImportParams.titleRows=2 |
标题占2行 |
@Excel 注解定义的列 |
ImportParams.headRows=1 |
表头占1行 |
主子表时 @ExcelCollection 增加的列 |
ImportParams.headRows=2 |
主子表表头占2行 |
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);
| 属性 | 说明 | 示例 |
|---|---|---|
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=数值 |
| 模式 | 注解写法 | 适用场景 |
|---|---|---|
| 基础文本 | @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 注解 |
不需要导入导出的字段 |
最大规范公约数 = 所有 CRUD + 导入导出实现中,最核心、最通用、最不可省略的代码模式和组件依赖。
| 组件 | 作用 | 位置 |
|---|---|---|
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 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));
}
}
// 导出(同时作为下载模板接口)
@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;
}
┌─────────────────────────────────────────────────────────────┐
│ 最大规范公约数 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 实体层公约数 │ │
│ │ • 继承 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 视图渲染 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
基于最大公约数,开发一个带完整 CRUD + 导入导出的新模块,只需以下步骤:
JeecgEntity,加 @TableName、@Excel 注解BaseMapper<T>IService<T>ServiceImpl<Mapper, T>JeecgController<T, IService<T>>,实现 6 个 CRUD 方法 + 2 个导入导出方法无需额外开发的:
QueryGenerator 已实现MybatisInterceptor 已实现DictAspect + AutoPoiDictConfig 已实现AutoLogAspect 已实现JeecgEntityExcelView 已实现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 三种存储方式,通过配置切换。
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。
| 控制器 | 路径前缀 | 功能 |
|---|---|---|
| SysUploadController | /sys/upload |
MinIO 专用上传,上传后保存文件信息到 oss_file 表 |
| OssFileController | /sys/oss/file |
OSS 文件管理(上传/列表/删除),需要 system:ossFile:upload 权限 |
CommonUtils 根据 uploadType 自动选择存储方式:
| 方法签名 | 功能 |
|---|---|
upload(MultipartFile, String bizPath, String uploadType) |
根据 uploadType 自动选择 MinIO 或阿里云 OSS |
uploadLocal(MultipartFile, String bizPath, String uploadpath) |
本地文件上传(含路径遍历防护) |
getFileName(String fileName) |
文件名安全处理(去盘符、特殊字符、空格等) |
| 工具类 | 文件路径 | 功能 |
|---|---|---|
| MinioUtil | jeecg-boot-base-core/.../util/MinioUtil.java |
MinIO 上传/下载/删除/预签名 URL |
| OssBootUtil | jeecg-boot-base-core/.../util/oss/OssBootUtil.java |
阿里云 OSS 上传/下载/删除/预签名 URL |
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
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";
WebMvcConfiguration.java 的 addResourceHandlers() 方法配置了静态资源映射,使上传目录中的文件可通过 Spring 静态资源机制直接访问:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getUpload() + "//")
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
}
文件上传接口 /sys/common/upload 需要 Token 认证。文件预览/下载接口 /sys/common/static/** 在 ShiroConfig.java 中配置为 anon(匿名访问):
filterChainDefinitionMap.put("/sys/common/static/**", "anon"); // 图片预览 & 下载文件不限制token
此外,框架还支持 @IgnoreAuth 注解实现方法级别的免认证控制,标注该注解的接口方法会自动免 Token 认证。
上传请求流程:
前端 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() // 下载类型校验
+-- 读取本地文件 -> 流式输出
当业务需要独立的文件存储路径(如 D:\sitefile\zjrs 而非框架默认的 /opt/upFiles)时,需要自定义上传控制器。此时需要注意:
CommonController.upload() 的模式,接收 MultipartFile,保存到自定义目录,返回 Result 格式(message 为相对路径)CommonController.view() 的模式,提供文件流输出@IgnoreAuth 注解让静态文件服务接口免认证,或通过 jeecg.shiro.excludeUrls 配置.. 字符)| 组件 | 文件路径 |
|---|---|
| 通用上传控制器 | 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 |