今日工作

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.javaFilterRegistrationBean 配置完全相同,只是包名不同)
  • @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-clientmall-order-client 各建了 RowsDTO ,以及用于 ID 返回的 IdDTO 。全局扫描一轮后,admin 模块 15 个 int + 3 个 void、order 模块 12 个 int/void/Long 全部包完

6. B 端订单管理补全

OrderFeignClient 里定义了 B 端配送地址和退货审核的接口,但一直没有 controller 实现。今天补上了:

配送地址管理(OrderDeliveryAddressController)

  • POST /v1/tradeDeliveryAddress/searchByPage
  • POST /v1/tradeDeliveryAddress/insert/update/deleteByIds
  • GET /v1/tradeDeliveryAddress/findById

退货审核(OrderReturnApprovalController)

  • POST /v1/trade/return/searchByPage
  • GET /v1/trade/return/findById
  • POST /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 会造成不必要的网络损耗,需要按业务场景区分。