# CRUD与导入导出代码实现模式分析 > 分析日期:2026-06-01\ > 项目版本:JeecgBoot 3.9.2 *** ## 一、概述 本文档深入分析湛江人社项目中**增删改查(CRUD)**、**数据导入导出**、**下载导入模板**等功能的代码实现方式,并提炼出这些功能的**最大规范公约数**——即所有实现中共享的最核心、最通用的代码模式。 *** ## 二、增删改查(CRUD)实现模式 ### 2.1 标准单表 CRUD(最大公约数模式) 这是项目中最常见、最规范的实现方式,以 [JeecgDemoController](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgDemoController.java) 为代表: ```java @Slf4j @RestController @RequestMapping("/test/jeecgDemo") public class JeecgDemoController extends JeecgController { // ========== 查询(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 queryWrapper = QueryGenerator.initQueryWrapper(jeecgDemo, req.getParameterMap()); Page page = new Page<>(pageNo, pageSize); IPage 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 变体 #### 变体一:多租户隔离查询 ```java // SysUserController 中的多租户隔离 if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "-1"); List userIds = userTenantService.getUserIdsByTenantId(Integer.valueOf(tenantId)); if (oConvertUtils.listIsNotEmpty(userIds)) { queryWrapper.in("id", userIds); } } ``` #### 变体二:复杂新增(关联表保存) ```java // 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); ``` #### 变体三:安全修改(防空值覆盖) ```java // 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); ``` #### 变体四:缓存清理删除 ```java // SysDictController 中的字典删除 @CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) public Result delete(@RequestParam(name="id",required=true) String id) { sysDictService.removeById(id); return Result.ok("删除成功!"); } ``` #### 变体五:主子表 CRUD ```java // 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 核心入口 ```java QueryWrapper 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(标准模式 — 最大公约数) ```java @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 导出 ```java @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 模式三:大数据量导出 ```java return super.exportXlsForBigData(request, entity, EntityClass.class, "文件名", 1000); ``` 使用 `IExcelExportServer` 流式分页查询,避免 OOM。 ### 4.4 模式四:主子表导出(手动实现) ```java @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, SysDict sysDict) { QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDict, request.getParameterMap()); List pageList = new ArrayList<>(); List 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(标准模式 — 最大公约数) ```java @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 Step 4: service.saveBatch(list) 批量保存 Step 5: 返回 Result(成功行数 / 错误信息) ``` ### 5.2 模式二:手动实现导入(主子表场景) ```java @RequestMapping(value = "/importExcel", method = RequestMethod.POST) public Result importExcel(HttpServletRequest request, HttpServletResponse response) { MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; Map fileMap = multipartRequest.getFileMap(); for (Map.Entry entity : fileMap.entrySet()) { MultipartFile file = entity.getValue(); ImportParams params = new ImportParams(); params.setTitleRows(2); params.setHeadRows(2); // 主子表时表头为2行 params.setNeedSave(true); try { List 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 模式三:带校验和错误汇总的导入 ```java // SysDictController 中的导入实现 List list = ExcelImportUtil.importExcel(file.getInputStream(), SysDictPage.class, params); List 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`** **返回格式**: ```json { "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 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](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java) 中,导出参数包含了详细的导入规则作为标题行: ```java 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` | Controller 基类,提供 exportXls/importExcel | jeecg-boot-base-core | | `QueryGenerator` | 查询条件自动生成 | jeecg-boot-base-core | | `Result` | 统一返回结果封装 | jeecg-boot-base-core | | `MybatisInterceptor` | 公共字段自动填充(createBy/createTime等) | jeecg-boot-base-core | | `JeecgEntity` | 实体基类(id/createBy/createTime/updateBy/updateTime) | jeecg-boot-base-core | #### 第二层:CRUD 公约数(6 个方法) ```java // 最简 CRUD Controller 模板 @RestController @RequestMapping("/模块/实体") public class XxxController extends JeecgController { // 1. 分页列表 @GetMapping("/list") public Result list(Xxx entity, @RequestParam(defaultValue="1") Integer pageNo, @RequestParam(defaultValue="10") Integer pageSize, HttpServletRequest req) { QueryWrapper 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 个方法) ```java // 导出(同时作为下载模板接口) @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); } ``` #### 第四层:实体注解公约数 ```java @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 │ │ │ │ • list: QueryGenerator + Page + service.page() │ │ │ │ • add: service.save() │ │ │ │ • edit: service.updateById() │ │ │ │ • delete: service.removeById() │ │ │ │ • exportXls: super.exportXls() │ │ │ │ • importExcel: super.importExcel() │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Service 层公约数 │ │ │ │ • 接口: IXxxService extends IService │ │ │ │ • 实现: XxxServiceImpl extends ServiceImpl implements IXxxService │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Mapper 层公约数 │ │ │ │ • XxxMapper extends BaseMapper │ │ │ │ • 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` 3. **创建 Service 接口**:继承 `IService` 4. **创建 Service 实现**:继承 `ServiceImpl` 5. **创建 Controller**:继承 `JeecgController>`,实现 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](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java) 是框架的通用文件上传控制器,路径前缀 `/sys/common`。 | 接口 | 方法 | HTTP方法 | 功能说明 | |------|------|----------|----------| | `/sys/common/upload` | `upload()` | POST | 文件上传统一方法,支持 local/minio/alioss 三种存储方式 | | `/sys/common/static/**` | `view()` | GET | 预览图片 & 下载文件(仅本地存储模式生效),Shiro 配置为 `anon` 免认证 | | `/sys/common/uploadImgByHttp` | `uploadImgByHttp()` | POST | 根据网络图片地址上传到服务器 | **上传接口核心逻辑:** ```java @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); // 云存储上传 } } ``` **静态文件服务核心逻辑:** ```java @GetMapping(value = "/static/**") public void view(HttpServletRequest request, HttpServletResponse response) { // 提取路径中 /** 匹配的部分 String imgPath = extractPathFromPattern(request); imgPath = imgPath.replace("..", ""); // 路径遍历防护 SsrfFileTypeFilter.checkDownloadFileType(imgPath); // 下载类型校验 // 读取本地文件 -> 流式输出 } ``` **返回格式:** ```json { "success": true, "message": "temp/20260601/xxxx.jpg", "code": 200 } ``` `message` 字段为文件的相对路径,前端据此拼接完整访问 URL。 ### 11.2 其他上传控制器 | 控制器 | 路径前缀 | 功能 | |--------|----------|------| | [SysUploadController](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java) | `/sys/upload` | MinIO 专用上传,上传后保存文件信息到 `oss_file` 表 | | [OssFileController](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/oss/controller/OssFileController.java) | `/sys/oss/file` | OSS 文件管理(上传/列表/删除),需要 `system:ossFile:upload` 权限 | ### 11.3 上传工具类(三层架构) #### 上传路由层:CommonUtils [CommonUtils](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java) 根据 `uploadType` 自动选择存储方式: | 方法签名 | 功能 | |----------|------| | `upload(MultipartFile, String bizPath, String uploadType)` | 根据 uploadType 自动选择 MinIO 或阿里云 OSS | | `uploadLocal(MultipartFile, String bizPath, String uploadpath)` | 本地文件上传(含路径遍历防护) | | `getFileName(String fileName)` | 文件名安全处理(去盘符、特殊字符、空格等) | #### 存储实现层:MinioUtil / OssBootUtil | 工具类 | 文件路径 | 功能 | |--------|----------|------| | [MinioUtil](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java) | `jeecg-boot-base-core/.../util/MinioUtil.java` | MinIO 上传/下载/删除/预签名 URL | | [OssBootUtil](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java) | `jeecg-boot-base-core/.../util/oss/OssBootUtil.java` | 阿里云 OSS 上传/下载/删除/预签名 URL | ### 11.4 安全校验:SsrfFileTypeFilter [SsrfFileTypeFilter](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java) 提供上传/下载安全校验: | 方法 | 功能 | |------|------| | `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`): ```yaml 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](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java)): ```java String UPLOAD_TYPE_LOCAL = "local"; String UPLOAD_TYPE_MINIO = "minio"; String UPLOAD_TYPE_OSS = "alioss"; ``` ### 11.6 静态资源服务配置 [WebMvcConfiguration.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java) 的 `addResourceHandlers()` 方法配置了静态资源映射,使上传目录中的文件可通过 Spring 静态资源机制直接访问: ```java @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](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java) 中配置为 `anon`(匿名访问): ```java 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` |