fix(transfer): 发起/同步异常错误消息按中文本地化解析后落库, 避免 errorMsg 存 messageKey 字符串

- TransferStartService: 新增 resolveErrorMsg 按 Locale.CHINA 解析 BizException messageKey 后落库;
  BizException 直接透传保留通道原始 messageKey(由 RestExceptionHandler 按请求 locale 解析), 避免双重前缀
- TransferSyncService: 同步查单失败同样按中文解析错误消息, 避免同步记录存 key
This commit is contained in:
daxpay
2026-08-08 14:49:23 +08:00
parent 39637e9af6
commit 19761b27e5
2 changed files with 48 additions and 3 deletions

View File

@@ -12,11 +12,13 @@ import cn.daxpay.open.payment.trade.transfer.entity.TransferTrade;
import cn.daxpay.open.payment.trade.transfer.param.TransferParam;
import cn.daxpay.open.payment.trade.transfer.runtime.mq.TransferSyncMessage;
import cn.daxpay.open.platform.common.artemis.service.ArtemisTemplateService;
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
import cn.daxpay.open.platform.common.redis.lock.LockExecutor;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
import cn.daxpay.open.platform.core.exception.BizException;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -24,6 +26,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
@@ -131,11 +134,35 @@ public class TransferStartService {
} catch (Exception e) {
// 发起异常: 置 FAIL 并记录错误, 然后向上抛出, 避免接口返回"假成功"误导调用方
log.error("转账发起失败: tradeNo={}, channel={}", trade.getTradeNo(), channel, e);
assistService.fail(channel, trade, e.getMessage());
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "pay.error.transfer.createFailed", e.getMessage());
// BizException.getMessage() 返回 messageKey(未解析), 需按固定中文解析为本地化文案再落库,
// 避免订单 errorMsg 存 key 字符串
String errorMsg = resolveErrorMsg(e);
assistService.fail(channel, trade, errorMsg);
// BizException 直接透传, 保留通道原始 messageKey(由 RestExceptionHandler 按请求 locale 解析),
// 避免外层"转账发起失败"与通道前缀叠加成双重前缀; 非 BizException(如网络异常)无 messageKey, 兜底包装
if (e instanceof BizException biz) {
throw biz;
}
throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "pay.error.transfer.createFailed", errorMsg);
}
}
/// 解析异常为本地化错误消息
///
/// [BizException] 的 getMessage() 返回 i18n messageKey(未经 I18nUtil 解析), 直接记录会导致订单 errorMsg
/// 存 key 字符串(如 "error.channel.alipay.transferFailed")。本方法按固定中文(Locale.CHINA)解析,
/// 与 [cn.daxpay.open.platform.system.handler.exception.RestExceptionHandler] 的日志行为一致,
/// 保证落库文案不随请求语言变化。非 BizException 的 getMessage() 已是真实文案, 直接使用。
private String resolveErrorMsg(Throwable e) {
if (e instanceof BizException biz) {
String key = biz.resolveMessageKey();
if (key != null) {
return I18nUtil.get(key, Locale.CHINA, biz.getArgs());
}
}
return e.getMessage();
}
/// 注册 2 分钟延迟同步(发送失败由定时同步任务兜底)
private void registerDelaySync(String tradeNo) {
try {

View File

@@ -9,14 +9,17 @@ import cn.daxpay.open.payment.trade.record.entity.PaySyncRecord;
import cn.daxpay.open.payment.trade.transfer.bo.TransferResultBo;
import cn.daxpay.open.payment.trade.transfer.dao.TransferTradeManager;
import cn.daxpay.open.payment.trade.transfer.entity.TransferTrade;
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
import cn.daxpay.open.platform.common.redis.lock.LockExecutor;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.enums.pay.trade.TradeTypeEnum;
import cn.daxpay.open.platform.core.exception.BizException;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.Objects;
/// # 转账同步服务
@@ -76,7 +79,7 @@ public class TransferSyncService {
} catch (Exception e) {
// 通道查单失败: 保持处理中, 落同步记录, 由定时任务兜底重试
log.warn("转账同步查单失败: tradeNo={}, 保持处理中", latestTrade.getTradeNo(), e);
this.saveSyncRecord(channel, latestTrade, null, false, e.getMessage());
this.saveSyncRecord(channel, latestTrade, null, false, resolveErrorMsg(e));
return;
}
// 按通道结果调整状态
@@ -110,4 +113,19 @@ public class TransferSyncService {
record.setMchNo(trade.getMchNo());
paySyncRecordManager.save(record);
}
/// 解析异常为本地化错误消息
///
/// [BizException] 的 getMessage() 返回 i18n messageKey(未经 I18nUtil 解析), 直接记录会导致同步记录
/// errorMsg 存 key 字符串。本方法按固定中文(Locale.CHINA)解析, 保证落库文案不随请求语言变化。
/// 非 BizException 的 getMessage() 已是真实文案, 直接使用。
private String resolveErrorMsg(Throwable e) {
if (e instanceof BizException biz) {
String key = biz.resolveMessageKey();
if (key != null) {
return I18nUtil.get(key, Locale.CHINA, biz.getArgs());
}
}
return e.getMessage();
}
}