Home 世界杯出线规则 JeecgBoot 项目理解与使用心得

JeecgBoot 项目理解与使用心得

JeecgBoot 项目理解与使用心得(内容增强版)

给第一次上手的同学一条清晰路线,也给已经在用的团队一套"更稳、更快、更可维护"的实践清单。本文聚焦 项目定位、模块地图、上手路线、生成器最佳实践、工程化与安全基线、性能优化、前端协作、DevOps 与可观测性、测试策略、常见坑排雷,并附带可直接落地的代码片段与检查清单。

1. 我对 JeecgBoot 的定位

一句话:JeecgBoot = 低代码平台(在线建模) + 代码生成器(前后端同生) + 企业级脚手架(Spring Boot + MyBatis-Plus + Ant Design Vue)。

优势

交付效率:模型 → 生成 → 小改小调,即可交付 CRUD 模块。

上手成本:分层清晰、生态主流、社区活跃。

可维护:生成的是真实工程代码,适合二次开发与长期运维。

边界

模板即规范:模板与公共库的质量,决定批量复制的质量。

复杂业务仍需良好领域建模和工程治理,低代码不是银弹。

2. 模块地图与协作关系(后端/前端)

后端

jeecg-boot-module-system:系统基础能力(用户、角色、字典、文件、日志等)

jeecg-boot-base-core:公共库(工具、异常、统一返回、查询生成器、拦截器)

技术栈:Spring Boot + MyBatis-Plus(Wrapper/分页/乐观锁/代码生成器)

前端

Ant Design Vue + 路由/权限指令 + 代码生成页面(列表/表单/导入导出)

表单、表格、字典组件完备,适合中后台场景

协作:模型与字段 → 生成前后端骨架 → 后端补业务逻辑/查询 → 前端补交互/校验/样式。

3. 快速上手路线图(从"能跑"到"跑得稳")

准备与启动

初始化数据库(官方脚本),配置数据源与文件存储;

启动后端、前端(或使用打包版);

登录系统,熟悉系统模块(用户/角色/字典)。

模型→生成

使用在线建模/代码生成器,生成单表/主从表页面;

生成后在 IDE 中补业务逻辑(Service/Mapper);

在前端完善校验与交互(必要时自定义组件)。

工程化加固(务必做)

把 安全基线/性能限幅/统一异常/日志脱敏 固化到公共库与模板;

接入 CI / 质量门禁 / 可观测性;

规范 DTO/VO 分离、包结构、单元/集成测试。

4. 代码生成器最佳实践(决定"复制出来"的质量)

4.1 数据建模与命名

字段统一命名风格(snake_case 或 camelCase),尽量一致;

通用字段提前规划:id、tenant_id、org_id、create_by、create_time、update_by、update_time、del_flag;

选择合适主键策略(雪花或数据库自增),大表建议雪花,避免热点自增锁。

4.2 主从表与关联

生成主子表结构时,外键字段与索引要就位;

读多写少的从表可考虑懒加载;写频繁则在 Service 里控制事务边界。

4.3 校验与字典

后端 DTO 加 JSR-380 校验注解(@NotBlank @Size @Pattern ...),Controller 上加 @Validated;

字典/枚举建议双轨:数据库字典 + 枚举常量(关键枚举落到代码可读)。

4.4 模板"开箱即工程化"(强烈建议)

Controller 模板固化:分页限幅、排序白名单、LIKE 转义、统一异常与状态码;

导出模板固化:Excel 公式注入防护;

上传模板固化:魔数/大小/后缀校验;

前端模板固化:表单校验 与后端 @Validated 对齐;新 API (如 v-model:open)。

5. 工程化与安全基线(可直接复制的片段)

5.1 LIKE 转义(防止"全表扫描"与越权匹配)

java

复制代码

public final class SqlLike {

private SqlLike(){}

public static String esc(String s){

if (s==null) return null;

return s.replace("\\","\\\\").replace("%","\\%").replace("_","\\_");

}

public static void like(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper w,

String col, String kw){

String v = "%" + esc(kw) + "%";

w.apply("`"+col+"` LIKE {0} ESCAPE '\\\\'", v);

}

}

5.2 排序白名单(禁止任意列/片段传入)

java

复制代码

public final class SortGuard {

private SortGuard(){}

public static void assertSortable(String col, java.util.Set whitelist){

if (col==null || !whitelist.contains(col))

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.BAD_REQUEST, "非法排序字段");

}

public static boolean isAsc(String dir){ return !"desc".equalsIgnoreCase(dir); }

}

5.3 下载/预览的路径规范化与软链接防护(Path Traversal)

java

复制代码

public final class PathSafe {

private PathSafe(){}

public static java.nio.file.Path safeResolve(java.nio.file.Path base, String userInput) throws IOException {

String decoded = java.net.URLDecoder.decode(userInput, java.nio.charset.StandardCharsets.UTF_8);

decoded = decoded.replace('\\','/'); // 统一分隔符

java.nio.file.Path baseReal = base.toRealPath(java.nio.file.LinkOption.NOFOLLOW_LINKS);

java.nio.file.Path normalized = baseReal.resolve(decoded).normalize();

if (!normalized.startsWith(baseReal)) throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.FORBIDDEN,"非法路径");

java.nio.file.Path real = normalized.toRealPath(); // 解析软链接

if (!real.startsWith(baseReal) || !java.nio.file.Files.isRegularFile(real))

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.NOT_FOUND,"目标不存在或非法");

return real;

}

}

5.4 SSRF 防护(URL 转发/外链中转)

java

复制代码

public final class UrlSafe {

private UrlSafe(){}

public static void assertSafe(java.net.URI u){

String s = u.getScheme();

if(!"http".equalsIgnoreCase(s) && !"https".equalsIgnoreCase(s))

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.BAD_REQUEST,"非法协议");

int p = u.getPort();

if(p!=-1 && p!=80 && p!=443)

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.BAD_REQUEST,"非法端口");

try {

for (java.net.InetAddress a : java.net.InetAddress.getAllByName(u.getHost())) {

if (a.isAnyLocalAddress() || a.isLoopbackAddress()

|| a.isLinkLocalAddress() || a.isSiteLocalAddress())

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.FORBIDDEN,"禁止访问内网地址");

}

} catch (java.net.UnknownHostException e) {

throw new org.springframework.web.server.ResponseStatusException(

org.springframework.http.HttpStatus.BAD_REQUEST,"域名解析失败");

}

}

}

5.5 Excel 公式注入(CSV/Excel Injection)

java

复制代码

public static String sanitizeExcel(String v){

return v!=null && v.matches("^[=+\\-@].*") ? "'"+v : v;

}

// 写单元格前:

cell.setCellValue(sanitizeExcel(value));

5.6 文件上传:统一入口校验 + 流式写入(避免 OOM)

java

复制代码

// Controller 入口(无论 local/oss/minio,一律先校验)

if (!(request instanceof org.springframework.web.multipart.MultipartHttpServletRequest)) {

return Result.error("需要 multipart/form-data");

}

MultipartFile file = ((org.springframework.web.multipart.MultipartHttpServletRequest)request).getFile("file");

if (file==null || file.isEmpty()) return Result.error("文件为空");

// 你们已有 SsrfFileTypeFilter,务必统一调用

org.jeecg.common.util.filter.SsrfFileTypeFilter.checkUploadFileType(file);

// 流式落盘(或 file.transferTo(target))

try (java.io.InputStream in = file.getInputStream();

java.io.OutputStream out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(target))) {

byte[] buf = new byte[8192];

for (int n; (n=in.read(buf))!=-1; ) out.write(buf,0,n);

}

5.7 统一异常与状态码(简版)

java

复制代码

@RestControllerAdvice

public class GlobalEx {

@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)

@ResponseStatus(HttpStatus.BAD_REQUEST)

public Result badReq(org.springframework.web.bind.MethodArgumentNotValidException e){

java.util.List msgs = new java.util.ArrayList<>();

e.getBindingResult().getFieldErrors().forEach(er -> msgs.add(er.getField()+": "+er.getDefaultMessage()));

return Result.error("参数校验失败: " + String.join("; ", msgs));

}

@ExceptionHandler(org.springframework.web.multipart.MaxUploadSizeExceededException.class)

@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)

public Result tooLarge(){ return Result.error("上传文件过大"); }

}

6. 性能与可维护性(从"单点优化"到"系统性治理")

分页限幅 :后端统一限制 pageSize<=1000,异常值回落默认。

索引与 SQL:每个模糊查询列评估索引;多条件组合下关注执行计划(MySQL 8 慢 SQL 日志务必开启)。

缓存策略:字典/常量/配置放 Redis,有效期与更新策略明确;热点 key 做本地二级缓存。

异步化:大导出、消息通知、耗时计算放入队列;导出按分页流式写入,避免卡接口。

连接池与线程池:压测后调优;给线程池"命名 + 指标暴露"。

读写分离/分库分表 :增长到一定量级后再考虑;先做好 观察 → 评估 → 演进。

7. 前端协作心得(Ant Design Vue)

表单校验 :与后端 @Validated 一致;对手机号、邮箱、金额等用统一校验器。

表格:大列表开启虚拟滚动/列宽控制;分页与后端同步。

字典组件:约定 value/label/disabled 字段格式;前后端一个口径。

弹窗组件 :注意新 API(如 v-model:open);生成器模板与组件版本保持同步。

跨端:如需 H5/小程序,尽早抽离"服务接口层"与"UI 层",减少耦合。

8. DevOps 与可观测性

CI/质量门禁:SonarQube(Bug/Vuln/Code Smell 阈值)、SpotBugs、Checkstyle/Spotless;

依赖安全:OWASP Dependency-Check/Snyk;

数据库迁移:Flyway/Liquibase 管理脚本版本;

日志:Logback JSON + TraceId/SpanId;

指标:Micrometer + Prometheus + Grafana(QPS、P95、错误率、线程池、JVM、连接池);

链路:OpenTelemetry 分析慢链路与异常。

9. 测试策略(最低配也要有)

单元测试:Service/Utils;

集成测试:Testcontainers 起 MySQL/Redis,覆盖核心查询/事务;

E2E:关键生成页面用 Playwright/Cypress 跑"查询/新增/编辑/导出"主链路;

回归用例:LIKE 转义、排序白名单、导出公式、路径/SSRF 等安全基线用例常驻。

10. 常见坑排雷

Long 精度丢失(前端):ID 用字符串传输;前端 json 解析用 bigInt 方案或统一转 string。

时间与时区 :后端存 UTC,前端按时区展示;统一 yyyy-MM-dd HH:mm:ss。

逻辑删除 :MyBatis-Plus @TableLogic 与数据库唯一键配合(避免"软删后唯一冲突")。

事务边界:主从表写入建议一事务;跨服务用消息保证最终一致性。

乐观锁 :大并发更新场景使用 @Version,冲突重试或提示。

导出大文件:务必异步化 + 流式;避免一次性读入内存。

11. 生产上线检查清单

代码生成模板已固化:分页限幅 / 排序白名单 / LIKE 转义 / 导出防注入 / 上传魔数

转发/下载口:SSRF 防护 / 路径规范化 + 软链接防护 / Content-Disposition 安全编码

CAS/外部调用:系统信任库 + 主机名校验 + 连接/读取超时

日志脱敏:password/token/Authorization/手机号 等统一过滤

大导出:异步任务 + 分页流式;前端可查看任务状态

质量门禁:Sonar/依赖安全扫描通过;Checkstyle/Spotless 已启用

测试:关键路径单测/集成/E2E 覆盖

可观测性:QPS、P95、错误率、线程池、JVM、连接池指标可见;Trace 打通

数据权限/多租户:拦截器注入范围过滤,导出/下载同样受控

12. 总结

JeecgBoot 的价值在于:把低代码效率与工程化可维护结合起来 。想要"跑得稳、跑得久",关键是把 安全、性能、规范、可观测性 这四条"地基",沉到模板与公共库里,让每个新模块 开箱即工程化 。 按照本文的清单逐条落地,你的 JeecgBoot 项目会更好地满足企业级场景对 稳定性、安全性与迭代效率 的双重要求。