今日工作
1. common-web 目录整理 + 注解化控制
common-web 之前的目录有点乱, @EnableXxx 注解和 @Configuration 配置类全混在 config/ 包里。今天拆了一刀:
改前:
config/
EnableApiResultWrapper.java ← 注解
EnableRequestLogFilter.java ← 注解
ApiResultWrapperConfiguration.java
RequestLogFilterConfiguration.java
WebAutoConfiguration.java
改后:
annotation/ ← 注解单独放
EnableApiResultWrapper.java
EnableRequestLogFilter.java
config/ ← 只放 @Configuration
ApiResultWrapperConfiguration.java
RequestLogFilterConfiguration.java
WebAutoConfiguration.java
同时引入了 @EnableXxx 模式替代原本的 @ConditionalOnProperty :
@EnableRequestLogFilter— 替代 3 个服务里重复的RequestLogFilterConfig.java(FilterRegistrationBean配置完全相同,只是包名不同)@EnableApiResultWrapper— 控制GlobalApiResultHandler是否生效
2. GlobalApiResultHandler 失效之谜
GlobalApiResultHandler 实现了 ResponseBodyAdvice<Object> ,通过 WebAutoConfiguration#@Bean 注册。按理说 Spring MVC 会自动发现,但实际上没生效——所有 @RestController 的返回都是裸数据,没有被 ApiResult 包装。
// 实际表现
GET /v1/auth/role/all → [{...}] // 裸数组,不是 {"code":200,"data":[{...}]}
排查后发现: @RestControllerAdvice 被注释掉了,只靠 @Bean 注册时 Spring MVC 不识别。取消注释 @RestControllerAdvice 后立即生效。
⚠️ 新手提示:Spring MVC 中
ResponseBodyAdvice必须配合@ControllerAdvice/@RestControllerAdvice使用,仅靠@Bean注册不会触发。这是因为RequestMappingHandlerAdapter在初始化时收集 advice beans,依赖的是注解元数据而非 bean 类型。
3. GlobalExceptionHandler 重构
原来的 handleException(Throwable) 一个方法里塞了三种异常处理 + INNER-REQUEST 头判断,还混了两个没被调用的死方法 getErrorMessage() / getErrorCode() 。
重构后拆成独立方法:
@ExceptionHandler(Throwable.class)
public Object handleException(Throwable e) {
if (isInnerRequest()) return handleInnerRequest(e);
if (e instanceof BusinessException be) return handleBusinessException(be);
if (e instanceof MethodArgumentNotValidException me) return handleValidationException(me);
return handleUnknownException(e);
}
删除了 40 行正则死代码( getErrorMessage / getErrorCode ),改用 Java 17 pattern matching 简化类型判断。
4. R4 降级:FallbackFactory 自动配置
之前引入 resilience4j 熔断时,为每个 FeignClient 创建了 FallbackFactory,标注为 @Component 。但这导致所有引用 mall-basic-client 的服务必须在 scanBasePackages 里加上 cn.net.mall.basic.client.fallback ,否则报错:
No fallbackFactory instance of type class XxxFallbackFactory found
今天把 FallbackFactory 统一注册为自动配置:
// mall-basic-client/config/FallbackFactoryAutoConfiguration.java
@Configuration
public class FallbackFactoryAutoConfiguration {
@Bean public DictFeignFallbackFactory dictFeignFallbackFactory() { return new DictFeignFallbackFactory(); }
@Bean public SmsFeignFallbackFactory smsFeignFallbackFactory() { return new SmsFeignFallbackFactory(); }
@Bean public SmsRecordFeignFallbackFactory smsRecordFeignFallbackFactory() { return new SmsRecordFeignFallbackFactory(); }
}
通过 AutoConfiguration.imports 自动生效,各服务不再需要改 scanBasePackages ,4 个服务的 hacks 全部回滚。这算一个设计教训—— @Component + scanBasePackages 的组合在跨模块场景下很脆弱,自动配置才是正解。
5. RowsDTO / IdDTO 统一响应
一直被 Swagger 显示裸 integer / string 困扰。问题根源是控制器方法直接返回原始类型:
// 改前:Swagger 只显示 "integer"
public int insert(@RequestBody MenuEntity entity)
// 改后:Swagger 显示 {"rows": 1}
public RowsDTO insert(@RequestBody MenuEntity entity) {
return new RowsDTO(menuService.insert(entity));
}
在 mall-admin-client 和 mall-order-client 各建了 RowsDTO ,以及用于 ID 返回的 IdDTO 。全局扫描一轮后,admin 模块 15 个 int + 3 个 void、order 模块 12 个 int/void/Long 全部包完。
6. B 端订单管理补全
OrderFeignClient 里定义了 B 端配送地址和退货审核的接口,但一直没有 controller 实现。今天补上了:
配送地址管理(OrderDeliveryAddressController)
POST /v1/tradeDeliveryAddress/searchByPagePOST /v1/tradeDeliveryAddress/insert/update/deleteByIdsGET /v1/tradeDeliveryAddress/findById
退货审核(OrderReturnApprovalController)
POST /v1/trade/return/searchByPageGET /v1/trade/return/findByIdPOST /v1/trade/return/approve(状态 1→2,记录审核时间)POST /v1/trade/return/reject(状态 1→3,需填写拒绝原因)
同时补了 OrderReturnApplyMapper.findById 的 SQL,以及 service 层的 approve / reject / findById 方法。
7. 技术枚举替代字典表
数据库 common_dict 表里混了 10 个字典,其中只有 coupon_type 是运营可能会动态添加的,其余 9 个全是技术常量:
order_status: 待支付/已支付/已发货/已完成/已取消...
pay_status: 未支付/已支付/已退款/支付失败
valid_status: 启用/禁用
...
把 9 个技术常量抽成代码枚举,放在 common-core/enums/ :
@Schema(description = "订单状态", enumAsRef = true)
public enum OrderStatus {
@Schema(description = "待支付") WAIT_PAY(1),
@Schema(description = "已支付") PAID(2),
...
}
DTO 的 @Schema(allowableValues = {...}) 标注后,Swagger 直接显示可选值和含义,不再需要查 DB 才知道 1 代表什么。
8. Nacos 配置:common.yaml 合并回各服务
之前把 Redis/JWT 密钥/R4 配置统一到了 common.yaml 通过 shared-configs 引用。但 Spring Cloud Alibaba 的 config.import 在某些版本加载 common.yaml 时会报 “does not exist”(即使 REST API 能查到),而 shared-configs 加载后又因为属性源优先级问题导致 @Value 解析失败。
最终决定把 common.yaml 内容合并回各服务的个性化 yaml,删除 common.yaml ,各服务只留一条 config.import: nacos:mall-xxx-api-dev.yaml 。虽然代码上有点冗余,但省掉了配置加载顺序的坑。
9. 分库分表下的 ES 双写
order 用了分库分表(8库32表),用户的"我的订单"列表通过 ES 检索避免跨分片广播查询。数据写入 ES 的时机:
- 订单创建:
OrderCreatedListener(@Async异步写 ES) - 订单状态变更:
OrderService里同步写 ES
没有定时任务或 Canal 同步,也没有失败重试机制——ES 写失败只是日志记录。后续可以考虑引入 Canal + Binlog 解耦双写问题。
10. BFF 路由规划
BFF 设计时需要注意的点:
- 读操作不追求实时性(首页数据、商品推荐)→ 适合走 BFF
- 读操作但需要实时(订单状态、支付结果)→ 应直通后端
- 写操作(下单、取消、确认收货)→ 直通后端,BFF 只做路由鉴权
全部请求抛向 BFF 会造成不必要的网络损耗,需要按业务场景区分。