diff --git a/_config/sql/table.sql b/_config/sql/table.sql index 489fdccf7..241ff2c9b 100644 --- a/_config/sql/table.sql +++ b/_config/sql/table.sql @@ -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 -- ---------------------------- diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 7161a27c3..f4f885dfe 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -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 '同一通道商户仅一条转账配置(部分唯一索引)'; diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/client/req/DouyinTransferReq.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/client/req/DouyinTransferReq.java index f2341d2f8..4b631e768 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/client/req/DouyinTransferReq.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/client/req/DouyinTransferReq.java @@ -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 reportInfos; + /// 异步通知地址(抖音→平台) private String notifyUrl; diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinDirectChannelMerchantController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinDirectChannelMerchantController.java index 5e3e3381e..2cfb9a11f 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinDirectChannelMerchantController.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinDirectChannelMerchantController.java @@ -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> sceneOptions() { + return Res.ok(douyinDirectChannelMerchantService.findSceneOptions()); + } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinTransferConfigController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinTransferConfigController.java new file mode 100644 index 000000000..6eabe1a10 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/direct/DouyinTransferConfigController.java @@ -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 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 save(@RequestBody @Validated DouyinTransferConfigParam param) { + douyinTransferConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectChannelMerchantController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectChannelMerchantController.java index 6f3e25d33..e32711eb5 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectChannelMerchantController.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectChannelMerchantController.java @@ -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> sceneOptions() { + return Res.ok(douyinDirectChannelMerchantService.findSceneOptions()); + } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinTransferConfigController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinTransferConfigController.java new file mode 100644 index 000000000..dcea92d4a --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinTransferConfigController.java @@ -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 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 save(@RequestBody DouyinTransferConfigParam param) { + this.assertChannelMchOwned(param.getChannelMchNo()); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param); + douyinTransferConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/convert/direct/DouyinTransferConfigConvert.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/convert/direct/DouyinTransferConfigConvert.java new file mode 100644 index 000000000..e51acd373 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/convert/direct/DouyinTransferConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigManager.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigManager.java new file mode 100644 index 000000000..dba16fa0e --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigManager.java @@ -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 { + + /// 按通道商户号查询转账配置(一对一) + public Optional findByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(DouyinTransferConfig::getChannelMchNo, channelMchNo) + .oneOpt(); + } + + /// 按通道商户号删除转账配置(逻辑删除, 通道商户删除时级联清理) + public void deleteByChannelMchNo(String channelMchNo) { + lambdaUpdate() + .eq(DouyinTransferConfig::getChannelMchNo, channelMchNo) + .remove(); + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigMapper.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigMapper.java new file mode 100644 index 000000000..d7ab05ac5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinTransferConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinDirectChannelMerchant.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinDirectChannelMerchant.java index f85d452b5..7c132d9db 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinDirectChannelMerchant.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinDirectChannelMerchant.java @@ -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() { diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinTransferConfig.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinTransferConfig.java new file mode 100644 index 000000000..3fe838f01 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/entity/direct/DouyinTransferConfig.java @@ -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 { + + /// 通道商户号 + @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); + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/enums/DouyinTransferSceneEnum.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/enums/DouyinTransferSceneEnum.java new file mode 100644 index 000000000..5d3eab6b3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/enums/DouyinTransferSceneEnum.java @@ -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 reportInfoTypes; + + /// 报备字段说明(与 reportInfoTypes 平行, 描述字段含义和抖音文档示例) + private final List reportInfoDescriptions; + + /// 用户收款感知可选值(收款人在抖音中看到的文案, 不传时取第一个为默认) + private final List userRecvPerceptionOptions; + + /// 根据场景ID 查找枚举 + public static DouyinTransferSceneEnum findByCode(String code) { + for (DouyinTransferSceneEnum scene : values()) { + if (scene.code.equals(code)) { + return scene; + } + } + return null; + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinDirectChannelMerchantCreateParam.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinDirectChannelMerchantCreateParam.java index 2a2086d24..001d30a98 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinDirectChannelMerchantCreateParam.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinDirectChannelMerchantCreateParam.java @@ -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; } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinTransferConfigParam.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinTransferConfigParam.java new file mode 100644 index 000000000..1a87821a3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/direct/DouyinTransferConfigParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinDirectChannelMerchantResult.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinDirectChannelMerchantResult.java index 2aa0a903a..7e2445d5b 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinDirectChannelMerchantResult.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinDirectChannelMerchantResult.java @@ -22,8 +22,5 @@ public class DouyinDirectChannelMerchantResult extends MchBaseResult { @Schema(description = "抖音商户号(MCHID)") private String dyMchId; - - @Schema(description = "转账场景ID(商家转账)") - private String transferScene; } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferConfigResult.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferConfigResult.java new file mode 100644 index 000000000..27dd78ee7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferConfigResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferSceneOptionResult.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferSceneOptionResult.java new file mode 100644 index 000000000..6113b1400 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/result/direct/DouyinTransferSceneOptionResult.java @@ -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 reportInfoTypes; + + @Schema(description = "报备字段说明(与 reportInfoTypes 平行, 含抖音文档示例)") + private List reportInfoDescriptions; + + @Schema(description = "用户收款感知可选值(收款人在抖音中看到的文案)") + private List userRecvPerceptionOptions; +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectChannelMerchantService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectChannelMerchantService.java index 98d609dfe..48d63ae72 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectChannelMerchantService.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectChannelMerchantService.java @@ -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 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(); + } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java index b8a62c0f8..afeff743d 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectConfigAssembler.java @@ -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) diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinTransferConfigService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinTransferConfigService.java new file mode 100644 index 000000000..00a51dd4b --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinTransferConfigService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/payment/transfer/DouyinTransferService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/payment/transfer/DouyinTransferService.java index c5dac4ab6..400facfa6 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/payment/transfer/DouyinTransferService.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/payment/transfer/DouyinTransferService.java @@ -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); diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/merchant/DouyinDirectChannelMerchantCleanupStrategy.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/merchant/DouyinDirectChannelMerchantCleanupStrategy.java index b493679b2..b3441862d 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/merchant/DouyinDirectChannelMerchantCleanupStrategy.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/merchant/DouyinDirectChannelMerchantCleanupStrategy.java @@ -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); } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/transfer/DouyinTransferStrategy.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/transfer/DouyinTransferStrategy.java index daa0d7242..f7eb0fa4f 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/transfer/DouyinTransferStrategy.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/transfer/DouyinTransferStrategy.java @@ -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()); } } - diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json index 06ab782a1..df67154da 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json index cb3f3c95a..acd1c3061 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json index f22e6434e..8f902a21f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json @@ -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: 振込发起アプリが未設定です" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json index ed3121914..28fe07fd8 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json @@ -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: 이체 발신 앱이 구성되지 않았습니다" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json index 7624e11c9..4a40e74cd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json index 7c8dbf13c..c07b356a6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json @@ -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: ยังไม่ได้กำหนดค่าแอปเริ่มการโอน" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json index 28ca729b9..634073454 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json index e9942f641..8a29d7eed 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json @@ -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": "抖音: 转账发起应用未配置" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json index 39c9afea2..13acf7ad7 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json @@ -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": "抖音: 轉帳發起應用未設定" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json index f218797d0..938fc6b84 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json @@ -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": "抖音: 轉帳發起應用未設定" }