diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 0702298aa..29fd7a0d5 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -209,6 +209,8 @@ INSERT INTO "public"."iam_perm_menu" VALUES (91401, 91400, 'merchant:app', 'merc INSERT INTO "public"."iam_perm_menu" VALUES (91402, 91400, 'channel:merchant', 'merchant', 'ChannelMerchantList', 'menu.payment.merchant.channelMerchant', 'lucide:store', 'f', 'f', '/payment/channel-merchant/ChannelMerchantList', '/mch/channel-merchant', NULL, 2, 'f', 'f', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, '', '', '2026-07-23 09:32:12.684068+00', '2026-07-23 09:32:12.684068+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91410, 91402, 'channel:merchant:create', 'merchant', 'ChannelMerchantCreate', 'menu.payment.merchant.channelMerchant.create', NULL, 't', 'f', '/payment/channel-merchant/ChannelMerchantCreate', '/mch/channel-merchant/create', NULL, 1, 'f', 'f', 'f', 0, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, '', '', '2026-07-23 09:32:12.687009+00', '2026-07-23 09:32:12.687009+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91411, 91402, 'channel:merchant:detail', 'merchant', 'ChannelMerchantDetail', 'menu.payment.merchant.channelMerchant.detail', NULL, 't', 'f', '/payment/channel-merchant/detail/ChannelMerchantDetailDispatch', '/mch/channel-merchant/detail', NULL, 2, 'f', 'f', 'f', 0, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, '', '', '2026-07-23 09:32:12.689613+00', '2026-07-23 09:32:12.689613+00'); +-- 抖音应用管理子页(商户端,隐藏 subpage,由通道商户详情页跳转) +INSERT INTO "public"."iam_perm_menu" VALUES (91420, 91402, 'channel:app', 'merchant', 'DouyinMchAppManage', 'menu.payment.merchant.channelMerchant.douyinApp', NULL, 't', 'f', '/payment/channel/douyin/manage/app/DouyinMchAppManage', '/mch/channel-merchant/douyin-app-manage', NULL, 3, 'f', 'f', 'f', 0, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, '', '', '2026-07-25 00:00:00+00', '2026-07-25 00:00:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91501, 91300, 'merchant:credential', 'merchant', 'MerchantCredentialConfig', 'menu.payment.merchant.credential', 'lucide:key', 'f', 'f', '/payment/merchant/credential/MerchantCredentialConfig', '/mch/credential', NULL, 5, 'f', 'f', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, '', '', '2026-07-24 10:30:00.000000+00', '2026-07-24 10:30:00.000000+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91305, 91300, 'merchant:wx-verify', 'merchant', 'MchWxDomainVerifyList', 'menu.payment.merchant.wxVerify', 'lucide:shield-check', 'f', 'f', '/payment/merchant/wx-verify/MchWxDomainVerifyList', '/mch/wx-verify', NULL, 6, 'f', 'f', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, '', '', '2026-07-24 10:30:00.000000+00', '2026-07-24 10:30:00.000000+00'); INSERT INTO "public"."iam_perm_menu" VALUES (40106, 4, 'payment:wx:platform-app', 'admin', 'WxAppHub', 'menu.payment.wx.app', 'lucide:app-window', 'f', 'f', '/payment/wx/WxAppHub', '/payment/wx/app', NULL, 2.5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-23 13:44:05.953634+00', '2026-07-23 13:44:05.953634+00'); diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppCapabilityController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppCapabilityController.java new file mode 100644 index 000000000..41f520596 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppCapabilityController.java @@ -0,0 +1,96 @@ +package cn.daxpay.open.channel.douyin.controller.merchant; + +import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppCapabilityBatchParam; +import cn.daxpay.open.channel.douyin.result.DouyinCapabilityOption; +import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppCapabilityResult; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService; +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.List; +import java.util.Objects; + +/// # 抖音直连商户应用支付能力关联管理(商户端) +/// +/// 对照运营端 [cn.daxpay.open.channel.douyin.controller.direct.DouyinDirectAppCapabilityController],路径前缀 `/mch/douyin/mch-app/capability`。 +/// 校验通道商户归属当前商户后委托 [DouyinDirectAppCapabilityService]。 +@PermCode(menuCode = PermCodes.Channel.App.MENU) +@Validated +@Tag(name = "抖音直连商户应用支付能力关联管理(商户端)") +@RestController +@RequestMapping("/mch/douyin/mch-app/capability") +@RequiredArgsConstructor +public class MchDouyinDirectAppCapabilityController { + + private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService; + 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.channel.douyin.mchAppNotFound")); + if (!Objects.equals(channelMerchant.getMchNo(), this.requireMchNo())) { + // 抖音: 通道商户与商户号不匹配 + throw new ConfigErrorException("error.channel.douyin.mchAppNotFound"); + } + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "查询通道商户的能力应用关联列表") + @GetMapping("/list-by-channel-mch-no") + public Result> listByChannelMchNo( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { + this.assertChannelMchOwned(channelMchNo); + return Res.ok(douyinDirectAppCapabilityService.listByChannelMchNo(this.requireMchNo(), channelMchNo)); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "查询抖音直连支持的支付能力候选") + @GetMapping("/list-supported-capabilities") + public Result> listSupportedCapabilities() { + return Res.ok(douyinDirectAppCapabilityService.listSupportedCapabilities()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "全量保存能力应用关联") + @PostMapping("/save-batch") + public Result saveBatch(@RequestBody DouyinDirectAppCapabilityBatchParam param) { + this.assertChannelMchOwned(param.getChannelMchNo()); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param); + douyinDirectAppCapabilityService.saveBatch(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppController.java new file mode 100644 index 000000000..7facfcf6e --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectAppController.java @@ -0,0 +1,180 @@ +package cn.daxpay.open.channel.douyin.controller.merchant; + +import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppAuthConfigParam; +import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppParam; +import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppAuthConfigResult; +import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppResult; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppAuthConfigService; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppService; +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.CommonErrorCode; +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 cn.daxpay.open.platform.core.validation.ValidationGroup; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +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.List; +import java.util.Objects; + +/// # 抖音直连商户应用管理(商户端) +/// +/// 对照运营端 [cn.daxpay.open.channel.douyin.controller.direct.DouyinDirectAppController],路径前缀 `/mch/douyin/mch-app`。 +/// 商户号一律取自 [PaymentContext],写操作覆盖请求中的 mchNo,防越权。 +@PermCode(menuCode = PermCodes.Channel.App.MENU) +@Validated +@Tag(name = "抖音直连商户应用管理(商户端)") +@RestController +@RequestMapping("/mch/douyin/mch-app") +@RequiredArgsConstructor +public class MchDouyinDirectAppController { + + private final DouyinDirectAppService douyinDirectAppService; + private final DouyinDirectAppAuthConfigService douyinDirectAppAuthConfigService; + 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.channel.douyin.mchAppNotFound")); + if (!Objects.equals(channelMerchant.getMchNo(), this.requireMchNo())) { + // 抖音: 通道商户与商户号不匹配 + throw new ConfigErrorException("error.channel.douyin.mchAppNotFound"); + } + } + + /// 校验应用归属当前商户 + private void assertAppOwned(DouyinDirectAppResult result) { + if (!Objects.equals(result.getMchNo(), this.requireMchNo())) { + // 抖音: 直连商户应用不存在或商户号归属不匹配 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.douyin.mchAppNotFound"); + } + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "根据通道商户号查询应用列表") + @GetMapping("/list-by-channel-mch-no") + public Result> listByChannelMchNo( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { + this.assertChannelMchOwned(channelMchNo); + return Res.ok(douyinDirectAppService.listByMchNoAndChannelMchNo(this.requireMchNo(), channelMchNo)); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "查询应用详情") + @GetMapping("/find-by-id") + public Result findById( + @NotNull(message = "{validation.field.id.notNull}") Long id) { + DouyinDirectAppResult result = douyinDirectAppService.findById(id); + this.assertAppOwned(result); + return Res.ok(result); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "同一通道商户下抖音应用ID是否已存在") + @GetMapping("/exists-douyin-app-id-by-channel") + public Result existsDouyinAppIdByChannel( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo, + @NotBlank(message = "{validation.field.douyinAppId.notBlank}") String douyinAppId) { + this.assertChannelMchOwned(channelMchNo); + return Res.ok(douyinDirectAppService.existsDouyinAppIdByChannel( + this.requireMchNo(), channelMchNo, douyinAppId, null)); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "同一通道商户下抖音应用ID是否已存在(排除自身)") + @GetMapping("/exists-douyin-app-id-by-channel-not-id") + public Result existsDouyinAppIdByChannelNotId( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo, + @NotBlank(message = "{validation.field.douyinAppId.notBlank}") String douyinAppId, + @NotNull(message = "{validation.field.id.notNull}") Long id) { + this.assertChannelMchOwned(channelMchNo); + return Res.ok(douyinDirectAppService.existsDouyinAppIdByChannel( + this.requireMchNo(), channelMchNo, douyinAppId, id)); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "新增直连商户应用") + @PostMapping("/add") + public Result add(@RequestBody DouyinDirectAppParam param) { + this.assertChannelMchOwned(param.getChannelMchNo()); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param, ValidationGroup.add.class); + douyinDirectAppService.add(param); + return Res.ok(); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "修改直连商户应用") + @PostMapping("/update") + public Result update(@RequestBody DouyinDirectAppParam param) { + this.assertAppOwned(douyinDirectAppService.findById(param.getId())); + this.assertChannelMchOwned(param.getChannelMchNo()); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param, ValidationGroup.edit.class); + douyinDirectAppService.update(param); + return Res.ok(); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "删除直连商户应用") + @PostMapping("/delete") + public Result delete(@NotNull(message = "{validation.field.id.notNull}") Long id) { + this.assertAppOwned(douyinDirectAppService.findById(id)); + douyinDirectAppService.delete(id); + return Res.ok(); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "查询应用授权认证配置") + @GetMapping("/find-auth-config-by-app-id") + public Result findAuthConfigByAppId( + @NotNull(message = "{validation.field.douyinDirectAppId.notNull}") Long douyinDirectAppId) { + this.assertAppOwned(douyinDirectAppService.findById(douyinDirectAppId)); + var config = douyinDirectAppAuthConfigService.findByDouyinDirectAppId(douyinDirectAppId); + return Res.ok(config.toResult()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "保存应用授权认证配置") + @PostMapping("/save-auth-config") + public Result saveAuthConfig(@RequestBody DouyinDirectAppAuthConfigParam param) { + this.assertAppOwned(douyinDirectAppService.findById(param.getDouyinDirectAppId())); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param); + douyinDirectAppAuthConfigService.save(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 new file mode 100644 index 000000000..6f3e25d33 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/merchant/MchDouyinDirectChannelMerchantController.java @@ -0,0 +1,103 @@ +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.service.direct.DouyinDirectChannelMerchantService; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectKeyConfigService; +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.DouyinDirectChannelMerchantController],路径前缀 `/mch/douyin/direct-channel-merchant`。 +/// 提供抖音直连通道商户的基础信息查看与密钥配置,商户号一律取自 [PaymentContext],防越权。 +@PermCode(menuCode = PermCodes.Channel.Merchant.MENU) +@Validated +@Tag(name = "抖音直连通道商户配置(商户端)") +@RestController +@RequestMapping("/mch/douyin/direct-channel-merchant") +@RequiredArgsConstructor +public class MchDouyinDirectChannelMerchantController { + + private final DouyinDirectChannelMerchantService douyinDirectChannelMerchantService; + private final DouyinDirectKeyConfigService douyinDirectKeyConfigService; + 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.channel.douyin.mchAppNotFound")); + if (!Objects.equals(channelMerchant.getMchNo(), this.requireMchNo())) { + // 抖音: 通道商户与商户号不匹配 + throw new ConfigErrorException("error.channel.douyin.mchAppNotFound"); + } + } + + @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(douyinDirectChannelMerchantService.findByChannelMchNo(channelMchNo)); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "根据通道商户号查询密钥配置") + @GetMapping("/find-key-config") + public Result findKeyConfig( + @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { + this.assertChannelMchOwned(channelMchNo); + var config = douyinDirectKeyConfigService.findByChannelMchNo(channelMchNo); + var result = config.toResult(); + result.setPrivateKeyConfigured(config.getMerchantPrivateKey() != null); + result.setEncryptKeyConfigured(config.getEncryptKey() != null); + return Res.ok(result); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "保存密钥配置") + @PostMapping("/save-key-config") + public Result saveKeyConfig(@RequestBody DouyinDirectKeyConfigParam param) { + this.assertChannelMchOwned(param.getChannelMchNo()); + // 强制当前商户号,忽略客户端传入(防越权) + param.setMchNo(this.requireMchNo()); + ValidationUtil.validateParam(param); + douyinDirectKeyConfigService.save(param); + return Res.ok(); + } +}