feat(transfer): 微信转账配置独立表(转账场景+发起应用), 转账策略按配置解析发起应用(10语)

This commit is contained in:
daxpay
2026-08-07 13:50:35 +08:00
parent d71d0adc4f
commit 0d10d18ea3
23 changed files with 520 additions and 34 deletions

View File

@@ -0,0 +1,35 @@
-- ============================================================
-- 支付宝转账场景预置数据(补跑段)
-- 表结构与索引(先删后增)已执行成功, 本段仅执行预置数据初始化:
-- 为现有直连通道商户预置8个转账场景行(enabled=false, is_default=false)
-- 场景为支付宝协议固定中文名称, 重复执行时靠场景唯一索引(部分索引)幂等跳过
-- ============================================================
-- ID 用时间戳毫秒*1000000 + 递增序号生成, 与应用运行时 Snowflake ID 范围相近且不冲突
DO $$
DECLARE
base_id bigint;
seq bigint := 0;
mch_record RECORD;
scene_name text;
BEGIN
base_id := (extract(epoch from now()) * 1000)::bigint * 1000000;
FOR mch_record IN
SELECT mch_no, channel_mch_no
FROM "public"."alipay_direct_channel_merchant"
WHERE deleted = false
LOOP
FOREACH scene_name IN ARRAY ARRAY['现金营销', '企业退款', '佣金报酬', '业务结算',
'二手回收', '公益补助', '行政补贴和退款', '保险理赔']
LOOP
INSERT INTO "public"."alipay_transfer_scene_config"
("id", "mch_no", "channel_mch_no", "scene_name", "enabled", "is_default",
"create_time", "last_modified_time", "version", "deleted")
VALUES
(base_id + seq, mch_record.mch_no, mch_record.channel_mch_no, scene_name, false, false,
now(), now(), 0, false)
ON CONFLICT DO NOTHING;
seq := seq + 1;
END LOOP;
END LOOP;
END $$;

View File

@@ -0,0 +1,52 @@
package cn.daxpay.open.channel.wechat.controller.direct;
import cn.daxpay.open.channel.wechat.param.direct.WechatTransferConfigParam;
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferConfigResult;
import cn.daxpay.open.channel.wechat.service.direct.WechatTransferConfigService;
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/wechat/transfer-config")
@RequiredArgsConstructor
public class WechatTransferConfigController {
private final WechatTransferConfigService wechatTransferConfigService;
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询通道商户的转账配置")
@GetMapping("/find-by-channel-mch-no")
public Result<WechatTransferConfigResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(wechatTransferConfigService.findByChannelMchNo(mchNo, channelMchNo));
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "保存或更新转账配置(一对一)")
@PostMapping("/save")
public Result<Void> save(@RequestBody @Validated WechatTransferConfigParam param) {
wechatTransferConfigService.saveOrUpdate(param);
return Res.ok();
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.channel.wechat.entity.direct;
import cn.daxpay.open.channel.wechat.convert.direct.WechatTransferConfigConvert;
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferConfigResult;
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.wechat.strategy.direct.transfer.WechatTransferStrategy]
/// 读取本配置注入 transfer_scene 并按 [transferAppRefId] 解析发起应用(公众号)的 wxAppId。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("wechat_transfer_config")
public class WechatTransferConfig extends MchBaseEntity implements ToResult<WechatTransferConfigResult> {
/// 通道商户号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 转账场景ID(微信 transfer_scene, 8 枚举之一, 允许为空待后续补配)
/// @see cn.daxpay.open.channel.wechat.enums.WechatTransferSceneEnum
private String transferScene;
/// 转账发起应用引用(指向 wx_mch_app 主键, 须为公众号类型, 决定 appid 与 openid 来源)
private Long transferAppRefId;
/// 转换
@Override
public WechatTransferConfigResult toResult() {
return WechatTransferConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -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 lombok.Data;
import lombok.experimental.Accessors;
/// # 微信转账配置保存参数
///
/// 一对一 upsert: 存在则更新, 不存在则新增。`transferScene` 与 `transferAppRefId` 均允许为空
/// (支持分步配置), 但发起转账时两者必须齐备。
///
@Data
@Accessors(chain = true)
@Schema(title = "微信转账配置保存参数")
public class WechatTransferConfigParam {
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "转账场景ID(微信 transfer_scene, 8 枚举之一, 允许为空待补配)")
private String transferScene;
@Schema(description = "转账发起应用引用(指向 wx_mch_app 主键, 须为公众号类型)")
private Long transferAppRefId;
}

View File

@@ -0,0 +1,46 @@
package cn.daxpay.open.channel.wechat.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;
/// # 微信转账配置
///
/// 转账配置返回结果对象。冗余展示字段(发起应用名/wxAppId/应用类型/场景名)由
/// [cn.daxpay.open.channel.wechat.service.direct.WechatTransferConfigService] 填充,
/// 不经 MapStruct 自动映射。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "微信转账配置")
public class WechatTransferConfigResult extends BaseResult {
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "转账场景ID")
private String transferScene;
@Schema(description = "转账发起应用引用")
private Long transferAppRefId;
// ===== 冗余展示(由 Service 填充) =====
@Schema(description = "场景名称(枚举推导, 便于展示)")
private String sceneName;
@Schema(description = "发起应用名称")
private String transferAppName;
@Schema(description = "发起应用 AppId(真实微信 AppId)")
private String wxAppId;
@Schema(description = "发起应用类型(须为 official_account 公众号)")
private String appType;
}

View File

@@ -5,9 +5,11 @@ import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManag
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectKeyConfig;
import cn.daxpay.open.channel.wechat.strategy.direct.pay.WechatDirectPayStrategy;
import cn.daxpay.open.payment.auth.core.AppScopeEnum;
import cn.daxpay.open.payment.wx.facade.WxAppFacade;
import cn.daxpay.open.payment.wx.facade.WxAppView;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
@@ -63,6 +65,25 @@ public class WechatDirectConfigAssembler {
return this.assemble(null, channelMchNo, channelMerchant);
}
/// 转账专用凭证组装(按发起应用引用解析, 不经 capability)
///
/// 转账场景的应用由「微信转账配置」显式指定(公众号), 不走支付能力绑定解析。
/// 直接按 [transferAppRefId] 加载商户档应用取 wxAppId, 装载密钥。
///
/// @param channelMchNo 通道商户号(密钥查询)
/// @param transferAppRefId 转账发起应用引用(wx_mch_app 主键)
/// @return 微信 SDK 凭证, wxAppId 来自转账配置指定的公众号应用
public WechatSdkCredential buildTransferConfig(String channelMchNo, Long transferAppRefId) {
WxAppView app = wxAppFacade.getById(AppScopeEnum.MERCHANT, transferAppRefId);
if (app == null) {
// 微信: 转账发起应用未配置或已删除
throw new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.channel.wechat.transferAppNotConfigured");
}
WechatDirectChannelMerchant channelMerchant = this.loadChannelMerchant(channelMchNo);
return this.assemble(app.wxAppId(), channelMchNo, channelMerchant);
}
/// 加载通道商户绑定(channelMchNo 是系统生成号, 不等于 wxMchId)
private WechatDirectChannelMerchant loadChannelMerchant(String channelMchNo) {
return wechatDirectChannelMerchantManager.findByChannelMchNo(channelMchNo)

View File

@@ -0,0 +1,133 @@
package cn.daxpay.open.channel.wechat.service.direct;
import cn.daxpay.open.channel.wechat.convert.direct.WechatTransferConfigConvert;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManager;
import cn.daxpay.open.channel.wechat.dao.direct.WechatTransferConfigManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
import cn.daxpay.open.channel.wechat.entity.direct.WechatTransferConfig;
import cn.daxpay.open.channel.wechat.enums.WechatTransferSceneEnum;
import cn.daxpay.open.channel.wechat.param.direct.WechatTransferConfigParam;
import cn.daxpay.open.channel.wechat.result.direct.WechatTransferConfigResult;
import cn.daxpay.open.payment.wx.dao.merchant.WxMchAppManager;
import cn.daxpay.open.payment.wx.entity.merchant.WxMchApp;
import cn.daxpay.open.payment.wx.enums.WxAppTypeEnum;
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.Objects;
import java.util.Optional;
/// # 微信转账配置
///
/// 管理通道商户的转账配置(一对一: 转账场景 + 转账发起应用)。
/// 发起转账时由转账策略读取本配置注入场景并按 [WechatTransferConfig#getTransferAppRefId]
/// 解析发起应用(公众号)的 wxAppId, 替代通道商户表上的单值 transferScene。
///
/// 运营端写 [WechatTransferConfig](MchBaseEntity) 显式 setMchNo, 避免上下文缺失。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatTransferConfigService {
private final WechatTransferConfigManager wechatTransferConfigManager;
private final WechatDirectChannelMerchantManager wechatDirectChannelMerchantManager;
private final WxMchAppManager wxMchAppManager;
/// 查询通道商户的转账配置(一对一, 未配置返回 null)
///
/// @param mchNo 商户号(归属校验)
/// @param channelMchNo 通道商户号
/// @return 转账配置(含冗余展示), 不存在返回 null
public WechatTransferConfigResult findByChannelMchNo(String mchNo, String channelMchNo) {
assertChannelMerchant(mchNo, channelMchNo);
return wechatTransferConfigManager.findByChannelMchNo(channelMchNo)
.map(this::toResultWithMeta)
.orElse(null);
}
/// 保存或更新转账配置(一对一 upsert)
///
/// transferScene / transferAppRefId 均允许为空(支持分步配置或清空),
/// 但发起转账时两者必须齐备, 由转账策略校验。
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(WechatTransferConfigParam param) {
// 校验通道商户存在与归属
assertChannelMerchant(param.getMchNo(), param.getChannelMchNo());
// 校验发起应用(若指定): 存在 + 归属 + 公众号类型
if (param.getTransferAppRefId() != null) {
WxMchApp app = wxMchAppManager.lambdaQuery()
.eq(WxMchApp::getId, param.getTransferAppRefId())
.oneOpt()
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.transferAppNotExist"));
if (!Objects.equals(app.getMchNo(), param.getMchNo())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.wechat.transferAppNotBelong");
}
if (!Objects.equals(app.getAppType(), WxAppTypeEnum.OFFICIAL_ACCOUNT.getCode())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.wechat.transferAppTypeNotOfficialAccount");
}
}
// upsert: 存在则全量覆盖(含清空), 不存在则新增
Optional<WechatTransferConfig> existing = wechatTransferConfigManager
.findByChannelMchNo(param.getChannelMchNo());
if (existing.isPresent()) {
WechatTransferConfig entity = existing.get();
entity.setTransferScene(param.getTransferScene());
entity.setTransferAppRefId(param.getTransferAppRefId());
wechatTransferConfigManager.updateById(entity);
} else {
WechatTransferConfig entity = WechatTransferConfigConvert.CONVERT.toEntity(param);
// 运营端写 MchBaseEntity 必须显式 setMchNo(父类 setter 返回类型不匹配, 单独赋值)
entity.setMchNo(param.getMchNo());
wechatTransferConfigManager.save(entity);
}
}
/// 删除通道商户的转账配置(通道商户删除时级联清理)
public void deleteByChannelMchNo(String channelMchNo) {
wechatTransferConfigManager.deleteByChannelMchNo(channelMchNo);
}
/// 校验通道商户存在且归属匹配
private void assertChannelMerchant(String mchNo, String channelMchNo) {
WechatDirectChannelMerchant channelMerchant = wechatDirectChannelMerchantManager
.findByChannelMchNo(channelMchNo)
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
if (!Objects.equals(channelMerchant.getMchNo(), mchNo)) {
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.payment.wx.channelMerchantMismatch");
}
}
/// 转Result并填充冗余展示(场景名 + 发起应用信息)
private WechatTransferConfigResult toResultWithMeta(WechatTransferConfig entity) {
WechatTransferConfigResult result = entity.toResult();
// 场景名(枚举推导)
if (StrUtil.isNotBlank(entity.getTransferScene())) {
WechatTransferSceneEnum scene = WechatTransferSceneEnum.findByCode(entity.getTransferScene());
if (scene != null) {
result.setSceneName(scene.getName());
}
}
// 发起应用展示信息
if (entity.getTransferAppRefId() != null) {
wxMchAppManager.lambdaQuery()
.eq(WxMchApp::getId, entity.getTransferAppRefId())
.oneOpt()
.ifPresent(app -> {
result.setTransferAppName(app.getAppName());
result.setWxAppId(app.getWxAppId());
result.setAppType(app.getAppType());
});
}
return result;
}
}

View File

@@ -2,6 +2,7 @@ package cn.daxpay.open.channel.wechat.strategy.direct.merchant;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManager;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectKeyConfigManager;
import cn.daxpay.open.channel.wechat.dao.direct.WechatTransferConfigManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectKeyConfig;
import cn.daxpay.open.payment.wx.service.channel.WxChannelAppCapabilityService;
@@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
///
/// 在通道商户删除时清理:
/// - 直连扩展表 + 密钥配置
/// - 微信转账配置(场景+发起应用)
/// - 主数据通道能力绑([WxChannelAppCapabilityService#deleteByChannelMchNo])
///
/// 与 [cn.daxpay.open.channel.wechat.strategy.isv.merchant.WechatIsvChannelMerchantCleanupStrategy]
@@ -28,6 +30,7 @@ public class WechatDirectChannelMerchantCleanupStrategy implements ChannelMercha
private final WechatDirectChannelMerchantManager wechatDirectChannelMerchantManager;
private final WechatDirectKeyConfigManager wechatDirectAppKeyConfigManager;
private final WechatTransferConfigManager wechatTransferConfigManager;
private final WxChannelAppCapabilityService wxChannelAppCapabilityService;
/// 对应产品: 微信支付直连
@@ -42,6 +45,8 @@ public class WechatDirectChannelMerchantCleanupStrategy implements ChannelMercha
public void deleteByChannelMchNo(String channelMchNo) {
wechatDirectChannelMerchantManager.deleteByField(WechatDirectChannelMerchant::getChannelMchNo, channelMchNo);
wechatDirectAppKeyConfigManager.deleteByField(WechatDirectKeyConfig::getChannelMchNo, channelMchNo);
// 微信转账配置(场景+发起应用)
wechatTransferConfigManager.deleteByChannelMchNo(channelMchNo);
// 主数据: 通道商户 × 能力绑
wxChannelAppCapabilityService.deleteByChannelMchNo(channelMchNo);
}

View File

@@ -1,8 +1,8 @@
package cn.daxpay.open.channel.wechat.strategy.direct.transfer;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
import cn.daxpay.open.channel.wechat.dao.direct.WechatTransferConfigManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatTransferConfig;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.transfer.WechatTransferService;
import cn.daxpay.open.payment.strategy.transfer.AbsTransferStrategy;
@@ -24,7 +24,7 @@ import org.springframework.stereotype.Service;
/// 通道差异:
/// - 仅支持 openid 收款人([TransferPayeeTypeEnum#OPENID])
/// - 金额档位姓名校验: 小于 0.3 元禁填姓名, 大于等于 2000 元必填姓名
/// - transfer_scene 取自通道商户配置, 未配置时报错
/// - transfer_scene 取自「微信转账配置」([WechatTransferConfig]), 发起应用由配置指定(公众号)
@Slf4j
@Service
@RequiredArgsConstructor
@@ -37,7 +37,7 @@ public class WechatTransferStrategy extends AbsTransferStrategy {
private final WechatTransferService wechatTransferService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
private final WechatDirectChannelMerchantManager wechatDirectChannelMerchantManager;
private final WechatTransferConfigManager wechatTransferConfigManager;
@Override
public String getChannel() {
@@ -83,20 +83,25 @@ public class WechatTransferStrategy extends AbsTransferStrategy {
/// 组装通道调用凭证并注入转账场景
///
/// 转账场景(transfer_scene)从通道商户配置读取, 经上下文回写, 由编排层在"处理中"镜像落库。
/// 转账场景(transfer_scene)与发起应用均取自「微信转账配置」([WechatTransferConfig]),
/// 经上下文回写场景, 由编排层在"处理中"镜像落库; 发起应用由凭证组装器按引用解析 wxAppId。
private WechatSdkCredential buildCredential(TransferStrategyContext context) {
WechatDirectChannelMerchant channelMerchant = wechatDirectChannelMerchantManager.lambdaQuery()
.eq(WechatDirectChannelMerchant::getChannelMchNo, context.getChannelMchNo())
.oneOpt()
WechatTransferConfig transferConfig = wechatTransferConfigManager
.findByChannelMchNo(context.getChannelMchNo())
.orElseThrow(() -> new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.payment.channel.channelMerchantNotExist"));
if (StrUtil.isBlank(channelMerchant.getTransferScene())) {
"error.channel.wechat.transferConfigNotConfigured"));
if (StrUtil.isBlank(transferConfig.getTransferScene())) {
// 微信: 转账场景未配置
throw new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.channel.wechat.transferSceneNotConfigured");
}
context.setTransferScene(channelMerchant.getTransferScene());
return wechatDirectConfigAssembler.buildConfig(
context.getMchNo(), context.getChannelMchNo(), null, null);
if (transferConfig.getTransferAppRefId() == null) {
// 微信: 转账发起应用未配置
throw new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.channel.wechat.transferAppNotConfigured");
}
context.setTransferScene(transferConfig.getTransferScene());
return wechatDirectConfigAssembler.buildTransferConfig(
context.getChannelMchNo(), transferConfig.getTransferAppRefId());
}
}

View File

@@ -73,9 +73,12 @@ public class WechatTransferOrder extends MchBaseEntity {
/// 收款人微信 openid
private String payeeOpenid;
/// 转账场景(冗余自通道商户配置)
/// 转账场景(冗余自转账配置)
private String transferScene;
/// 转账发起应用AppId(从转账配置解析, openid归属校验/对账回查用)
private String wxAppId;
/// 拉起转账确认参数(微信二次确认)
private String transferBody;

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChat only supports openid payee",
"transferNameForbidden": "WeChat: payee name not allowed for amount below 0.3 CNY",
"transferNameRequired": "WeChat: payee name required for amount >= 2000 CNY",
"transferSceneNotConfigured": "WeChat: transfer scene not configured"
"transferSceneNotConfigured": "WeChat: transfer scene not configured",
"transferConfigNotConfigured": "WeChat: Transfer config not configured",
"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"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChat hanya mendukung penerima openid",
"transferNameForbidden": "WeChat: nama penerima tidak diizinkan untuk jumlah di bawah 0.3 CNY",
"transferNameRequired": "WeChat: nama penerima wajib untuk jumlah >= 2000 CNY",
"transferSceneNotConfigured": "WeChat: skenario transfer belum dikonfigurasi"
"transferSceneNotConfigured": "WeChat: skenario transfer belum dikonfigurasi",
"transferConfigNotConfigured": "WeChat: Konfigurasi transfer belum dikonfigurasi",
"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"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChatはopenid受取人のみサポートしています",
"transferNameForbidden": "WeChat: 0.3元未満は受取人氏名を入力できません",
"transferNameRequired": "WeChat: 2000元以上は受取人氏名が必須です",
"transferSceneNotConfigured": "WeChat: 送金シーンが設定されていません"
"transferSceneNotConfigured": "WeChat: 送金シーンが設定されていません",
"transferConfigNotConfigured": "WeChat: 振込設定が未設定です",
"transferAppNotConfigured": "WeChat: 振込发起アプリが未設定です",
"transferAppNotExist": "WeChat: 振込发起アプリが見つかりません",
"transferAppNotBelong": "WeChat: 振込发起アプリは現在のマーチャントに属していません",
"transferAppTypeNotOfficialAccount": "WeChat: 振込发起アプリは公式アカウントタイプである必要があります"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "위챗은 openid 수취인만 지원합니다",
"transferNameForbidden": "위챗: 0.3원 미만은 수취인 이름을 입력할 수 없습니다",
"transferNameRequired": "위챗: 2000원 이상은 수취인 이름이 필수입니다",
"transferSceneNotConfigured": "WeChat: 송금 장면이 구성되지 않았습니다"
"transferSceneNotConfigured": "WeChat: 송금 장면이 구성되지 않았습니다",
"transferConfigNotConfigured": "WeChat: 이체 설정이 구성되지 않았습니다",
"transferAppNotConfigured": "WeChat: 이체 발신 앱이 구성되지 않았습니다",
"transferAppNotExist": "WeChat: 이체 발신 앱을 찾을 수 없습니다",
"transferAppNotBelong": "WeChat: 이체 발신 앱이 현재 가맹점에 속하지 않습니다",
"transferAppTypeNotOfficialAccount": "WeChat: 이체 발신 앱은 공식계정 유형이어야 합니다"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChat hanya menyokong penerima openid",
"transferNameForbidden": "WeChat: nama penerima tidak dibenarkan untuk jumlah di bawah 0.3 CNY",
"transferNameRequired": "WeChat: nama penerima wajib untuk jumlah >= 2000 CNY",
"transferSceneNotConfigured": "WeChat: senario pindahan belum dikonfigurasi"
"transferSceneNotConfigured": "WeChat: senario pindahan belum dikonfigurasi",
"transferConfigNotConfigured": "WeChat: Konfigurasi pemindahan belum dikonfigurasi",
"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"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChat รองรับผู้รับ openid เท่านั้น",
"transferNameForbidden": "WeChat: ไม่อนุญาตให้ใส่ชื่อผู้รับเมื่อต่ำกว่า 0.3 หยวน",
"transferNameRequired": "WeChat: ต้องใส่ชื่อผู้รับเมื่อมากกว่าหรือเท่ากับ 2000 หยวน",
"transferSceneNotConfigured": "WeChat: ยังไม่ได้กำหนดสถานการณ์การโอนเงิน"
"transferSceneNotConfigured": "WeChat: ยังไม่ได้กำหนดสถานการณ์การโอนเงิน",
"transferConfigNotConfigured": "WeChat: ยังไม่ได้กำหนดค่าการโอนเงิน",
"transferAppNotConfigured": "WeChat: ยังไม่ได้กำหนดค่าแอปเริ่มการโอน",
"transferAppNotExist": "WeChat: ไม่พบแอปเริ่มการโอน",
"transferAppNotBelong": "WeChat: แอปเริ่มการโอนไม่ได้เป็นของร้านค้าปัจจุบัน",
"transferAppTypeNotOfficialAccount": "WeChat: แอปเริ่มการโอนต้องเป็นประเภท Official Account"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "WeChat chỉ hỗ trợ người nhận openid",
"transferNameForbidden": "WeChat: không được nhập tên người nhận khi dưới 0.3 CNY",
"transferNameRequired": "WeChat: bắt buộc nhập tên người nhận khi >= 2000 CNY",
"transferSceneNotConfigured": "WeChat: chưa cấu hình kịch bản chuyển khoản"
"transferSceneNotConfigured": "WeChat: chưa cấu hình kịch bản chuyển khoản",
"transferConfigNotConfigured": "WeChat: Cấu hình chuyển khoản chưa được thiết lập",
"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"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "微信仅支持 openid 收款人",
"transferNameForbidden": "微信: 小于0.3元不允许填收款人姓名",
"transferNameRequired": "微信: 大于等于2000元必须填收款人姓名",
"transferSceneNotConfigured": "微信: 转账场景未配置"
"transferSceneNotConfigured": "微信: 转账场景未配置",
"transferConfigNotConfigured": "微信: 转账配置未配置",
"transferAppNotConfigured": "微信: 转账发起应用未配置",
"transferAppNotExist": "微信: 转账发起应用不存在",
"transferAppNotBelong": "微信: 转账发起应用不属于当前商户",
"transferAppTypeNotOfficialAccount": "微信: 转账发起应用必须是公众号类型"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "微信僅支持 openid 收款人",
"transferNameForbidden": "微信: 少於0.3元不允許填收款人姓名",
"transferNameRequired": "微信: 大於等於2000元必須填收款人姓名",
"transferSceneNotConfigured": "微信: 轉賬場景未設定"
"transferSceneNotConfigured": "微信: 轉賬場景未設定",
"transferConfigNotConfigured": "微信: 轉帳設定未設定",
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
"transferAppNotExist": "微信: 轉帳發起應用不存在",
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
}

View File

@@ -61,6 +61,10 @@
"transferOnlyOpenid": "微信僅支持 openid 收款人",
"transferNameForbidden": "微信: 小於0.3元不允許填收款人姓名",
"transferNameRequired": "微信: 大於等於2000元必須填收款人姓名",
"transferSceneNotConfigured": "微信: 轉帳場景未設定"
"transferSceneNotConfigured": "微信: 轉帳場景未設定",
"transferConfigNotConfigured": "微信: 轉帳設定未設定",
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
"transferAppNotExist": "微信: 轉帳發起應用不存在",
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
}