feat(channel-alipay): 支付宝转账申请配套(转账应用/场景配置/资金流水号回写/时间解析) + 错误码与校验词条(10语)

This commit is contained in:
daxpay
2026-08-07 16:39:50 +08:00
parent 824b3f1fa6
commit 1b1470fa6c
45 changed files with 1440 additions and 20 deletions

View File

@@ -1,8 +1,11 @@
package cn.daxpay.open.channel.alipay.client.req;
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
import cn.daxpay.open.payment.trade.transfer.param.TransferReportInfo;
import lombok.Data;
import java.util.List;
/// # 支付宝通道转账请求(发起/同步共用)
///
/// 与子应用 dax-pay-channel-one 的 `AlipayTransferReq` 镜像, 字段对齐。
@@ -33,6 +36,12 @@ public class AlipayTransferReq {
/// 异步通知地址(支付宝→平台)
private String notifyUrl;
/// 转账场景名称(2026新商户必填,由通道商户转账场景配置注入)
private String transferSceneName;
/// 转账场景上报信息列表(对应支付宝 transfer_scene_report_infos,多条)
private List<TransferReportInfo> reportInfos;
/// 通道调用凭证
private AlipaySdkCredential credential;
}

View File

@@ -19,4 +19,13 @@ public class AlipayTransferResp {
/// 转账完成时间(支付宝 gmt_finish)
private String finishTime;
/// 支付宝资金流水号(pay_fund_order_id,财务对账用)
private String payFundOrderId;
/// 订单支付时间(发起响应 trans_date; 同步响应 pay_date)
private String transDate;
/// 错误码(同步查询 FAIL/REFUND 时返回,用于精准报错)
private String errorCode;
}

View File

@@ -0,0 +1,52 @@
package cn.daxpay.open.channel.alipay.controller.direct;
import cn.daxpay.open.channel.alipay.param.direct.AlipayTransferConfigParam;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferConfigResult;
import cn.daxpay.open.channel.alipay.service.direct.AlipayTransferConfigService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.code.PermCodes;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// # 支付宝转账配置管理(运营端)
///
/// 管理通道商户的转账配置(转账转出应用), 挂在通道商户菜单下。
///
@PermCode(menuCode = PermCodes.Channel.Merchant.MENU)
@Validated
@Tag(name = "支付宝转账配置管理")
@RestController
@RequestMapping("/admin/alipay/transfer-config")
@RequiredArgsConstructor
public class AlipayTransferConfigController {
private final AlipayTransferConfigService alipayTransferConfigService;
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询通道商户的转账配置")
@GetMapping("/find-by-channel-mch-no")
public Result<AlipayTransferConfigResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayTransferConfigService.findByChannelMchNo(mchNo, channelMchNo));
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "保存或更新转账配置(一对一)")
@PostMapping("/save")
public Result<Void> save(@RequestBody @Validated AlipayTransferConfigParam param) {
alipayTransferConfigService.saveOrUpdate(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,76 @@
package cn.daxpay.open.channel.alipay.controller.direct;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneConfigResult;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneOptionResult;
import cn.daxpay.open.channel.alipay.service.direct.AlipayTransferSceneConfigService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.code.PermCodes;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// # 支付宝转账场景配置管理(运营端)
///
/// 按通道商户维度管理转账场景(2026 新商户转账必配),挂在通道商户菜单下。
///
@PermCode(menuCode = PermCodes.Channel.Merchant.MENU)
@Validated
@Tag(name = "支付宝转账场景配置管理")
@RestController
@RequestMapping("/admin/alipay/transfer-scene")
@RequiredArgsConstructor
public class AlipayTransferSceneConfigController {
private final AlipayTransferSceneConfigService alipayTransferSceneConfigService;
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询通道商户的转账场景列表")
@GetMapping("/list")
public Result<List<AlipayTransferSceneConfigResult>> list(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayTransferSceneConfigService.list(mchNo, channelMchNo));
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询支付宝转账场景选项列表(主数据枚举投影)")
@GetMapping("/scene-options")
public Result<List<AlipayTransferSceneOptionResult>> sceneOptions() {
return Res.ok(alipayTransferSceneConfigService.findSceneOptions());
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "设为默认转账场景(自动启用, 按场景名称按需创建)")
@PostMapping("/set-default")
public Result<Void> setDefault(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.transferSceneName.notBlank}") String sceneName) {
alipayTransferSceneConfigService.setDefault(mchNo, channelMchNo, sceneName);
return Res.ok();
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "切换转账场景启用状态(最多启用3个, 按场景名称按需创建)")
@PostMapping("/set-enabled")
public Result<Void> setEnabled(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.transferSceneName.notBlank}") String sceneName,
@NotNull Boolean enabled) {
alipayTransferSceneConfigService.setEnabled(mchNo, channelMchNo, sceneName, enabled);
return Res.ok();
}
}

View File

@@ -0,0 +1,63 @@
package cn.daxpay.open.channel.alipay.controller.direct;
import cn.daxpay.open.channel.alipay.param.direct.AlipayTransferConfigParam;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferConfigResult;
import cn.daxpay.open.channel.alipay.service.direct.AlipayTransferConfigService;
import cn.daxpay.open.payment.common.context.PaymentContext;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// # 支付宝转账配置管理(商户端)
///
/// 对照运营端 [AlipayTransferConfigController],路径前缀 `/mch/alipay/transfer-config`。
/// 商户号一律取自 [PaymentContext],忽略请求中的 mchNo,防越权。
///
@Validated
@Tag(name = "支付宝转账配置管理(商户端)")
@RestController
@RequestMapping("/mch/alipay/transfer-config")
@RequiredArgsConstructor
public class MchAlipayTransferConfigController {
private final AlipayTransferConfigService alipayTransferConfigService;
private final PaymentContext paymentContext;
/// 当前登录商户号(上下文必有;缺则视为会话异常)
private String requireMchNo() {
String mchNo = paymentContext.getMchNo();
if (mchNo == null || mchNo.isBlank()) {
// 商户上下文缺失
throw new BizInfoException(CommonCode.FAIL_CODE, "pay.error.assist.mchContextMissing");
}
return mchNo;
}
@Operation(summary = "查询通道商户的转账配置")
@GetMapping("/find-by-channel-mch-no")
public Result<AlipayTransferConfigResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayTransferConfigService.findByChannelMchNo(requireMchNo(), channelMchNo));
}
@Operation(summary = "保存或更新转账配置(一对一)")
@PostMapping("/save")
public Result<Void> save(@RequestBody @Validated AlipayTransferConfigParam param) {
// 商户号强制取自上下文, 防越权
param.setMchNo(requireMchNo());
alipayTransferConfigService.saveOrUpdate(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,81 @@
package cn.daxpay.open.channel.alipay.controller.direct;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneConfigResult;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneOptionResult;
import cn.daxpay.open.channel.alipay.service.direct.AlipayTransferSceneConfigService;
import cn.daxpay.open.payment.common.context.PaymentContext;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// # 支付宝转账场景配置管理(商户端)
///
/// 对照运营端 [AlipayTransferSceneConfigController],路径前缀 `/mch/alipay/transfer-scene`。
/// 商户号一律取自 [PaymentContext],忽略请求中的 mchNo,防越权。
///
@Validated
@Tag(name = "支付宝转账场景配置管理(商户端)")
@RestController
@RequestMapping("/mch/alipay/transfer-scene")
@RequiredArgsConstructor
public class MchAlipayTransferSceneConfigController {
private final AlipayTransferSceneConfigService alipayTransferSceneConfigService;
private final PaymentContext paymentContext;
/// 当前登录商户号(上下文必有;缺则视为会话异常)
private String requireMchNo() {
String mchNo = paymentContext.getMchNo();
if (mchNo == null || mchNo.isBlank()) {
// 商户上下文缺失
throw new BizInfoException(CommonCode.FAIL_CODE, "pay.error.assist.mchContextMissing");
}
return mchNo;
}
@Operation(summary = "查询通道商户的转账场景列表")
@GetMapping("/list")
public Result<List<AlipayTransferSceneConfigResult>> list(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayTransferSceneConfigService.list(requireMchNo(), channelMchNo));
}
@Operation(summary = "查询支付宝转账场景选项列表(主数据枚举投影)")
@GetMapping("/scene-options")
public Result<List<AlipayTransferSceneOptionResult>> sceneOptions() {
return Res.ok(alipayTransferSceneConfigService.findSceneOptions());
}
@Operation(summary = "设为默认转账场景(自动启用, 按场景名称按需创建)")
@PostMapping("/set-default")
public Result<Void> setDefault(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.transferSceneName.notBlank}") String sceneName) {
alipayTransferSceneConfigService.setDefault(requireMchNo(), channelMchNo, sceneName);
return Res.ok();
}
@Operation(summary = "切换转账场景启用状态(最多启用3个, 按场景名称按需创建)")
@PostMapping("/set-enabled")
public Result<Void> setEnabled(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.transferSceneName.notBlank}") String sceneName,
@NotNull Boolean enabled) {
alipayTransferSceneConfigService.setEnabled(requireMchNo(), channelMchNo, sceneName, enabled);
return Res.ok();
}
}

View File

@@ -0,0 +1,31 @@
package cn.daxpay.open.channel.alipay.convert.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig;
import cn.daxpay.open.channel.alipay.param.direct.AlipayTransferConfigParam;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 支付宝转账配置转换
///
/// MapStruct 转换器, 负责转账配置在实体、参数和返回结果之间的转换, 更新时空值不覆盖。
/// 冗余展示字段(转出应用名/aliAppId/应用类型)由 Service 填充, 不经 Convert。
///
@Mapper
public interface AlipayTransferConfigConvert {
AlipayTransferConfigConvert CONVERT = Mappers.getMapper(AlipayTransferConfigConvert.class);
/// 转换为返回对象
AlipayTransferConfigResult toResult(AlipayTransferConfig entity);
/// 转换为实体
AlipayTransferConfig toEntity(AlipayTransferConfigParam param);
/// 更新源数据到实体(空值不覆盖)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(AlipayTransferConfigParam param, @MappingTarget AlipayTransferConfig entity);
}

View File

@@ -0,0 +1,19 @@
package cn.daxpay.open.channel.alipay.convert.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferSceneConfig;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneConfigResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/// # 支付宝转账场景配置转换
///
/// MapStruct 转换器,负责转账场景配置在实体与返回结果之间的转换。
///
@Mapper
public interface AlipayTransferSceneConfigConvert {
AlipayTransferSceneConfigConvert CONVERT = Mappers.getMapper(AlipayTransferSceneConfigConvert.class);
/// 转换为返回对象
AlipayTransferSceneConfigResult toResult(AlipayTransferSceneConfig entity);
}

View File

@@ -0,0 +1,29 @@
package cn.daxpay.open.channel.alipay.dao.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/// # 支付宝转账配置
///
/// 一个通道商户一条转账配置(一对一), 提供按通道商户号查询/删除。
///
@Repository
public class AlipayTransferConfigManager extends BaseManager<AlipayTransferConfigMapper, AlipayTransferConfig> {
/// 按通道商户号查询转账配置(一对一)
public Optional<AlipayTransferConfig> findByChannelMchNo(String channelMchNo) {
return lambdaQuery()
.eq(AlipayTransferConfig::getChannelMchNo, channelMchNo)
.oneOpt();
}
/// 按通道商户号删除转账配置(逻辑删除, 通道商户删除时级联清理)
public void deleteByChannelMchNo(String channelMchNo) {
lambdaUpdate()
.eq(AlipayTransferConfig::getChannelMchNo, channelMchNo)
.remove();
}
}

View File

@@ -0,0 +1,13 @@
package cn.daxpay.open.channel.alipay.dao.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 支付宝转账配置
///
/// 支付宝转账配置 MyBatis-Plus Mapper, 继承 MPJBaseMapper 支持多表联查。
///
@Mapper
public interface AlipayTransferConfigMapper extends MPJBaseMapper<AlipayTransferConfig> {
}

View File

@@ -0,0 +1,91 @@
package cn.daxpay.open.channel.alipay.dao.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferSceneConfig;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/// # 支付宝转账场景配置
///
/// 转账场景配置数据访问管理器,提供按通道商户号查询列表、查询默认场景、清空默认等方法。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayTransferSceneConfigManager extends BaseManager<AlipayTransferSceneConfigMapper, AlipayTransferSceneConfig> {
/// 按通道商户号查询全部场景配置(默认项排前, 启用排前)
public List<AlipayTransferSceneConfig> listByChannelMchNo(String channelMchNo) {
return lambdaQuery()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.orderByDesc(AlipayTransferSceneConfig::getIsDefault)
.orderByDesc(AlipayTransferSceneConfig::getEnabled)
.orderByDesc(AlipayTransferSceneConfig::getCreateTime)
.list();
}
/// 查询通道商户的默认场景(默认场景必须启用)
public Optional<AlipayTransferSceneConfig> findDefault(String channelMchNo) {
return lambdaQuery()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.eq(AlipayTransferSceneConfig::getIsDefault, true)
.eq(AlipayTransferSceneConfig::getEnabled, true)
.oneOpt();
}
/// 统计通道商户已启用的场景数量
public long countEnabled(String channelMchNo) {
return lambdaQuery()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.eq(AlipayTransferSceneConfig::getEnabled, true)
.count();
}
/// 按主键查询
public Optional<AlipayTransferSceneConfig> findById(Long id) {
if (id == null) {
return Optional.empty();
}
return lambdaQuery()
.eq(AlipayTransferSceneConfig::getId, id)
.oneOpt();
}
/// 按通道商户号与场景名称查询(场景行不存在时返回空, 由调用方按需创建)
public Optional<AlipayTransferSceneConfig> findByChannelMchNoAndSceneName(String channelMchNo, String sceneName) {
return lambdaQuery()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.eq(AlipayTransferSceneConfig::getSceneName, sceneName)
.oneOpt();
}
/// 按主键删除(逻辑删除, 由 deleted 字段标记)
public void deleteById(Long id) {
lambdaUpdate()
.eq(AlipayTransferSceneConfig::getId, id)
.remove();
}
/// 清空指定通道商户的默认标记(设新默认前调用)
public void clearDefault(String channelMchNo) {
lambdaUpdate()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.eq(AlipayTransferSceneConfig::getIsDefault, true)
.set(AlipayTransferSceneConfig::getIsDefault, false)
.update();
}
/// 清空指定通道商户的默认标记(排除指定 id,用于更新自身为默认)
public void clearDefaultExclude(String channelMchNo, Long excludeId) {
lambdaUpdate()
.eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo)
.eq(AlipayTransferSceneConfig::getIsDefault, true)
.ne(AlipayTransferSceneConfig::getId, excludeId)
.set(AlipayTransferSceneConfig::getIsDefault, false)
.update();
}
}

View File

@@ -0,0 +1,13 @@
package cn.daxpay.open.channel.alipay.dao.direct;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferSceneConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 支付宝转账场景配置
///
/// 支付宝转账场景配置 MyBatis-Plus Mapper,继承 MPJBaseMapper 支持多表联查。
///
@Mapper
public interface AlipayTransferSceneConfigMapper extends MPJBaseMapper<AlipayTransferSceneConfig> {
}

View File

@@ -0,0 +1,38 @@
package cn.daxpay.open.channel.alipay.entity.direct;
import cn.daxpay.open.channel.alipay.convert.direct.AlipayTransferConfigConvert;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferConfigResult;
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝转账配置
///
/// 一个通道商户一条转账配置(一对一), 绑定「转账转出应用」。
/// 发起转账时由 [cn.daxpay.open.channel.alipay.strategy.direct.transfer.AlipayTransferStrategy]
/// 读取本配置按 [transferAppRefId] 解析转出应用(支付宝直连应用)的 aliAppId 与密钥。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("alipay_transfer_config")
public class AlipayTransferConfig extends MchBaseEntity implements ToResult<AlipayTransferConfigResult> {
/// 通道商户号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 转账转出应用引用(指向 alipay_direct_app 主键, 决定转账使用的 aliAppId 与密钥)
private Long transferAppRefId;
/// 转换
@Override
public AlipayTransferConfigResult toResult() {
return AlipayTransferConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,43 @@
package cn.daxpay.open.channel.alipay.entity.direct;
import cn.daxpay.open.channel.alipay.convert.direct.AlipayTransferSceneConfigConvert;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneConfigResult;
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝转账场景配置
///
/// 按通道商户维度管理转账场景(一对多),承载 2026 年起支付宝对新接入商户强制要求的
/// `transfer_scene_name` 与 `transfer_scene_report_infos`。发起转账时按本表配置注入请求。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("alipay_transfer_scene_config")
public class AlipayTransferSceneConfig extends MchBaseEntity implements ToResult<AlipayTransferSceneConfigResult> {
/// 通道商户号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 转账场景名称(8 枚举之一:现金营销/企业退款/佣金报酬/业务结算/二手回收/公益补助/行政补贴和退款/保险理赔)
private String sceneName;
/// 是否启用(一个通道商户最多启用3个, 发起转账时仅可选择启用场景)
private Boolean enabled;
/// 是否默认场景(一个通道商户最多一个默认, 默认场景必须启用, 由部分唯一索引约束)
private Boolean isDefault;
/// 转换
@Override
public AlipayTransferSceneConfigResult toResult() {
return AlipayTransferSceneConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,80 @@
package cn.daxpay.open.channel.alipay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/// # 支付宝转账场景枚举
///
/// 对应支付宝资金转账(alipay.fund.trans.uni.transfer)的转账场景, 场景名称(transfer_scene_name)
/// 为支付宝协议固定的中文取值, 直传支付宝。每个场景要求不同的报备字段([reportInfoTypes]),
/// 字段值为支付宝协议固定的中文 [infoType], 不可更改。
///
/// 报备字段内容(infoContent)由商户在发起转账时填写。
/// [reportInfoDescriptions] 与 [reportInfoTypes] 一一平行, 描述每个字段含义和支付宝文档示例。
@Getter
@AllArgsConstructor
public enum AlipayTransferSceneEnum {
/// 现金营销
CASH_MARKETING("现金营销",
List.of("活动名称", "奖励说明"),
List.of("请描述收款方参与活动的名称", "请描述收款方因什么奖励获取这笔资金")),
/// 企业退款
ENTERPRISE_REFUND("企业退款",
List.of("退款原因"),
List.of("请描述退款原因,如商品质量问题退款")),
/// 佣金报酬
COMMISSION_REWARD("佣金报酬",
List.of("佣金报酬说明"),
List.of("请描述接收款项原因,如8月家政服务报酬")),
/// 业务结算
BUSINESS_SETTLEMENT("业务结算",
List.of("结算款项名称"),
List.of("请描述款项名称,如材料货款")),
/// 二手回收
SECOND_HAND_RECYCLING("二手回收",
List.of("回收商品名称"),
List.of("请描述回收商品名称,如衣服")),
/// 公益补助
PUBLIC_WELFARE_SUBSIDY("公益补助",
List.of("公益活动名称"),
List.of("请描述公益活动在民政部的备案名称")),
/// 行政补贴和退款
ADMINISTRATIVE_SUBSIDY("行政补贴和退款",
List.of("补贴/退款类型"),
List.of("请描述补贴/退款类型,如某地人才补贴")),
/// 保险理赔
INSURANCE_CLAIM("保险理赔",
List.of("业务类型", "保险险种", "业务交易订单号"),
List.of("请描述业务类型,如理赔、退保、其他",
"请描述保险险种及产品名称,如医疗险-某百万医疗保险",
"请描述这笔转账的业务内部交易订单号"));
/// 转账场景名称(支付宝协议固定中文取值, 直传 transfer_scene_name)
private final String sceneName;
/// 报备字段定义(支付宝协议固定中文 infoType, 顺序即报备明细下标)
private final List<String> reportInfoTypes;
/// 报备字段说明(与 reportInfoTypes 平行, 描述字段含义和支付宝文档示例)
private final List<String> reportInfoDescriptions;
/// 根据场景名称查找枚举
public static AlipayTransferSceneEnum findBySceneName(String sceneName) {
for (AlipayTransferSceneEnum scene : values()) {
if (scene.sceneName.equals(sceneName)) {
return scene;
}
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
package cn.daxpay.open.channel.alipay.param.direct;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 支付宝转账配置保存参数
///
/// 一对一 upsert: 存在则更新, 不存在则新增。`transferAppRefId` 必填,
/// 未绑定时发起转账将报错提示先绑定转出应用。
///
@Data
@Accessors(chain = true)
@Schema(title = "支付宝转账配置保存参数")
public class AlipayTransferConfigParam {
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@NotNull(message = "{validation.field.transferAppRefId.notNull}")
@Schema(description = "转账转出应用引用(指向 alipay_direct_app 主键)")
private Long transferAppRefId;
}

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.channel.alipay.result.direct;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝转账配置
///
/// 转账配置返回结果对象。冗余展示字段(转出应用名/aliAppId/应用类型)由
/// [cn.daxpay.open.channel.alipay.service.direct.AlipayTransferConfigService] 填充,
/// 不经 MapStruct 自动映射。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付宝转账配置")
public class AlipayTransferConfigResult extends BaseResult {
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "转账转出应用引用")
private Long transferAppRefId;
// ===== 冗余展示(由 Service 填充) =====
@Schema(description = "转出应用名称")
private String transferAppName;
@Schema(description = "转出应用支付宝AppId")
private String aliAppId;
@Schema(description = "转出应用类型(mini_program-小程序/mobile_app-移动应用/web_app-网站应用)")
private String appType;
}

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.channel.alipay.result.direct;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.List;
/// # 支付宝转账场景配置
///
/// 转账场景配置返回结果对象。报备字段元数据([reportInfoTypes]/[reportInfoDescriptions])
/// 由枚举 [cn.daxpay.open.channel.alipay.enums.AlipayTransferSceneEnum] 推导, 供前端动态渲染报备输入框。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付宝转账场景配置")
public class AlipayTransferSceneConfigResult extends BaseResult {
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "转账场景名称")
private String sceneName;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "是否默认场景")
private Boolean isDefault;
@Schema(description = "报备字段定义(支付宝协议固定中文 infoType, 由枚举推导)")
private List<String> reportInfoTypes;
@Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含支付宝文档示例)")
private List<String> reportInfoDescriptions;
}

View File

@@ -0,0 +1,29 @@
package cn.daxpay.open.channel.alipay.result.direct;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/// # 支付宝转账场景选项结果
///
/// 供前端场景卡片渲染与报备字段动态展示。报备字段 [reportInfoTypes] 为支付宝协议固定中文
/// [infoType], 不可更改; 顺序即发起转账时 [infoContent] 的填写下标。
///
/// [reportInfoDescriptions] 与 [reportInfoTypes] 平行, 描述每个字段含义和支付宝文档示例。
/// 由枚举 [cn.daxpay.open.channel.alipay.enums.AlipayTransferSceneEnum] 投影, 不查库。
@Data
@Accessors(chain = true)
@Schema(title = "支付宝转账场景选项")
public class AlipayTransferSceneOptionResult {
@Schema(description = "转账场景名称(支付宝协议固定中文取值)")
private String sceneName;
@Schema(description = "报备字段定义(支付宝协议固定中文 infoType)")
private List<String> reportInfoTypes;
@Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含支付宝文档示例)")
private List<String> reportInfoDescriptions;
}

View File

@@ -1,5 +1,6 @@
package cn.daxpay.open.channel.alipay.service.direct;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectChannelMerchantManager;
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectApp;
@@ -12,6 +13,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional;
/// # 支付宝直连通道凭证组装器
@@ -22,7 +24,9 @@ import java.util.Optional;
/// 沙箱标识直接读通道商户固化的 [AlipayDirectChannelMerchant#isSandbox]
/// (创建时按当时产品 activeEnv 写入, 不随产品切换改变), 据此选择对应环境的密钥与网关地址。
///
/// 应用解析优先级:能力关联(显式配置 > appType 唯一推导) > 未命中报错(拒绝首个兜底)
/// 支付应用解析优先级:能力关联(显式配置 > appType 唯一推导) > 未命中报错(拒绝首个兜底);
/// 转账不走能力关联, 由转账配置([cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig])
/// 显式指定转出应用, 见 [#buildTransferConfig]。
///
/// 供支付策略([cn.daxpay.open.channel.alipay.strategy.direct.AlipayDirectPayStrategy])组装通道调用凭证。
@Slf4j
@@ -33,6 +37,7 @@ public class AlipayDirectConfigAssembler {
private final AlipayDirectChannelMerchantManager alipayDirectChannelMerchantManager;
private final AlipayDirectAppKeyConfigService alipayDirectAppKeyConfigService;
private final AlipayDirectAppCapabilityService alipayDirectAppCapabilityService;
private final AlipayDirectAppManager alipayDirectAppManager;
/// 组装直连商户的通道调用凭证(下发给子应用)
///
@@ -42,6 +47,29 @@ public class AlipayDirectConfigAssembler {
/// @return 支付宝 SDK 凭证, 字段对齐子应用 AlipaySdkCredential
public AlipaySdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
AlipayDirectApp app = resolveApp(mchNo, channelMchNo, capability);
return assembleCredential(channelMchNo, app);
}
/// 组装转账通道调用凭证(转账无能力维度, 按转账配置显式指定的应用)
///
/// @param mchNo 商户号(归属校验)
/// @param channelMchNo 通道商户号
/// @param appRefId 转账转出应用引用(alipay_direct_app 主键, 由转账配置绑定)
/// @return 支付宝 SDK 凭证
public AlipaySdkCredential buildTransferConfig(String mchNo, String channelMchNo, Long appRefId) {
AlipayDirectApp app = alipayDirectAppManager.lambdaQuery()
.eq(AlipayDirectApp::getId, appRefId)
.oneOpt()
.orElseThrow(() -> new DataNotExistException("error.channel.alipay.transferAppNotExist"));
if (!Objects.equals(app.getMchNo(), mchNo)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferAppNotBelong");
}
return assembleCredential(channelMchNo, app);
}
/// 按通道商户与应用组装凭证(通道商户沙箱快照 + 应用级密钥)
private AlipaySdkCredential assembleCredential(String channelMchNo, AlipayDirectApp app) {
AlipayDirectChannelMerchant channelMerchant = alipayDirectChannelMerchantManager.lambdaQuery()
.eq(AlipayDirectChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt()

View File

@@ -0,0 +1,114 @@
package cn.daxpay.open.channel.alipay.service.direct;
import cn.daxpay.open.channel.alipay.convert.direct.AlipayTransferConfigConvert;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectChannelMerchantManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayTransferConfigManager;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectApp;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectChannelMerchant;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig;
import cn.daxpay.open.channel.alipay.param.direct.AlipayTransferConfigParam;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferConfigResult;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.Optional;
/// # 支付宝转账配置
///
/// 管理通道商户的转账配置(一对一: 转账转出应用)。
/// 发起转账时由转账策略读取本配置按 [AlipayTransferConfig#getTransferAppRefId]
/// 解析转出应用(支付宝直连应用)的 aliAppId 与密钥。
///
/// 运营端写 [AlipayTransferConfig](MchBaseEntity) 显式 setMchNo, 避免上下文缺失。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayTransferConfigService {
private final AlipayTransferConfigManager alipayTransferConfigManager;
private final AlipayDirectChannelMerchantManager alipayDirectChannelMerchantManager;
private final AlipayDirectAppManager alipayDirectAppManager;
/// 查询通道商户的转账配置(一对一, 未配置返回 null)
///
/// @param mchNo 商户号(归属校验)
/// @param channelMchNo 通道商户号
/// @return 转账配置(含冗余展示), 不存在返回 null
public AlipayTransferConfigResult findByChannelMchNo(String mchNo, String channelMchNo) {
assertChannelMerchant(mchNo, channelMchNo);
return alipayTransferConfigManager.findByChannelMchNo(channelMchNo)
.map(this::toResultWithMeta)
.orElse(null);
}
/// 保存或更新转账配置(一对一 upsert, 转账应用必填)
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(AlipayTransferConfigParam param) {
// 校验通道商户存在与归属
assertChannelMerchant(param.getMchNo(), param.getChannelMchNo());
// 校验转出应用: 存在 + 归属
AlipayDirectApp app = alipayDirectAppManager.lambdaQuery()
.eq(AlipayDirectApp::getId, param.getTransferAppRefId())
.oneOpt()
.orElseThrow(() -> new DataNotExistException("error.channel.alipay.transferAppNotExist"));
if (!Objects.equals(app.getMchNo(), param.getMchNo())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferAppNotBelong");
}
// upsert: 存在则全量覆盖, 不存在则新增
Optional<AlipayTransferConfig> existing = alipayTransferConfigManager
.findByChannelMchNo(param.getChannelMchNo());
if (existing.isPresent()) {
AlipayTransferConfig entity = existing.get();
entity.setTransferAppRefId(param.getTransferAppRefId());
alipayTransferConfigManager.updateById(entity);
} else {
AlipayTransferConfig entity = AlipayTransferConfigConvert.CONVERT.toEntity(param);
// 运营端写 MchBaseEntity 必须显式 setMchNo(父类 setter 返回类型不匹配, 单独赋值)
entity.setMchNo(param.getMchNo());
alipayTransferConfigManager.save(entity);
}
}
/// 删除通道商户的转账配置(通道商户删除时级联清理)
public void deleteByChannelMchNo(String channelMchNo) {
alipayTransferConfigManager.deleteByChannelMchNo(channelMchNo);
}
/// 校验通道商户存在且归属匹配
private void assertChannelMerchant(String mchNo, String channelMchNo) {
AlipayDirectChannelMerchant channelMerchant = alipayDirectChannelMerchantManager.lambdaQuery()
.eq(AlipayDirectChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt()
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
if (!Objects.equals(channelMerchant.getMchNo(), mchNo)) {
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.channel.alipay.channelMerchantMismatch");
}
}
/// 转Result并填充冗余展示(转出应用信息)
private AlipayTransferConfigResult toResultWithMeta(AlipayTransferConfig entity) {
AlipayTransferConfigResult result = entity.toResult();
// 转出应用展示信息
if (entity.getTransferAppRefId() != null) {
alipayDirectAppManager.lambdaQuery()
.eq(AlipayDirectApp::getId, entity.getTransferAppRefId())
.oneOpt()
.ifPresent(app -> {
result.setTransferAppName(app.getAppName());
result.setAliAppId(app.getAliAppId());
result.setAppType(app.getAppType());
});
}
return result;
}
}

View File

@@ -0,0 +1,221 @@
package cn.daxpay.open.channel.alipay.service.direct;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayTransferSceneConfigManager;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferSceneConfig;
import cn.daxpay.open.channel.alipay.enums.AlipayTransferSceneEnum;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneConfigResult;
import cn.daxpay.open.channel.alipay.result.direct.AlipayTransferSceneOptionResult;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/// # 支付宝转账场景配置
///
/// 管理转账场景配置的启用/默认状态, 采用主数据枚举驱动模式:
/// - 场景元数据(8个场景 + 报备字段)由 [AlipayTransferSceneEnum] 投影, 不查库、不预置, 见 [AlipayTransferSceneOptionResult]
/// - 配置表仅存"被操作过的"场景行(启用/默认状态), 启用或设默认时按需创建, 无行即为未启用
/// - 最多启用3个, 默认1个(必须启用), 发起转账时通过 [findEffective] 解析生效配置
///
/// 2026 年起支付宝对新接入商户强制要求 `transfer_scene_name` 与 `transfer_scene_report_infos`,
/// 未配置场景的商户发起转账将被支付宝拒单。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayTransferSceneConfigService {
/// 最大启用场景数
private static final int MAX_ENABLED = 3;
private final AlipayTransferSceneConfigManager alipayTransferSceneConfigManager;
/// 查询支付宝转账场景选项列表(主数据枚举投影, 不查库, 供前端卡片渲染与报备字段动态展示)
public List<AlipayTransferSceneOptionResult> findSceneOptions() {
return Arrays.stream(AlipayTransferSceneEnum.values())
.map(scene -> new AlipayTransferSceneOptionResult()
.setSceneName(scene.getSceneName())
.setReportInfoTypes(scene.getReportInfoTypes())
.setReportInfoDescriptions(scene.getReportInfoDescriptions()))
.toList();
}
/// 查询通道商户下的场景配置行(仅已操作过的行, 按枚举固定顺序排序, 填充报备元数据)
///
/// 未操作过的场景无行, 前端以场景选项为基准渲染, 本方法返回的状态映射到对应卡片。
public List<AlipayTransferSceneConfigResult> list(String mchNo, String channelMchNo) {
List<AlipayTransferSceneConfig> entities = alipayTransferSceneConfigManager.listByChannelMchNo(channelMchNo);
// 按枚举固定顺序排序
Map<String, Integer> orderMap = new HashMap<>();
AlipayTransferSceneEnum[] enums = AlipayTransferSceneEnum.values();
for (int i = 0; i < enums.length; i++) {
orderMap.put(enums[i].getSceneName(), i);
}
return entities.stream()
.filter(e -> mchNo == null || mchNo.equals(e.getMchNo()))
.sorted(Comparator.comparingInt(e -> orderMap.getOrDefault(e.getSceneName(), Integer.MAX_VALUE)))
.map(this::toResultWithMeta)
.toList();
}
/// 将实体转为Result并填充报备字段元数据(由枚举推导)
private AlipayTransferSceneConfigResult toResultWithMeta(AlipayTransferSceneConfig entity) {
AlipayTransferSceneConfigResult result = entity.toResult();
AlipayTransferSceneEnum scene = AlipayTransferSceneEnum.findBySceneName(entity.getSceneName());
if (scene != null) {
result.setReportInfoTypes(scene.getReportInfoTypes());
result.setReportInfoDescriptions(scene.getReportInfoDescriptions());
}
return result;
}
/// 设置场景启用状态(按场景名称操作, 场景行不存在时自动创建)
///
/// @param mchNo 商户号(归属校验, 新行写入用)
/// @param channelMchNo 通道商户号
/// @param sceneName 场景名称(支付宝协议固定中文取值)
/// @param enabled 是否启用; 启用时校验上限(最多3个), 禁用时校验非默认
@Transactional(rollbackFor = Exception.class)
public void setEnabled(String mchNo, String channelMchNo, String sceneName, boolean enabled) {
// 校验场景名称合法性
AlipayTransferSceneEnum scene = AlipayTransferSceneEnum.findBySceneName(sceneName);
if (scene == null) {
// 支付宝: 不支持的转账场景: {0}
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNameInvalid", sceneName);
}
AlipayTransferSceneConfig entity = alipayTransferSceneConfigManager
.findByChannelMchNoAndSceneName(channelMchNo, sceneName)
// 主数据模式下无行即为未操作过, 按需创建(启用/设默认才落行)
.orElseGet(() -> {
var newEntity = new AlipayTransferSceneConfig();
// 运营端写 MchBaseEntity 必须显式 setMchNo(父类 setter 返回类型不匹配, 单独赋值)
newEntity.setMchNo(mchNo);
newEntity.setChannelMchNo(channelMchNo);
newEntity.setSceneName(sceneName);
newEntity.setEnabled(false);
newEntity.setIsDefault(false);
return newEntity;
});
if (!mchNo.equals(entity.getMchNo())) {
// 支付宝: 转账场景配置不属于当前商户
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNotBelong");
}
if (enabled) {
// 已启用的幂等返回, 未启用的校验上限
if (!Boolean.TRUE.equals(entity.getEnabled())) {
long count = alipayTransferSceneConfigManager.countEnabled(channelMchNo);
if (count >= MAX_ENABLED) {
// 支付宝: 启用场景不能超过{0}个
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneEnabledLimit", MAX_ENABLED);
}
}
entity.setEnabled(true);
} else {
// 默认场景不允许禁用
if (Boolean.TRUE.equals(entity.getIsDefault())) {
// 支付宝: 默认场景不能禁用, 请先取消默认或切换默认到其他场景
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneCannotDisableDefault");
}
entity.setEnabled(false);
}
if (entity.getId() == null) {
// 新行先落库(自动填充主键)
alipayTransferSceneConfigManager.save(entity);
} else {
alipayTransferSceneConfigManager.updateById(entity);
}
}
/// 设为默认场景(按场景名称操作, 场景行不存在时自动创建; 自动启用, 含上限校验, 事务内清旧默认)
@Transactional(rollbackFor = Exception.class)
public void setDefault(String mchNo, String channelMchNo, String sceneName) {
// 校验场景名称合法性
AlipayTransferSceneEnum scene = AlipayTransferSceneEnum.findBySceneName(sceneName);
if (scene == null) {
// 支付宝: 不支持的转账场景: {0}
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNameInvalid", sceneName);
}
AlipayTransferSceneConfig entity = alipayTransferSceneConfigManager
.findByChannelMchNoAndSceneName(channelMchNo, sceneName)
.orElseGet(() -> {
var newEntity = new AlipayTransferSceneConfig();
// 运营端写 MchBaseEntity 必须显式 setMchNo(父类 setter 返回类型不匹配, 单独赋值)
newEntity.setMchNo(mchNo);
newEntity.setChannelMchNo(channelMchNo);
newEntity.setSceneName(sceneName);
newEntity.setEnabled(false);
newEntity.setIsDefault(false);
return newEntity;
});
if (!mchNo.equals(entity.getMchNo())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNotBelong");
}
// 设默认时自动启用(含上限校验)
if (!Boolean.TRUE.equals(entity.getEnabled())) {
long count = alipayTransferSceneConfigManager.countEnabled(channelMchNo);
if (count >= MAX_ENABLED) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneEnabledLimit", MAX_ENABLED);
}
entity.setEnabled(true);
}
// 清旧默认
alipayTransferSceneConfigManager.clearDefault(channelMchNo);
entity.setIsDefault(true);
if (entity.getId() == null) {
// 新行先落库(自动填充主键)
alipayTransferSceneConfigManager.save(entity);
} else {
alipayTransferSceneConfigManager.updateById(entity);
}
}
/// 解析发起转账时生效的场景配置
///
/// @param channelMchNo 通道商户号
/// @param configIdStr 订单指定的配置 id(字符串,可空); 非空则优先用指定,空则用默认
/// @return 生效的场景配置, 无可用配置抛 transferSceneNotConfigured
public AlipayTransferSceneConfig findEffective(String channelMchNo, String configIdStr) {
if (StrUtil.isNotBlank(configIdStr)) {
Long configId;
try {
configId = Long.parseLong(configIdStr);
} catch (NumberFormatException e) {
// 支付宝: 转账场景配置ID格式错误
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneIdInvalid", configIdStr);
}
var config = alipayTransferSceneConfigManager.findById(configId)
.orElseThrow(() -> new DataNotExistException("error.channel.alipay.transferSceneNotFound"));
if (!channelMchNo.equals(config.getChannelMchNo())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNotBelong");
}
// 禁用的场景不可用
if (!Boolean.TRUE.equals(config.getEnabled())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNotConfigured");
}
return config;
}
return alipayTransferSceneConfigManager.findDefault(channelMchNo)
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferSceneNotConfigured"));
}
}

View File

@@ -4,10 +4,14 @@ import cn.daxpay.open.channel.alipay.client.AlipayChannelClient;
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
import cn.daxpay.open.channel.alipay.client.req.AlipayTransferReq;
import cn.daxpay.open.channel.alipay.client.resp.AlipayTransferResp;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferSceneConfig;
import cn.daxpay.open.channel.alipay.service.direct.AlipayTransferSceneConfigService;
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.dao.AlipayTransferOrderManager;
import cn.daxpay.open.payment.trade.transfer.entity.AlipayTransferOrder;
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;
@@ -16,26 +20,45 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/// # 支付宝转账执行业务服务
///
/// 通过 [AlipayChannelClient] 调用子应用 dax-pay-channel-one 完成支付宝单笔转账
/// (alipay.fund.trans.uni.transfer)。请求构建、响应解析与状态映射在本类中完成。
///
/// 转账场景(transfer_scene_name): 2026 年起新接入商户必填, 由 [AlipayTransferSceneConfigService]
/// 按通道商户配置注入(显式 configId 优先, 否则用默认场景)。
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayTransferService {
/// 支付宝时间格式(东八区本地时间字面量)
private static final DateTimeFormatter ALIPAY_DATE_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final AlipayChannelClient alipayChannelClient;
private final PlatformUrlConfigService platformUrlConfigService;
// 转账场景配置(2026 新商户必填)
private final AlipayTransferSceneConfigService alipayTransferSceneConfigService;
// 支付宝容器(回写资金流水号等通道特有字段)
private final AlipayTransferOrderManager alipayTransferOrderManager;
/// 执行支付宝转账
///
/// @param context 转账策略上下文(通道特有字段: payeeType/payeeAccount/payeeName)
/// @param context 转账策略上下文(通道特有字段: payeeType/payeeAccount/payeeName/transferSceneConfigId)
/// @param credential 通道调用凭证
/// @return 转账结果
public TransferResultBo transfer(TransferStrategyContext context, AlipaySdkCredential credential) {
// 转账场景配置(2026 新商户必填): 显式 configId 优先, 否则用通道商户默认
AlipayTransferSceneConfig scene = alipayTransferSceneConfigService.findEffective(
context.getChannelMchNo(), context.getTransferSceneConfigId());
AlipayTransferReq req = new AlipayTransferReq();
req.setOutBizNo(context.getTransferNo());
req.setAmount(context.getAmount());
@@ -45,6 +68,9 @@ public class AlipayTransferService {
req.setPayeeAccount(context.getPayeeAccount());
req.setPayeeName(context.getPayeeName());
req.setNotifyUrl(this.buildNotifyUrl(context));
req.setTransferSceneName(scene.getSceneName());
// 转账场景报备信息由发起方手动填写,从上下文透传(不再从场景配置取)
req.setReportInfos(context.getReportInfos());
req.setCredential(credential);
DaxResult<AlipayTransferResp> result = alipayChannelClient.transfer(req);
@@ -53,13 +79,17 @@ public class AlipayTransferService {
}
AlipayTransferResp resp = result.getData();
TransferResultBo bo = new TransferResultBo()
.setOutTransferNo(resp.getOrderId());
.setOutTransferNo(resp.getOrderId())
.setPayFundOrderId(resp.getPayFundOrderId())
.setFinishTime(this.parseAlipayDate(resp.getTransDate()));
// 同步返回 SUCCESS 直接成功, 其余(DEALING等)视为处理中, 交同步/回调确认
if (Objects.equals(resp.getStatus(), "SUCCESS")) {
bo.setStatus(PayFundStatusEnum.SUCCESS);
} else {
bo.setStatus(PayFundStatusEnum.PROCESSING);
}
// 回写支付宝特有字段(资金流水号)到容器
this.writeBackPayFundOrderId(context, resp.getPayFundOrderId());
return bo;
}
@@ -80,28 +110,60 @@ public class AlipayTransferService {
.setSyncErrorMsg(result.getMsg())
.setStatus(PayFundStatusEnum.PROCESSING);
}
return mapSyncResult(result.getData());
AlipayTransferResp resp = result.getData();
// 回写资金流水号(同步查询可能补获)
this.writeBackPayFundOrderId(context, resp.getPayFundOrderId());
return mapSyncResult(resp);
}
/// 映射同步结果为平台资金态
///
/// 支付宝状态: SUCCESS(成功) / FAIL(失败) / REFUND(退回) / DEALING(处理中) / CLOSED(关闭)
/// 支付宝状态(uni.transfer): SUCCESS(成功) / FAIL(失败) / DEALING(处理中) / REFUND(退票)
/// - FAIL → fail(终态失败, 允许复用原单号重试, 与支付宝"相同 out_biz_no 重发"指引一致)
/// - REFUND → close(退票资金退回, 等同关闭)
private TransferResultBo mapSyncResult(AlipayTransferResp resp) {
String status = resp.getStatus();
TransferResultBo bo = new TransferResultBo()
.setOutTransferNo(resp.getOrderId())
.setPayFundOrderId(resp.getPayFundOrderId())
.setFinishTime(this.parseAlipayDate(resp.getFinishTime()))
.setSyncErrorCode(resp.getErrorCode())
.setSyncErrorMsg(resp.getFailReason());
if (Objects.equals(status, "SUCCESS")) {
bo.setStatus(PayFundStatusEnum.SUCCESS);
} else if (Objects.equals(status, "FAIL") || Objects.equals(status, "CLOSED")) {
} else if (Objects.equals(status, "FAIL")) {
bo.setStatus(PayFundStatusEnum.FAIL);
} else if (Objects.equals(status, "CLOSED") || Objects.equals(status, "REFUND")) {
bo.setStatus(PayFundStatusEnum.CLOSE);
} else {
// DEALING/REFUND/未知: 保持处理中, 由后续同步轮询确认
// DEALING/未知: 保持处理中, 由后续同步轮询确认
bo.setStatus(PayFundStatusEnum.PROCESSING);
}
return bo;
}
/// 回写支付宝资金流水号到容器(支付宝特有, 非状态流转, 直接更新)
private void writeBackPayFundOrderId(TransferStrategyContext context, String payFundOrderId) {
if (StrUtil.isBlank(payFundOrderId) || context.getTrade() == null) {
return;
}
Long containerId = context.getTrade().getContainerId();
alipayTransferOrderManager.lambdaUpdate()
.eq(AlipayTransferOrder::getId, containerId)
.set(AlipayTransferOrder::getPayFundOrderId, payFundOrderId)
.update();
}
/// 解析支付宝时间字面量(东八区 yyyy-MM-dd HH:mm:ss)为 OffsetDateTime
///
/// 支付宝返回的时间字段无时区后缀, 按通道时间解析规范先用 LocalDateTime 接住再附加东八区偏移。
private OffsetDateTime parseAlipayDate(String dateStr) {
if (StrUtil.isBlank(dateStr)) {
return null;
}
return LocalDateTime.parse(dateStr, ALIPAY_DATE_FMT).atOffset(ZoneOffset.ofHours(8));
}
/// 生成支付宝转账异步通知地址(支付宝→平台)
///
/// 路径约定: `{backendBaseUrl}/unipay/callback/{mchNo}/{channelMchNo}/alipay`

View File

@@ -5,6 +5,7 @@ import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppCapabilityManager
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppKeyConfigManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectAppManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayDirectChannelMerchantManager;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayTransferConfigManager;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectApp;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectAppAuthConfig;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayDirectAppCapability;
@@ -34,6 +35,7 @@ public class AlipayDirectChannelMerchantCleanupStrategy implements ChannelMercha
private final AlipayDirectAppKeyConfigManager alipayDirectAppKeyConfigManager;
private final AlipayDirectAppCapabilityManager alipayDirectAppCapabilityManager;
private final AlipayDirectAppAuthConfigManager alipayDirectAppAuthConfigManager;
private final AlipayTransferConfigManager alipayTransferConfigManager;
/// 对应产品: 支付宝直连
@Override
@@ -50,5 +52,7 @@ public class AlipayDirectChannelMerchantCleanupStrategy implements ChannelMercha
alipayDirectAppKeyConfigManager.deleteByField(AlipayDirectAppKeyConfig::getChannelMchNo, channelMchNo);
alipayDirectAppCapabilityManager.deleteByField(AlipayDirectAppCapability::getChannelMchNo, channelMchNo);
alipayDirectAppAuthConfigManager.deleteByField(AlipayDirectAppAuthConfig::getChannelMchNo, channelMchNo);
// 转账配置(转账应用绑定)
alipayTransferConfigManager.deleteByChannelMchNo(channelMchNo);
}
}

View File

@@ -1,6 +1,8 @@
package cn.daxpay.open.channel.alipay.strategy.direct.transfer;
import cn.daxpay.open.channel.alipay.client.credential.AlipaySdkCredential;
import cn.daxpay.open.channel.alipay.dao.direct.AlipayTransferConfigManager;
import cn.daxpay.open.channel.alipay.entity.direct.AlipayTransferConfig;
import cn.daxpay.open.channel.alipay.service.direct.AlipayDirectConfigAssembler;
import cn.daxpay.open.channel.alipay.service.payment.transfer.AlipayTransferService;
import cn.daxpay.open.payment.strategy.transfer.AbsTransferStrategy;
@@ -36,6 +38,7 @@ public class AlipayTransferStrategy extends AbsTransferStrategy {
private final AlipayTransferService alipayTransferService;
private final AlipayDirectConfigAssembler alipayDirectConfigAssembler;
private final AlipayTransferConfigManager alipayTransferConfigManager;
@Override
public String getChannel() {
@@ -55,6 +58,11 @@ public class AlipayTransferStrategy extends AbsTransferStrategy {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferPayeeAccountRequired");
}
if (StrUtil.isBlank(param.getTitle())) {
// 支付宝: 转账标题必填(order_title 支付宝要求必选)
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferTitleRequired");
}
}
/// 发起转账
@@ -71,9 +79,18 @@ public class AlipayTransferStrategy extends AbsTransferStrategy {
return alipayTransferService.sync(context, credential);
}
/// 组装通道调用凭证(转账无能力维度, 走直连密钥配置)
/// 组装通道调用凭证(转账无能力维度, 按转账配置显式绑定应用解析)
private AlipaySdkCredential buildCredential(TransferStrategyContext context) {
return alipayDirectConfigAssembler.buildConfig(
context.getMchNo(), context.getChannelMchNo(), null);
// 读取转账配置(一对一绑定转出应用, 未绑定不允许发起)
AlipayTransferConfig transferConfig = alipayTransferConfigManager
.findByChannelMchNo(context.getChannelMchNo())
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferAppNotConfigured"));
if (transferConfig.getTransferAppRefId() == null) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.alipay.transferAppNotConfigured");
}
return alipayDirectConfigAssembler.buildTransferConfig(
context.getMchNo(), context.getChannelMchNo(), transferConfig.getTransferAppRefId());
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "Alipay transfer failed: {0}",
"transferQueryFailed": "Alipay transfer query failed: {0}",
"transferPayeeTypeInvalid": "Alipay: unsupported payee account type: {0}",
"transferPayeeAccountRequired": "Alipay: payee account required"
"transferPayeeAccountRequired": "Alipay: payee account required",
"transferTitleRequired": "Alipay: transfer title required",
"transferSceneNotFound": "Alipay: transfer scene config not found",
"transferSceneNameInvalid": "Alipay: unsupported transfer scene: {0}",
"transferSceneNotConfigured": "Alipay: no transfer scene configured for this channel merchant, please configure one in 'Transfer Scene' tab of channel merchant detail",
"transferSceneNotBelong": "Alipay: transfer scene config does not belong to the current merchant",
"transferSceneIdRequired": "Alipay: transfer scene config id required",
"transferSceneIdInvalid": "Alipay: invalid transfer scene config id: {0}",
"transferSceneEnabledLimit": "Alipay: Cannot enable more than {0} transfer scenes",
"transferSceneCannotDisableDefault": "Alipay: Default scene cannot be disabled, please switch default to another scene first",
"transferAppNotConfigured": "Alipay: no transfer app bound for this channel merchant, please bind one in 'Transfer App' of channel merchant detail",
"transferAppNotExist": "Alipay: transfer app not found",
"transferAppNotBelong": "Alipay: transfer app does not belong to the current merchant",
"channelMerchantMismatch": "Alipay: channel merchant does not belong to the current merchant"
}

View File

@@ -835,6 +835,20 @@
},
"runtime": {
"size": "Runtime code cannot exceed 16 characters"
},
"transferSceneName": {
"notBlank": "Transfer scene name cannot be empty",
"size": "Transfer scene name cannot exceed 64 characters"
},
"transferSceneReportInfoType": {
"size": "Transfer scene report info type cannot exceed 64 characters"
},
"transferSceneReportInfoContent": {
"size": "Transfer scene report info content cannot exceed 200 characters"
},
"transferSceneId": {
"notBlank": "Transfer scene ID cannot be empty",
"size": "Transfer scene ID cannot exceed 16 characters"
}
}
}

View File

@@ -50,5 +50,7 @@
"transferFailed": "Transfer Alipay gagal: {0}",
"transferQueryFailed": "Kueri transfer Alipay gagal: {0}",
"transferPayeeTypeInvalid": "Alipay: jenis akun penerima tidak didukung: {0}",
"transferPayeeAccountRequired": "Alipay: akun penerima wajib"
"transferPayeeAccountRequired": "Alipay: akun penerima wajib",
"transferSceneEnabledLimit": "Alipay: Tidak dapat mengaktifkan lebih dari {0} adegan transfer",
"transferSceneCannotDisableDefault": "Alipay: Adegan default tidak dapat dinonaktifkan, alihkan default ke adegan lain terlebih dahulu"
}

View File

@@ -831,6 +831,10 @@
},
"runtime": {
"size": "Kode runtime tidak boleh lebih dari 16 karakter"
},
"transferSceneId": {
"notBlank": "ID skenario transfer tidak boleh kosong",
"size": "ID skenario transfer tidak boleh melebihi 16 karakter"
}
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "Alipay送金エラー: {0}",
"transferQueryFailed": "Alipay送金照会エラー: {0}",
"transferPayeeTypeInvalid": "Alipay: 未対応の受取人アカウントタイプ: {0}",
"transferPayeeAccountRequired": "Alipay: 受取人アカウントは必須です"
"transferPayeeAccountRequired": "Alipay: 受取人アカウントは必須です",
"transferTitleRequired": "Alipay: 振込タイトルは必須です",
"transferSceneNotFound": "Alipay: 振込シーン設定が存在しません",
"transferSceneNameInvalid": "Alipay: サポートされていない振込シーン: {0}",
"transferSceneNotConfigured": "Alipay: 当該チャネル加盟店に振込シーンが設定されていません。チャネル加盟店詳細の「振込シーン」で設定してください",
"transferSceneNotBelong": "Alipay: 振込シーン設定は現在の加盟店に属していません",
"transferSceneIdRequired": "Alipay: 振込シーン設定IDは必須です",
"transferSceneIdInvalid": "Alipay: 振込シーン設定IDの形式エラー: {0}",
"transferSceneEnabledLimit": "Alipay: 有効化する送金シーンは{0}個までです",
"transferSceneCannotDisableDefault": "Alipay: デフォルトシーンは無効化できません, 先にデフォルトを他のシーンに切り替えてください",
"transferAppNotConfigured": "Alipay: 当該チャネル加盟店に振込アプリがバインドされていません。チャネル加盟店詳細の「振込アプリ」で設定してください",
"transferAppNotExist": "Alipay: 振込アプリが存在しません",
"transferAppNotBelong": "Alipay: 振込アプリは現在の加盟店に属していません",
"channelMerchantMismatch": "Alipay: チャネル加盟店は現在の加盟店に属していません"
}

View File

@@ -831,6 +831,20 @@
},
"runtime": {
"size": "実行形態コードは16文字以内です"
},
"transferSceneName": {
"notBlank": "振込シーン名は空にできません",
"size": "振込シーン名は64文字を超えることはできません"
},
"transferSceneReportInfoType": {
"size": "振込シーン報告情報タイプは64文字を超えることはできません"
},
"transferSceneReportInfoContent": {
"size": "振込シーン報告情報内容は200文字を超えることはできません"
},
"transferSceneId": {
"notBlank": "送金シーンIDは必須です",
"size": "送金シーンIDは16文字以内で入力してください"
}
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "알리페이 송금 실패: {0}",
"transferQueryFailed": "알리페이 송금 조회 실패: {0}",
"transferPayeeTypeInvalid": "알리페이: 지원하지 않는 수취인 계정 유형: {0}",
"transferPayeeAccountRequired": "알리페이: 수취인 계정 필수"
"transferPayeeAccountRequired": "알리페이: 수취인 계정 필수",
"transferTitleRequired": "Alipay: 이체 제목은 필수입니다",
"transferSceneNotFound": "Alipay: 이체 시나리오 설정이 존재하지 않습니다",
"transferSceneNameInvalid": "Alipay: 지원되지 않는 이체 시나리오: {0}",
"transferSceneNotConfigured": "Alipay: 이 채널 가맹점에 이체 시나리오가 설정되지 않았습니다. 채널 가맹점 상세의 '이체 시나리오'에서 설정해 주세요",
"transferSceneNotBelong": "Alipay: 이체 시나리오 설정이 현재 가맹점에 속하지 않습니다",
"transferSceneIdRequired": "Alipay: 이체 시나리오 설정 ID는 필수입니다",
"transferSceneIdInvalid": "Alipay: 이체 시나리오 설정 ID 형식 오류: {0}",
"transferSceneEnabledLimit": "Alipay: 활성화된 이체 장면은 {0}개를 초과할 수 없습니다",
"transferSceneCannotDisableDefault": "Alipay: 기본 장면은 비활성화할 수 없습니다, 먼저 기본을 다른 장면으로 전환하세요",
"transferAppNotConfigured": "Alipay: 이 채널 가맹점에 이체 앱이 바인딩되지 않았습니다. 채널 가맹점 상세의 '이체 앱'에서 설정해 주세요",
"transferAppNotExist": "Alipay: 이체 앱이 존재하지 않습니다",
"transferAppNotBelong": "Alipay: 이체 앱이 현재 가맹점에 속하지 않습니다",
"channelMerchantMismatch": "Alipay: 채널 가맹점이 현재 가맹점에 속하지 않습니다"
}

View File

@@ -831,6 +831,20 @@
},
"runtime": {
"size": "런타임 코드는 16자를 초과할 수 없습니다"
},
"transferSceneName": {
"notBlank": "이체 시나리오 이름은 비워둘 수 없습니다",
"size": "이체 시나리오 이름은 64자를 초과할 수 없습니다"
},
"transferSceneReportInfoType": {
"size": "이체 시나리오 보고 정보 유형은 64자를 초과할 수 없습니다"
},
"transferSceneReportInfoContent": {
"size": "이체 시나리오 보고 정보 내용은 200자를 초과할 수 없습니다"
},
"transferSceneId": {
"notBlank": "송금 시나리오 ID는 필수입니다",
"size": "송금 시나리오 ID는 16자 이내여야 합니다"
}
}
}

View File

@@ -50,5 +50,7 @@
"transferFailed": "Pindahan Alipay gagal: {0}",
"transferQueryFailed": "Pertanyaan pindahan Alipay gagal: {0}",
"transferPayeeTypeInvalid": "Alipay: jenis akaun penerima tidak disokong: {0}",
"transferPayeeAccountRequired": "Alipay: akaun penerima wajib"
"transferPayeeAccountRequired": "Alipay: akaun penerima wajib",
"transferSceneEnabledLimit": "Alipay: Tidak boleh mengaktifkan lebih daripada {0} babak pemindahan",
"transferSceneCannotDisableDefault": "Alipay: Babak lalai tidak boleh dilumpuhkan, sila tukar lalai ke babak lain dahulu"
}

View File

@@ -831,6 +831,10 @@
},
"runtime": {
"size": "Kod runtime tidak boleh melebihi 16 aksara"
},
"transferSceneId": {
"notBlank": "ID senario pindahan tidak boleh kosong",
"size": "ID senario pindahan tidak boleh melebihi 16 aksara"
}
}
}

View File

@@ -50,5 +50,7 @@
"transferFailed": "การโอนเงิน Alipay ล้มเหลว: {0}",
"transferQueryFailed": "การสอบถามการโอนเงิน Alipay ล้มเหลว: {0}",
"transferPayeeTypeInvalid": "Alipay: ไม่รองรับประเภทบัญชีผู้รับ: {0}",
"transferPayeeAccountRequired": "Alipay: ต้องระบุบัญชีผู้รับ"
"transferPayeeAccountRequired": "Alipay: ต้องระบุบัญชีผู้รับ",
"transferSceneEnabledLimit": "Alipay: ไม่สามารถเปิดใช้งานเกิน {0} สถานการณ์การโอน",
"transferSceneCannotDisableDefault": "Alipay: ไม่สามารถปิดใช้งานสถานการณ์เริ่มต้น โปรดสลับเริ่มต้นไปยังสถานการณ์อื่นก่อน"
}

View File

@@ -831,6 +831,10 @@
},
"runtime": {
"size": "รหัส runtime ต้องไม่เกิน 16 ตัวอักษร"
},
"transferSceneId": {
"notBlank": "รหัสสถานการณ์การโอนต้องไม่ว่าง",
"size": "รหัสสถานการณ์การโอนต้องไม่เกิน 16 ตัวอักษร"
}
}
}

View File

@@ -50,5 +50,7 @@
"transferFailed": "Chuyển khoản Alipay thất bại: {0}",
"transferQueryFailed": "Truy vấn chuyển khoản Alipay thất bại: {0}",
"transferPayeeTypeInvalid": "Alipay: loại tài khoản người nhận không được hỗ trợ: {0}",
"transferPayeeAccountRequired": "Alipay: bắt buộc nhập tài khoản người nhận"
"transferPayeeAccountRequired": "Alipay: bắt buộc nhập tài khoản người nhận",
"transferSceneEnabledLimit": "Alipay: Không thể kích hoạt quá {0} cảnh chuyển khoản",
"transferSceneCannotDisableDefault": "Alipay: Cảnh mặc định không thể vô hiệu hóa, vui lòng chuyển mặc định sang cảnh khác trước"
}

View File

@@ -831,6 +831,10 @@
},
"runtime": {
"size": "Mã runtime không được vượt quá 16 ký tự"
},
"transferSceneId": {
"notBlank": "ID kịch bản chuyển khoản không được để trống",
"size": "ID kịch bản chuyển khoản không được vượt quá 16 ký tự"
}
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "支付宝转账失败: {0}",
"transferQueryFailed": "支付宝转账查询失败: {0}",
"transferPayeeTypeInvalid": "支付宝: 不支持的收款人账号类型: {0}",
"transferPayeeAccountRequired": "支付宝: 收款人账号必填"
"transferPayeeAccountRequired": "支付宝: 收款人账号必填",
"transferTitleRequired": "支付宝: 转账标题必填",
"transferSceneNotFound": "支付宝: 转账场景配置不存在",
"transferSceneNameInvalid": "支付宝: 不支持的转账场景: {0}",
"transferSceneNotConfigured": "支付宝: 该通道商户尚未配置转账场景, 请先在通道商户详情的「转账场景」中配置",
"transferSceneNotBelong": "支付宝: 转账场景配置不属于当前商户",
"transferSceneIdRequired": "支付宝: 转账场景配置主键必填",
"transferSceneIdInvalid": "支付宝: 转账场景配置ID格式错误: {0}",
"transferSceneEnabledLimit": "支付宝: 启用的转账场景不能超过{0}个",
"transferSceneCannotDisableDefault": "支付宝: 默认场景不能禁用, 请先切换默认到其他场景",
"transferAppNotConfigured": "支付宝: 该通道商户尚未绑定转账应用, 请先在通道商户详情的「转账应用」中配置",
"transferAppNotExist": "支付宝: 转账应用不存在",
"transferAppNotBelong": "支付宝: 转账应用不属于当前商户",
"channelMerchantMismatch": "支付宝: 通道商户不属于当前商户"
}

View File

@@ -835,6 +835,20 @@
},
"runtime": {
"size": "运行形态编码不可超过16位"
},
"transferSceneName": {
"notBlank": "转账场景名称不可为空",
"size": "转账场景名称不可超过64位"
},
"transferSceneReportInfoType": {
"size": "转账场景上报信息类型不可超过64位"
},
"transferSceneReportInfoContent": {
"size": "转账场景上报信息内容不可超过200位"
},
"transferSceneId": {
"notBlank": "转账场景ID不可为空",
"size": "转账场景ID不可超过16位"
}
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "支付寶轉賬失敗: {0}",
"transferQueryFailed": "支付寶轉賬查詢失敗: {0}",
"transferPayeeTypeInvalid": "支付寶: 不支持的收款人賬號類型: {0}",
"transferPayeeAccountRequired": "支付寶: 收款人賬號必填"
"transferPayeeAccountRequired": "支付寶: 收款人賬號必填",
"transferTitleRequired": "支付寶: 轉賬標題必填",
"transferSceneNotFound": "支付寶: 轉賬場景設定不存在",
"transferSceneNameInvalid": "支付寶: 不支援的轉賬場景: {0}",
"transferSceneNotConfigured": "支付寶: 該通道商戶尚未設定轉賬場景, 請先在通道商戶詳情的「轉賬場景」中設定",
"transferSceneNotBelong": "支付寶: 轉賬場景設定不屬於目前商戶",
"transferSceneIdRequired": "支付寶: 轉賬場景設定主鍵必填",
"transferSceneIdInvalid": "支付寶: 轉賬場景設定ID格式錯誤: {0}",
"transferSceneEnabledLimit": "支付寶: 啟用的轉賬場景不能超過{0}個",
"transferSceneCannotDisableDefault": "支付寶: 預設場景不能停用, 請先切換預設到其他場景",
"transferAppNotConfigured": "支付寶: 該通道商戶尚未綁定轉賬應用, 請先在通道商戶詳情的「轉賬應用」中設定",
"transferAppNotExist": "支付寶: 轉賬應用不存在",
"transferAppNotBelong": "支付寶: 轉賬應用不屬於目前商戶",
"channelMerchantMismatch": "支付寶: 通道商戶不屬於目前商戶"
}

View File

@@ -831,6 +831,20 @@
},
"runtime": {
"size": "運行形態編碼不可超過16位"
},
"transferSceneName": {
"notBlank": "轉賬場景名稱不可為空",
"size": "轉賬場景名稱不可超過64位"
},
"transferSceneReportInfoType": {
"size": "轉賬場景上報資訊類型不可超過64位"
},
"transferSceneReportInfoContent": {
"size": "轉賬場景上報資訊內容不可超過200位"
},
"transferSceneId": {
"notBlank": "轉賬場景ID不可為空",
"size": "轉賬場景ID不可超過16位"
}
}
}

View File

@@ -50,5 +50,18 @@
"transferFailed": "支付寶轉帳失敗: {0}",
"transferQueryFailed": "支付寶轉帳查詢失敗: {0}",
"transferPayeeTypeInvalid": "支付寶: 不支持的收款人賬號類型: {0}",
"transferPayeeAccountRequired": "支付寶: 收款人賬號必填"
"transferPayeeAccountRequired": "支付寶: 收款人賬號必填",
"transferTitleRequired": "支付寶: 轉帳標題必填",
"transferSceneNotFound": "支付寶: 轉帳場景設定不存在",
"transferSceneNameInvalid": "支付寶: 不支援的轉帳場景: {0}",
"transferSceneNotConfigured": "支付寶: 該通道商戶尚未設定轉帳場景, 請先在通道商戶詳情的「轉帳場景」中設定",
"transferSceneNotBelong": "支付寶: 轉帳場景設定不屬於當前商戶",
"transferSceneIdRequired": "支付寶: 轉帳場景設定主鍵必填",
"transferSceneIdInvalid": "支付寶: 轉帳場景設定ID格式錯誤: {0}",
"transferSceneEnabledLimit": "支付寶: 啟用的轉賬場景不能超過{0}個",
"transferSceneCannotDisableDefault": "支付寶: 預設場景不能停用, 請先切換預設到其他場景",
"transferAppNotConfigured": "支付寶: 該通道商戶尚未綁定轉帳應用, 請先在通道商戶詳情的「轉帳應用」中設定",
"transferAppNotExist": "支付寶: 轉帳應用不存在",
"transferAppNotBelong": "支付寶: 轉帳應用不屬於當前商戶",
"channelMerchantMismatch": "支付寶: 通道商戶不屬於當前商戶"
}

View File

@@ -831,6 +831,20 @@
},
"runtime": {
"size": "執行形態編碼不可超過16位"
},
"transferSceneName": {
"notBlank": "轉帳場景名稱不可為空",
"size": "轉帳場景名稱不可超過64位"
},
"transferSceneReportInfoType": {
"size": "轉帳場景上報資訊類型不可超過64位"
},
"transferSceneReportInfoContent": {
"size": "轉帳場景上報資訊內容不可超過200位"
},
"transferSceneId": {
"notBlank": "轉帳場景ID不可為空",
"size": "轉帳場景ID不可超過16位"
}
}
}