mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 14:56:06 +08:00
feat(channel): 支付宝转账校验收紧(仅user_id/login_name/姓名/金额下限/大额理由) + 抖音手机号收款与转账号格式校验
This commit is contained in:
@@ -51,13 +51,13 @@ public class AlipayTransferService {
|
||||
|
||||
/// 执行支付宝转账
|
||||
///
|
||||
/// @param context 转账策略上下文(通道特有字段: payeeType/payeeAccount/payeeName/transferSceneConfigId)
|
||||
/// @param context 转账策略上下文(通道特有字段: payeeType/payeeAccount/payeeName/transferScene)
|
||||
/// @param credential 通道调用凭证
|
||||
/// @return 转账结果
|
||||
public TransferResultBo transfer(TransferStrategyContext context, AlipaySdkCredential credential) {
|
||||
// 转账场景配置(2026 新商户必填): 显式 configId 优先, 否则用通道商户默认
|
||||
// 转账场景配置(2026 新商户必填): 显式场景配置ID优先, 否则用通道商户默认
|
||||
AlipayTransferSceneConfig scene = alipayTransferSceneConfigService.findEffective(
|
||||
context.getChannelMchNo(), context.getTransferSceneConfigId());
|
||||
context.getChannelMchNo(), context.getTransferScene());
|
||||
|
||||
AlipayTransferReq req = new AlipayTransferReq();
|
||||
req.setOutBizNo(context.getTransferNo());
|
||||
|
||||
@@ -17,25 +17,33 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Set;
|
||||
|
||||
/// # 支付宝直连转账策略
|
||||
///
|
||||
/// 支付宝单笔转账(uni.transfer)的通道策略。
|
||||
/// 通道差异:
|
||||
/// - 支持三种收款人类型([TransferPayeeTypeEnum]: user_id/open_id/login_name)
|
||||
/// - 收款人姓名非必填, 但部分场景(转账到他人账号)支付宝要求姓名校验
|
||||
/// - 支持两种收款人类型([TransferPayeeTypeEnum]: user_id/login_name)
|
||||
/// - 收款人姓名: 登录号(login_name)转账必填, 其余场景可选(填了则校验姓名一致性)
|
||||
/// - 转账金额最低 0.1 元(文档 trans_amount 取值范围)
|
||||
/// - 转账金额达到 50000 元时, 付款理由(remark)必填(监管要求)
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlipayTransferStrategy extends AbsTransferStrategy {
|
||||
|
||||
/// 支付宝支持的收款人类型
|
||||
/// 支付宝支持的收款人类型(仅会员ID/登录号, 不支持开放ID)
|
||||
private static final Set<String> SUPPORTED_PAYEE_TYPES = Set.of(
|
||||
TransferPayeeTypeEnum.USER_ID.getCode(),
|
||||
TransferPayeeTypeEnum.OPEN_ID.getCode(),
|
||||
TransferPayeeTypeEnum.LOGIN_NAME.getCode());
|
||||
|
||||
/// 转账金额下限(元, 文档 trans_amount 取值范围 [0.1, 100000000])
|
||||
private static final BigDecimal AMOUNT_MIN = new BigDecimal("0.1");
|
||||
|
||||
/// 大额档位(元): 达到该金额后付款理由必填(错误码 MEMO_REQUIRED_IN_TRANSFER_ERROR)
|
||||
private static final BigDecimal LARGE_AMOUNT_LIMIT = new BigDecimal("50000");
|
||||
|
||||
private final AlipayTransferService alipayTransferService;
|
||||
private final AlipayDirectConfigAssembler alipayDirectConfigAssembler;
|
||||
private final AlipayTransferConfigManager alipayTransferConfigManager;
|
||||
@@ -63,6 +71,25 @@ public class AlipayTransferStrategy extends AbsTransferStrategy {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.alipay.transferTitleRequired");
|
||||
}
|
||||
// 登录号(login_name)转账时姓名必填(文档 payee_info.name 条件必填)
|
||||
if (TransferPayeeTypeEnum.LOGIN_NAME.getCode().equals(param.getPayeeType())
|
||||
&& StrUtil.isBlank(param.getPayeeName())) {
|
||||
// 支付宝: 登录号收款时必须填写收款人姓名
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.alipay.transferLogonNameRequired");
|
||||
}
|
||||
// 转账金额不可低于 0.1 元
|
||||
if (param.getAmount().compareTo(AMOUNT_MIN) < 0) {
|
||||
// 支付宝: 转账金额不可低于0.1元
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.alipay.transferAmountMin");
|
||||
}
|
||||
// 转账金额达到 50000 元时, 付款理由必填(监管要求, 错误码 MEMO_REQUIRED_IN_TRANSFER_ERROR)
|
||||
if (param.getAmount().compareTo(LARGE_AMOUNT_LIMIT) >= 0 && StrUtil.isBlank(param.getReason())) {
|
||||
// 支付宝: 转账金额达到50000元时, 必须填写付款理由
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.alipay.transferRemarkRequired");
|
||||
}
|
||||
}
|
||||
|
||||
/// 发起转账
|
||||
|
||||
@@ -21,9 +21,12 @@ public class DouyinTransferReq {
|
||||
/// 转账金额(分, 同步可空)
|
||||
private Long amount;
|
||||
|
||||
/// 收款人 openid
|
||||
/// 收款人 openid(与手机号二选一, 不可同时填入)
|
||||
private String openid;
|
||||
|
||||
/// 收款人手机号(phone 模式, 子应用证书加密上送; 与 openid 二选一, 不可同时填入)
|
||||
private String phoneNumber;
|
||||
|
||||
/// 转账场景ID(transfer_scene_id, 主数据枚举1001-1007)
|
||||
private String scene;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.strategy.transfer.TransferStrategyContext;
|
||||
import cn.daxpay.open.payment.trade.enums.PayFundStatusEnum;
|
||||
import cn.daxpay.open.payment.trade.transfer.bo.TransferResultBo;
|
||||
import cn.daxpay.open.payment.trade.transfer.enums.TransferPayeeTypeEnum;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.system.service.config.infra.PlatformUrlConfigService;
|
||||
@@ -37,19 +38,23 @@ public class DouyinTransferService {
|
||||
|
||||
/// 执行抖音转账
|
||||
///
|
||||
/// @param context 转账策略上下文(通道特有字段: payeeAccount/payeeName/transferScene)
|
||||
/// @param context 转账策略上下文(通道特有字段: payeeType/payeeAccount/payeeName/transferScene)
|
||||
/// @param credential 通道调用凭证
|
||||
/// @return 转账结果
|
||||
public TransferResultBo transfer(TransferStrategyContext context, DouyinSdkCredential credential) {
|
||||
DouyinTransferReq req = new DouyinTransferReq();
|
||||
req.setOutBillNo(context.getTransferNo());
|
||||
req.setAmount(context.getAmount());
|
||||
req.setOpenid(context.getPayeeAccount());
|
||||
// 收款人: openid 或手机号(phone 模式, 子应用证书加密上送)
|
||||
if (TransferPayeeTypeEnum.PHONE.getCode().equals(context.getPayeeType())) {
|
||||
req.setPhoneNumber(context.getPayeeAccount());
|
||||
} else {
|
||||
req.setOpenid(context.getPayeeAccount());
|
||||
}
|
||||
req.setScene(context.getTransferScene());
|
||||
req.setUserName(context.getPayeeName());
|
||||
req.setRemark(StrUtil.sub(context.getTitle(), 0, 32));
|
||||
// 收款感知使用请求参数(按场景枚举选项), 不再用转账原因顶替
|
||||
req.setPerception(context.getUserRecvPerception());
|
||||
// 用户收款感知不传, 抖音按场景取默认(第一个)
|
||||
// 转账场景报备信息(按场景要求填写)
|
||||
req.setReportInfos(context.getReportInfos());
|
||||
req.setNotifyUrl(this.buildNotifyUrl(context));
|
||||
|
||||
@@ -19,14 +19,19 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/// # 抖音直连转账策略
|
||||
///
|
||||
/// 抖音商家转账的通道策略。
|
||||
/// 通道差异:
|
||||
/// - openid 收款人([TransferPayeeTypeEnum#OPENID]), 收款人 openId 由「转账发起应用」(网站应用)承接 H5 授权
|
||||
/// - 收款人支持 openid / phone(手机号) 两种([TransferPayeeTypeEnum]),
|
||||
/// 手机号需与抖音支付实名一致, 由子应用证书加密上送;
|
||||
/// 收款人 openId 由「转账发起应用」(网站应用)承接 H5 授权
|
||||
/// - transfer_scene_id 为主数据枚举(1001-1007), 发起转账时由前端选择传入, 无需预配置
|
||||
/// - 转账发起应用由通道商户的转账配置([DouyinTransferConfig])显式指定
|
||||
/// - 金额大于等于 2000 元必填收款人姓名(子应用加密上送)
|
||||
/// - 商户转账号(out_bill_no)规则: 6-32位, 仅数字/大小写字母/_/-/*
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -35,6 +40,14 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
|
||||
/// 大额档位(分): 大于等于该金额必填姓名
|
||||
private static final long LARGE_AMOUNT_LIMIT = 200_000L;
|
||||
|
||||
/// 商户转账号规则(数字/大小写字母/_-*)
|
||||
private static final String OUT_BILL_NO_PATTERN = "^[0-9A-Za-z_\\-*]{6,32}$";
|
||||
|
||||
/// 抖音支持的收款人类型
|
||||
private static final Set<String> SUPPORTED_PAYEE_TYPES = Set.of(
|
||||
TransferPayeeTypeEnum.OPENID.getCode(),
|
||||
TransferPayeeTypeEnum.PHONE.getCode());
|
||||
|
||||
private final DouyinTransferService douyinTransferService;
|
||||
private final DouyinDirectConfigAssembler douyinDirectConfigAssembler;
|
||||
private final DouyinTransferConfigManager douyinTransferConfigManager;
|
||||
@@ -47,11 +60,18 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
|
||||
/// 通道特有参数校验
|
||||
@Override
|
||||
public void doValidateParam(TransferParam param) {
|
||||
// 抖音仅支持 openid 收款
|
||||
if (!StrUtil.equals(param.getPayeeType(), TransferPayeeTypeEnum.OPENID.getCode())) {
|
||||
// 抖音: 仅支持 openid 收款人
|
||||
// 抖音仅支持 openid / phone(手机号) 收款
|
||||
if (!SUPPORTED_PAYEE_TYPES.contains(param.getPayeeType())) {
|
||||
// 抖音: 不支持的收款人账号类型
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.transferOnlyOpenid");
|
||||
"error.channel.douyin.transferPayeeTypeInvalid", param.getPayeeType());
|
||||
}
|
||||
// 手机号模式: 11位数字且与抖音支付实名一致
|
||||
if (TransferPayeeTypeEnum.PHONE.getCode().equals(param.getPayeeType())
|
||||
&& !param.getPayeeAccount().matches("^\\d{11}$")) {
|
||||
// 抖音: 收款手机号格式不正确(需为11位数字)
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.transferPhoneInvalid");
|
||||
}
|
||||
long amountFen = param.getAmount().movePointRight(2).longValue();
|
||||
if (amountFen >= LARGE_AMOUNT_LIMIT && StrUtil.isBlank(param.getPayeeName())) {
|
||||
@@ -59,6 +79,12 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.transferNameRequired");
|
||||
}
|
||||
// 商户转账号规则(数字/大小写字母/_-*, 6-32位)
|
||||
if (!param.getBizTransferNo().matches(OUT_BILL_NO_PATTERN)) {
|
||||
// 抖音: 商户转账号格式不正确(6-32位, 仅数字/大小写字母/_/-/*)
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.transferOutBillNoInvalid");
|
||||
}
|
||||
// 转账场景ID必填且必须为合法枚举
|
||||
if (StrUtil.isBlank(param.getTransferScene())) {
|
||||
// 抖音: 转账场景ID必填
|
||||
|
||||
@@ -11,8 +11,8 @@ import java.util.Arrays;
|
||||
///
|
||||
/// 转账收款人账号类型,按通道差异化使用:
|
||||
/// - 微信: 仅支持 openid
|
||||
/// - 支付宝: 支持 user_id / open_id / login_name
|
||||
/// - 抖音: 按通道能力选取
|
||||
/// - 支付宝: 支持 user_id / login_name
|
||||
/// - 抖音: 支持 openid / phone(手机号, 复用收款人账号字段)
|
||||
/// 字典: transfer_payee_type
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,6 +26,8 @@ public enum TransferPayeeTypeEnum implements I18nSupport {
|
||||
OPEN_ID("open_id"),
|
||||
/// 支付宝登录账号(手机号/邮箱)
|
||||
LOGIN_NAME("login_name"),
|
||||
/// 抖音收款手机号(需抖音支付实名一致, 子应用证书加密上送)
|
||||
PHONE("phone"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
Reference in New Issue
Block a user