通用后端基础能力平台(多模块技术总结与避坑指南)

本文档基于一个真实落地的 Spring Boot 多模块后端项目,提炼其可跨项目复用 的核心设计、代码组织方式,以及开发中真实踩过的坑与应保持的警惕

文中已去除一切企业专属命名与业务术语,统一使用中性概念(基础公共服务、主数据平台、对象存储、工作流引擎、搜索引擎等)。

站在开发者视角:每个模块都会说明「解决了什么问题 / 该怎么用 / 必须注意的坑 / 潜在问题 / 该提前考虑什么」。


1. 整体架构与分层

项目采用 Maven 多模块(Multi-Module) 组织,按"依赖方向单向、能力逐层沉淀"的原则分为三层:

复制代码
业务系统(聚合层)
   │  依赖
   ▼
通用能力模块(业务无关的可复用能力:缓存 / 文件 / 定时任务 / 审计日志 / 消息 / 流程 / 主数据适配 / 规则 / 树 / 队列 / 算法)
   │  依赖
   ▼
基础公共服务模块(统一响应、统一异常、通用工具、公共配置)
   │  依赖
   ▼
基础设施封装(Redis / 对象存储 / MQ / 搜索引擎 / 分布式调度 SDK 的轻量封装)

设计要点与潜在问题:

  • 基础模块不反向依赖任何业务模块,保证可整体平移到其他项目。
  • 通用能力模块只有一个父依赖(基础模块),彼此之间按需依赖,可被任意业务系统一站式引入。
  • 基础设施封装把第三方 SDK 的差异收敛在底层,上层只面对稳定接口。
  • ⚠️ 潜在问题:模块一旦增多,容易出现"循环依赖"或"通用模块偷偷依赖了业务模块"。建议在 CI 中做依赖方向校验,基础模块 pom 不应出现任何业务 artifact。

关键技术栈:Spring Boot 2.6.x、Spring Cloud 2021.x、MyBatis-Plus 3.5.x、Redis(Redisson)、对象存储(S3 兼容)、分布式调度(ElasticJob-Lite)、消息队列(RocketMQ)、搜索引擎(Elasticsearch)、工作流引擎(Camunda / 第三方平台)。


2. 基础公共服务模块(common)

这是所有模块的公共父依赖,沉淀"放之四海皆准"的能力。

2.1 统一响应与异常

  • BaseResponse<T>:统一 API 返回体(code / message / data),所有接口返回它,前端无需为不同接口写多种解析。
  • 业务异常体系:自定义 BusinessException,配合全局异常处理器统一转换为 BaseResponse,避免散落的 try-catch。
  • ⚠️ BaseResponse 一旦被当作"万能载体"塞入非响应字段(如审计用的 operateRecord 列表),就会污染响应结构。该项目中审计数据正是通过 BaseResponse.operateRecord 透传给切面,写入后必须 setOperateRecord(null) 再返回前端(见第 6 节),否则会把内部审计数据泄露到响应体。

2.2 公共配置(自动生效)

  • 数据访问 :MyBatis-Plus 分页拦截器(PaginationInnerInterceptor)统一注册,业务模块直接 page(...) 即可分页,无需重复配置。
  • 服务调用透传 :Feign RequestInterceptor 实现,在微服务间自动透传链路追踪 ID(traceId)、鉴权令牌、平台标识、客户端标识等请求头,解决分布式调用上下文丢失问题。
  • 序列化:FastJson / Jackson 统一配置(含时间类型序列化处理)。
  • HTTP 客户端:Feign OkHttp 连接池配置,提升服务间调用性能。

⚠️ 坑 / 注意

  • 分页拦截器必须注意多数据源 场景------若项目有多个 SqlSessionFactory,分页插件只对在它所属工厂创建的 SqlSession 生效,漏配的库分页会失效但仍返回"看似正确"的第一页数据。
  • Feign 透传拦截器里读取的 header(如 bucketNamestoragePath)只在 HTTP 调用链 中存在;内部方法直接调用、定时任务、MQ 消费线程里 RequestContextHolder 为空,此时透传头取不到,要靠参数显式传递(见第 4、5 节)。
  • ⚠️ 潜在问题RequestInterceptor 属于"隐式传参",排查问题时容易忽略"为什么这个线程没有 token"------建议在该拦截器里对缺失关键头时打印 WARN 日志,而非静默放过。

2.3 通用工具集(utils)

集中沉淀高频工具,避免各模块重复造轮子:

  • ExcelUtil / ExcelUtils:基于 EasyExcel 的导入导出。
  • BeanUtil / BeanCopier:对象拷贝(Orika/MapStruct 封装)。
  • DateUtil:日期处理;PageUtil:分页对象转换。
  • ValidatorUtil:参数校验;WebUtil:IP / 设备信息提取。
  • SpringUtil:Spring 上下文静态获取(getBean);JacksonUtil / LogStashUtil:JSON 与日志。
  • PinyinUtil:中文转拼音。

⚠️ SpringUtil.getBean() 是静态拿 Bean 的便捷通道,但不能在 Bean 的构造器或 @PostConstruct 早期 使用(容器未就绪会 NPE)。另外它会绕过依赖注入,使依赖关系不可见,大量使用会让单测难以 mock------仅在"非 Bean 上下文(如监听器、静态方法)"中使用,正常业务优先用 @Resource 注入。

2.4 通用模型与枚举

  • bean:通用 DTO / VO / 分页 PageData / 操作记录 OperateRecordDTO
  • enums:通用枚举(操作类型、结果等,带 code/value 便于落库)。
  • constant / exceptions / valid / feign(主数据客户端抽象)。

可复用点 :整个 common 模块可直接作为新项目的 starter 基础;其中的 Feign 透传拦截器、统一响应、分页配置是强复用样板


3. 缓存模块(cache)

解决"缓存数据来源多样、需要热插拔不同加载逻辑"的问题。

3.1 核心设计:策略工厂 + Spring Cache 抽象

  • CacheService:基于 Spring @Cacheable / @CachePut 提供基础缓存读写。
  • CacheProvider<T, E>:缓存数据提供者策略接口 ,仅一个方法 E getCacheDataFrom(T param)------不同数据源(用户、配置、字典等)各自实现。
  • CacheProviderFactory:实现 ApplicationContextAware,启动时自动收集容器中所有 CacheProvider Bean,并按"缓存 key 前缀 → provider 名称"的映射路由。调用方只需拼出带前缀的 key,即可命中对应加载逻辑。
  • 多级缓存:RedissonV2Config 提供 Redis(Redisson)分布式缓存与分布式锁;本地可结合 Caffeine,形成"本地 + 分布式"两级缓存。
java 复制代码
// 示意接口
public interface CacheProvider<T, E> {
    E getCacheDataFrom(T param);
}
// 调用方:key = "userInfo#123" -> 工厂按 "userInfo" 前缀路由到对应 provider

3.2 ⚠️ 必须注意的坑与潜在问题

  1. 前缀映射是"手工静态表" :工厂内部维护 cacheKeyProviderNameMap(如 "userInfo" -> "userInfoCacheProvider")。新增一类缓存数据时,除了写 CacheProvider 实现类,还必须手动往这张映射表注册 ,否则调用方拼出带新前缀的 key 会抛 IllegalArgumentException("无效的入参")。这是最易漏、最难排查的点------建议改为"约定优于配置"(provider 用注解声明前缀,启动时自动注册),消除手工映射。
  2. ApplicationContextAware 把 Bean 放进 static MapcacheProviderMap 是静态字段。在同一 JVM 多 ApplicationContext(如测试用独立 context、或模块被多次启动)场景下,后启动的 context 会覆盖前者,导致前一个 context 的 provider 失效。生产单 context 没问题,但单测要警惕。
  3. "本地 + 分布式"两级缓存的一致性 :本地 Caffeine 没有自动失效广播,更新分布式缓存不会主动清本地。写少读多、允许短暂不一致的场景才适合;强一致数据不要用本地缓存层,或引入 Redis 发布订阅主动失效本地。
  4. 分布式锁用 Redisson 但缓存读用 Spring Cache :两者 key 命名体系不同,别把 @Cacheable 的 key 和分布式锁 key 混为一谈,避免误以为"加了缓存就线程安全"。
  5. 潜在问题CacheProvider.getCacheDataFrom 内部若访问数据库且 SQL 慢,会被缓存框架"雪崩式"并发触发(缓存未命中时多个线程同时回源)。建议对热点 key 加"互斥重建"或 sync=true@Cacheable

4. 文件模块(file)

对对象存储做门面(Facade)封装,屏蔽底层 SDK,统一文件全生命周期管理。

4.1 核心组件:FileComponent

对外提供与存储引擎无关的方法:

  • upload / uploadBatch:单文件、多文件上传(自动按 年/月 分目录 + UUID 重命名防冲突)。
  • uploadBatchZip:批量上传并自动解压 ZIP,对 docx 类文件额外提取正文内容,普通文件直接转存。
  • download / downloadBatch / downloadRealName:单文件、打包下载、保留原文件名下载。
  • preview / remotePreview:本地对象预览与远程 URL 预览。
  • delete / deleteBatch:删除。
  • 桶名(bucket)与存储路径支持从方法参数或 HTTP 请求头(bucketName / storagePath)解析,多租户/多空间场景通用。

4.2 ⚠️ 必须注意的坑与潜在问题

  1. 流资源泄漏风险(最实际的一坑)analyzeZipZipArchiveInputStreamBufferedInputStreamXWPFDocument 三个流嵌套,虽然 finally 关闭了三者,但 convertZipArchiveInputStreamToInputStream 会把 zip 输入流完整读入内存 ByteArrayOutputStream ------大 ZIP(几百 MB)会直接 OOM。更严重的是:外层 zipIn.getNextEntry() 循环里,每次 convert(...) 已消耗了 inputStream,但 docx 分支把同一 inputStream 既用于解析正文又用于 minioComponent.put,依赖"流可被重复读"的假设,实际 ByteArrayInputStream 可以,但代码耦合脆弱。建议:大文件走流式边读边传,禁止一次性读入 byte\[\]。
  2. 中文文件名下载乱码 / 空格被截断download() 直接对 objectName 做 URLEncoder.encode,若 objectName 含路径斜杠会被一并编码;downloadRealName()attachment;fileName= 而非标准的 filename*=UTF-8'',部分浏览器(旧版 Safari)会乱码。建议 :统一用 RFC 5987 的 filename*=UTF-8'' + 双写兼容。
  3. ZIP 中文条目名编码 :解压用 GB2312,但打的 ZIP 若由 Windows(GBK)或 macOS(UTF-8)生成会解错名。应探测编码或强制约定,否则文件名乱码导致后续按名查询失败。
  4. 删除是"硬删"delete 直接调底层 remove无回收站、无引用计数 。业务表还引用着该 objectName 时误删,文件即永久丢失。建议:删除前校验业务引用,或先标记软删 + 异步物理清理。
  5. remotePreview 走 HttpRequest 拉远程流 :没有超时与大小限制,恶意 URL 或超大文件会拖垮业务线程。建议:加连接/读取超时与最大字节上限。
  6. 桶名/路径从请求头取 :意味着调用方可以伪造 bucket 。必须做白名单校验(当前 getBucketNameInternal 取不到才回退到配置默认 bucket,存在越权风险)。
  7. 路径分片用 LocalDate.now() 拼字符串 :月/日不带前导零(如 2026/8/2),与很多对象存储"层级前缀"最佳实践(带零便于分区)略有出入,但能用;注意查询时拼接要与之一致。

5. 定时任务模块(timer)

基于 ElasticJob-Lite 的注解驱动分布式调度框架,含执行日志。

5.1 注解声明式作业:@Scheduler

作用于类(@Component),属性包括:

  • name:作业名(缺省取 服务名_类名)。
  • cron:表达式。
  • shardingTotalCount / shardingItemParameters:分片总数与分片参数(支持集群分片执行)。
  • jobParameters:自定义参数。

框架在启动时扫描该注解,自动向调度中心(ZooKeeper)注册作业,无需 XML 配置。

5.2 执行日志

  • TimerExecuteLogListener:作业生命周期钩子(beforeJobExecuted 记录开始、afterJobExecuted 调用 reportCron 写入执行结果)。注意它被声明为 ElasticJobListener(非 Spring 组件),通过 SpringUtil.getBean(...) 反向拿组件,以免"额外注册 Job Class 导致 ZK 元数据冲突"。
  • TimerComponent:作业的动态注册与执行入口。
  • TimerExecuteLogComponent.reportCron:把执行结果(成功/失败、耗时、异常)写入日志表。

5.3 ⚠️ 必须注意的坑与潜在问题

  1. 执行日志的"元数据缺失"坑(高频)reportCron(jobName) 写日志时需要的字段(作业中文名、所属模块、责任人等)依赖作业元数据。若这些元数据来自进程内缓存或远程查询beforeJobExecuted 能拿到但 afterJobExecuted 时若缓存被清空、节点切换、或刚重启还没预热,就会出现日志字段为 NULL / 空 。这正是先前排查到的"执行日志里部分字段为 NULL"的根因。建议reportCron 时直接通过 Feign 查询完整作业配置(带失败兜底默认值),不要只依赖内存缓存的那个 vo ;且 before/after 之间不要共享可变状态。
  2. ElasticJobListener 不是 Spring Bean 的隐患 :它在 ZK 侧注册,若被当作普通 @Component 还会额外生成一个 Job 类引用,可能触发 ZK 元数据冲突。用 SpringUtil.getBean 取依赖是对的,但要确保监听器自身不被 Spring 扫成 Bean 又同时被作业注册两次
  3. 分片参数与作业逻辑必须对齐shardingItemParameters="0=a,1=b,2=c" 必须与 shardingTotalCount=3 对应的分片项一一对应 ,否则某分片永远拿不到参数、某分片被重复执行。分片逻辑里务必用 shardingContext.getShardingItem() 取自己的分片,不要硬编码。
  4. cron 与集群 :ElasticJob 保证"同一作业在集群中只在一台机器跑某分片",但同一份代码部署多套环境 (测试/生产共用同一 ZK 命名空间)会互相抢作业。建议:ZK namespace 按环境隔离。
  5. 潜在问题 :作业方法内若抛的是 Error 而非 ExceptionafterJobExecuted 仍会触发,但异常信息可能没被 reportCron 捕获,日志显示"成功"实际失败------异常捕获要对 Throwable 兜底。
  6. 幂等 :定时任务可能因网络抖动被 ZK 重新触发(失效转移),作业逻辑必须幂等,否则重复执行会重复写数据。

6. 审计日志模块(log)------重点:ThreadLocal 计时与上下文

注解 + AOP 实现无侵入操作审计,统一落搜索引擎。这是本项目最值得讲清"为什么这么写、坑在哪"的模块。

6.1 注解与切面

  • @OperationRecord(type, content):标注在需要记录的方法上,声明操作类型与内容。
  • OperationHistoryAspect:切面在方法 @Before 记录开始时间,@AfterReturning / @AfterThrowing 分别处理成功/失败,自动采集:
    • 操作人(从当前用户上下文获取,缺失时取系统用户)。
    • 客户端 IP、设备信息、模块/功能编码。
    • 执行耗时(endTime - startTime)。
    • 业务数据变更前后值OperateRecordDTO.businessData / businessDataAfter)。
    • 操作结果(成功/失败)。
  • 最终批量写入搜索引擎(索引 OperationRecordIndex),支持全文检索与审计追溯。
  • 另有 @Action / @QueryRecord / @LogTrace 等注解及对应切面,覆盖动作日志、查询记录、链路追踪。

6.2 🔑 为什么用 ThreadLocal 计时(核心设计说明)

切面需要计算"方法执行耗时 = 结束时间 − 开始时间"。开始时间在 @Before、结束时间在 @AfterReturning/@AfterThrowing两者属于同一次方法调用的前后两个通知 ,但它们处于不同的方法栈帧,没有共同的局部变量可以传递 startTime

常见的错误写法:把 startTime 声明成切面的成员变量(字段)

复制代码
// 错误:成员变量在并发下会串数据
private long startTime; // 多线程共用一个 aspect 实例(Spring 默认单例)
@Before ... startTime = now();
@AfterReturning ... long cost = now() - startTime; // 可能读到别的线程设的值

因为 Spring 的 @Aspect 默认是单例@Before@After 又可能落在不同请求线程上(即使是同一线程,AOP 通知之间也无法通过方法参数传值),用成员变量必然导致并发串值、耗时计算错误

因此项目使用 org.springframework.core.NamedThreadLocal

java 复制代码
private final NamedThreadLocal<Long> startTimeThreadLocal =
        new NamedThreadLocal<>("StartTime-EndTime");

@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
    startTimeThreadLocal.set(System.currentTimeMillis());   // 绑定到当前线程
}

@AfterReturning(...) / @AfterThrowing(...)
private void addRecord(...) {
    long startTime = startTimeThreadLocal.get();             // 同一线程取回
    long endTime = System.currentTimeMillis();
    BigDecimal executeTime = BigDecimal.valueOf((endTime - startTime) / 1000.0)...;
}

NamedThreadLocal线程隔离 的:每个请求线程有自己的一份 startTime,前后通知同处一个线程即可安全传递,且 Named 前缀便于排查内存泄漏时定位是哪个 ThreadLocal。这是"跨通知传递上下文"的标准解法,必须这样写,不能退化成成员变量。

6.3 ⚠️ ThreadLocal 计时方案的坑与潜在问题(开发者视角)

  1. ThreadLocal 必须 remove,否则内存泄漏 + 脏数据 :当前实现只在 set 后取值,没有 remove@Before 设值后,若方法异常且未走 @AfterThrowing(如被别的切面吞掉、或 Error)、或线程被线程池复用,下次同一线程跑别的请求时会 get上一次的旧值 ,导致耗时/上下文错乱。尤其是 Web 容器用线程池,线程会被复用。强烈建议 :在 finallystartTimeThreadLocal.remove()。这是本项目真实存在的隐患点。
  2. 异步方法会让 ThreadLocal 失效@OperationRecord 标注的方法如果内部用 @Async 或自己 new Thread/CompletableFuture 执行真正逻辑,那么 @Before(主线程)设的 ThreadLocal,在子线程的 @AfterReturningget 不到(子线程没有父线程的 ThreadLocal)。结论 :审计切面只适用于同步执行的同一线程 场景;异步方法要么把审计逻辑放进异步体内单独记,要么用 TaskDecorator 做 ThreadLocal 透传。
  3. RequestContextHolder 在异步/非 Web 线程为空 :切面里取 IP、模块编码依赖 RequestContextHolder.getRequestAttributes()。定时任务、MQ 消费线程里没有 Request,会 null,此时 host=deviceInfo=空moduleCode 取不到------必须对每个从 request 取的值做空兜底 (当前对 host 有兜底 0.0.0.0,但 moduleName/functionName 可能为 null 落库,需确认索引字段是否允许 null)。
  4. 异常分支拿不到"业务数据前后值"@AfterThrowing 分支只记了操作类型/内容/结果,没有 businessDataAfter(因为方法没返回 OperateRecordDTO)。这是合理的------失败时本就没有"新值"。但要注意:如果业务在抛异常前已部分写库,审计只记"失败"却看不到"改了一半的数据",排障时缺信息。建议:异常时尽量把入参 DTO 的 before 值也记上。
  5. 返回值约定脆弱 :切面靠"返回值必须是 BaseResponseOperateRecordDTOList<OperateRecordDTO>"来提取审计数据;若开发者返回了别的类型(如直接返回 String、或 BaseResponse 但忘了塞 operateRecord),切面会静默 return(只打 info 日志),审计悄无声息地丢失 。这是"魔法约定"的代价------建议抽一个接口(如 OperateRecordCarrier)让返回类型显式声明"我携带审计数据",编译期就能约束。
  6. 写入搜索引擎是"同步远程调用"EsTemplateComponent.save/saveList 在切面里同步执行,意味着任何被审计的方法都会因 ES 抖动而变慢甚至失败 。ES 不可用时应降级(记本地文件/丢弃 + 告警),绝不能让审计影响主业务。当前实现若 ES 抛异常会沿切面抛到业务方法,导致业务也失败------这是高风险点。
  7. @Order 与事务切面的顺序 :审计切面需要"方法已提交完 DB 再记 before/after 值"。若与 @Transactional 切面顺序不当,可能记录的是事务未提交前的数据。需保证审计切面在事务切面之后(order 值更大)执行。
  8. 用户上下文同样来自 ThreadLocalUserInfoPrincipalHolder.getPrincipal() 也是 ThreadLocal;异步线程/非 Web 入口同样取不到,会回退到"系统用户"------批量脚本操作的审计会全部归因到系统账号,需评估是否符合审计合规要求。

可复用点 :注解 + AOP 审计是通用强复用模式;把存储介质从搜索引擎换成数据库/消息队列也只是改 save 一行。但上述 ThreadLocal/异步/降级/顺序坑,是任何想照搬该模式的团队都必须提前处理的


7. 消息模块(message)

注解声明式 MQ 消费 + 消费日志。

7.1 核心注解:@MessageListener

作用于消费者类(@Target TYPE),属性:

  • topic:消费主题。
  • batch:是否批量消费(默认 false)。

框架扫描该注解自动注册消费者,并内置 MqLogComponent 记录消息消费日志(投递/消费/异常)。

7.2 ⚠️ 必须注意的坑与潜在问题

  1. 消费幂等是硬要求 :MQ(RocketMQ/Kafka)在"至少一次"语义下可能重复投递。同一 topic 的消费逻辑若写库,必须按业务主键去重或用数据库唯一键兜底,否则重复消息会产生脏数据。@MessageListener 本身不提供幂等能力,需业务自己保证。
  2. 注解在类上,一个类一个 topic:若想同一逻辑消费多个 topic,需写多个类;类级注解也意味着"消费逻辑与 topic 强绑定",重构 topic 名时要同步改注解------建议 topic 抽到常量类,避免散落字符串。
  3. 消费线程里同样没有 Request 上下文RequestContextHolder 为空,需要 traceId 时应在消费入口手动生成并 MDC.put,否则链路追踪断在 MQ 处。
  4. MqLogComponent 落库不能拖慢消费:消费日志若同步写 DB 且 DB 慢,会积压消费线程。建议消费日志异步化或采样。
  5. 潜在问题batch=true 时消费到的是消息列表,单条失败的处理策略(整批重试 / 跳过单条)要和产品确认,否则一条坏消息会卡住整批。

8. 流程模块(flow)

适配器模式统一多工作流引擎,业务层无感知。

8.1 核心设计

  • FlowComponent:对外提供流程查询/发起等能力;内部按配置项 workflow-type 选择具体引擎实现。
  • WorkFlowAdapter:工作流适配器接口;AdapterWorkflowEnum 按类型路由到不同实现(如 Camunda 实现、第三方平台实现)。
  • 配套 CamundaComponent / AuditFlowComponent / FormComponent:封装具体引擎的表单与审批能力。

8.2 ⚠️ 必须注意的坑与潜在问题

  1. 配置路由一旦配错,运行时才暴露workflow-type 决定走哪套引擎,配错值会落到默认分支或直接抛"不支持"。建议启动期校验配置合法性,Fail-Fast。
  2. 适配器之间的行为差异要收敛在接口契约内 :不同引擎对"审批人、会签、退回"语义不同,若接口方法把这些差异透传给业务层,适配器就失去意义。应在适配器内抹平差异(统一返回结构),否则业务层还是要写 if (Camunda) ... else ...
  3. 弱复用提醒:仅多引擎或需预留切换时才值得引入该抽象;单引擎项目直接依赖 SDK 即可,过度抽象反而增加理解成本。
  4. 潜在问题:跨引擎迁移历史流程数据时,实例状态机不兼容,需单独做数据迁移脚本,不是换个 Adapter 就能解决。

9. 主数据 / 组织适配模块(adapter)

统一人员、组织、角色查询,屏蔽外部主数据平台差异。

9.1 核心设计

  • OrgAdapter:组织/人员查询统一接口(按组织取人、按父组织取子树、获取当前登录人、按角色取人等)。
  • Org4XxxAdapter:具体平台实现(实现 support(adapter) 做兼容性判断)。
  • XxxComponent:提供"获取当前登录人 / 系统用户 ID"等上下文能力(同样基于 ThreadLocal)。

9.2 ⚠️ 必须注意的坑与潜在问题

  1. 外部平台不可用的降级OrgAdapter 实现里若直接远程调用主数据平台,平台抖动会让所有"取当前人/取组织"的接口失败,连带核心业务 500。必须:对"取当前登录人"这类高频只读场景做本地缓存 + 失败兜底(回退到 token 中的冗余信息),不要把可用性绑死在外部平台。
  2. support(adapter) 路由判断要无歧义 :多个 Adapter 实现并存时,若 support 条件重叠,可能命中错的实现。建议用明确枚举匹配,避免模糊字符串包含判断。
  3. 用户上下文 ThreadLocal 的清空 :同第 6 节,登录人信息用 ThreadLocal 持有,请求结束时必须 remove,否则线程池复用会串号(A 用户的操作记成 B 用户)------这是合规与安全的严重问题。
  4. 潜在问题 :主数据模型升级(字段改名、接口版本)会穿透到 OrgAdapter 接口;接口方法签名若直接耦合对方模型,升级成本高。建议适配器内部做 DTO 转换,对外暴露本项目自己的模型。

10. 其他能力模块(简介)

模块 职责 通用借鉴点 主要坑
树结构(tree) 组织/分类树构建与遍历工具 父ID→树的通用递归/栈算法可复用 深层级递归小心栈溢出;超大树用 Map 索引法而非递归
规则引擎(rule) 基于表达式(QLExpress)的规则计算 规则与代码解耦,配置化业务规则 表达式由运营配置时存在注入/死循环风险,必须沙箱化 + 超时限制
振动/算法(vibrate) 信号处理与算法分析 算法模块独立封装,不污染业务 计算密集,避免在主请求线程跑;注意数值精度与 NaN
队列(queue) 队列封装 异步解耦的通用封装 内存队列重启丢数据,重要消息要用持久化 MQ
聚合(all) 聚合所有通用能力模块 业务系统一站式引入的"bom 式"模块 引入即带入全部传递依赖,注意依赖冲突与体积
基础设施封装(wipi-*) Redis/对象存储/MQ/搜索引擎/调度 SDK 封装 把第三方 SDK 收敛在底层 SDK 版本升级集中在此,需做兼容测试

11. 跨项目复用建议(含避坑优先级)

强复用(建议直接照搬,但先修掉文中标注的坑)

  1. 基础模块 :统一响应 BaseResponse、业务异常、MyBatis-Plus 分页配置、Feign 透传拦截器。→ 先处理 BaseResponse 误塞审计字段问题。
  2. 缓存策略工厂CacheProvider + CacheProviderFactory):热插拔缓存数据源。→ 先把"手工前缀映射表"改为自动注册。
  3. 文件门面组件:上传/下载/批量/ZIP 解析/预览。→ 先修流泄漏、中文名、bucket 越权三坑。
  4. 注解驱动定时任务@Scheduler + 分片 + 执行日志)。→ 先解决 reportCron 元数据 NULL 坑与分片参数对齐。
  5. 注解 + AOP 审计日志@OperationRecord)。→ 必须补 ThreadLocal.remove、ES 降级、异步失效说明、切面 Order。
  6. 注解声明式 MQ 消费@MessageListener + 消费日志)。→ 补消费幂等与 MQ 处 traceId。

弱复用 / 需改造

  1. 工作流适配器:依赖具体引擎,仅多引擎场景需要。
  2. 主数据/组织适配器:接口需随外部平台模型调整,且必须加降级与 ThreadLocal 清空。
  3. 业务系统(sys):纯业务聚合层,无通用价值,不在借鉴范围。

架构样板

  • 多模块单向依赖、基础模块零业务依赖,是大型后端项目的通用骨架。
  • "基础设施封装 → 基础公共服务 → 通用能力 → 业务系统"四层结构,可直接作为新项目的分层模板。
  • 贯穿所有模块的一条铁律 :凡是"请求级 / 用户级上下文"都靠 ThreadLocal 传递(耗时、登录人、traceId),用完必 remove,且异步线程不继承------这是本项目反复踩坑后最该写进规范的结论。

12. 关键技术栈清单

类别 技术
框架 Spring Boot 2.6.x、Spring Cloud 2021.x
数据访问 MyBatis-Plus 3.5.x、MySQL
缓存 Redis(Redisson)、Caffeine(本地)
对象存储 S3 兼容对象存储(MinIO 等)
分布式调度 ElasticJob-Lite(ZooKeeper 协调)
消息队列 RocketMQ
搜索引擎 Elasticsearch
工作流 Camunda / 第三方流程平台(适配器切换)
规则引擎 QLExpress 表达式
文档处理 EasyExcel、Apache POI、PDF(flying-saucer/itext)
工具 Hutool、Guava、Orika、Lombok、FastJson/Jackson
服务调用 OpenFeign + OkHttp

本总结描述架构、设计模式与真实避坑经验,可作为新项目的通用参考样板与"踩坑 checklist"。

相关推荐
CodeStats18 分钟前
【Java 表达式引擎】如何设计一套 Java 表达式引擎:从递归下降到 AST 求值的完整实践
java·ai编程·表达式·引擎
vipxieliang1 小时前
ValidX错误消息国际化完全指南:8种语言9个语言包与三级回退机制
java·spring boot
xbgRS1 小时前
RocketMQ消费者消息获取
java·rocketmq·java-rocketmq
云和数据.ChenGuang1 小时前
git revert回退问题
java·服务器·人工智能·git·fastapi·强化学习
lhldsg1 小时前
AI智能商城实战指南:从架构设计到落地开发经验分享
java·人工智能·经验分享·小程序
lhldsg1 小时前
树洞交友系统开发实战:从匿名社交需求到技术落地
java·小程序·uni-app·交友
数智启示录2 小时前
实时数据湖 Flink CDC + Kafka +Doris 【企业级实战】之Kafka 事件缓冲层 【附核心源码】 04
java·大数据·flink·kafka·数据库开发
AI人工智能+电脑小能手2 小时前
大白话说Java设计模式-41-备忘录模式(源码剖析篇)
java·设计模式·备忘录模式·源码分析·serializable·undo log·spring statemachine
lv__pf2 小时前
Sentinel【TL微服务10、11】
java·微服务·sentinel