From 1b1470fa6c91b02257291e90892204fe88601415 Mon Sep 17 00:00:00 2001 From: daxpay Date: Fri, 7 Aug 2026 16:39:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(channel-alipay):=20=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=AE=9D=E8=BD=AC=E8=B4=A6=E7=94=B3=E8=AF=B7=E9=85=8D=E5=A5=97?= =?UTF-8?q?(=E8=BD=AC=E8=B4=A6=E5=BA=94=E7=94=A8/=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE/=E8=B5=84=E9=87=91=E6=B5=81=E6=B0=B4?= =?UTF-8?q?=E5=8F=B7=E5=9B=9E=E5=86=99/=E6=97=B6=E9=97=B4=E8=A7=A3?= =?UTF-8?q?=E6=9E=90)=20+=20=E9=94=99=E8=AF=AF=E7=A0=81=E4=B8=8E=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=AF=8D=E6=9D=A1(10=E8=AF=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alipay/client/req/AlipayTransferReq.java | 9 + .../client/resp/AlipayTransferResp.java | 9 + .../AlipayTransferConfigController.java | 52 +++++ .../AlipayTransferSceneConfigController.java | 76 ++++++ .../MchAlipayTransferConfigController.java | 63 +++++ ...chAlipayTransferSceneConfigController.java | 81 +++++++ .../direct/AlipayTransferConfigConvert.java | 31 +++ .../AlipayTransferSceneConfigConvert.java | 19 ++ .../direct/AlipayTransferConfigManager.java | 29 +++ .../direct/AlipayTransferConfigMapper.java | 13 ++ .../AlipayTransferSceneConfigManager.java | 91 ++++++++ .../AlipayTransferSceneConfigMapper.java | 13 ++ .../entity/direct/AlipayTransferConfig.java | 38 +++ .../direct/AlipayTransferSceneConfig.java | 43 ++++ .../alipay/enums/AlipayTransferSceneEnum.java | 80 +++++++ .../direct/AlipayTransferConfigParam.java | 30 +++ .../direct/AlipayTransferConfigResult.java | 40 ++++ .../AlipayTransferSceneConfigResult.java | 42 ++++ .../AlipayTransferSceneOptionResult.java | 29 +++ .../direct/AlipayDirectConfigAssembler.java | 30 ++- .../direct/AlipayTransferConfigService.java | 114 +++++++++ .../AlipayTransferSceneConfigService.java | 221 ++++++++++++++++++ .../transfer/AlipayTransferService.java | 74 +++++- ...yDirectChannelMerchantCleanupStrategy.java | 4 + .../transfer/AlipayTransferStrategy.java | 23 +- .../i18n/en-US/error/channel/alipay.json | 15 +- .../main/resources/i18n/en-US/validation.json | 14 ++ .../i18n/id-ID/error/channel/alipay.json | 4 +- .../main/resources/i18n/id-ID/validation.json | 4 + .../i18n/ja-JP/error/channel/alipay.json | 15 +- .../main/resources/i18n/ja-JP/validation.json | 14 ++ .../i18n/ko-KR/error/channel/alipay.json | 15 +- .../main/resources/i18n/ko-KR/validation.json | 14 ++ .../i18n/ms-MY/error/channel/alipay.json | 4 +- .../main/resources/i18n/ms-MY/validation.json | 4 + .../i18n/th-TH/error/channel/alipay.json | 4 +- .../main/resources/i18n/th-TH/validation.json | 4 + .../i18n/vi-VN/error/channel/alipay.json | 4 +- .../main/resources/i18n/vi-VN/validation.json | 4 + .../i18n/zh-CN/error/channel/alipay.json | 15 +- .../main/resources/i18n/zh-CN/validation.json | 14 ++ .../i18n/zh-HK/error/channel/alipay.json | 15 +- .../main/resources/i18n/zh-HK/validation.json | 14 ++ .../i18n/zh-TW/error/channel/alipay.json | 15 +- .../main/resources/i18n/zh-TW/validation.json | 14 ++ 45 files changed, 1440 insertions(+), 20 deletions(-) create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferConfigController.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferSceneConfigController.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferConfigController.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferSceneConfigController.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferConfigConvert.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferSceneConfigConvert.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigManager.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigMapper.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigManager.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigMapper.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferConfig.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferSceneConfig.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/enums/AlipayTransferSceneEnum.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/param/direct/AlipayTransferConfigParam.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferConfigResult.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneConfigResult.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneOptionResult.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferConfigService.java create mode 100644 daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferSceneConfigService.java diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipayTransferReq.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipayTransferReq.java index cfe013412..7eab2927a 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipayTransferReq.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/req/AlipayTransferReq.java @@ -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 reportInfos; + /// 通道调用凭证 private AlipaySdkCredential credential; } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipayTransferResp.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipayTransferResp.java index 183a1ac4d..695072891 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipayTransferResp.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/client/resp/AlipayTransferResp.java @@ -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; } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferConfigController.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferConfigController.java new file mode 100644 index 000000000..bdb581b58 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferConfigController.java @@ -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 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 save(@RequestBody @Validated AlipayTransferConfigParam param) { + alipayTransferConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferSceneConfigController.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferSceneConfigController.java new file mode 100644 index 000000000..f03ce5b23 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/AlipayTransferSceneConfigController.java @@ -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( + @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> sceneOptions() { + return Res.ok(alipayTransferSceneConfigService.findSceneOptions()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "设为默认转账场景(自动启用, 按场景名称按需创建)") + @PostMapping("/set-default") + public Result 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 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(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferConfigController.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferConfigController.java new file mode 100644 index 000000000..69695edba --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferConfigController.java @@ -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 findByChannelMchNo( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { + return Res.ok(alipayTransferConfigService.findByChannelMchNo(requireMchNo(), channelMchNo)); + } + + @Operation(summary = "保存或更新转账配置(一对一)") + @PostMapping("/save") + public Result save(@RequestBody @Validated AlipayTransferConfigParam param) { + // 商户号强制取自上下文, 防越权 + param.setMchNo(requireMchNo()); + alipayTransferConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferSceneConfigController.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferSceneConfigController.java new file mode 100644 index 000000000..8fac20d33 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/controller/direct/MchAlipayTransferSceneConfigController.java @@ -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( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { + return Res.ok(alipayTransferSceneConfigService.list(requireMchNo(), channelMchNo)); + } + + @Operation(summary = "查询支付宝转账场景选项列表(主数据枚举投影)") + @GetMapping("/scene-options") + public Result> sceneOptions() { + return Res.ok(alipayTransferSceneConfigService.findSceneOptions()); + } + + @Operation(summary = "设为默认转账场景(自动启用, 按场景名称按需创建)") + @PostMapping("/set-default") + public Result 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 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(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferConfigConvert.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferConfigConvert.java new file mode 100644 index 000000000..f9c8be6eb --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferSceneConfigConvert.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferSceneConfigConvert.java new file mode 100644 index 000000000..6fff10738 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/convert/direct/AlipayTransferSceneConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigManager.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigManager.java new file mode 100644 index 000000000..686e3f763 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigManager.java @@ -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 { + + /// 按通道商户号查询转账配置(一对一) + public Optional findByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(AlipayTransferConfig::getChannelMchNo, channelMchNo) + .oneOpt(); + } + + /// 按通道商户号删除转账配置(逻辑删除, 通道商户删除时级联清理) + public void deleteByChannelMchNo(String channelMchNo) { + lambdaUpdate() + .eq(AlipayTransferConfig::getChannelMchNo, channelMchNo) + .remove(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigMapper.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigMapper.java new file mode 100644 index 000000000..a08c432ed --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigManager.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigManager.java new file mode 100644 index 000000000..eaa190f5a --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigManager.java @@ -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 { + + /// 按通道商户号查询全部场景配置(默认项排前, 启用排前) + public List listByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(AlipayTransferSceneConfig::getChannelMchNo, channelMchNo) + .orderByDesc(AlipayTransferSceneConfig::getIsDefault) + .orderByDesc(AlipayTransferSceneConfig::getEnabled) + .orderByDesc(AlipayTransferSceneConfig::getCreateTime) + .list(); + } + + /// 查询通道商户的默认场景(默认场景必须启用) + public Optional 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 findById(Long id) { + if (id == null) { + return Optional.empty(); + } + return lambdaQuery() + .eq(AlipayTransferSceneConfig::getId, id) + .oneOpt(); + } + + /// 按通道商户号与场景名称查询(场景行不存在时返回空, 由调用方按需创建) + public Optional 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(); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigMapper.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigMapper.java new file mode 100644 index 000000000..d401b66e1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/dao/direct/AlipayTransferSceneConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferConfig.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferConfig.java new file mode 100644 index 000000000..764d21ad4 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferConfig.java @@ -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 { + + /// 通道商户号 + @TableField(updateStrategy = FieldStrategy.NEVER) + private String channelMchNo; + + /// 转账转出应用引用(指向 alipay_direct_app 主键, 决定转账使用的 aliAppId 与密钥) + private Long transferAppRefId; + + /// 转换 + @Override + public AlipayTransferConfigResult toResult() { + return AlipayTransferConfigConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferSceneConfig.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferSceneConfig.java new file mode 100644 index 000000000..558cb66d7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/entity/direct/AlipayTransferSceneConfig.java @@ -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 { + + /// 通道商户号 + @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); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/enums/AlipayTransferSceneEnum.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/enums/AlipayTransferSceneEnum.java new file mode 100644 index 000000000..8f19b9e73 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/enums/AlipayTransferSceneEnum.java @@ -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 reportInfoTypes; + + /// 报备字段说明(与 reportInfoTypes 平行, 描述字段含义和支付宝文档示例) + private final List reportInfoDescriptions; + + /// 根据场景名称查找枚举 + public static AlipayTransferSceneEnum findBySceneName(String sceneName) { + for (AlipayTransferSceneEnum scene : values()) { + if (scene.sceneName.equals(sceneName)) { + return scene; + } + } + return null; + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/param/direct/AlipayTransferConfigParam.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/param/direct/AlipayTransferConfigParam.java new file mode 100644 index 000000000..f4775631d --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/param/direct/AlipayTransferConfigParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferConfigResult.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferConfigResult.java new file mode 100644 index 000000000..25ae66908 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferConfigResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneConfigResult.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneConfigResult.java new file mode 100644 index 000000000..d76f30cf0 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneConfigResult.java @@ -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 reportInfoTypes; + + @Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含支付宝文档示例)") + private List reportInfoDescriptions; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneOptionResult.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneOptionResult.java new file mode 100644 index 000000000..a69dbbb05 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/result/direct/AlipayTransferSceneOptionResult.java @@ -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 reportInfoTypes; + + @Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含支付宝文档示例)") + private List reportInfoDescriptions; +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java index 6dfdc08df..76039bc46 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectConfigAssembler.java @@ -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() diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferConfigService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferConfigService.java new file mode 100644 index 000000000..30ad48871 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferConfigService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferSceneConfigService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferSceneConfigService.java new file mode 100644 index 000000000..e0b330f26 --- /dev/null +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayTransferSceneConfigService.java @@ -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 findSceneOptions() { + return Arrays.stream(AlipayTransferSceneEnum.values()) + .map(scene -> new AlipayTransferSceneOptionResult() + .setSceneName(scene.getSceneName()) + .setReportInfoTypes(scene.getReportInfoTypes()) + .setReportInfoDescriptions(scene.getReportInfoDescriptions())) + .toList(); + } + + /// 查询通道商户下的场景配置行(仅已操作过的行, 按枚举固定顺序排序, 填充报备元数据) + /// + /// 未操作过的场景无行, 前端以场景选项为基准渲染, 本方法返回的状态映射到对应卡片。 + public List list(String mchNo, String channelMchNo) { + List entities = alipayTransferSceneConfigManager.listByChannelMchNo(channelMchNo); + // 按枚举固定顺序排序 + Map 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")); + } +} diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/transfer/AlipayTransferService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/transfer/AlipayTransferService.java index 3fc63e92d..708735869 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/transfer/AlipayTransferService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/payment/transfer/AlipayTransferService.java @@ -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 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` diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/merchant/AlipayDirectChannelMerchantCleanupStrategy.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/merchant/AlipayDirectChannelMerchantCleanupStrategy.java index 50a7ebcf1..6b087a977 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/merchant/AlipayDirectChannelMerchantCleanupStrategy.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/merchant/AlipayDirectChannelMerchantCleanupStrategy.java @@ -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); } } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/transfer/AlipayTransferStrategy.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/transfer/AlipayTransferStrategy.java index 4b7310a1a..4c4b757dc 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/transfer/AlipayTransferStrategy.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/strategy/direct/transfer/AlipayTransferStrategy.java @@ -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()); } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/alipay.json index 3c2a05416..a133c0884 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/alipay.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 1090437ec..ce8c90cf7 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/alipay.json index 29532f93d..faf7b01c7 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/alipay.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json index cacbec252..52c4d5900 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/alipay.json index 5380289d9..325d2b4c9 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/alipay.json @@ -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: チャネル加盟店は現在の加盟店に属していません" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json index 668df378e..122309a22 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json @@ -831,6 +831,20 @@ }, "runtime": { "size": "実行形態コードは16文字以内です" + }, + "transferSceneName": { + "notBlank": "振込シーン名は空にできません", + "size": "振込シーン名は64文字を超えることはできません" + }, + "transferSceneReportInfoType": { + "size": "振込シーン報告情報タイプは64文字を超えることはできません" + }, + "transferSceneReportInfoContent": { + "size": "振込シーン報告情報内容は200文字を超えることはできません" + }, + "transferSceneId": { + "notBlank": "送金シーンIDは必須です", + "size": "送金シーンIDは16文字以内で入力してください" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/alipay.json index 3e23a7d09..fbeaf0067 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/alipay.json @@ -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: 채널 가맹점이 현재 가맹점에 속하지 않습니다" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json index 02c408180..1e67fe561 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json @@ -831,6 +831,20 @@ }, "runtime": { "size": "런타임 코드는 16자를 초과할 수 없습니다" + }, + "transferSceneName": { + "notBlank": "이체 시나리오 이름은 비워둘 수 없습니다", + "size": "이체 시나리오 이름은 64자를 초과할 수 없습니다" + }, + "transferSceneReportInfoType": { + "size": "이체 시나리오 보고 정보 유형은 64자를 초과할 수 없습니다" + }, + "transferSceneReportInfoContent": { + "size": "이체 시나리오 보고 정보 내용은 200자를 초과할 수 없습니다" + }, + "transferSceneId": { + "notBlank": "송금 시나리오 ID는 필수입니다", + "size": "송금 시나리오 ID는 16자 이내여야 합니다" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/alipay.json index ea2770b03..dd48dabcf 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/alipay.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json index 448ce3986..973b6bea5 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/alipay.json index f1e9175f1..035c1029d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/alipay.json @@ -50,5 +50,7 @@ "transferFailed": "การโอนเงิน Alipay ล้มเหลว: {0}", "transferQueryFailed": "การสอบถามการโอนเงิน Alipay ล้มเหลว: {0}", "transferPayeeTypeInvalid": "Alipay: ไม่รองรับประเภทบัญชีผู้รับ: {0}", - "transferPayeeAccountRequired": "Alipay: ต้องระบุบัญชีผู้รับ" + "transferPayeeAccountRequired": "Alipay: ต้องระบุบัญชีผู้รับ", + "transferSceneEnabledLimit": "Alipay: ไม่สามารถเปิดใช้งานเกิน {0} สถานการณ์การโอน", + "transferSceneCannotDisableDefault": "Alipay: ไม่สามารถปิดใช้งานสถานการณ์เริ่มต้น โปรดสลับเริ่มต้นไปยังสถานการณ์อื่นก่อน" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json index cdc109078..6c5f66c27 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json @@ -831,6 +831,10 @@ }, "runtime": { "size": "รหัส runtime ต้องไม่เกิน 16 ตัวอักษร" + }, + "transferSceneId": { + "notBlank": "รหัสสถานการณ์การโอนต้องไม่ว่าง", + "size": "รหัสสถานการณ์การโอนต้องไม่เกิน 16 ตัวอักษร" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/alipay.json index e044a8f2c..da4ac1282 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/alipay.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json index 71a732dc3..c28bb77ad 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json @@ -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ự" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/alipay.json index 5dcc462b5..ec1ccb840 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/alipay.json @@ -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": "支付宝: 通道商户不属于当前商户" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index 5d3b45042..c5ed7d244 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -835,6 +835,20 @@ }, "runtime": { "size": "运行形态编码不可超过16位" + }, + "transferSceneName": { + "notBlank": "转账场景名称不可为空", + "size": "转账场景名称不可超过64位" + }, + "transferSceneReportInfoType": { + "size": "转账场景上报信息类型不可超过64位" + }, + "transferSceneReportInfoContent": { + "size": "转账场景上报信息内容不可超过200位" + }, + "transferSceneId": { + "notBlank": "转账场景ID不可为空", + "size": "转账场景ID不可超过16位" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/alipay.json index 81fc7bde3..f351cd965 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/alipay.json @@ -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": "支付寶: 通道商戶不屬於目前商戶" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json index e022e2923..42eb7c832 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json @@ -831,6 +831,20 @@ }, "runtime": { "size": "運行形態編碼不可超過16位" + }, + "transferSceneName": { + "notBlank": "轉賬場景名稱不可為空", + "size": "轉賬場景名稱不可超過64位" + }, + "transferSceneReportInfoType": { + "size": "轉賬場景上報資訊類型不可超過64位" + }, + "transferSceneReportInfoContent": { + "size": "轉賬場景上報資訊內容不可超過200位" + }, + "transferSceneId": { + "notBlank": "轉賬場景ID不可為空", + "size": "轉賬場景ID不可超過16位" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/alipay.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/alipay.json index 0d9b1683f..b9e889e74 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/alipay.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/alipay.json @@ -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": "支付寶: 通道商戶不屬於當前商戶" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json index f7e9760da..7613a40a2 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json @@ -831,6 +831,20 @@ }, "runtime": { "size": "執行形態編碼不可超過16位" + }, + "transferSceneName": { + "notBlank": "轉帳場景名稱不可為空", + "size": "轉帳場景名稱不可超過64位" + }, + "transferSceneReportInfoType": { + "size": "轉帳場景上報資訊類型不可超過64位" + }, + "transferSceneReportInfoContent": { + "size": "轉帳場景上報資訊內容不可超過200位" + }, + "transferSceneId": { + "notBlank": "轉帳場景ID不可為空", + "size": "轉帳場景ID不可超過16位" } } }