feat(transfer): 抖音转账场景主数据枚举(1001-1007) + 转账发起应用绑定(仅网站应用), 策略按转账配置解析发起应用(10语)

This commit is contained in:
daxpay
2026-08-07 15:23:17 +08:00
parent 0d10d18ea3
commit 80810851c3
34 changed files with 849 additions and 77 deletions

View File

@@ -810,6 +810,46 @@ COMMENT ON COLUMN "public"."dy_platform_app_capability"."dy_platform_app_id" IS
COMMENT ON COLUMN "public"."dy_platform_app_capability"."product" IS '产品编码';
COMMENT ON TABLE "public"."dy_platform_app_capability" IS '平台抖音应用默认能力绑定(全局一能力一应用)';
-- ----------------------------
-- Table structure for douyin_transfer_config
-- ----------------------------
DROP TABLE IF EXISTS "public"."douyin_transfer_config";
CREATE TABLE "public"."douyin_transfer_config" (
"id" int8 NOT NULL,
"creator" int8,
"create_time" timestamptz(6),
"last_modifier" int8,
"last_modified_time" timestamptz(6),
"version" int4 NOT NULL DEFAULT 0,
"deleted" bool NOT NULL DEFAULT false,
"mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"channel_mch_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"transfer_app_ref_id" int8
)
;
COMMENT ON COLUMN "public"."douyin_transfer_config"."id" IS '主键';
COMMENT ON COLUMN "public"."douyin_transfer_config"."creator" IS '创建者ID';
COMMENT ON COLUMN "public"."douyin_transfer_config"."create_time" IS '创建时间';
COMMENT ON COLUMN "public"."douyin_transfer_config"."last_modifier" IS '最后修改ID';
COMMENT ON COLUMN "public"."douyin_transfer_config"."last_modified_time" IS '最后修改时间';
COMMENT ON COLUMN "public"."douyin_transfer_config"."version" IS '版本号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."deleted" IS '删除标志';
COMMENT ON COLUMN "public"."douyin_transfer_config"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."channel_mch_no" IS '通道商户号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."transfer_app_ref_id" IS '转账发起应用引用(dy_mch_app 主键, 须为网站应用 web_app, 支持手机H5获取OpenId)';
COMMENT ON TABLE "public"."douyin_transfer_config" IS '抖音转账配置(一对一, 指定转账发起应用, 决定转出主体与收款人openId来源)';
-- 主键约束
ALTER TABLE "public"."douyin_transfer_config" ADD CONSTRAINT "pk_douyin_transfer_config" PRIMARY KEY ("id");
-- 通道商户查询索引
CREATE INDEX "idx_douyin_transfer_config_mch" ON "public"."douyin_transfer_config" USING btree ("channel_mch_no", "deleted");
COMMENT ON INDEX "public"."idx_douyin_transfer_config_mch" IS '按通道商户号查询转账配置';
-- 通道商户唯一索引(一个通道商户一条转账配置)
CREATE UNIQUE INDEX "uk_douyin_transfer_config_mch" ON "public"."douyin_transfer_config" ("channel_mch_no") WHERE deleted = false;
COMMENT ON INDEX "public"."uk_douyin_transfer_config_mch" IS '同一通道商户仅一条转账配置(部分唯一索引)';
-- ----------------------------
-- Table structure for fuyou_isv_channel_merchant
-- ----------------------------

View File

@@ -1,35 +1,43 @@
-- ============================================================
-- 支付宝转账场景预置数据(补跑段)
-- 表结构与索引(先删后增)已执行成功, 本段仅执行预置数据初始化:
-- 为现有直连通道商户预置8个转账场景行(enabled=false, is_default=false)
-- 场景为支付宝协议固定中文名称, 重复执行时靠场景唯一索引(部分索引)幂等跳过
-- 抖音转账配置表(转账发起应用绑定, 发起转账时决定转出主体与收款人openId来源)
-- 对齐微信 wechat_transfer_config 范式; 转账场景为主数据枚举无需落库, 本表仅存发起应用
-- ============================================================
DROP TABLE IF EXISTS "public"."douyin_transfer_config";
-- 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 $$;
CREATE TABLE "public"."douyin_transfer_config" (
"id" int8 NOT NULL,
"creator" int8,
"create_time" timestamptz(6),
"last_modifier" int8,
"last_modified_time" timestamptz(6),
"version" int4 NOT NULL DEFAULT 0,
"deleted" bool NOT NULL DEFAULT false,
"mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"channel_mch_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"transfer_app_ref_id" int8
);
COMMENT ON COLUMN "public"."douyin_transfer_config"."id" IS '主键';
COMMENT ON COLUMN "public"."douyin_transfer_config"."creator" IS '创建者ID';
COMMENT ON COLUMN "public"."douyin_transfer_config"."create_time" IS '创建时间';
COMMENT ON COLUMN "public"."douyin_transfer_config"."last_modifier" IS '最后修改ID';
COMMENT ON COLUMN "public"."douyin_transfer_config"."last_modified_time" IS '最后修改时间';
COMMENT ON COLUMN "public"."douyin_transfer_config"."version" IS '版本号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."deleted" IS '删除标志';
COMMENT ON COLUMN "public"."douyin_transfer_config"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."channel_mch_no" IS '通道商户号';
COMMENT ON COLUMN "public"."douyin_transfer_config"."transfer_app_ref_id" IS '转账发起应用引用(dy_mch_app 主键, 须为网站应用 web_app, 支持手机H5获取OpenId)';
COMMENT ON TABLE "public"."douyin_transfer_config" IS '抖音转账配置(一对一, 指定转账发起应用, 决定转出主体与收款人openId来源)';
-- 主键约束 先删后增
ALTER TABLE "public"."douyin_transfer_config" DROP CONSTRAINT IF EXISTS "pk_douyin_transfer_config";
ALTER TABLE "public"."douyin_transfer_config" ADD CONSTRAINT "pk_douyin_transfer_config" PRIMARY KEY ("id");
-- 通道商户查询索引 先删后增
DROP INDEX IF EXISTS "public"."idx_douyin_transfer_config_mch";
CREATE INDEX "idx_douyin_transfer_config_mch" ON "public"."douyin_transfer_config" USING btree ("channel_mch_no", "deleted");
COMMENT ON INDEX "public"."idx_douyin_transfer_config_mch" IS '按通道商户号查询转账配置';
-- 通道商户唯一索引(一个通道商户一条转账配置) 先删后增
DROP INDEX IF EXISTS "public"."uk_douyin_transfer_config_mch";
CREATE UNIQUE INDEX "uk_douyin_transfer_config_mch" ON "public"."douyin_transfer_config" ("channel_mch_no") WHERE deleted = false;
COMMENT ON INDEX "public"."uk_douyin_transfer_config_mch" IS '同一通道商户仅一条转账配置(部分唯一索引)';

View File

@@ -1,8 +1,11 @@
package cn.daxpay.open.channel.douyin.client.req;
import cn.daxpay.open.channel.douyin.client.credential.DouyinSdkCredential;
import cn.daxpay.open.payment.trade.transfer.param.TransferReportInfo;
import lombok.Data;
import java.util.List;
/// # 抖音通道转账请求(发起/同步共用)
///
/// 与子应用 dax-pay-channel-one 的 `DouyinTransferReq` 镜像, 字段对齐。
@@ -21,7 +24,7 @@ public class DouyinTransferReq {
/// 收款人 openid
private String openid;
/// 转账场景ID(transfer_scene_id, 来自通道商户配置)
/// 转账场景ID(transfer_scene_id, 主数据枚举1001-1007)
private String scene;
/// 收款人姓名(金额>=2000元必填, 子应用加密上送)
@@ -30,9 +33,12 @@ public class DouyinTransferReq {
/// 转账备注(对应 transfer_remark)
private String remark;
/// 收款感知文案(对应 user_recv_perception)
/// 收款感知文案(对应 user_recv_perception, 按场景枚举选项)
private String perception;
/// 转账场景报备信息(对应 transfer_scene_report_infos, 按场景要求填写)
private List<TransferReportInfo> reportInfos;
/// 异步通知地址(抖音→平台)
private String notifyUrl;

View File

@@ -8,6 +8,7 @@ import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectChannelMerchantCre
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectKeyConfigParam;
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectChannelMerchantResult;
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectKeyConfigResult;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferSceneOptionResult;
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectChannelMerchantService;
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectKeyConfigService;
import io.swagger.v3.oas.annotations.Operation;
@@ -21,6 +22,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)
@@ -69,4 +72,11 @@ public class DouyinDirectChannelMerchantController {
douyinDirectKeyConfigService.save(param);
return Res.ok();
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询抖音转账场景选项列表(主数据枚举)")
@GetMapping("/scene-options")
public Result<List<DouyinTransferSceneOptionResult>> sceneOptions() {
return Res.ok(douyinDirectChannelMerchantService.findSceneOptions());
}
}

View File

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

View File

@@ -3,6 +3,7 @@ package cn.daxpay.open.channel.douyin.controller.merchant;
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectKeyConfigParam;
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectChannelMerchantResult;
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectKeyConfigResult;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferSceneOptionResult;
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectChannelMerchantService;
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectKeyConfigService;
import cn.daxpay.open.payment.common.context.PaymentContext;
@@ -27,6 +28,7 @@ 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;
import java.util.Objects;
/// # 抖音直连通道商户配置(商户端)
@@ -100,4 +102,10 @@ public class MchDouyinDirectChannelMerchantController {
douyinDirectKeyConfigService.save(param);
return Res.ok();
}
@Operation(summary = "查询抖音转账场景选项列表(主数据枚举)")
@GetMapping("/scene-options")
public Result<List<DouyinTransferSceneOptionResult>> sceneOptions() {
return Res.ok(douyinDirectChannelMerchantService.findSceneOptions());
}
}

View File

@@ -0,0 +1,88 @@
package cn.daxpay.open.channel.douyin.controller.merchant;
import cn.daxpay.open.channel.douyin.param.direct.DouyinTransferConfigParam;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferConfigResult;
import cn.daxpay.open.channel.douyin.service.direct.DouyinTransferConfigService;
import cn.daxpay.open.payment.common.context.PaymentContext;
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.code.PermCodes;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.config.ConfigErrorException;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.platform.core.util.ValidationUtil;
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.RestController;
import java.util.Objects;
/// # 抖音转账配置管理(商户端)
///
/// 对照运营端 [cn.daxpay.open.channel.douyin.controller.direct.DouyinTransferConfigController]
/// 路径前缀 `/mch/douyin/transfer-config`。
/// 商户号一律取自 [PaymentContext],防越权。
@PermCode(menuCode = PermCodes.Channel.Merchant.MENU)
@Validated
@Tag(name = "抖音转账配置管理(商户端)")
@RestController
@RequestMapping("/mch/douyin/transfer-config")
@RequiredArgsConstructor
public class MchDouyinTransferConfigController {
private final DouyinTransferConfigService douyinTransferConfigService;
private final ChannelMerchantManager channelMerchantManager;
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;
}
/// 校验通道商户属于当前商户(防越权)
private void assertChannelMchOwned(String channelMchNo) {
ChannelMerchant channelMerchant = channelMerchantManager.findByChannelMchNo(channelMchNo)
// 抖音: 通道商户不存在或商户号不匹配
.orElseThrow(() -> new ConfigErrorException("error.payment.douyin.channelMerchantMismatch"));
if (!Objects.equals(channelMerchant.getMchNo(), this.requireMchNo())) {
// 抖音: 通道商户与商户号不匹配
throw new ConfigErrorException("error.payment.douyin.channelMerchantMismatch");
}
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "查询通道商户的转账配置")
@GetMapping("/find-by-channel-mch-no")
public Result<DouyinTransferConfigResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
this.assertChannelMchOwned(channelMchNo);
return Res.ok(douyinTransferConfigService.findByChannelMchNo(this.requireMchNo(), channelMchNo));
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "保存或更新转账配置(一对一)")
@PostMapping("/save")
public Result<Void> save(@RequestBody DouyinTransferConfigParam param) {
this.assertChannelMchOwned(param.getChannelMchNo());
// 强制当前商户号,忽略客户端传入(防越权)
param.setMchNo(this.requireMchNo());
ValidationUtil.validateParam(param);
douyinTransferConfigService.saveOrUpdate(param);
return Res.ok();
}
}

View File

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

View File

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

View File

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

View File

@@ -29,9 +29,6 @@ public class DouyinDirectChannelMerchant extends MchBaseEntity implements ToResu
/// 抖音商户号(MCHID)
private String dyMchId;
/// 转账场景ID(商家转账 transfer_scene_id, 未配置时发起转账报错)
private String transferScene;
/// 转换
@Override
public DouyinDirectChannelMerchantResult toResult() {

View File

@@ -0,0 +1,39 @@
package cn.daxpay.open.channel.douyin.entity.direct;
import cn.daxpay.open.channel.douyin.convert.direct.DouyinTransferConfigConvert;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferConfigResult;
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.douyin.strategy.transfer.DouyinTransferStrategy]
/// 读取本配置按 [transferAppRefId] 解析发起应用(网站应用)的 douyinAppId, 决定转出主体与
/// 收款人 openId 的来源(H5 授权由网站应用承接)。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("douyin_transfer_config")
public class DouyinTransferConfig extends MchBaseEntity implements ToResult<DouyinTransferConfigResult> {
/// 通道商户号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 转账发起应用引用(指向 dy_mch_app 主键, 须为网站应用 web_app, 决定转出 appid 与 openid 来源)
private Long transferAppRefId;
/// 转换
@Override
public DouyinTransferConfigResult toResult() {
return DouyinTransferConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,87 @@
package cn.daxpay.open.channel.douyin.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/// # 抖音转账场景枚举
///
/// 对应抖音商家转账(/v1/fund_trade/mch-transfer/transfer-bills)的转账场景,
/// 场景ID 为固定枚举值(1001-1007),商户在抖音商户平台「产品中心-商家转账到抖音零钱」开通。
/// 每个场景要求不同的报备字段([reportInfoTypes]), 字段值为抖音协议固定的中文 [infoType], 不可更改。
///
/// 报备字段内容(infoContent)由商户在发起转账时填写。
/// [reportInfoDescriptions] 与 [reportInfoTypes] 一一平行, 描述每个字段的含义和抖音文档示例。
/// [userRecvPerceptionOptions] 为收款人在抖音中看到的感知文案可选值, 不传时抖音按场景取默认(第一个)。
@Getter
@AllArgsConstructor
public enum DouyinTransferSceneEnum {
/// 现金营销
CASH_MARKETING("1001", "现金营销",
List.of("活动名称", "奖励说明"),
List.of("商户自定义内容,如「新会员有礼」", "商户自定义内容,如「注册会员抽奖一等奖」"),
List.of("活动奖励", "现金奖励")),
/// 企业赔付
ENTERPRISE_COMPENSATION("1002", "企业赔付",
List.of("赔付原因"),
List.of("商户自定义内容,如「商品质量问题退款」"),
List.of("退款", "商家赔付")),
/// 佣金报酬
COMMISSION_REWARD("1003", "佣金报酬",
List.of("岗位类型", "报酬说明"),
List.of("商户自定义内容,如「外卖员」", "商户自定义内容如「7月份配送费」"),
List.of("劳务报酬", "报销款", "企业补贴", "开工利是")),
/// 采购货款
PURCHASE_PAYMENT("1004", "采购货款",
List.of("采购商品名称"),
List.of("商户自定义内容,如「戴尔笔记本电脑」"),
List.of("货款")),
/// 二手回收
SECOND_HAND_RECYCLING("1005", "二手回收",
List.of("回收商品名称"),
List.of("商户自定义内容,如「塑料瓶」"),
List.of("二手回收货款")),
/// 公益补助
PUBLIC_WELFARE_SUBSIDY("1006", "公益补助",
List.of("公益活动名称", "公益活动备案编号"),
List.of("请填写在民政部的备案名称", "请填写在民政部的备案编号"),
List.of("公益补助金")),
/// 行政补贴
ADMINISTRATIVE_SUBSIDY("1007", "行政补贴",
List.of("补贴类型"),
List.of("商户自定义内容,如「购车补贴」"),
List.of("行政补贴", "行政奖励"));
/// 转账场景ID
private final String code;
/// 场景名称
private final String name;
/// 报备字段定义(抖音协议固定中文 infoType, 顺序即报备明细下标)
private final List<String> reportInfoTypes;
/// 报备字段说明(与 reportInfoTypes 平行, 描述字段含义和抖音文档示例)
private final List<String> reportInfoDescriptions;
/// 用户收款感知可选值(收款人在抖音中看到的文案, 不传时取第一个为默认)
private final List<String> userRecvPerceptionOptions;
/// 根据场景ID 查找枚举
public static DouyinTransferSceneEnum findByCode(String code) {
for (DouyinTransferSceneEnum scene : values()) {
if (scene.code.equals(code)) {
return scene;
}
}
return null;
}
}

View File

@@ -31,9 +31,5 @@ public class DouyinDirectChannelMerchantCreateParam {
@Schema(description = "抖音商户号(MCHID)")
@NotBlank(message = "{validation.field.dyMchId.notBlank}")
private String dyMchId;
/// 转账场景ID(商家转账, 抖音转账时必填)
@Schema(description = "转账场景ID")
private String transferScene;
}

View File

@@ -0,0 +1,28 @@
package cn.daxpay.open.channel.douyin.param.direct;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 抖音转账配置保存参数
///
/// 一对一 upsert: 存在则更新, 不存在则新增。`transferAppRefId` 允许为空(支持清空),
/// 但发起转账时必须已配置, 由转账策略校验。
///
@Data
@Accessors(chain = true)
@Schema(title = "抖音转账配置保存参数")
public class DouyinTransferConfigParam {
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "转账发起应用引用(指向 dy_mch_app 主键, 须为网站应用 web_app)")
private Long transferAppRefId;
}

View File

@@ -22,8 +22,5 @@ public class DouyinDirectChannelMerchantResult extends MchBaseResult {
@Schema(description = "抖音商户号(MCHID)")
private String dyMchId;
@Schema(description = "转账场景ID(商家转账)")
private String transferScene;
}

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.channel.douyin.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;
/// # 抖音转账配置
///
/// 转账配置返回结果对象。冗余展示字段(发起应用名/douyinAppId/应用类型)由
/// [cn.daxpay.open.channel.douyin.service.direct.DouyinTransferConfigService] 填充,
/// 不经 MapStruct 自动映射。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "抖音转账配置")
public class DouyinTransferConfigResult 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(真实抖音应用 AppId)")
private String douyinAppId;
@Schema(description = "发起应用类型(须为 web_app 网站应用)")
private String appType;
}

View File

@@ -0,0 +1,35 @@
package cn.daxpay.open.channel.douyin.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 DouyinTransferSceneOptionResult {
@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;
}

View File

@@ -2,8 +2,10 @@ package cn.daxpay.open.channel.douyin.service.direct;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant;
import cn.daxpay.open.channel.douyin.enums.DouyinTransferSceneEnum;
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectChannelMerchantCreateParam;
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectChannelMerchantResult;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferSceneOptionResult;
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
import cn.daxpay.open.payment.masterdata.dao.product.PayProductConfigManager;
import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant;
@@ -17,6 +19,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;
/// # 抖音直连通道商户管理
///
/// 一个抖音商户号(dyMchId)对应一个 channelMchNo, 商户的多个应用共享此绑定。
@@ -61,7 +66,6 @@ public class DouyinDirectChannelMerchantService {
entity.setChannelMchNo(channelMchNo);
entity.setProduct(param.getProduct());
entity.setDyMchId(param.getDyMchId());
entity.setTransferScene(param.getTransferScene());
douyinDirectChannelMerchantManager.save(entity);
}
@@ -74,5 +78,17 @@ public class DouyinDirectChannelMerchantService {
// 通道: 通道商户配置不存在
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
}
/// 查询抖音转账场景选项列表(主数据枚举投影, 不查库, 供前端下拉与报备字段动态渲染)
public List<DouyinTransferSceneOptionResult> findSceneOptions() {
return Arrays.stream(DouyinTransferSceneEnum.values())
.map(scene -> new DouyinTransferSceneOptionResult()
.setCode(scene.getCode())
.setName(scene.getName())
.setReportInfoTypes(scene.getReportInfoTypes())
.setReportInfoDescriptions(scene.getReportInfoDescriptions())
.setUserRecvPerceptionOptions(scene.getUserRecvPerceptionOptions()))
.toList();
}
}

View File

@@ -4,6 +4,7 @@ import cn.daxpay.open.channel.douyin.client.credential.DouyinSdkCredential;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectKeyConfig;
import cn.daxpay.open.payment.auth.core.AppScopeEnum;
import cn.daxpay.open.payment.douyin.facade.DouyinAppFacade;
import cn.daxpay.open.payment.douyin.facade.DyAppView;
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
@@ -41,6 +42,28 @@ public class DouyinDirectConfigAssembler {
DyAppView app = douyinAppFacade.resolve(mchNo, channelMchNo, capability, null,
ProductEnum.DOUYIN_PAY.getCode());
// 2-4. 组装凭证
return assembleCredential(app, channelMchNo);
}
/// 组装转账使用的通道调用凭证(下发给子应用)
///
/// 转账发起应用由「抖音转账配置」显式指定(网站应用, 支持手机H5获取OpenId), 不走支付能力绑定解析,
/// 与支付链路([#buildConfig])的应用解析相互独立。
///
/// @param mchNo 商户号(应用归属校验)
/// @param channelMchNo 通道商户号(定位密钥/商户绑定)
/// @param transferAppRefId 转账发起应用引用(dy_mch_app 主键)
/// @return 抖音 SDK 凭证, 字段对齐子应用 DouyinSdkCredential
public DouyinSdkCredential buildTransferConfig(String mchNo, String channelMchNo, Long transferAppRefId) {
// 1. 按引用加载转账发起应用(仅商户档, 直连商户不使用平台应用)
DyAppView app = douyinAppFacade.getById(AppScopeEnum.MERCHANT, transferAppRefId);
// 2-4. 组装凭证
return assembleCredential(app, channelMchNo);
}
/// 读取通道商户绑定与密钥配置, 组装凭证(第 2-4 步公共部分)
private DouyinSdkCredential assembleCredential(DyAppView app, String channelMchNo) {
// 2. 读取通道商户绑定(获取抖音商户号 dyMchId 作为 mchId)
DouyinDirectChannelMerchant merchant = channelMerchantManager.lambdaQuery()
.eq(DouyinDirectChannelMerchant::getChannelMchNo, channelMchNo)

View File

@@ -0,0 +1,121 @@
package cn.daxpay.open.channel.douyin.service.direct;
import cn.daxpay.open.channel.douyin.convert.direct.DouyinTransferConfigConvert;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManager;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinTransferConfigManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinTransferConfig;
import cn.daxpay.open.channel.douyin.param.direct.DouyinTransferConfigParam;
import cn.daxpay.open.channel.douyin.result.direct.DouyinTransferConfigResult;
import cn.daxpay.open.payment.douyin.dao.merchant.DyMchAppManager;
import cn.daxpay.open.payment.douyin.entity.merchant.DyMchApp;
import cn.daxpay.open.payment.douyin.enums.DyAppTypeEnum;
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;
/// # 抖音转账配置
///
/// 管理通道商户的转账配置(一对一: 转账发起应用)。
/// 发起转账时由转账策略读取本配置按 [DouyinTransferConfig#getTransferAppRefId]
/// 解析发起应用(网站应用)的 douyinAppId, 决定转出主体与收款人 openId 的来源。
///
/// 运营端写 [DouyinTransferConfig](MchBaseEntity) 显式 setMchNo, 避免上下文缺失。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class DouyinTransferConfigService {
private final DouyinTransferConfigManager douyinTransferConfigManager;
private final DouyinDirectChannelMerchantManager douyinDirectChannelMerchantManager;
private final DyMchAppManager dyMchAppManager;
/// 查询通道商户的转账配置(一对一, 未配置返回 null)
///
/// @param mchNo 商户号(归属校验)
/// @param channelMchNo 通道商户号
/// @return 转账配置(含冗余展示), 不存在返回 null
public DouyinTransferConfigResult findByChannelMchNo(String mchNo, String channelMchNo) {
assertChannelMerchant(mchNo, channelMchNo);
return douyinTransferConfigManager.findByChannelMchNo(channelMchNo)
.map(this::toResultWithMeta)
.orElse(null);
}
/// 保存或更新转账配置(一对一 upsert)
///
/// transferAppRefId 允许为空(支持清空), 但发起转账时必须已配置, 由转账策略校验。
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(DouyinTransferConfigParam param) {
// 校验通道商户存在与归属
assertChannelMerchant(param.getMchNo(), param.getChannelMchNo());
// 校验发起应用(若指定): 存在 + 归属 + 网站应用类型(仅网站应用支持手机H5获取OpenId)
if (param.getTransferAppRefId() != null) {
DyMchApp app = dyMchAppManager.findById(param.getTransferAppRefId())
.orElseThrow(() -> new DataNotExistException("error.channel.douyin.transferAppNotExist"));
if (!Objects.equals(app.getMchNo(), param.getMchNo())) {
// 抖音: 转账发起应用不属于当前商户
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.douyin.transferAppNotBelong");
}
if (!Objects.equals(app.getAppType(), DyAppTypeEnum.WEB_APP.getCode())) {
// 抖音: 转账发起应用必须是网站应用类型
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.douyin.transferAppTypeNotWebApp");
}
}
// upsert: 存在则全量覆盖(含清空), 不存在则新增
Optional<DouyinTransferConfig> existing = douyinTransferConfigManager
.findByChannelMchNo(param.getChannelMchNo());
if (existing.isPresent()) {
DouyinTransferConfig entity = existing.get();
entity.setTransferAppRefId(param.getTransferAppRefId());
douyinTransferConfigManager.updateById(entity);
} else {
DouyinTransferConfig entity = DouyinTransferConfigConvert.CONVERT.toEntity(param);
// 运营端写 MchBaseEntity 必须显式 setMchNo(父类 setter 返回类型不匹配, 单独赋值)
entity.setMchNo(param.getMchNo());
douyinTransferConfigManager.save(entity);
}
}
/// 删除通道商户的转账配置(通道商户删除时级联清理)
public void deleteByChannelMchNo(String channelMchNo) {
douyinTransferConfigManager.deleteByChannelMchNo(channelMchNo);
}
/// 校验通道商户存在且归属匹配
private void assertChannelMerchant(String mchNo, String channelMchNo) {
DouyinDirectChannelMerchant channelMerchant = douyinDirectChannelMerchantManager
.findByChannelMchNo(channelMchNo)
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
if (!Objects.equals(channelMerchant.getMchNo(), mchNo)) {
// 抖音: 通道商户与商户号不匹配
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
"error.payment.douyin.channelMerchantMismatch");
}
}
/// 转Result并填充冗余展示(发起应用信息)
private DouyinTransferConfigResult toResultWithMeta(DouyinTransferConfig entity) {
DouyinTransferConfigResult result = entity.toResult();
// 发起应用展示信息
if (entity.getTransferAppRefId() != null) {
dyMchAppManager.findById(entity.getTransferAppRefId())
.ifPresent(app -> {
result.setTransferAppName(app.getAppName());
result.setDouyinAppId(app.getDouyinAppId());
result.setAppType(app.getAppType());
});
}
return result;
}
}

View File

@@ -48,7 +48,10 @@ public class DouyinTransferService {
req.setScene(context.getTransferScene());
req.setUserName(context.getPayeeName());
req.setRemark(StrUtil.sub(context.getTitle(), 0, 32));
req.setPerception(context.getReason());
// 收款感知使用请求参数(按场景枚举选项), 不再用转账原因顶替
req.setPerception(context.getUserRecvPerception());
// 转账场景报备信息(按场景要求填写)
req.setReportInfos(context.getReportInfos());
req.setNotifyUrl(this.buildNotifyUrl(context));
req.setCredential(credential);

View File

@@ -2,6 +2,7 @@ package cn.daxpay.open.channel.douyin.strategy.merchant;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManager;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectKeyConfigManager;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinTransferConfigManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectKeyConfig;
import cn.daxpay.open.payment.douyin.dao.channel.DyChannelAppCapabilityManager;
@@ -17,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
/// 在通道商户删除时清理抖音直连相关的扩展数据:
/// - 通道商户绑定(douyin_direct_channel_merchant)、密钥配置(douyin_direct_key_config)
/// - 通道能力绑定(dy_channel_app_capability, 释放对商户/平台应用的引用)
/// - 转账配置(douyin_transfer_config, 释放转账发起应用引用)
///
/// 注意:商户/平台级应用主数据(dy_mch_app / dy_platform_app)不在此清理 —— 它们可被多个通道商户引用,
/// 归属商户/平台级生命周期管理。
@@ -28,6 +30,7 @@ public class DouyinDirectChannelMerchantCleanupStrategy implements ChannelMercha
private final DouyinDirectChannelMerchantManager douyinDirectChannelMerchantManager;
private final DouyinDirectKeyConfigManager douyinDirectAppKeyConfigManager;
private final DyChannelAppCapabilityManager dyChannelAppCapabilityManager;
private final DouyinTransferConfigManager douyinTransferConfigManager;
/// 对应产品: 抖音支付直连
@Override
@@ -43,5 +46,7 @@ public class DouyinDirectChannelMerchantCleanupStrategy implements ChannelMercha
douyinDirectAppKeyConfigManager.deleteByField(DouyinDirectKeyConfig::getChannelMchNo, channelMchNo);
// 清理通道能力绑定(释放对商户/平台抖音应用的引用)
dyChannelAppCapabilityManager.deleteByChannelMchNo(channelMchNo);
// 清理转账配置(释放转账发起应用引用)
douyinTransferConfigManager.deleteByChannelMchNo(channelMchNo);
}
}

View File

@@ -1,8 +1,9 @@
package cn.daxpay.open.channel.douyin.strategy.transfer;
import cn.daxpay.open.channel.douyin.client.credential.DouyinSdkCredential;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectChannelMerchantManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectChannelMerchant;
import cn.daxpay.open.channel.douyin.dao.direct.DouyinTransferConfigManager;
import cn.daxpay.open.channel.douyin.entity.direct.DouyinTransferConfig;
import cn.daxpay.open.channel.douyin.enums.DouyinTransferSceneEnum;
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectConfigAssembler;
import cn.daxpay.open.channel.douyin.service.payment.transfer.DouyinTransferService;
import cn.daxpay.open.payment.strategy.transfer.AbsTransferStrategy;
@@ -11,8 +12,8 @@ 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.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.config.ConfigErrorException;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -22,8 +23,9 @@ import org.springframework.stereotype.Service;
///
/// 抖音商家转账的通道策略。
/// 通道差异:
/// - openid 收款人([TransferPayeeTypeEnum#OPENID])
/// - transfer_scene_id 取自通道商户配置, 未配置时报错
/// - openid 收款人([TransferPayeeTypeEnum#OPENID]), 收款人 openId 由「转账发起应用」(网站应用)承接 H5 授权
/// - transfer_scene_id 为主数据枚举(1001-1007), 发起转账时由前端选择传入, 无需预配置
/// - 转账发起应用由通道商户的转账配置([DouyinTransferConfig])显式指定
/// - 金额大于等于 2000 元必填收款人姓名(子应用加密上送)
@Slf4j
@Service
@@ -35,7 +37,7 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
private final DouyinTransferService douyinTransferService;
private final DouyinDirectConfigAssembler douyinDirectConfigAssembler;
private final DouyinDirectChannelMerchantManager douyinDirectChannelMerchantManager;
private final DouyinTransferConfigManager douyinTransferConfigManager;
@Override
public String getChannel() {
@@ -57,6 +59,17 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.douyin.transferNameRequired");
}
// 转账场景ID必填且必须为合法枚举
if (StrUtil.isBlank(param.getTransferScene())) {
// 抖音: 转账场景ID必填
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.douyin.transferSceneIdRequired");
}
if (DouyinTransferSceneEnum.findByCode(param.getTransferScene()) == null) {
// 抖音: 不支持的转账场景ID[{0}]
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.douyin.transferSceneNameInvalid", param.getTransferScene());
}
}
/// 发起转账
@@ -73,23 +86,20 @@ public class DouyinTransferStrategy extends AbsTransferStrategy {
return douyinTransferService.sync(context, credential);
}
/// 组装通道调用凭证并注入转账场景
/// 组装通道调用凭证
///
/// 转账场景(transfer_scene_id)从通道商户配置读取, 经上下文回写, 由编排层在"处理中"镜像落库
/// 转账场景(transfer_scene_id)来自请求参数(前端选择的主数据枚举), 已在 [#doValidateParam] 校验合法性
/// 转账发起应用(决定转出主体与收款人 openId 来源)由通道商户的转账配置显式指定,
/// 读取 [DouyinTransferConfig#getTransferAppRefId] 组装凭证。
private DouyinSdkCredential buildCredential(TransferStrategyContext context) {
DouyinDirectChannelMerchant channelMerchant = douyinDirectChannelMerchantManager.lambdaQuery()
.eq(DouyinDirectChannelMerchant::getChannelMchNo, context.getChannelMchNo())
.oneOpt()
.orElseThrow(() -> new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.payment.channel.channelMerchantNotExist"));
if (StrUtil.isBlank(channelMerchant.getTransferScene())) {
// 抖音: 转账场景未配置
throw new BizInfoException(DaxPayErrorCode.CONFIG_NOT_EXIST,
"error.channel.douyin.transferSceneNotConfigured");
DouyinTransferConfig transferConfig = douyinTransferConfigManager
.findByChannelMchNo(context.getChannelMchNo())
.orElseThrow(() -> new ConfigErrorException("error.channel.douyin.transferAppNotConfigured"));
if (transferConfig.getTransferAppRefId() == null) {
// 抖音: 转账发起应用未配置
throw new ConfigErrorException("error.channel.douyin.transferAppNotConfigured");
}
context.setTransferScene(channelMerchant.getTransferScene());
return douyinDirectConfigAssembler.buildConfig(
context.getMchNo(), context.getChannelMchNo(), null);
return douyinDirectConfigAssembler.buildTransferConfig(
context.getMchNo(), context.getChannelMchNo(), transferConfig.getTransferAppRefId());
}
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "Douyin transfer name encryption failed: {0}",
"transferOnlyOpenid": "Douyin only supports openid payee",
"transferNameRequired": "Douyin: payee name required for amount >= 2000 CNY",
"transferSceneNotConfigured": "Douyin: transfer scene not configured"
"transferSceneNotConfigured": "Douyin: transfer scene not configured",
"transferSceneNotFound": "Douyin: transfer scene config not found",
"transferSceneNameInvalid": "Douyin: unsupported transfer scene ID: {0}",
"transferSceneNotBelong": "Douyin: transfer scene config does not belong to the current merchant",
"transferSceneIdRequired": "Douyin: Transfer scene ID is required",
"transferSceneIdInvalid": "Douyin: invalid transfer scene config id: {0}",
"transferAppNotExist": "Douyin: Transfer initiator app not found",
"transferAppNotBelong": "Douyin: Transfer initiator app does not belong to the current merchant",
"transferAppTypeNotWebApp": "Douyin: Transfer initiator app must be a Web App",
"transferAppNotConfigured": "Douyin: Transfer initiator app not configured"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "Enkripsi nama transfer Douyin gagal: {0}",
"transferOnlyOpenid": "Douyin hanya mendukung penerima openid",
"transferNameRequired": "Douyin: nama penerima wajib untuk jumlah >= 2000 CNY",
"transferSceneNotConfigured": "Douyin: skenario transfer belum dikonfigurasi"
"transferSceneNotConfigured": "Douyin: skenario transfer belum dikonfigurasi",
"transferSceneNotFound": "Douyin: konfigurasi skenario transfer tidak ditemukan",
"transferSceneNameInvalid": "Douyin: ID skenario transfer tidak didukung: {0}",
"transferSceneNotBelong": "Douyin: konfigurasi skenario transfer bukan milik merchant saat ini",
"transferSceneIdRequired": "Douyin: ID adegan transfer wajib diisi",
"transferSceneIdInvalid": "Douyin: format id konfigurasi skenario transfer tidak valid: {0}",
"transferAppNotExist": "Douyin: Aplikasi inisiator transfer tidak ditemukan",
"transferAppNotBelong": "Douyin: Aplikasi inisiator transfer bukan milik merchant saat ini",
"transferAppTypeNotWebApp": "Douyin: Aplikasi inisiator transfer harus berjenis Web App",
"transferAppNotConfigured": "Douyin: Aplikasi inisiator transfer belum dikonfigurasi"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "抖音送金氏名暗号化エラー: {0}",
"transferOnlyOpenid": "抖音はopenid受取人のみサポートしています",
"transferNameRequired": "抖音: 2000元以上は受取人氏名が必須です",
"transferSceneNotConfigured": "抖音: 送金シーンが未設定です"
"transferSceneNotConfigured": "抖音: 送金シーンが未設定です",
"transferSceneNotFound": "抖音: 送金シーン設定が存在しません",
"transferSceneNameInvalid": "抖音: サポートされていない送金シーンID: {0}",
"transferSceneNotBelong": "抖音: 送金シーン設定は現在の加盟店に属していません",
"transferSceneIdRequired": "Douyin: 送金シーンIDは必須です",
"transferSceneIdInvalid": "抖音: 送金シーン設定IDの形式エラー: {0}",
"transferAppNotExist": "Douyin: 振込发起アプリが見つかりません",
"transferAppNotBelong": "Douyin: 振込发起アプリは現在のマーチャントに属していません",
"transferAppTypeNotWebApp": "Douyin: 振込发起アプリはサイトアプリ(Webアプリ)タイプである必要があります",
"transferAppNotConfigured": "Douyin: 振込发起アプリが未設定です"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "더우인 송금 이름 암호화 실패: {0}",
"transferOnlyOpenid": "더우인은 openid 수취인만 지원합니다",
"transferNameRequired": "더우인: 2000원 이상은 수취인 이름 필수",
"transferSceneNotConfigured": "더우인: 송금 시나리오 미설정"
"transferSceneNotConfigured": "더우인: 송금 시나리오 미설정",
"transferSceneNotFound": "더우인: 송금 시나리오 설정이 존재하지 않습니다",
"transferSceneNameInvalid": "더우인: 지원되지 않는 송금 시나리오 ID: {0}",
"transferSceneNotBelong": "더우인: 송금 시나리오 설정이 현재 가맹점에 속하지 않습니다",
"transferSceneIdRequired": "Douyin: 이체 장면 ID는 필수입니다",
"transferSceneIdInvalid": "더우인: 송금 시나리오 설정 ID 형식 오류: {0}",
"transferAppNotExist": "Douyin: 이체 발신 앱을 찾을 수 없습니다",
"transferAppNotBelong": "Douyin: 이체 발신 앱이 현재 가맹점에 속하지 않습니다",
"transferAppTypeNotWebApp": "Douyin: 이체 발신 앱은 웹 앱(Web App) 유형이어야 합니다",
"transferAppNotConfigured": "Douyin: 이체 발신 앱이 구성되지 않았습니다"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "Penyulitan nama pindahan Douyin gagal: {0}",
"transferOnlyOpenid": "Douyin hanya menyokong penerima openid",
"transferNameRequired": "Douyin: nama penerima wajib untuk jumlah >= 2000 CNY",
"transferSceneNotConfigured": "Douyin: senario pindahan belum dikonfigurasi"
"transferSceneNotConfigured": "Douyin: senario pindahan belum dikonfigurasi",
"transferSceneNotFound": "Douyin: konfigurasi senario pindahan tidak dijumpai",
"transferSceneNameInvalid": "Douyin: ID senario pindahan tidak disokong: {0}",
"transferSceneNotBelong": "Douyin: konfigurasi senario pindahan bukan milik merchant semasa",
"transferSceneIdRequired": "Douyin: ID babak pemindahan diperlukan",
"transferSceneIdInvalid": "Douyin: format id konfigurasi senario pindahan tidak sah: {0}",
"transferAppNotExist": "Douyin: Aplikasi pemula pemindahan tidak dijumpai",
"transferAppNotBelong": "Douyin: Aplikasi pemula pemindahan tidak milik merchant semasa",
"transferAppTypeNotWebApp": "Douyin: Aplikasi pemula pemindahan mesti jenis Web App",
"transferAppNotConfigured": "Douyin: Aplikasi pemula pemindahan belum dikonfigurasi"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "การเข้ารหัสชื่อการโอน Douyin ล้มเหลว: {0}",
"transferOnlyOpenid": "Douyin รองรับผู้รับ openid เท่านั้น",
"transferNameRequired": "Douyin: ต้องใส่ชื่อผู้รับเมื่อมากกว่าหรือเท่ากับ 2000 หยวน",
"transferSceneNotConfigured": "Douyin: ยังไม่ได้กำหนดค่าสถานการณ์การโอน"
"transferSceneNotConfigured": "Douyin: ยังไม่ได้กำหนดค่าสถานการณ์การโอน",
"transferSceneNotFound": "Douyin: ไม่พบการกำหนดค่าสถานการณ์การโอน",
"transferSceneNameInvalid": "Douyin: ไม่รองรับรหัสสถานการณ์การโอน: {0}",
"transferSceneNotBelong": "Douyin: การกำหนดค่าสถานการณ์การโอนไม่ใช่ของผู้ค้าปัจจุบัน",
"transferSceneIdRequired": "Douyin: ต้องระบุรหัสสถานการณ์การโอน",
"transferSceneIdInvalid": "Douyin: รูปแบบรหัสการกำหนดค่าสถานการณ์การโอนไม่ถูกต้อง: {0}",
"transferAppNotExist": "Douyin: ไม่พบแอปเริ่มการโอน",
"transferAppNotBelong": "Douyin: แอปเริ่มการโอนไม่ได้เป็นของร้านค้าปัจจุบัน",
"transferAppTypeNotWebApp": "Douyin: แอปเริ่มการโอนต้องเป็นประเภทเว็บแอป (Web App)",
"transferAppNotConfigured": "Douyin: ยังไม่ได้กำหนดค่าแอปเริ่มการโอน"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "Mã hóa tên chuyển khoản Douyin thất bại: {0}",
"transferOnlyOpenid": "Douyin chỉ hỗ trợ người nhận openid",
"transferNameRequired": "Douyin: bắt buộc nhập tên người nhận khi >= 2000 CNY",
"transferSceneNotConfigured": "Douyin: chưa cấu hình kịch bản chuyển khoản"
"transferSceneNotConfigured": "Douyin: chưa cấu hình kịch bản chuyển khoản",
"transferSceneNotFound": "Douyin: cấu hình kịch bản chuyển khoản không tồn tại",
"transferSceneNameInvalid": "Douyin: ID kịch bản chuyển khoản không được hỗ trợ: {0}",
"transferSceneNotBelong": "Douyin: cấu hình kịch bản chuyển khoản không thuộc merchant hiện tại",
"transferSceneIdRequired": "Douyin: ID cảnh chuyển khoản là bắt buộc",
"transferSceneIdInvalid": "Douyin: định dạng id cấu hình kịch bản chuyển khoản không hợp lệ: {0}",
"transferAppNotExist": "Douyin: Không tìm thấy ứng dụng khởi tạo chuyển khoản",
"transferAppNotBelong": "Douyin: Ứng dụng khởi tạo chuyển khoản không thuộc merchant hiện tại",
"transferAppTypeNotWebApp": "Douyin: Ứng dụng khởi tạo chuyển khoản phải là loại Web App",
"transferAppNotConfigured": "Douyin: Ứng dụng khởi tạo chuyển khoản chưa được thiết lập"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "抖音转账姓名加密失败: {0}",
"transferOnlyOpenid": "抖音仅支持 openid 收款人",
"transferNameRequired": "抖音: 大于等于2000元必须填收款人姓名",
"transferSceneNotConfigured": "抖音: 转账场景未配置"
"transferSceneNotConfigured": "抖音: 转账场景未配置",
"transferSceneNotFound": "抖音: 转账场景配置不存在",
"transferSceneNameInvalid": "抖音: 不支持的转账场景ID: {0}",
"transferSceneNotBelong": "抖音: 转账场景配置不属于当前商户",
"transferSceneIdRequired": "抖音: 转账场景ID必填",
"transferSceneIdInvalid": "抖音: 转账场景配置ID格式错误: {0}",
"transferAppNotExist": "抖音: 转账发起应用不存在",
"transferAppNotBelong": "抖音: 转账发起应用不属于当前商户",
"transferAppTypeNotWebApp": "抖音: 转账发起应用必须是网站应用类型",
"transferAppNotConfigured": "抖音: 转账发起应用未配置"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "抖音轉賬姓名加密失敗: {0}",
"transferOnlyOpenid": "抖音僅支持 openid 收款人",
"transferNameRequired": "抖音: 大於等於2000元必須填收款人姓名",
"transferSceneNotConfigured": "抖音: 轉賬場景未配置"
"transferSceneNotConfigured": "抖音: 轉賬場景未配置",
"transferSceneNotFound": "抖音: 轉賬場景設定不存在",
"transferSceneNameInvalid": "抖音: 不支援的轉賬場景ID: {0}",
"transferSceneNotBelong": "抖音: 轉賬場景設定不屬於目前商戶",
"transferSceneIdRequired": "抖音: 轉賬場景ID必填",
"transferSceneIdInvalid": "抖音: 轉賬場景設定ID格式錯誤: {0}",
"transferAppNotExist": "抖音: 轉帳發起應用不存在",
"transferAppNotBelong": "抖音: 轉帳發起應用不屬於目前商戶",
"transferAppTypeNotWebApp": "抖音: 轉賬發起應用必須是網站應用類型",
"transferAppNotConfigured": "抖音: 轉帳發起應用未設定"
}

View File

@@ -25,5 +25,14 @@
"transferEncryptFailed": "抖音轉帳姓名加密失敗: {0}",
"transferOnlyOpenid": "抖音僅支持 openid 收款人",
"transferNameRequired": "抖音: 大於等於2000元必須填收款人姓名",
"transferSceneNotConfigured": "抖音: 轉帳場景未配置"
"transferSceneNotConfigured": "抖音: 轉帳場景未配置",
"transferSceneNotFound": "抖音: 轉帳場景設定不存在",
"transferSceneNameInvalid": "抖音: 不支援的轉帳場景ID: {0}",
"transferSceneNotBelong": "抖音: 轉帳場景設定不屬於目前商戶",
"transferSceneIdRequired": "抖音: 轉賬場景ID必填",
"transferSceneIdInvalid": "抖音: 轉帳場景設定ID格式錯誤: {0}",
"transferAppNotExist": "抖音: 轉帳發起應用不存在",
"transferAppNotBelong": "抖音: 轉帳發起應用不屬於目前商戶",
"transferAppTypeNotWebApp": "抖音: 轉帳發起應用必須是網站應用類型",
"transferAppNotConfigured": "抖音: 轉帳發起應用未設定"
}