# 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` | *** ## 十二、主子表(Master-Detail)CRUD 实现模式 > 追加日期:2026-06-02 ### 12.1 整体架构概览 JeecgBoot 中主子表的实现采用经典的 **一对多(OneToMany)** 模式,核心思路是: - **主表实体** 和 **子表实体** 分别独立定义,子表通过 **外键字段** 关联主表 - **VO/Page类** 将主表字段与子表列表组合在一起,用于前后端数据传输 - **Service层** 负责事务管理,统一处理主表和子表的增删改 - **Mapper层** 提供通过外键查询/删除子表数据的专用方法 项目中存在两个完整的主子表示例: 1. **Demo模块** — `JeecgOrderMain`(订单主表) + `JeecgOrderCustomer`(客户子表) + `JeecgOrderTicket`(机票子表) 2. **系统模块** — `SysDict`(字典主表) + `SysDictItem`(字典项子表) > **注意**:湛江人社业务模块(`jeecg-module-zjrs`)当前不存在标准的主子表实现,所有业务均使用单表 CRUD 模式。如需在 zjrs > 模块中实现主子表功能,应参照本节所述的标准模式。 ### 12.2 实体类定义 — 主子表关系 #### 主表实体 [JeecgOrderMain.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgOrderMain.java) — 主表实体与普通单表实体无异: ```java @Data @TableName("jeecg_order_main") public class JeecgOrderMain implements Serializable { @TableId(type = IdType.ASSIGN_ID) private String id; private String orderCode; private String ctype; @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date orderDate; private Double orderMoney; private String content; private String bpmStatus; // ... 审计字段 createBy, createTime, updateBy, updateTime } ``` **关键点**:主表实体**不包含子表列表引用**,与单表实体结构完全一致。 #### 子表实体 [JeecgOrderCustomer.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgOrderCustomer.java) — 子表通过外键关联主表: ```java @Data @TableName("jeecg_order_customer") public class JeecgOrderCustomer implements Serializable { @TableId(type = IdType.ASSIGN_ID) private String id; @Excel(name = "客户名字", width = 15) private String name; private String sex; @Excel(name = "身份证号码", width = 15) private String idcard; /**外键*/ private String orderId; // <--- 关键:外键字段关联主表 // ... 审计字段 } ``` **关键点**:子表实体通过 **`orderId` 外键字段** 关联主表,这是主子表关系的核心纽带。外键字段命名规范为 `{主表实体名小写}Id` ,如 `orderId`。 #### VO/Page类 — 主子表数据组合 [JeecgOrderMainPage.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/vo/JeecgOrderMainPage.java) — 将主表字段和子表列表组合在一起: ```java @Data public class JeecgOrderMainPage { private String id; @Excel(name = "订单号", width = 15) private String orderCode; private String ctype; @Excel(name = "订单日期", width = 15, format = "yyyy-MM-dd") private Date orderDate; @Excel(name = "订单金额", width = 15) private Double orderMoney; private String content; private String bpmStatus; @ExcelCollection(name = "客户") // <--- 关键注解:标记子表集合 private List jeecgOrderCustomerList; @ExcelCollection(name = "机票") // <--- 关键注解:标记子表集合 private List jeecgOrderTicketList; } ``` **关键点**: - `@ExcelCollection` 注解是 AutoPoi 的注解,用于 Excel 导入导出时识别子表数据 - VO类将主表字段和多个子表列表组合在一起,是前后端交互的数据传输对象 - Controller层接收此VO对象,然后通过 `BeanUtils.copyProperties` 拆分为主表实体 ### 12.3 Controller层 — 主子表增删改查 [JeecgOrderMainController.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgOrderMainController.java) #### 新增(主表+子表一起保存) ```java @PostMapping(value = "/add") public Result add(@RequestBody JeecgOrderMainPage jeecgOrderMainPage) { JeecgOrderMain jeecgOrderMain = new JeecgOrderMain(); BeanUtils.copyProperties(jeecgOrderMainPage, jeecgOrderMain); // VO -> 主表实体 jeecgOrderMainService.saveMain(jeecgOrderMain, jeecgOrderMainPage.getJeecgOrderCustomerList(), jeecgOrderMainPage.getJeecgOrderTicketList()); return Result.ok("添加成功!"); } ``` #### 编辑 ```java @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) public Result eidt(@RequestBody JeecgOrderMainPage jeecgOrderMainPage) { JeecgOrderMain jeecgOrderMain = new JeecgOrderMain(); BeanUtils.copyProperties(jeecgOrderMainPage, jeecgOrderMain); jeecgOrderMainService.updateCopyMain(jeecgOrderMain, jeecgOrderMainPage.getJeecgOrderCustomerList(), jeecgOrderMainPage.getJeecgOrderTicketList()); return Result.ok("编辑成功!"); } ``` #### 删除(主表+子表一起删除) ```java @DeleteMapping(value = "/delete") public Result delete(@RequestParam(name = "id", required = true) String id) { jeecgOrderMainService.delMain(id); return Result.ok("删除成功!"); } @DeleteMapping(value = "/deleteBatch") public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { this.jeecgOrderMainService.delBatchMain(Arrays.asList(ids.split(","))); return Result.ok("批量删除成功!"); } ``` #### 查询(主表和子表分开查询) ```java // 查询主表 @GetMapping(value = "/queryById") public Result queryById(@RequestParam(name = "id", required = true) String id) { JeecgOrderMain jeecgOrderMain = jeecgOrderMainService.getById(id); return Result.ok(jeecgOrderMain); } // 查询子表 — 客户(每个子表一个独立接口) @GetMapping(value = "/queryOrderCustomerListByMainId") public Result queryOrderCustomerListByMainId(@RequestParam(name = "id", required = true) String id) { List list = jeecgOrderCustomerService.selectCustomersByMainId(id); return Result.ok(list); } // 查询子表 — 机票 @GetMapping(value = "/queryOrderTicketListByMainId") public Result queryOrderTicketListByMainId(@RequestParam(name = "id", required = true) String id) { List list = jeecgOrderTicketService.selectTicketsByMainId(id); return Result.ok(list); } ``` #### Excel导出(主子表一起导出) ```java @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, JeecgOrderMain jeecgOrderMain) { QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(jeecgOrderMain, request.getParameterMap()); List pageList = new ArrayList<>(); List jeecgOrderMainList = jeecgOrderMainService.list(queryWrapper); for (JeecgOrderMain orderMain : jeecgOrderMainList) { JeecgOrderMainPage vo = new JeecgOrderMainPage(); BeanUtils.copyProperties(orderMain, vo); // 查询子表数据并设置到VO vo.setJeecgOrderTicketList(jeecgOrderTicketService.selectTicketsByMainId(orderMain.getId())); vo.setJeecgOrderCustomerList(jeecgOrderCustomerService.selectCustomersByMainId(orderMain.getId())); pageList.add(vo); } ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); mv.addObject(NormalExcelConstants.FILE_NAME, "订单"); mv.addObject(NormalExcelConstants.CLASS, JeecgOrderMainPage.class); mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("订单列表", "导出人:", "订单")); mv.addObject(NormalExcelConstants.DATA_LIST, pageList); return mv; } ``` ### 12.4 Service层 — 事务和业务逻辑 #### 主表Service接口 [IJeecgOrderMainService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/IJeecgOrderMainService.java) — 在标准 `IService` 基础上扩展主子表操作方法: ```java public interface IJeecgOrderMainService extends IService { /** 添加一对多 */ void saveMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList); /** 修改一对多(先删后插) */ void updateMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList); /** 修改一对多(增量更新) */ void updateCopyMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList); /** 删除一对多 */ void delMain(String id); /** 批量删除一对多 */ void delBatchMain(Collection idList); } ``` #### 主表Service实现 [JeecgOrderMainServiceImpl.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/impl/JeecgOrderMainServiceImpl.java) — 所有主子表操作方法都标注了 `@Transactional`: **新增 — 先插主表,再循环插子表:** ```java @Override @Transactional(rollbackFor = Exception.class) public void saveMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList) { jeecgOrderMainMapper.insert(jeecgOrderMain); if (jeecgOrderCustomerList != null) { for (JeecgOrderCustomer entity : jeecgOrderCustomerList) { entity.setOrderId(jeecgOrderMain.getId()); // 设置外键 jeecgOrderCustomerMapper.insert(entity); } } if (jeecgOrderTicketList != null) { for (JeecgOrderTicket entity : jeecgOrderTicketList) { entity.setOrderId(jeecgOrderMain.getId()); // 设置外键 jeecgOrderTicketMapper.insert(entity); } } } ``` **修改策略一 — 先删后插(updateMain):** ```java @Override @Transactional(rollbackFor = Exception.class) public void updateMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList) { jeecgOrderMainMapper.updateById(jeecgOrderMain); // 1.先删除子表数据 jeecgOrderTicketMapper.deleteTicketsByMainId(jeecgOrderMain.getId()); jeecgOrderCustomerMapper.deleteCustomersByMainId(jeecgOrderMain.getId()); // 2.子表数据重新插入 if (jeecgOrderCustomerList != null) { for (JeecgOrderCustomer entity : jeecgOrderCustomerList) { entity.setOrderId(jeecgOrderMain.getId()); jeecgOrderCustomerMapper.insert(entity); } } if (jeecgOrderTicketList != null) { for (JeecgOrderTicket entity : jeecgOrderTicketList) { entity.setOrderId(jeecgOrderMain.getId()); jeecgOrderTicketMapper.insert(entity); } } } ``` **修改策略二 — 增量更新/差异对比(updateCopyMain):** ```java @Override @Transactional(rollbackFor = Exception.class) public void updateCopyMain(JeecgOrderMain jeecgOrderMain, List jeecgOrderCustomerList, List jeecgOrderTicketList) { jeecgOrderMainMapper.updateById(jeecgOrderMain); // 循环前台传过来的数据 for (JeecgOrderTicket ticket : jeecgOrderTicketList) { JeecgOrderTicket orderTicket = jeecgOrderTicketMapper.selectById(ticket.getId()); if (orderTicket == null) { // 数据库不存在 -> 新增 ticket.setOrderId(jeecgOrderMain.getId()); jeecgOrderTicketMapper.insert(ticket); continue; } if (orderTicket.getId().equals(ticket.getId())) { // 数据库存在 -> 更新 jeecgOrderTicketMapper.updateById(ticket); } } // ... 客户子表同理 ... // 取差集:数据库有但前端没传的 -> 删除 List dbTickets = jeecgOrderTicketMapper.selectTicketsByMainId(jeecgOrderMain.getId()); List toDelete = dbTickets.stream() .filter(item -> !jeecgOrderTicketList.stream() .map(e -> e.getId()) .collect(Collectors.toList()) .contains(item.getId())) .collect(Collectors.toList()); for (JeecgOrderTicket ticket : toDelete) { jeecgOrderTicketMapper.deleteById(ticket.getId()); } // ... 客户子表同理 ... } ``` **删除 — 先删子表,再删主表:** ```java @Override @Transactional(rollbackFor = Exception.class) public void delMain(String id) { jeecgOrderMainMapper.deleteById(id); jeecgOrderTicketMapper.deleteTicketsByMainId(id); jeecgOrderCustomerMapper.deleteCustomersByMainId(id); } @Override @Transactional(rollbackFor = Exception.class) public void delBatchMain(Collection idList) { for (Serializable id : idList) { jeecgOrderMainMapper.deleteById(id); jeecgOrderTicketMapper.deleteTicketsByMainId(id.toString()); jeecgOrderCustomerMapper.deleteCustomersByMainId(id.toString()); } } ``` #### 子表Service接口和实现 [IJeecgOrderCustomerService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/IJeecgOrderCustomerService.java) — 子表Service在标准 `IService` 基础上增加按外键查询方法: ```java public interface IJeecgOrderCustomerService extends IService { /** 根据订单id获取订单客户数据 */ List selectCustomersByMainId(String mainId); } ``` ```java @Service public class JeecgOrderCustomerServiceImpl extends ServiceImpl implements IJeecgOrderCustomerService { @Autowired private JeecgOrderCustomerMapper jeecgOrderCustomerMapper; @Override public List selectCustomersByMainId(String mainId) { return jeecgOrderCustomerMapper.selectCustomersByMainId(mainId); } } ``` ### 12.5 Mapper层 — 主子表数据操作 #### 主表Mapper(标准MyBatis-Plus) [JeecgOrderMainMapper.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/mapper/JeecgOrderMainMapper.java) — 主表无需自定义方法: ```java public interface JeecgOrderMainMapper extends BaseMapper { // 使用 BaseMapper 提供的 insert/updateById/deleteById/selectById 等 } ``` #### 子表Mapper(增加外键操作方法) [JeecgOrderCustomerMapper.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/mapper/JeecgOrderCustomerMapper.java) — 子表Mapper在 `BaseMapper` 基础上增加两个核心方法: ```java public interface JeecgOrderCustomerMapper extends BaseMapper { /** 通过主表外键批量删除客户 */ @Delete("DELETE FROM JEECG_ORDER_CUSTOMER WHERE ORDER_ID = #{mainId}") boolean deleteCustomersByMainId(String mainId); /** 通过主表订单外键查询客户 */ @Select("SELECT * FROM JEECG_ORDER_CUSTOMER WHERE ORDER_ID = #{mainId}") List selectCustomersByMainId(String mainId); } ``` **关键点**:子表Mapper在 `BaseMapper` 基础上增加了两个核心方法: - `deleteByMainId` — 通过外键批量删除子表数据 - `selectByMainId` — 通过外键查询子表数据 #### Mapper XML模板版 代码生成器模板 [[1-n]Mapper.xml](file:///d: /Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/$ {entityPackage}/mapper/xml/[1-n]Mapper.xml) 展示了XML方式实现: ```xml DELETE FROM ${subTab.tableName} WHERE<#list originalForeignKeys as key>${key} = #{mainId} ``` ### 12.6 代码生成器模板 JeecgBoot 提供了多种风格的主子表代码生成模板,位于 `jeecg-system-biz/src/main/resources/jeecg/code-template-online/` 下: | 模板风格 | 路径 | 说明 | |-----------------|--------------------------|-----------| | **default** | `default/onetomany/` | 默认一对多模板 | | **tab** | `tab/onetomany/` | Tab页签风格 | | **jvxe** | `jvxe/onetomany/` | JVxe行编辑风格 | | **inner-table** | `inner-table/onetomany/` | 内嵌表格风格 | | **erp** | `erp/onetomany/` | ERP风格 | ### 12.7 主子表CRUD策略对比 | 操作 | 策略 | 说明 | |-------------|------------------------------|-------------------------------------| | **新增** | 先插主表 → 循环插子表 | 子表设置外键为主表ID | | **修改(策略一)** | 更新主表 → 删子表 → 重新插子表 | 简单粗暴,先删后插 | | **修改(策略二)** | 更新主表 → 差异对比子表 | 新增/更新/删除分别处理,保留已有子表ID | | **删除** | 删主表 → 删子表 | 或先删子表再删主表 | | **查询** | 主表子表分开查询 | 前端按需加载子表数据,每个子表一个独立接口 | | **导出** | 遍历主表 → 查子表 → 组装VO | 使用 `@ExcelCollection` 注解 | | **导入** | 解析Excel → 拆分主子表 → 调用saveMain | `ImportParams.headRows=2`(主子表表头占2行) | ### 12.8 主子表关键注解 | 注解 | 用途 | 位置 | |-------------------------------------------------|--------------------|-----------------| | `@TableName` | 映射数据库表名 | 主表/子表实体类 | | `@TableId(type = IdType.ASSIGN_ID)` | 主键生成策略 | 主表/子表实体类 | | `@ExcelCollection` | 标记子表集合,用于Excel导入导出 | VO类中的子表列表字段 | | `@Excel` | Excel字段映射 | 实体类/VO类字段 | | `@Transactional(rollbackFor = Exception.class)` | 事务控制 | Service层主子表操作方法 | ### 12.9 主子表文件结构规范 ``` org.jeecg.modules..{ controller/ ${EntityName}Controller.java -- 主表Controller entity/ ${EntityName}.java -- 主表实体 ${SubEntityName}.java -- 子表实体(可多个) vo/ ${EntityName}Page.java -- 主子表组合VO service/ I${EntityName}Service.java -- 主表Service接口 I${SubEntityName}Service.java -- 子表Service接口 service/impl/${EntityName}ServiceImpl.java -- 主表Service实现(含主子表事务逻辑) ${SubEntityName}ServiceImpl.java -- 子表Service实现 mapper/ ${EntityName}Mapper.java -- 主表Mapper ${SubEntityName}Mapper.java -- 子表Mapper(含外键操作方法) mapper/xml/ ${EntityName}Mapper.xml ${SubEntityName}Mapper.xml } ``` ### 12.10 主子表与单表CRUD的差异对比 | 对比维度 | 单表 CRUD | 主子表 CRUD | |------------------|------------------------------|--------------------------------------------------------------------------------| | **Controller入参** | `@RequestBody Entity` | `@RequestBody EntityPage`(VO对象) | | **新增方法** | `service.save(entity)` | `service.saveMain(main, subList1, subList2)` | | **修改方法** | `service.updateById(entity)` | `service.updateMain(main, subList1, subList2)` 或 `service.updateCopyMain(...)` | | **删除方法** | `service.removeById(id)` | `service.delMain(id)` | | **查询子表** | 无 | 每个子表一个独立查询接口 `queryXxxListByMainId` | | **Service事务** | 框架默认 | 显式 `@Transactional(rollbackFor = Exception.class)` | | **Mapper方法** | 仅 `BaseMapper` | 子表增加 `deleteByMainId` + `selectByMainId` | | **Excel导出** | `super.exportXls()` | 手动组装VO列表(遍历主表查子表) | | **Excel导入** | `super.importExcel()` | 手动解析(`headRows=2`)+ 逐条 `saveMain` | | **VO类** | 不需要 | 需要,包含 `@ExcelCollection` 子表集合字段 | ### 12.11 开发主子表模块的最小步骤 基于以上分析,开发一个带完整 CRUD + 导入导出的主子表新模块,需要以下步骤: 1. **创建主表实体**:继承 `JeecgEntity`(或 `Serializable`),加 `@TableName`、`@TableId` 注解 2. **创建子表实体**:继承 `JeecgEntity`(或 `Serializable`),加 `@TableName`、`@TableId` 注解,**必须包含外键字段** 3. **创建VO类**:包含主表字段 + `@ExcelCollection` 标注的子表列表字段 4. **创建主表Mapper**:继承 `BaseMapper` 5. **创建子表Mapper**:继承 `BaseMapper`,增加 `deleteByMainId` + `selectByMainId` 方法 6. **创建子表Service接口**:继承 `IService`,增加 `selectByMainId` 方法 7. **创建子表Service实现**:继承 `ServiceImpl`,实现 `selectByMainId` 8. **创建主表Service接口**:继承 `IService`,增加 `saveMain`、`updateMain`、`delMain`、`delBatchMain` 方法 9. **创建主表Service实现**:继承 `ServiceImpl`,注入所有子表Mapper,实现主子表事务方法 10. **创建Controller**:继承 `JeecgController>`,实现主表CRUD + 子表查询 + 导入导出 ### 12.12 涉及的文件清单 | 文件 | 操作 | 原因 | |--------------------------------------|----|---------------------| | `.docs/260601-后端各功能接口实现方式与代码规范分析.md` | 追加 | 本次分析文档追加主子表CRUD实现模式 | ### 12.13 关键源码索引(主子表) | 组件 | 文件路径 | |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 主表Controller示例 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgOrderMainController.java` | | 主表Service接口 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/IJeecgOrderMainService.java` | | 主表Service实现 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/impl/JeecgOrderMainServiceImpl.java` | | 主表实体 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgOrderMain.java` | | 子表实体-客户 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgOrderCustomer.java` | | 子表实体-机票 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/entity/JeecgOrderTicket.java` | | 主子表VO | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/vo/JeecgOrderMainPage.java` | | 主表Mapper | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/mapper/JeecgOrderMainMapper.java` | | 子表Mapper-客户 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/mapper/JeecgOrderCustomerMapper.java` | | 子表Mapper-机票 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/mapper/JeecgOrderTicketMapper.java` | | 子表Service接口-客户 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/IJeecgOrderCustomerService.java` | | 子表Service接口-机票 | `jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/IJeecgOrderTicketService.java` | | 字典VO | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/vo/SysDictPage.java` | | 字典Controller | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java` | | 字典Service实现 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysDictServiceImpl.java` | | 字典项Mapper | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/mapper/SysDictItemMapper.java` | | 代码生成器-Controller模板 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai` | | 代码生成器-ServiceImpl模板 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai` | | 代码生成器-子表Mapper模板 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai` | | 代码生成器-子表Mapper XML模板 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml` | | 代码生成器-VO模板 | `jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai` | *** ## 十三、异常处理体系与业务异常抛出方式 > 追加日期:2026-06-02 ### 13.1 异常类继承体系 项目中定义了 **5 个核心自定义异常类**,按继承关系整理如下: ``` RuntimeException ├── JeecgBootException (核心业务异常, errCode=500) │ └── JeecgBootAssertException (断言异常, 由AssertUtils使用) ├── JeecgBootBizTipException (业务提醒异常, errCode=500) ├── JeecgBoot401Exception (认证/鉴权异常) └── JeecgSqlInjectionException (SQL注入防护异常) ``` ### 13.2 各异常类详解 #### JeecgBootException — 核心业务异常 [JeecgBootException.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootException.java) 继承 `RuntimeException`,是项目中最主要的自定义异常。用于**系统级/不可恢复的业务错误**,抛出后**会记录系统日志**(写入 `sys_log` 表)。 ```java public class JeecgBootException extends RuntimeException { private int errCode = CommonConstant.SC_INTERNAL_SERVER_ERROR_500; // 默认500 public JeecgBootException(String message) { ...} public JeecgBootException(String message, int errCode) { ...} public JeecgBootException(Throwable cause) { ...} public JeecgBootException(String message, Throwable cause) { ...} } ``` #### JeecgBootBizTipException — 业务提醒异常 [JeecgBootBizTipException.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootBizTipException.java) 继承 `RuntimeException`,结构与 `JeecgBootException` 几乎相同,但语义不同:用于**可预期的业务操作提醒**(如" 旧密码输入错误"、"您已是该租户成员")。抛出后**不会记录系统日志**,只做 `log.error` 打印。 ```java public class JeecgBootBizTipException extends RuntimeException { private int errCode = CommonConstant.SC_INTERNAL_SERVER_ERROR_500; // 默认500 public JeecgBootBizTipException(String message) { ...} public JeecgBootBizTipException(String message, int errCode) { ...} public JeecgBootBizTipException(Throwable cause) { ...} public JeecgBootBizTipException(String message, Throwable cause) { ...} } ``` #### JeecgBootAssertException — 断言异常 [JeecgBootAssertException.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootAssertException.java) 继承 `JeecgBootException` ,是断言工具类 [AssertUtils](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java) 专用的异常。由于继承了 `JeecgBootException`,会被 `@ExceptionHandler(JeecgBootException.class)` 统一捕获,行为与 `JeecgBootException` 一致。 ```java public class JeecgBootAssertException extends JeecgBootException { public JeecgBootAssertException(String message) { super(message); } public JeecgBootAssertException(String message, int errCode) { super(message, errCode); } } ``` #### JeecgBoot401Exception — 认证异常 [JeecgBoot401Exception.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBoot401Exception.java) 继承 `RuntimeException`,专门用于认证/鉴权失败场景。处理器中会设置 HTTP 状态码为 `401 UNAUTHORIZED`。 #### JeecgSqlInjectionException — SQL注入异常 [JeecgSqlInjectionException.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgSqlInjectionException.java) 继承 `RuntimeException`,用于 SQL 注入防护场景。处理器中会对敏感信息(`extractvalue`、`updatexml`)进行脱敏处理。 ### 13.3 全局异常处理器 [JeecgBootExceptionHandler.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java) 使用 `@RestControllerAdvice` 注解,统一拦截所有 Controller 层抛出的异常,转换为 `Result` 对象返回给前端。 **处理的异常类型及返回逻辑**: | 异常类型 | 返回code | 记录系统日志 | 说明 | |------------------------------------------|--------------------------|--------|-------------------| | `JeecgBootException` | 自定义errCode(默认500) | **是** | 核心业务异常 | | `JeecgBootAssertException` | 同上(继承JeecgBootException) | **是** | 断言异常 | | `JeecgBootBizTipException` | 自定义errCode(默认500) | **否** | 业务提醒,仅log.error | | `JeecgBoot401Exception` | 401 | **是** | 认证失败,HTTP状态码设为401 | | `JeecgSqlInjectionException` | 500 | 视情况 | SQL注入风险,敏感信息脱敏 | | `MethodArgumentNotValidException` | 500 | **是** | 参数校验失败 | | `DuplicateKeyException` | 500 | **是** | 数据库主键重复 | | `DataIntegrityViolationException` | 500 | **是** | 数据完整性约束违反 | | `UnauthorizedException` | 510 | **否** | Shiro权限不足 | | `MaxUploadSizeExceededException` | 500 | **是** | 文件上传超限 | | `NoHandlerFoundException` | 404 | **是** | 路径不存在 | | `HttpRequestMethodNotSupportedException` | 405 | **是** | 请求方法不支持 | | `Exception`(兜底) | 500 | **是** | 含Sentinel限流判断 | **核心处理方法源码**: ```java @RestControllerAdvice @Slf4j public class JeecgBootExceptionHandler { @Resource BaseCommonService baseCommonService; // 核心业务异常 — 记录系统日志 @ExceptionHandler(JeecgBootException.class) public Result handleJeecgBootException(JeecgBootException e) { log.error(e.getMessage(), e); addSysLog(e); return Result.error(e.getErrCode(), e.getMessage()); } // 业务提醒异常 — 不记录系统日志 @ExceptionHandler(JeecgBootBizTipException.class) public Result handleJeecgBootBizTipException(JeecgBootBizTipException e) { log.error(e.getMessage()); return Result.error(e.getErrCode(), e.getMessage()); } // 认证异常 — 设置HTTP状态码401 @ExceptionHandler(JeecgBoot401Exception.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public Result handleJeecgBoot401Exception(JeecgBoot401Exception e) { log.error(e.getMessage(), e); addSysLog(e); return new Result(401, e.getMessage()); } // 兜底异常 — 含Sentinel限流判断 @ExceptionHandler(Exception.class) public Result handleException(Exception e) { log.error(e.getMessage(), e); Throwable throwable = e.getCause(); SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable); if (ObjectUtil.isNotEmpty(errorInfoEnum)) { return Result.error(errorInfoEnum.getError()); } addSysLog(e); return Result.error("操作失败," + e.getMessage()); } } ``` ### 13.4 Result 类 — 异常返回给前端的结构 [Result.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/api/vo/Result.java) ```java @Data public class Result implements Serializable { private boolean success = true; // 成功标志 private String message = ""; // 返回处理消息 private Integer code = 0; // 返回代码 private T result; // 返回数据对象 private long timestamp = System.currentTimeMillis(); // 时间戳 } ``` **Result 静态工厂方法**: ```java // 成功 Result.OK() // success=true, code=200 Result. OK(String msg) // success=true, code=200, message=msg, result=msg Result. OK(T data) // success=true, code=200, result=data Result. OK(String msg, T data) // success=true, code=200, message=msg, result=data // 失败 Result. error(String msg) // success=false, code=500, message=msg Result. error(int code, String msg) // success=false, code=自定义, message=msg Result. error(String msg, T data) // success=false, code=500, message=msg, result=data // 无权限 Result. noauth(String msg) // success=false, code=510, message=msg ``` **前端接收到的异常响应示例**: ```json // JeecgBootException(默认errCode=500) { "success": false, "message": "admin用户,不允许删除!", "code": 500, "result": null, "timestamp": 1717000000000 } // JeecgBootBizTipException(默认errCode=500) { "success": false, "message": "旧密码输入错误!", "code": 500, "result": null, "timestamp": 1717000000000 } // JeecgBoot401Exception { "success": false, "message": "token不能为空!", "code": 401, "result": null, "timestamp": 1717000000000 } // 自定义errCode { "success": false, "message": "短信接口请求太多,请稍后再试!", "code": 40002, "result": null, "timestamp": 1717000000000 } ``` ### 13.5 业务异常抛出方式 #### 方式一:直接抛出 JeecgBootException(最常用) 用于系统级/不可恢复的业务错误,**会记录系统日志**。这是项目中使用最多的方式。 ```java // 基本用法 — 默认 errCode=500 throw new JeecgBootException("操作数据不存在!"); throw new JeecgBootException("XX字段不能为空!"); // 自定义错误码 throw new JeecgBootException("短信接口请求太多,请稍后再试!",CommonConstant.PHONE_SMS_FAIL_CODE); // 包装原因异常 throw new JeecgBootException("获取钉钉用户信息失败",cause); ``` **项目中的典型使用场景** (来自 [SysUserServiceImpl.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java)): ```java throw new JeecgBootException("离职失败,该用户已不存在"); throw new JeecgBootException("admin用户,不允许删除!"); throw new JeecgBootException("验证码失效,请重新发送验证码!"); throw new JeecgBootException("手机号已被注册,请尝试其他手机号!"); ``` #### 方式二:抛出 JeecgBootBizTipException(业务提醒) 用于可预期的业务操作提醒,**不会记录系统日志**,只做简单的日志打印。适合"提示性"而非"错误性"的场景。 ```java // 密码修改提醒 throw new JeecgBootBizTipException("旧密码输入错误!"); throw new JeecgBootBizTipException("新密码不允许为空!"); // 租户操作提醒 throw new JeecgBootBizTipException("管理员已拒绝您加入租户,请联系租户管理员"); throw new JeecgBootBizTipException("您已是该租户成员"); // 组织架构操作提醒 throw new JeecgBootBizTipException("当前子公司/部门下存在子级,无法变更为岗位!"); ``` #### 方式三:通过 AssertUtils 断言工具(间接抛出 JeecgBootAssertException) [AssertUtils.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java) 提供了声明式的断言方法,断言失败时抛出 `JeecgBootAssertException`(继承自 `JeecgBootException`),最终被 `@ExceptionHandler(JeecgBootException.class)` 捕获,行为与 `JeecgBootException` 一致。 ```java // 确保对象不为空 — 适用于"XX字段不能为空" AssertUtils.assertNotEmpty("用户ID不能为空",userId); AssertUtils. assertNotEmpty("姓名不能为空",name); // 确保对象为空 — 适用于"该记录已存在" AssertUtils. assertEmpty("该记录已存在",existingRecord); // 确保条件为真 — 适用于"操作数据不存在" AssertUtils. assertTrue("操作数据不存在",data !=null); AssertUtils. assertTrue("余额不足",balance >=amount); // 确保相等 — 适用于"验证码不匹配" AssertUtils. assertEquals("验证码不匹配",expectedCode, actualCode); // 数值比较 AssertUtils. assertGt("数量必须大于0",quantity, 0); AssertUtils. assertLt("超出限制",count, maxCount); // 集合包含 AssertUtils. assertIn("非法状态",status, "ACTIVE","PENDING"); ``` #### 方式四:直接返回 Result.error()(不抛异常) 在 Controller 层直接返回错误结果,不经过异常处理器。这种方式不会记录系统日志,适用于简单的参数校验场景。 ```java @GetMapping(value = "/queryById") public Result queryById(@RequestParam(name = "id", required = true) String id) { Xxx obj = xxxService.getById(id); if (obj == null) { return Result.error("未找到对应数据"); } return Result.OK(obj); } ``` ### 13.6 JeecgBootException vs JeecgBootBizTipException 选择指南 | 维度 | JeecgBootException | JeecgBootBizTipException | |------------|--------------------------|--------------------------| | **语义** | 系统级错误、不可恢复的业务错误 | 业务操作提醒、可预期的用户提示 | | **记录系统日志** | 是(写入 `sys_log` 表) | 否(仅 `log.error` 打印) | | **日志打印** | `log.error(msg, e)` 含堆栈 | `log.error(msg)` 不含堆栈 | | **适用场景** | 数据不存在、管理员保护、验证码失效 | 密码错误、权限不足提醒、组织架构约束 | | **典型示例** | "admin用户不允许删除"、"token非法" | "旧密码输入错误"、"您已是该租户成员" | **选择原则**: - 如果异常**需要被运维人员关注和排查**(如数据不一致、关键操作失败),使用 `JeecgBootException` - 如果异常**只是给用户的友好提示**(如输入校验、操作冲突),使用 `JeecgBootBizTipException` - 在 zjrs 业务模块中,**"XX字段不能为空"、"操作数据不存在"这类提示性校验推荐使用 `JeecgBootBizTipException`**,避免大量无意义的系统日志 ### 13.7 异常处理完整流程 ``` Controller / Service 层抛出异常 │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ JeecgBootExceptionHandler │ │ (@RestControllerAdvice) │ │ │ │ JeecgBootException ──→ Result.error(errCode, msg) │ │ + 记录系统日志(addSysLog) │ │ │ │ JeecgBootAssertException ──→ 同上(继承JeecgBootException) │ │ │ │ JeecgBootBizTipException ──→ Result.error(errCode, msg) │ │ 不记录系统日志(仅log.error) │ │ │ │ JeecgBoot401Exception ──→ Result(401, msg) │ │ HTTP 401 + 记录系统日志 │ │ │ │ JeecgSqlInjectionException ──→ Result.error(500, msg) │ │ 敏感信息脱敏 │ │ │ │ UnauthorizedException ──→ Result.noauth(msg) code=510 │ │ │ │ DuplicateKeyException ──→ Result.error("数据库中已存在该记录") │ │ │ │ MaxUploadSizeExceededException ──→ Result.error("文件大小超出10MB限制") │ │ │ │ DataIntegrityViolationException ──→ Result.error("执行数据库异常...") │ │ │ │ Exception (兜底) ──→ Result.error("操作失败,"+msg) │ │ + Sentinel限流判断 │ └──────────────────────────────────────────────────────────────┘ │ ▼ JSON 响应返回前端 { "success": false, "message": "错误信息", "code": 500, "result": null, "timestamp": 1717000000000 } ``` ### 13.8 涉及的文件清单 | 文件 | 操作 | 原因 | |--------------------------------------|----|------------------| | `.docs/260601-后端各功能接口实现方式与代码规范分析.md` | 追加 | 本次分析文档追加异常处理体系分析 | ### 13.9 关键源码索引(异常处理) | 组件 | 文件路径 | |---------|------------------------------------------------------------------------------------------------------------| | 核心业务异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootException.java` | | 业务提醒异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootBizTipException.java` | | 断言异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootAssertException.java` | | 认证异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBoot401Exception.java` | | SQL注入异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgSqlInjectionException.java` | | 全局异常处理器 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java` | | 断言工具类 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java` | | 统一返回结果 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/api/vo/Result.java` | | 常量定义 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java` | *** ## 十四、实体类数据校验体系分析 > 追加日期:2026-06-02 ### 14.1 核心结论:JSR303 声明式校验几乎未使用 虽然框架在 [jeecg-boot-base-core/pom.xml](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/pom.xml) 中引入了 `spring-boot-starter-validation` 依赖: ```xml org.springframework.boot spring-boot-starter-validation ``` 但**整个项目中仅有1处测试代码**使用了 JSR303 校验注解,**zjrs 业务模块和系统模块均未使用任何声明式校验**。项目以手动编码校验为主。 ### 14.2 JSR303 校验注解使用情况 #### 唯一使用处:Seata 分布式事务测试模块 [PlaceOrderRequest.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/jeecg-cloud-test-seata-order/src/main/java/org/jeecg/modules/test/seata/order/dto/PlaceOrderRequest.java) — DTO 上使用 `@NotNull`: ```java public class PlaceOrderRequest { @NotNull private Long userId; @NotNull private Long productId; @NotNull private Integer count; } ``` [SeataOrderController.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/jeecg-cloud-test-seata-order/src/main/java/org/jeecg/modules/test/seata/order/controller/SeataOrderController.java) — Controller 上使用 `@Validated`: ```java @PostMapping("/placeOrder") public String placeOrder(@Validated @RequestBody PlaceOrderRequest request) { orderService.placeOrder(request); return "下单成功"; } ``` #### zjrs 模块和系统模块 — 零使用 经全量搜索确认: | 模块 | JSR303 注解使用 | @Valid/@Validated 使用 | AssertUtils 使用 | |----------------------------|-------------|----------------------|----------------| | **jeecg-module-zjrs** | 无 | 无 | 无 | | **jeecg-module-system** | 无 | 无 | 有(少量) | | **jeecg-module-airag** | 无 | 无 | 有(大量) | | **jeecg-cloud-test-seata** | 有(@NotNull) | 有(@Validated) | 无 | ### 14.3 项目现有的校验方式 #### 方式一:Controller 层 if-null 判断 + Result.error()(zjrs 模块主流方式) zjrs 模块所有 Controller 均采用此模式,仅在编辑/查询时做简单的 null 判断: ```java // PersonalInfoController.java @PutMapping(value = "/edit") public Result edit(@RequestBody PersonalInfo personalInfo) { PersonalInfo personalInfoEntity = personalInfoService.getById(personalInfo.getId()); if (personalInfoEntity == null) { return Result.error("未找到对应数据"); } personalInfoService.updateById(personalInfo); return Result.OK("编辑成功!"); } ``` **局限**:只校验了"数据是否存在",**没有校验字段非空、数据类型、数据内容、长度等**。 #### 方式二:Service 层 throw new JeecgBootException / JeecgBootBizTipException(系统模块主流方式) 系统模块在 Service 层进行业务校验,校验失败直接抛异常: ```java // SysUserServiceImpl.java throw new JeecgBootException("admin用户,不允许删除!"); throw new JeecgBootException("验证码失效,请重新发送验证码!"); throw new JeecgBootBizTipException("旧密码输入错误!"); throw new JeecgBootBizTipException("新密码不允许为空!"); ``` #### 方式三:AssertUtils 断言工具(airag 模块使用较多) [AssertUtils.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java) 提供声明式断言,校验失败抛出 `JeecgBootAssertException`: ```java // AigcWordTemplateController.java AssertUtils.assertNotEmpty("参数异常",eoaWordTemplate); AssertUtils. assertNotEmpty("模版名称不能为空",eoaWordTemplate.getName()); AssertUtils. assertFalse("模版编码已存在",isCodeExists); // AiragKnowledgeDocServiceImpl.java AssertUtils. assertNotEmpty("文档不能未空",airagKnowledgeDoc); AssertUtils. assertNotEmpty("知识库不能未空",airagKnowledgeDoc.getKnowledgeId()); AssertUtils. assertNotEmpty("文档标题不能未空",airagKnowledgeDoc.getTitle()); ``` **AssertUtils 完整方法列表**: | 方法 | 说明 | 适用场景 | |------------------------------------------|---------|--------| | `assertNotEmpty(msg, obj)` | 确保对象不为空 | 字段非空校验 | | `assertEmpty(msg, obj)` | 确保对象为空 | 唯一性校验 | | `assertEquals(msg, expected, actual)` | 验证相等 | 值匹配校验 | | `assertNotEquals(msg, expected, actual)` | 验证不相等 | 值互斥校验 | | `assertTrue(msg, condition)` | 验证条件为真 | 通用条件校验 | | `assertFalse(msg, condition)` | 验证条件为假 | 通用条件校验 | | `assertIn(msg, obj, objs)` | 验证存在 | 枚举值校验 | | `assertNotIn(msg, obj, objs)` | 验证不存在 | 排除值校验 | | `assertGt(msg, src, des)` | 确保大于 | 数值下限校验 | | `assertGe(msg, src, des)` | 确保大于等于 | 数值下限校验 | | `assertLt(msg, src, des)` | 确保小于 | 数值上限校验 | | `assertLe(msg, src, des)` | 确保小于等于 | 数值上限校验 | #### 方式四:throw new RuntimeException(zjrs SSO 模块 — 不推荐) zjrs 模块的 SSO 相关代码直接抛出 `RuntimeException`,不符合框架规范: ```java // LoginSSOServiceImpl.java throw new RuntimeException("未找到用户信息"); throw new RuntimeException(checkResult.getMessage()); // VRsUsersServiceImpl.java throw new RuntimeException("用户信息同步失败",e); ``` **应改为**:`throw new JeecgBootException(...)` 或 `throw new JeecgBootBizTipException(...)`。 ### 14.4 全局校验异常处理 [JeecgBootExceptionHandler.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java) 中仅有一处校验异常处理: ```java @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidationExceptions(MethodArgumentNotValidException e) { log.error(e.getMessage(), e); addSysLog(e); return Result.error("校验失败!" + e.getBindingResult().getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining(","))); } ``` **注意**:该处理器仅处理了 `MethodArgumentNotValidException`(对应 `@Valid`/`@Validated` + `@RequestBody` 的校验失败),**缺少 **以下两种校验异常的处理: | 缺少的异常类型 | 触发场景 | 当前后果 | |--------------------------------|------------------------------------|-----------------------------------| | `BindException` | `@Valid`/`@Validated` + 表单参数绑定校验失败 | 会被兜底 `Exception` 处理器捕获,返回不友好的错误信息 | | `ConstraintViolationException` | `@Validated` 在方法参数上的校验失败 | 同上 | ### 14.5 自定义校验注解 项目中**没有**基于 `@Constraint` + `ConstraintValidator` 的标准自定义校验注解。 但存在一个**签名校验注解**,属于 AOP 级别的校验而非字段级校验: [SignatureCheck.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java) — 方法级签名校验注解: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SignatureCheck { boolean enabled() default true; String errorMessage() default "Sign签名校验失败!"; } ``` 配套切面 [SignatureCheckAspect.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java) ,通过 `@Before` 拦截标注了 `@SignatureCheck` 的方法进行签名验证。 ### 14.6 Hibernate Validator 配置 项目中**没有**任何 Hibernate Validator 的自定义配置: - 没有 `HibernateValidatorConfiguration` 相关配置类 - 没有 `ValidationMessages.properties` 或 `ValidationMessages_zh_CN.properties` 资源文件 - 没有 `hibernate.validator.*` 相关的 YAML 配置 - 没有自定义的 `Validator` Bean 注册 完全使用 `spring-boot-starter-validation` 的默认配置。 ### 14.7 SQL 注入防护体系(安全层面校验) 虽然不属于传统的"数据校验",但项目有一套完善的安全校验体系: | 工具类 | 功能 | |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------| | [SqlInjectionUtil](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java) | SQL注入关键词/正则检测、表名/字段名/排序字段合法性校验 | | [AbstractQueryBlackListHandler](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/security/AbstractQueryBlackListHandler.java) | 查询表/字段黑名单(如 `sys_user.password` 禁止查询) | | [SsrfFileTypeFilter](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java) | 上传文件类型白名单 + 文件头黑名单检测 | ### 14.8 校验体系架构总览 ``` ┌──────────────────────────────────────────────────────────────────┐ │ 请求入口 │ ├──────────────────────────────────────────────────────────────────┤ │ 签名校验层 │ @SignatureCheck / SignAuthInterceptor │ ├──────────────────────────────────────────────────────────────────┤ │ Controller层 │ ❌ 几乎没有 @Valid/@Validated(仅1处测试代码) │ │ │ ✅ 简单 if-null 判断 → Result.error() │ ├──────────────────────────────────────────────────────────────────┤ │ Service层 │ ✅ AssertUtils.assertXxx() → JeecgBootAssertException │ │ │ ✅ throw new JeecgBootException() │ │ │ ✅ throw new JeecgBootBizTipException() │ │ │ ❌ throw new RuntimeException()(不推荐) │ ├──────────────────────────────────────────────────────────────────┤ │ 实体类 │ ❌ 无任何 JSR303 校验注解 │ │ │ ✅ @Excel 注解(仅用于导入导出,非校验) │ ├──────────────────────────────────────────────────────────────────┤ │ SQL安全层 │ ✅ SqlInjectionUtil / AbstractQueryBlackListHandler │ ├──────────────────────────────────────────────────────────────────┤ │ 全局异常处理 │ ✅ MethodArgumentNotValidException │ │ │ ❌ 缺少 BindException 处理 │ │ │ ❌ 缺少 ConstraintViolationException 处理 │ │ │ ✅ JeecgBootException / JeecgBootBizTipException │ └──────────────────────────────────────────────────────────────────┘ ``` ### 14.9 zjrs 模块校验现状与改进建议 #### 现状问题 1. **实体类无任何校验注解** — 字段非空、长度、格式等完全依赖数据库约束或前端校验 2. **Controller 层仅做 null 判断** — 编辑/查询时只校验数据是否存在,不校验字段内容 3. **Service 层无业务校验** — 新增/修改时没有字段级别的业务规则校验 4. **SSO 模块使用 RuntimeException** — 不符合框架异常体系规范 #### 改进建议 **方案一:引入 JSR303 声明式校验(推荐用于字段级校验)** 在实体类上添加校验注解,Controller 层配合 `@Validated` 使用: ```java // 实体类 @Data @TableName("zjrs_personal_info") public class PersonalInfo extends JeecgEntity { @NotBlank(message = "姓名不能为空") @Size(max = 50, message = "姓名长度不能超过50") @Excel(name = "姓名", width = 15) private String name; @NotBlank(message = "身份证号不能为空") @Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "身份证号格式不正确") @Excel(name = "身份证号", width = 15) private String idcard; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") @Excel(name = "手机号", width = 15) private String phone; } // Controller @PostMapping(value = "/add") public Result add(@Validated @RequestBody PersonalInfo personalInfo) { personalInfoService.save(personalInfo); return Result.OK("添加成功!"); } ``` **方案二:使用 AssertUtils 手动校验(推荐用于业务规则校验)** 在 Service 层使用 `AssertUtils` 进行业务规则校验: ```java @Override public void savePersonalInfo(PersonalInfo personalInfo) { AssertUtils.assertNotEmpty("姓名不能为空", personalInfo.getName()); AssertUtils.assertNotEmpty("身份证号不能为空", personalInfo.getIdcard()); // 业务规则校验 long count = this.count(new LambdaQueryWrapper() .eq(PersonalInfo::getIdcard, personalInfo.getIdcard())); AssertUtils.assertTrue("身份证号已存在", count == 0); this.save(personalInfo); } ``` **方案三:混合使用(最佳实践)** - **字段级校验**(非空、长度、格式)→ JSR303 注解 + `@Validated`,自动触发 - **业务规则校验**(唯一性、关联性、状态约束)→ `AssertUtils` 或 `JeecgBootBizTipException`,在 Service 层手动编码 - **补充全局异常处理**:增加 `BindException` 和 `ConstraintViolationException` 的处理 ### 14.10 涉及的文件清单 | 文件 | 操作 | 原因 | |--------------------------------------|----|---------------------| | `.docs/260601-后端各功能接口实现方式与代码规范分析.md` | 追加 | 本次分析文档追加实体类数据校验体系分析 | ### 14.11 关键源码索引(数据校验) | 组件 | 文件路径 | |------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 校验依赖引入 | `jeecg-boot/jeecg-boot-base-core/pom.xml` | | 全局校验异常处理 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java` | | 断言工具类 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java` | | 断言异常 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootAssertException.java` | | 签名校验注解 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java` | | 签名校验切面 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java` | | SQL注入防护 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java` | | 查询黑名单 | `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/security/AbstractQueryBlackListHandler.java` | | JSR303使用示例(DTO) | `jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/jeecg-cloud-test-seata-order/src/main/java/org/jeecg/modules/test/seata/order/dto/PlaceOrderRequest.java` | | JSR303使用示例(Controller) | `jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/jeecg-cloud-test-seata-order/src/main/java/org/jeecg/modules/test/seata/order/controller/SeataOrderController.java` |