mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 06:46:04 +08:00
feat(channel-wechat): 微信转账申请配套(报备兜底/确认收款查询/标题与报备校验) + 错误码词条(10语)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
package cn.daxpay.open.channel.wechat.client.req;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
|
||||
import cn.daxpay.open.payment.trade.transfer.param.TransferReportInfo;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信通道转账请求(发起/同步共用)
|
||||
///
|
||||
/// 与子应用 dax-pay-channel-one 的 `WechatTransferReq` 镜像, 字段对齐。
|
||||
@@ -34,6 +37,9 @@ public class WechatTransferReq {
|
||||
/// 异步通知地址(微信→平台)
|
||||
private String notifyUrl;
|
||||
|
||||
/// 转账场景报备信息(发起时必填, 同步可空)
|
||||
private List<TransferReportInfo> reportInfos;
|
||||
|
||||
/// 通道调用凭证
|
||||
private WechatSdkCredential credential;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.channel.wechat.controller.callback;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferConfirmResult;
|
||||
import cn.daxpay.open.channel.wechat.service.payment.transfer.WechatTransferConfirmQueryService;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// # 微信转账确认收款(公开接口)
|
||||
///
|
||||
/// C 端收款人(无登录态)凭 transferNo 查询确认收款信息, 用于在微信内拉起收款确认页。
|
||||
/// 与回调控制器同为 @IgnoreAuth, 不走 Sa-Token 认证。
|
||||
@IgnoreAuth
|
||||
@Validated
|
||||
@Tag(name = "微信转账确认收款(公开)")
|
||||
@RestController
|
||||
@RequestMapping("/unipay/transfer/wechat")
|
||||
@RequiredArgsConstructor
|
||||
public class WechatTransferConfirmController {
|
||||
|
||||
private final WechatTransferConfirmQueryService wechatTransferConfirmQueryService;
|
||||
|
||||
@Operation(summary = "查询微信转账确认收款信息")
|
||||
@GetMapping("/confirm-info/{transferNo}")
|
||||
public Result<WechatTransferConfirmResult> getConfirmInfo(
|
||||
@PathVariable @NotBlank(message = "转账单号不可为空") String transferNo) {
|
||||
return Res.ok(wechatTransferConfirmQueryService.queryByTransferNo(transferNo));
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectChannelMerchantUpdateParam;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectKeyConfigParam;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectChannelMerchantResult;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectKeyConfigResult;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferSceneOptionResult;
|
||||
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectChannelMerchantService;
|
||||
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectKeyConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -21,6 +23,8 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信直连通道商户管理
|
||||
///
|
||||
@PermCode(menuCode = PermCodes.Channel.Merchant.MENU)
|
||||
@@ -50,6 +54,21 @@ public class WechatDirectChannelMerchantController {
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.MANAGE)
|
||||
@Operation(summary = "更新微信直连通道商户(转账场景/微信商户号)")
|
||||
@PostMapping("/update")
|
||||
public Result<Void> update(@RequestBody @Validated WechatDirectChannelMerchantUpdateParam param) {
|
||||
wechatDirectChannelMerchantService.update(param);
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.VIEW)
|
||||
@Operation(summary = "查询微信转账场景选项列表")
|
||||
@GetMapping("/scene-options")
|
||||
public Result<List<WechatTransferSceneOptionResult>> sceneOptions() {
|
||||
return Res.ok(wechatDirectChannelMerchantService.findSceneOptions());
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.VIEW)
|
||||
@Operation(summary = "根据通道商户号查询密钥配置")
|
||||
@GetMapping("/find-key-config")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package cn.daxpay.open.channel.wechat.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信转账场景枚举
|
||||
///
|
||||
/// 对应微信商家转账(新版 fund-app/mch-transfer)的转账场景, 场景ID 由商户在微信商户平台
|
||||
/// 「产品中心-商家转账」申请开通。每个场景要求不同的报备字段([reportInfoTypes]),
|
||||
/// 字段值为微信协议固定的中文 [infoType], 不可更改。
|
||||
///
|
||||
/// 报备字段内容(infoContent)由商户在发起转账时填写, 留空用 `-` 兜底(1009 采购货款用空串)。
|
||||
///
|
||||
/// [reportInfoDescriptions] 与 [reportInfoTypes] 一一平行, 描述每个字段的含义和微信文档示例。
|
||||
/// [userRecvPerceptionOptions] 为收款人在微信中看到的感知文案可选值, 不传时微信按场景取默认(第一个)。
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum WechatTransferSceneEnum {
|
||||
|
||||
/// 现金营销
|
||||
CASH_MARKETING("1000", "现金营销",
|
||||
List.of("活动名称", "奖励说明"),
|
||||
List.of("商户自定义内容,如「新会员有礼」", "商户自定义内容,如「注册会员抽奖一等奖」"),
|
||||
List.of("现金奖励", "活动奖励")),
|
||||
|
||||
/// 行政补贴
|
||||
ADMINISTRATIVE_SUBSIDY("1002", "行政补贴",
|
||||
List.of("补贴类型"),
|
||||
List.of("商户自定义内容,如「购车补贴」"),
|
||||
List.of("行政补贴", "行政奖励")),
|
||||
|
||||
/// 保险理赔
|
||||
INSURANCE_CLAIM("1004", "保险理赔",
|
||||
List.of("保险产品备案编号", "保险名称", "保险操作单号"),
|
||||
List.of("保险产品备案编号,如「01212121212」", "保险名称,如「意外险」", "保险操作单号,如「12121245442」"),
|
||||
List.of("保险理赔款")),
|
||||
|
||||
/// 佣金报酬
|
||||
COMMISSION_REWARD("1005", "佣金报酬",
|
||||
List.of("岗位类型", "报酬说明"),
|
||||
List.of("商户自定义内容,如「外卖员」", "商户自定义内容,如「7月份配送费」"),
|
||||
List.of("劳务报酬", "报销款", "企业补贴", "开工利是")),
|
||||
|
||||
/// 采购货款
|
||||
PROCUREMENT_PAYMENT("1009", "采购货款",
|
||||
List.of("采购商品名称"),
|
||||
List.of("商户自定义内容,如「戴尔笔记本电脑」"),
|
||||
List.of("货款")),
|
||||
|
||||
/// 二手回收
|
||||
SECONDHAND_RECYCLING("1010", "二手回收",
|
||||
List.of("回收商品名称"),
|
||||
List.of("商户自定义内容,如「塑料瓶」"),
|
||||
List.of("二手回收货款")),
|
||||
|
||||
/// 企业赔付
|
||||
ENTERPRISE_COMPENSATION("1011", "企业赔付",
|
||||
List.of("赔付原因"),
|
||||
List.of("商户自定义内容,如「商品质量问题退款」"),
|
||||
List.of("退款", "商家赔付")),
|
||||
|
||||
/// 公益补助
|
||||
PUBLIC_WELFARE_SUBSIDY("1013", "公益补助",
|
||||
List.of("公益活动名称", "公益活动备案编号"),
|
||||
List.of("请填写在民政部的备案名称", "请填写在民政部的备案编号"),
|
||||
List.of("公益补助金"));
|
||||
|
||||
/// 转账场景ID
|
||||
private final String code;
|
||||
|
||||
/// 场景名称
|
||||
private final String name;
|
||||
|
||||
/// 报备字段定义(微信协议固定中文 infoType, 顺序即 infoContentList 下标)
|
||||
private final List<String> reportInfoTypes;
|
||||
|
||||
/// 报备字段说明(与 reportInfoTypes 平行, 描述字段含义和微信文档示例)
|
||||
private final List<String> reportInfoDescriptions;
|
||||
|
||||
/// 用户收款感知可选值(收款人在微信中看到的文案, 不传时取第一个为默认)
|
||||
private final List<String> userRecvPerceptionOptions;
|
||||
|
||||
/// 根据场景ID 查找枚举
|
||||
public static WechatTransferSceneEnum findByCode(String code) {
|
||||
for (WechatTransferSceneEnum scene : values()) {
|
||||
if (scene.code.equals(code)) {
|
||||
return scene;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.daxpay.open.channel.wechat.param.direct;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 微信直连通道商户更新参数
|
||||
///
|
||||
/// 当前仅支持更新转账场景与微信商户号, 通道商户号创建后不可变。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信直连通道商户更新参数")
|
||||
public class WechatDirectChannelMerchantUpdateParam {
|
||||
|
||||
/// 通道商户号(定位用, 不可变)
|
||||
@Schema(description = "通道商户号")
|
||||
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
|
||||
private String channelMchNo;
|
||||
|
||||
/// 微信直连商户号
|
||||
@Schema(description = "微信直连商户号")
|
||||
@Size(max = 32, message = "微信直连商户号不可超过32位")
|
||||
private String wxMchId;
|
||||
|
||||
/// 转账场景ID(商家转账到零钱, 微信转账时必填)
|
||||
/// @see cn.daxpay.open.channel.wechat.enums.WechatTransferSceneEnum
|
||||
@Schema(description = "转账场景ID")
|
||||
private String transferScene;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.channel.wechat.result.direct;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 微信转账确认收款信息
|
||||
///
|
||||
/// 供 C 端收款人在微信内拉起确认收款页(`WeixinJSBridge.invoke('requestMerchantTransfer')`)使用。
|
||||
/// [mchId]/[appId]/[packageInfo] 为拉起 JSAPI 必需参数, 全部由后端从订单与通道配置反查返回。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信转账确认收款信息")
|
||||
public class WechatTransferConfirmResult {
|
||||
|
||||
@Schema(description = "微信商户号(拉起 requestMerchantTransfer 用)")
|
||||
private String mchId;
|
||||
|
||||
@Schema(description = "商户 AppID(拉起 requestMerchantTransfer 用)")
|
||||
private String appId;
|
||||
|
||||
@Schema(description = "拉起确认参数 package_info")
|
||||
private String packageInfo;
|
||||
|
||||
@Schema(description = "转账金额(分)")
|
||||
private Long amount;
|
||||
|
||||
@Schema(description = "转账标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "转账状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "是否已终态(不可再操作)")
|
||||
private boolean received;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.daxpay.open.channel.wechat.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] 平行, 描述每个字段含义和微信文档示例。
|
||||
/// [userRecvPerceptionOptions] 为收款人在微信中看到的感知文案可选值。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信转账场景选项")
|
||||
public class WechatTransferSceneOptionResult {
|
||||
|
||||
@Schema(description = "转账场景ID")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "场景名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "报备字段定义(微信协议固定中文 infoType)")
|
||||
private List<String> reportInfoTypes;
|
||||
|
||||
@Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含微信文档示例)")
|
||||
private List<String> reportInfoDescriptions;
|
||||
|
||||
@Schema(description = "用户收款感知可选值(收款人在微信中看到的文案)")
|
||||
private List<String> userRecvPerceptionOptions;
|
||||
}
|
||||
@@ -2,8 +2,11 @@ package cn.daxpay.open.channel.wechat.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
|
||||
import cn.daxpay.open.channel.wechat.enums.WechatTransferSceneEnum;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectChannelMerchantCreateParam;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectChannelMerchantUpdateParam;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectChannelMerchantResult;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferSceneOptionResult;
|
||||
import cn.daxpay.open.channel.wechat.strategy.direct.merchant.WechatDirectChannelMerchantCleanupStrategy;
|
||||
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
|
||||
import cn.daxpay.open.payment.masterdata.dao.product.PayProductConfigManager;
|
||||
@@ -18,6 +21,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信直连通道商户管理
|
||||
///
|
||||
/// 一个微信商户号(wxMchId)对应一个 channelMchNo, 商户的多个应用共享此绑定。
|
||||
@@ -75,4 +81,40 @@ public class WechatDirectChannelMerchantService {
|
||||
// 微信: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
}
|
||||
|
||||
/// 更新微信直连通道商户(转账场景/微信商户号)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(WechatDirectChannelMerchantUpdateParam param) {
|
||||
WechatDirectChannelMerchant entity = wechatDirectChannelMerchantManager.lambdaQuery()
|
||||
.eq(WechatDirectChannelMerchant::getChannelMchNo, param.getChannelMchNo())
|
||||
.oneOpt()
|
||||
// 微信: 通道商户配置不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
|
||||
// 微信商户号变更时校验同一商户下不重复
|
||||
if (param.getWxMchId() != null && !param.getWxMchId().equals(entity.getWxMchId())) {
|
||||
if (wechatDirectChannelMerchantManager.existsByMchNoAndWxMchId(
|
||||
entity.getMchNo(), param.getWxMchId())) {
|
||||
// 微信: 同一商户下该微信商户已存在
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.directMchDuplicate");
|
||||
}
|
||||
entity.setWxMchId(param.getWxMchId());
|
||||
}
|
||||
// 转账场景(允许清空, 清空后发起转账会报"场景未配置")
|
||||
if (param.getTransferScene() != null) {
|
||||
entity.setTransferScene(param.getTransferScene());
|
||||
}
|
||||
wechatDirectChannelMerchantManager.updateById(entity);
|
||||
}
|
||||
|
||||
/// 查询微信转账场景选项列表(供前端下拉与报备字段动态渲染)
|
||||
public List<WechatTransferSceneOptionResult> findSceneOptions() {
|
||||
return Arrays.stream(WechatTransferSceneEnum.values())
|
||||
.map(scene -> new WechatTransferSceneOptionResult()
|
||||
.setCode(scene.getCode())
|
||||
.setName(scene.getName())
|
||||
.setReportInfoTypes(scene.getReportInfoTypes())
|
||||
.setReportInfoDescriptions(scene.getReportInfoDescriptions())
|
||||
.setUserRecvPerceptionOptions(scene.getUserRecvPerceptionOptions()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.daxpay.open.channel.wechat.service.payment.transfer;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferConfirmResult;
|
||||
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
|
||||
import cn.daxpay.open.payment.common.context.MerchantContextLoader;
|
||||
import cn.daxpay.open.payment.trade.transfer.dao.WechatTransferOrderManager;
|
||||
import cn.daxpay.open.payment.trade.transfer.entity.WechatTransferOrder;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/// # 微信转账确认收款查询服务
|
||||
///
|
||||
/// 供 C 端收款人(无登录态)凭 transferNo 查询确认收款信息。
|
||||
/// 跨租户引导读订单([WechatTransferOrderManager#findByTransferNoNotTenant]) →
|
||||
/// 装载商户上下文 → 通道凭证组装获取 wxMchId/wxAppId → 返回 packageInfo 等拉起参数。
|
||||
///
|
||||
/// 安全模型: 凭 transferNo 高熵不可猜防枚举, 仅返回该订单的公开收款信息。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatTransferConfirmQueryService {
|
||||
|
||||
/// 终态状态(不可再确认收款)
|
||||
private static final Set<String> TERMINAL_STATES = Set.of("success", "close", "fail");
|
||||
|
||||
private final WechatTransferOrderManager wechatTransferOrderManager;
|
||||
private final MerchantContextLoader merchantContextLoader;
|
||||
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
|
||||
|
||||
/// 查询确认收款信息
|
||||
///
|
||||
/// @param transferNo 平台转账单号(URL 路径参数)
|
||||
/// @return 确认收款信息(mchId/appId/packageInfo 等)
|
||||
public WechatTransferConfirmResult queryByTransferNo(String transferNo) {
|
||||
// 跨租户查订单(收款人 C 端无商户上下文)
|
||||
WechatTransferOrder order = wechatTransferOrderManager.findByTransferNoNotTenant(transferNo)
|
||||
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.transfer.notFound"));
|
||||
// 装载商户上下文, 后续通道配置查询走租户隔离
|
||||
merchantContextLoader.initMch(order.getMchNo());
|
||||
// 通道凭证组装(wxMchId + wxAppId)
|
||||
WechatSdkCredential credential = wechatDirectConfigAssembler.buildConfig(
|
||||
order.getMchNo(), order.getChannelMchNo(), null, null);
|
||||
boolean terminal = TERMINAL_STATES.contains(order.getStatus());
|
||||
return new WechatTransferConfirmResult()
|
||||
.setMchId(credential.getWxMchId())
|
||||
.setAppId(credential.getWxAppId())
|
||||
.setPackageInfo(terminal ? null : order.getTransferBody())
|
||||
.setAmount(order.getAmount())
|
||||
.setTitle(order.getTitle())
|
||||
.setStatus(order.getStatus())
|
||||
.setReceived(terminal);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import cn.daxpay.open.channel.wechat.client.WechatChannelClient;
|
||||
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
|
||||
import cn.daxpay.open.channel.wechat.client.req.WechatTransferReq;
|
||||
import cn.daxpay.open.channel.wechat.client.resp.WechatTransferResp;
|
||||
import cn.daxpay.open.channel.wechat.enums.WechatTransferSceneEnum;
|
||||
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.param.TransferReportInfo;
|
||||
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,6 +18,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -54,6 +57,7 @@ public class WechatTransferService {
|
||||
req.setUserName(context.getUserName());
|
||||
req.setRemark(StrUtil.sub(context.getTitle(), 0, 32));
|
||||
req.setNotifyUrl(this.buildNotifyUrl(context));
|
||||
req.setReportInfos(this.ensureReportInfos(context));
|
||||
req.setCredential(credential);
|
||||
|
||||
DaxResult<WechatTransferResp> result = wechatChannelClient.transfer(req);
|
||||
@@ -121,4 +125,26 @@ public class WechatTransferService {
|
||||
return StrUtil.format("{}/unipay/callback/{}/{}/wechat/transfer",
|
||||
base, context.getMchNo(), context.getChannelMchNo());
|
||||
}
|
||||
|
||||
/// 确保报备信息非空
|
||||
///
|
||||
/// 微信 `/transfer-bills` 的 `transfer_scene_report_infos` 为必填。
|
||||
/// 商户传入的 [TransferStrategyContext#getReportInfos] 非空时直接透传;
|
||||
/// 为空时按场景枚举 [WechatTransferSceneEnum] 的报备字段模板构建默认值(`-`)。
|
||||
private List<TransferReportInfo> ensureReportInfos(TransferStrategyContext context) {
|
||||
List<TransferReportInfo> reportInfos = context.getReportInfos();
|
||||
if (reportInfos != null && !reportInfos.isEmpty()) {
|
||||
return reportInfos;
|
||||
}
|
||||
// 兜底: 按场景枚举构建默认报备信息
|
||||
WechatTransferSceneEnum scene = WechatTransferSceneEnum.findByCode(context.getTransferScene());
|
||||
if (scene == null) {
|
||||
return reportInfos;
|
||||
}
|
||||
List<TransferReportInfo> defaults = new ArrayList<>();
|
||||
for (String infoType : scene.getReportInfoTypes()) {
|
||||
defaults.add(new TransferReportInfo().setInfoType(infoType).setInfoContent("-"));
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import cn.daxpay.open.payment.strategy.transfer.TransferStrategyContext;
|
||||
import cn.daxpay.open.payment.trade.transfer.bo.TransferResultBo;
|
||||
import cn.daxpay.open.payment.trade.transfer.enums.TransferPayeeTypeEnum;
|
||||
import cn.daxpay.open.payment.trade.transfer.param.TransferParam;
|
||||
import cn.daxpay.open.payment.trade.transfer.param.TransferReportInfo;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
@@ -18,12 +19,15 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信直连转账策略
|
||||
///
|
||||
/// 微信商家转账到零钱(V3)的通道策略。
|
||||
/// 通道差异:
|
||||
/// - 仅支持 openid 收款人([TransferPayeeTypeEnum#OPENID])
|
||||
/// - 金额档位姓名校验: 小于 0.3 元禁填姓名, 大于等于 2000 元必填姓名
|
||||
/// - 转账备注(标题)与场景报备信息必填(微信接口 transfer_remark / transfer_scene_report_infos)
|
||||
/// - transfer_scene 取自「微信转账配置」([WechatTransferConfig]), 发起应用由配置指定(公众号)
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -53,6 +57,18 @@ public class WechatTransferStrategy extends AbsTransferStrategy {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.wechat.transferOnlyOpenid");
|
||||
}
|
||||
// 微信: 转账备注(标题)必填, 对应接口 transfer_remark
|
||||
if (StrUtil.isBlank(param.getTitle())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.wechat.transferRemarkRequired");
|
||||
}
|
||||
// 微信: 转账场景报备信息必填(接口 transfer_scene_report_infos 必填, 且 info_content 不可为空)
|
||||
List<TransferReportInfo> reportInfos = param.getReportInfos();
|
||||
if (reportInfos == null || reportInfos.isEmpty()
|
||||
|| reportInfos.stream().anyMatch(info -> StrUtil.isBlank(info.getInfoContent()))) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.wechat.transferReportInfoRequired");
|
||||
}
|
||||
long amountFen = param.getAmount().movePointRight(2).longValue();
|
||||
String payeeName = param.getPayeeName();
|
||||
if (amountFen < SMALL_AMOUNT_LIMIT && StrUtil.isNotBlank(payeeName)) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.daxpay.open.payment.trade.transfer.dao;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreTenant;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import cn.daxpay.open.payment.trade.transfer.entity.WechatTransferOrder;
|
||||
import cn.daxpay.open.payment.trade.transfer.param.WechatTransferOrderQuery;
|
||||
@@ -24,6 +25,12 @@ public class WechatTransferOrderManager
|
||||
return findByField(WechatTransferOrder::getTransferNo, transferNo);
|
||||
}
|
||||
|
||||
/// 根据平台转账单号查询(忽略租户, 确认收款页引导读用)
|
||||
@IgnoreTenant
|
||||
public Optional<WechatTransferOrder> findByTransferNoNotTenant(String transferNo) {
|
||||
return findByField(WechatTransferOrder::getTransferNo, transferNo);
|
||||
}
|
||||
|
||||
/// 根据商户转账号和应用号查询(幂等查重主路径)
|
||||
public Optional<WechatTransferOrder> findByBizTransferNo(String bizTransferNo, String appId) {
|
||||
return lambdaQuery()
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: Transfer initiator app not configured",
|
||||
"transferAppNotExist": "WeChat: Transfer initiator app not found",
|
||||
"transferAppNotBelong": "WeChat: Transfer initiator app does not belong to the current merchant",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Transfer initiator app must be an Official Account"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Transfer initiator app must be an Official Account",
|
||||
"transferRemarkRequired": "WeChat: transfer remark (title) is required",
|
||||
"transferReportInfoRequired": "WeChat: transfer scene report info is required, please fill in all report fields"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: Aplikasi inisiator transfer belum dikonfigurasi",
|
||||
"transferAppNotExist": "WeChat: Aplikasi inisiator transfer tidak ditemukan",
|
||||
"transferAppNotBelong": "WeChat: Aplikasi inisiator transfer bukan milik merchant saat ini",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Aplikasi inisiator transfer harus berjenis Official Account"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Aplikasi inisiator transfer harus berjenis Official Account",
|
||||
"transferRemarkRequired": "WeChat: catatan transfer (judul) wajib diisi",
|
||||
"transferReportInfoRequired": "WeChat: informasi pelaporan skema transfer wajib diisi, silakan isi semua bidang pelaporan"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: 振込发起アプリが未設定です",
|
||||
"transferAppNotExist": "WeChat: 振込发起アプリが見つかりません",
|
||||
"transferAppNotBelong": "WeChat: 振込发起アプリは現在のマーチャントに属していません",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: 振込发起アプリは公式アカウントタイプである必要があります"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: 振込发起アプリは公式アカウントタイプである必要があります",
|
||||
"transferRemarkRequired": "WeChat: 振込備考(タイトル)は必須です",
|
||||
"transferReportInfoRequired": "WeChat: 振込シーン報備情報は必須です。すべての報備項目を入力してください"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: 이체 발신 앱이 구성되지 않았습니다",
|
||||
"transferAppNotExist": "WeChat: 이체 발신 앱을 찾을 수 없습니다",
|
||||
"transferAppNotBelong": "WeChat: 이체 발신 앱이 현재 가맹점에 속하지 않습니다",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: 이체 발신 앱은 공식계정 유형이어야 합니다"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: 이체 발신 앱은 공식계정 유형이어야 합니다",
|
||||
"transferRemarkRequired": "WeChat: 이체 메모(제목)는 필수입니다",
|
||||
"transferReportInfoRequired": "WeChat: 이체 시나리오 보고 정보는 필수입니다. 모든 보고 항목을 입력해 주세요"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: Aplikasi pemula pemindahan belum dikonfigurasi",
|
||||
"transferAppNotExist": "WeChat: Aplikasi pemula pemindahan tidak dijumpai",
|
||||
"transferAppNotBelong": "WeChat: Aplikasi pemula pemindahan tidak milik merchant semasa",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Aplikasi pemula pemindahan mesti jenis Official Account"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Aplikasi pemula pemindahan mesti jenis Official Account",
|
||||
"transferRemarkRequired": "WeChat: catatan pindah wang (tajuk) wajib diisi",
|
||||
"transferReportInfoRequired": "WeChat: maklumat laporan senario pindah wang wajib diisi, sila isi semua medan laporan"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: ยังไม่ได้กำหนดค่าแอปเริ่มการโอน",
|
||||
"transferAppNotExist": "WeChat: ไม่พบแอปเริ่มการโอน",
|
||||
"transferAppNotBelong": "WeChat: แอปเริ่มการโอนไม่ได้เป็นของร้านค้าปัจจุบัน",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: แอปเริ่มการโอนต้องเป็นประเภท Official Account"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: แอปเริ่มการโอนต้องเป็นประเภท Official Account",
|
||||
"transferRemarkRequired": "WeChat: หมายเหตุการโอน (หัวข้อ) จำเป็นต้องระบุ",
|
||||
"transferReportInfoRequired": "WeChat: ข้อมูลรายงานสถานการณ์การโอนเงินจำเป็นต้องระบุ กรุณากรอกข้อมูลรายงานทั้งหมด"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "WeChat: Ứng dụng khởi tạo chuyển khoản chưa được thiết lập",
|
||||
"transferAppNotExist": "WeChat: Không tìm thấy ứng dụng khởi tạo chuyển khoản",
|
||||
"transferAppNotBelong": "WeChat: Ứng dụng khởi tạo chuyển khoản không thuộc merchant hiện tại",
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Ứng dụng khởi tạo chuyển khoản phải là loại Official Account"
|
||||
"transferAppTypeNotOfficialAccount": "WeChat: Ứng dụng khởi tạo chuyển khoản phải là loại Official Account",
|
||||
"transferRemarkRequired": "WeChat: ghi chú chuyển khoản (tiêu đề) là bắt buộc",
|
||||
"transferReportInfoRequired": "WeChat: thông tin báo cáo kịch bản chuyển khoản là bắt buộc, vui lòng điền đầy đủ các trường báo cáo"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "微信: 转账发起应用未配置",
|
||||
"transferAppNotExist": "微信: 转账发起应用不存在",
|
||||
"transferAppNotBelong": "微信: 转账发起应用不属于当前商户",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 转账发起应用必须是公众号类型"
|
||||
"transferAppTypeNotOfficialAccount": "微信: 转账发起应用必须是公众号类型",
|
||||
"transferRemarkRequired": "微信: 转账备注(标题)必填",
|
||||
"transferReportInfoRequired": "微信: 转账场景报备信息必填, 请填写全部报备字段"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
|
||||
"transferAppNotExist": "微信: 轉帳發起應用不存在",
|
||||
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型",
|
||||
"transferRemarkRequired": "微信: 轉賬備註(標題)必填",
|
||||
"transferReportInfoRequired": "微信: 轉賬場景報備資訊必填, 請填寫全部報備欄位"
|
||||
}
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
|
||||
"transferAppNotExist": "微信: 轉帳發起應用不存在",
|
||||
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型",
|
||||
"transferRemarkRequired": "微信: 轉帳備註(標題)必填",
|
||||
"transferReportInfoRequired": "微信: 轉帳場景報備資訊必填, 請填寫全部報備欄位"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user