feat(transfer): 转账场景标识三通道统一(transferScene 合并场景配置ID/收款感知字段), 发起失败抛异常, 支付宝转账单表加列

This commit is contained in:
daxpay
2026-08-07 22:18:19 +08:00
parent 1b1470fa6c
commit cd1384fcc3
8 changed files with 28 additions and 36 deletions

View File

@@ -3626,6 +3626,7 @@ CREATE TABLE "public"."pay_transfer_order_alipay" (
"payee_type" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"payee_account" varchar(100) COLLATE "pg_catalog"."default" NOT NULL,
"payee_name" varchar(100) COLLATE "pg_catalog"."default",
"transfer_scene" varchar(50) COLLATE "pg_catalog"."default",
"pay_fund_order_id" varchar(64) COLLATE "pg_catalog"."default",
"creator" int8,
"create_time" timestamptz(6),
@@ -3654,6 +3655,7 @@ COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."error_msg" IS '错误信
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."payee_type" IS '收款人账号类型(user_id/open_id/login_name)';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."payee_account" IS '收款人账号';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."payee_name" IS '收款人姓名';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."transfer_scene" IS '转账场景标识(支付宝=转账场景配置ID, FAIL重试时恢复场景用)';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."pay_fund_order_id" IS '支付宝资金流水号(财务对账,区别于 out_transfer_no)';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."creator" IS '创建人';
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."create_time" IS '创建时间';

View File

@@ -0,0 +1,8 @@
-- ----------------------------
-- 数据库升级脚本(增量)
-- 按时间顺序追加, 已有列/表请勿重复执行
-- ----------------------------
-- 支付宝转账单新增转账场景标识列(支付宝=转账场景配置ID, FAIL重试时恢复场景用)
ALTER TABLE "public"."pay_transfer_order_alipay" ADD COLUMN "transfer_scene" varchar(50) COLLATE "pg_catalog"."default";
COMMENT ON COLUMN "public"."pay_transfer_order_alipay"."transfer_scene" IS '转账场景标识(支付宝=转账场景配置ID, FAIL重试时恢复场景用)';

View File

@@ -74,7 +74,8 @@ public class TransferStrategyContext {
/// 收款人微信 openid
private String payeeOpenid;
/// 转账场景(微信/抖音, 抖音发起时可经本字段回写)
/// 转账场景标识(三通道通用: 支付宝=场景配置ID/抖音=场景枚举码, 由请求传入;
/// 微信=通道商户配置场景, 发起时经本字段回写)
private String transferScene;
/// 收款人姓名(微信金额档位校验用)
@@ -89,12 +90,6 @@ public class TransferStrategyContext {
/// 收款人姓名(支付宝/抖音)
private String payeeName;
/// 转账场景配置ID(支付宝专用,发起时指定用哪个场景; 空则用通道商户默认)
private String transferSceneConfigId;
/// 转账场景报备信息(微信/抖音转账必填, 各场景要求不同)
private List<TransferReportInfo> reportInfos;
/// 用户收款感知(抖音专用, 收款人在抖音中看到的文案)
private String userRecvPerception;
}

View File

@@ -83,8 +83,8 @@ public class AlipayTransferOrder extends MchBaseEntity {
/// 收款人姓名
private String payeeName;
/// 转账场景配置ID(支付宝专用, FAIL重试时恢复场景用)
private String transferSceneConfigId;
/// 转账场景标识(支付宝=转账场景配置ID, FAIL重试时恢复场景用)
private String transferScene;
/// 转账场景报备信息(JSON序列化, FAIL重试时恢复报备用)
private String reportInfos;

View File

@@ -82,9 +82,6 @@ public class DouyinTransferOrder extends MchBaseEntity {
/// 转账场景ID(发起转账时由前端选择的主数据枚举, 如1001-1007)
private String transferScene;
/// 用户收款感知(收款人在抖音中看到的文案)
private String userRecvPerception;
/// 转账场景报备信息(JSON序列化, FAIL重试时恢复报备用)
private String reportInfos;

View File

@@ -62,6 +62,7 @@ public class TransferParam {
/// 收款人账号类型
/// @see cn.daxpay.open.payment.trade.transfer.enums.TransferPayeeTypeEnum
/// 抖音支持 openid/phone 两种(手机号复用收款人账号字段, 子应用证书加密上送)
@NotBlank(message = "收款人账号类型必填")
@Size(max = 32, message = "收款人账号类型不可超过32位")
@Schema(description = "收款人账号类型")
@@ -92,18 +93,8 @@ public class TransferParam {
@Schema(description = "转账场景报备信息")
private List<TransferReportInfo> reportInfos;
/// 转账场景配置ID(支付宝专用,可选; 不传则用通道商户的默认转账场景配置)
@Size(max = 32, message = "转账场景配置ID不可超过32位")
@Schema(description = "转账场景配置ID(支付宝专用,不传用默认)")
private String transferSceneConfigId;
/// 转账场景ID(抖音专用, 主数据枚举如1001-1007, 发起转账时由前端选择传入)
@Size(max = 16, message = "转账场景ID不可超过16位")
@Schema(description = "转账场景ID(抖音专用,主数据枚举)")
/// 转账场景标识(三通道通用: 支付宝=转账场景配置ID, 抖音=主数据枚举码如1001, 发起时由前端选择传入; 微信不传, 用通道商户配置场景)
@Size(max = 32, message = "转账场景不可超过32位")
@Schema(description = "转账场景标识(支付宝=场景配置ID/抖音=场景枚举码, 微信不传用配置)")
private String transferScene;
/// 用户收款感知(抖音专用, 收款人在抖音中看到的文案, 按场景枚举选项)
@Size(max = 64, message = "用户收款感知不可超过64位")
@Schema(description = "用户收款感知(抖音专用)")
private String userRecvPerception;
}

View File

@@ -159,14 +159,14 @@ public class TransferAssistService {
.setReqTime(OffsetDateTime.now());
// 商户号独立赋值(父类 setter 返回 MchBaseEntity, 禁止链式)
order.setMchNo(mchNo);
// 持久化场景配置ID与报备信息(FAIL重试时恢复)
order.setTransferSceneConfigId(param.getTransferSceneConfigId());
// 持久化场景标识与报备信息(FAIL重试时恢复)
order.setTransferScene(param.getTransferScene());
order.setReportInfos(serializeReportInfos(param.getReportInfos()));
alipayTransferOrderManager.save(order);
TransferTrade trade = this.buildTrade(order, channel, transferNo);
transferTradeManager.save(trade);
return buildAlipayContext(order).setChannel(channel).setTrade(trade)
.setTransferSceneConfigId(param.getTransferSceneConfigId())
.setTransferScene(param.getTransferScene())
.setReportInfos(param.getReportInfos());
}
case "douyin" -> {
@@ -189,15 +189,13 @@ public class TransferAssistService {
.setReqTime(OffsetDateTime.now());
// 商户号独立赋值(父类 setter 返回 MchBaseEntity, 禁止链式)
order.setMchNo(mchNo);
// 持久化收款感知与报备信息(FAIL重试时恢复)
order.setUserRecvPerception(param.getUserRecvPerception());
// 持久化场景与报备信息(FAIL重试时恢复)
order.setReportInfos(serializeReportInfos(param.getReportInfos()));
douyinTransferOrderManager.save(order);
TransferTrade trade = this.buildTrade(order, channel, transferNo);
transferTradeManager.save(trade);
return buildDouyinContext(order).setChannel(channel).setTrade(trade)
.setTransferScene(param.getTransferScene())
.setUserRecvPerception(param.getUserRecvPerception())
.setReportInfos(param.getReportInfos());
}
default -> throw new IllegalArgumentException("未知转账通道: " + channel);
@@ -483,8 +481,8 @@ public class TransferAssistService {
.setPayeeType(order.getPayeeType())
.setPayeeAccount(order.getPayeeAccount())
.setPayeeName(order.getPayeeName())
// 恢复场景配置ID与报备信息(FAIL重试时使用)
.setTransferSceneConfigId(order.getTransferSceneConfigId())
// 恢复场景标识与报备信息(FAIL重试时使用)
.setTransferScene(order.getTransferScene())
.setReportInfos(deserializeReportInfos(order.getReportInfos()));
}
@@ -506,8 +504,7 @@ public class TransferAssistService {
.setPayeeAccount(order.getPayeeAccount())
.setPayeeName(order.getPayeeName())
.setTransferScene(order.getTransferScene())
// 恢复收款感知与报备信息(FAIL重试时使用)
.setUserRecvPerception(order.getUserRecvPerception())
// 恢复报备信息(FAIL重试时使用)
.setReportInfos(deserializeReportInfos(order.getReportInfos()));
}

View File

@@ -17,6 +17,7 @@ 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.BizInfoException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -130,9 +131,10 @@ public class TransferStartService {
this.registerDelaySync(trade.getTradeNo());
}
} catch (Exception e) {
// 发起异常: 置 FAIL 并记录错误
// 发起异常: 置 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());
}
}