mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-11 23:25:33 +08:00
feat(transfer): 微信转账配置独立表(转账场景+发起应用), 转账策略按配置解析发起应用(10语)
This commit is contained in:
@@ -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 $$;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,12 @@ public class WechatTransferOrder extends MchBaseEntity {
|
||||
/// 收款人微信 openid
|
||||
private String payeeOpenid;
|
||||
|
||||
/// 转账场景(冗余自通道商户配置)
|
||||
/// 转账场景(冗余自转账配置)
|
||||
private String transferScene;
|
||||
|
||||
/// 转账发起应用AppId(从转账配置解析, openid归属校验/对账回查用)
|
||||
private String wxAppId;
|
||||
|
||||
/// 拉起转账确认参数(微信二次确认)
|
||||
private String transferBody;
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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: 振込发起アプリは公式アカウントタイプである必要があります"
|
||||
}
|
||||
|
||||
@@ -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: 이체 발신 앱은 공식계정 유형이어야 합니다"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
"transferOnlyOpenid": "微信仅支持 openid 收款人",
|
||||
"transferNameForbidden": "微信: 小于0.3元不允许填收款人姓名",
|
||||
"transferNameRequired": "微信: 大于等于2000元必须填收款人姓名",
|
||||
|
||||
"transferSceneNotConfigured": "微信: 转账场景未配置"
|
||||
"transferSceneNotConfigured": "微信: 转账场景未配置",
|
||||
"transferConfigNotConfigured": "微信: 转账配置未配置",
|
||||
"transferAppNotConfigured": "微信: 转账发起应用未配置",
|
||||
"transferAppNotExist": "微信: 转账发起应用不存在",
|
||||
"transferAppNotBelong": "微信: 转账发起应用不属于当前商户",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 转账发起应用必须是公众号类型"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
"transferOnlyOpenid": "微信僅支持 openid 收款人",
|
||||
"transferNameForbidden": "微信: 少於0.3元不允許填收款人姓名",
|
||||
"transferNameRequired": "微信: 大於等於2000元必須填收款人姓名",
|
||||
|
||||
"transferSceneNotConfigured": "微信: 轉賬場景未設定"
|
||||
"transferSceneNotConfigured": "微信: 轉賬場景未設定",
|
||||
"transferConfigNotConfigured": "微信: 轉帳設定未設定",
|
||||
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
|
||||
"transferAppNotExist": "微信: 轉帳發起應用不存在",
|
||||
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
"transferOnlyOpenid": "微信僅支持 openid 收款人",
|
||||
"transferNameForbidden": "微信: 小於0.3元不允許填收款人姓名",
|
||||
"transferNameRequired": "微信: 大於等於2000元必須填收款人姓名",
|
||||
|
||||
"transferSceneNotConfigured": "微信: 轉帳場景未設定"
|
||||
"transferSceneNotConfigured": "微信: 轉帳場景未設定",
|
||||
"transferConfigNotConfigured": "微信: 轉帳設定未設定",
|
||||
"transferAppNotConfigured": "微信: 轉帳發起應用未設定",
|
||||
"transferAppNotExist": "微信: 轉帳發起應用不存在",
|
||||
"transferAppNotBelong": "微信: 轉帳發起應用不屬於目前商戶",
|
||||
"transferAppTypeNotOfficialAccount": "微信: 轉帳發起應用必須是公眾號類型"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user