From a95947af5e19f4449873ccd1fcec72f61cdb70dd Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Mon, 20 Jul 2026 10:25:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(check):=20=E9=85=8D=E7=BD=AE=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=A8=A1=E5=9D=97,=20=E6=A3=80=E6=B5=8B=E8=BF=90?= =?UTF-8?q?=E8=90=A5/=E5=95=86=E6=88=B7=E6=9C=AA=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运营端(AdminConfigCheckService)聚合站点信息/访问地址/OSS/社交登录 4 个检查器; 商户端(MerchantConfigCheckService)聚合应用/凭证/通道商户/路由/通知 5 个检查器。 通过 AdminConfigChecker/MerchantConfigChecker 接口扩展, 检测纯计算无状态, 单项失败降级跳过。 --- .../check/checker/PlatformOssChecker.java | 43 ++++++++++++ .../check/checker/PlatformUrlChecker.java | 49 +++++++++++++ .../check/checker/PlatformWebsiteChecker.java | 50 ++++++++++++++ .../check/checker/SocialLoginChecker.java | 46 +++++++++++++ .../service/AdminConfigCheckService.java | 54 +++++++++++++++ .../check/AdminConfigCheckController.java | 34 ++++++++++ .../check/checker/AdminConfigChecker.java | 22 ++++++ .../check/checker/MerchantConfigChecker.java | 22 ++++++ .../merchant/ChannelMerchantChecker.java | 45 ++++++++++++ .../check/checker/merchant/MchAppChecker.java | 46 +++++++++++++ .../merchant/MchCredentialChecker.java | 46 +++++++++++++ .../merchant/MchNotifyConfigChecker.java | 62 +++++++++++++++++ .../checker/merchant/PayRouteChecker.java | 56 +++++++++++++++ .../check/enums/ConfigCheckCategoryEnum.java | 37 ++++++++++ .../check/enums/ConfigCheckSeverityEnum.java | 19 ++++++ .../payment/check/model/ConfigCheckItem.java | 50 ++++++++++++++ .../check/model/ConfigCheckResult.java | 33 +++++++++ .../service/MerchantConfigCheckService.java | 68 +++++++++++++++++++ .../check/MerchantConfigCheckController.java | 37 ++++++++++ 19 files changed, 819 insertions(+) create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformOssChecker.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformUrlChecker.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformWebsiteChecker.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/SocialLoginChecker.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/AdminConfigCheckService.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/AdminConfigCheckController.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/AdminConfigChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/MerchantConfigChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/ChannelMerchantChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchAppChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchCredentialChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchNotifyConfigChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/PayRouteChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckCategoryEnum.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckSeverityEnum.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckItem.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckResult.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/service/MerchantConfigCheckService.java create mode 100644 daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/check/MerchantConfigCheckController.java diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformOssChecker.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformOssChecker.java new file mode 100644 index 000000000..9495dcfee --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformOssChecker.java @@ -0,0 +1,43 @@ +package cn.daxpay.open.payment.admin.check.checker; + +import cn.daxpay.open.payment.check.checker.AdminConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.platform.system.enums.EncryptPlatformConfigTypeEnum; +import cn.daxpay.open.platform.system.service.config.SystemPlatformEncryptConfigService; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/// # 对象存储配置检查器 +/// +/// 检测 OSS 配置是否存在。OSS 未配置时文件上传/Logo 存储等能力不可用。 +@Component +@Order(4) +@RequiredArgsConstructor +public class PlatformOssChecker implements AdminConfigChecker { + + private final SystemPlatformEncryptConfigService systemPlatformEncryptConfigService; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.PLATFORM_OSS; + } + + @Override + public ConfigCheckItem check() { + // OSS 配置记录不存在 => 告警 + boolean unconfigured = !systemPlatformEncryptConfigService.existsConfig( + EncryptPlatformConfigTypeEnum.OSS); + if (unconfigured) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.PLATFORM_OSS, + ConfigCheckCategoryEnum.PLATFORM_OSS.getCode(), + "configCheck.platformOss.title", + "configCheck.platformOss.description", + "/system/config/platform" + ); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformUrlChecker.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformUrlChecker.java new file mode 100644 index 000000000..74a05bc44 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformUrlChecker.java @@ -0,0 +1,49 @@ +package cn.daxpay.open.payment.admin.check.checker; + +import cn.daxpay.open.payment.check.checker.AdminConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformUrlConfig; +import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum; +import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/// # 平台访问地址检查器 +/// +/// 检测平台后端 API 地址与管理端地址是否已配置。 +/// 后端地址为支付回调拼接的必需项, 缺失会导致通道回调地址无法生成。 +@Component +@Order(3) +@RequiredArgsConstructor +public class PlatformUrlChecker implements AdminConfigChecker { + + private final SystemPlatformConfigService systemPlatformConfigService; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.PLATFORM_URL; + } + + @Override + public ConfigCheckItem check() { + PlatformUrlConfig config = systemPlatformConfigService.getConfig( + PlatformConfigTypeEnum.URL, PlatformUrlConfig.class); + // 后端 API 地址与管理端地址是核心: 缺后端地址回调无法拼接, 缺管理端地址第三方登录回调失效 + boolean unconfigured = config == null + || StrUtil.isBlank(config.getBackendBaseUrl()) + || StrUtil.isBlank(config.getAdminBaseUrl()); + if (unconfigured) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.PLATFORM_URL, + ConfigCheckCategoryEnum.PLATFORM_URL.getCode(), + "configCheck.platformUrl.title", + "configCheck.platformUrl.description", + "/system/config/platform" + ); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformWebsiteChecker.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformWebsiteChecker.java new file mode 100644 index 000000000..c6720af73 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/PlatformWebsiteChecker.java @@ -0,0 +1,50 @@ +package cn.daxpay.open.payment.admin.check.checker; + +import cn.daxpay.open.payment.check.checker.AdminConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformWebsiteConfig; +import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum; +import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/// # 平台站点信息检查器 +/// +/// 检测站点关键展示字段(系统名称/Logo/ICP 备案)是否已配置。 +/// 任一关键字段为空视为未配置。 +@Component +@Order(2) +@RequiredArgsConstructor +public class PlatformWebsiteChecker implements AdminConfigChecker { + + private final SystemPlatformConfigService systemPlatformConfigService; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.PLATFORM_WEBSITE; + } + + @Override + public ConfigCheckItem check() { + PlatformWebsiteConfig config = systemPlatformConfigService.getConfig( + PlatformConfigTypeEnum.WEBSITE, PlatformWebsiteConfig.class); + // 关键字段任一为空 => 告警(系统名称与 Logo 是站点基础展示, ICP 为合规要求) + boolean unconfigured = config == null + || StrUtil.isBlank(config.getSystemName()) + || StrUtil.isBlank(config.getLogo()) + || StrUtil.isBlank(config.getIcpInfo()); + if (unconfigured) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.PLATFORM_WEBSITE, + ConfigCheckCategoryEnum.PLATFORM_WEBSITE.getCode(), + "configCheck.platformWebsite.title", + "configCheck.platformWebsite.description", + "/system/config/platform" + ); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/SocialLoginChecker.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/SocialLoginChecker.java new file mode 100644 index 000000000..4bf868fe6 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/checker/SocialLoginChecker.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.payment.admin.check.checker; + +import cn.daxpay.open.payment.check.checker.AdminConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.platform.iam.dao.social.SocialLoginConfigManager; +import cn.daxpay.open.platform.iam.entity.social.SocialLoginConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/// # 社交登录配置检查器 +/// +/// 检测社交登录平台中, 已启用但未完成参数配置的数量。 +/// 仅统计 `enabled = true && configured = false` 的记录(避免对未启用平台误报)。 +@Component +@Order(5) +@RequiredArgsConstructor +public class SocialLoginChecker implements AdminConfigChecker { + + private final SocialLoginConfigManager socialLoginConfigManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.SOCIAL_LOGIN; + } + + @Override + public ConfigCheckItem check() { + // 统计"已启用但未完成配置"的社交平台数量 + long unconfiguredCount = socialLoginConfigManager.lambdaQuery() + .eq(SocialLoginConfig::getEnabled, true) + .eq(SocialLoginConfig::isConfigured, false) + .count(); + if (unconfiguredCount > 0) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.SOCIAL_LOGIN, + ConfigCheckCategoryEnum.SOCIAL_LOGIN.getCode(), + "configCheck.socialLogin.title", + "configCheck.socialLogin.description", + "/system/config/third-platform" + ).setCount((int) unconfiguredCount); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/AdminConfigCheckService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/AdminConfigCheckService.java new file mode 100644 index 000000000..08cad9be6 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/AdminConfigCheckService.java @@ -0,0 +1,54 @@ +package cn.daxpay.open.payment.admin.check.service; + +import cn.daxpay.open.payment.check.checker.AdminConfigChecker; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.check.model.ConfigCheckResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/// # 运营端配置检查聚合服务 +/// +/// 自动收集所有 [AdminConfigChecker] 实现, 执行检测并聚合结果。 +/// 检测为纯计算无状态, 配置变更后立即生效(无缓存延迟)。 +/// 单个检查器抛异常时降级跳过, 不影响其他维度检测。 +@Slf4j +@Service +@RequiredArgsConstructor +public class AdminConfigCheckService { + + private final List checkers; + + /// 检测平台级未完成配置项 + public ConfigCheckResult check() { + List items = new ArrayList<>(); + Map categoryCounts = new LinkedHashMap<>(); + for (AdminConfigChecker checker : checkers) { + ConfigCheckItem item = safeCheck(checker); + if (item != null) { + items.add(item); + categoryCounts.merge(item.getCategory(), 1, Integer::sum); + } + } + return new ConfigCheckResult() + .setItems(items) + .setTotalCount(items.size()) + .setCategoryCounts(categoryCounts); + } + + /// 单个检查器执行保护: 抛异常时记录日志并返回 null(视为该维度已配置或不可检测) + private ConfigCheckItem safeCheck(AdminConfigChecker checker) { + try { + return checker.check(); + } catch (Exception e) { + log.warn("运营配置检查器执行失败 checker={} error={}", + checker.getClass().getSimpleName(), e.getMessage()); + return null; + } + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/AdminConfigCheckController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/AdminConfigCheckController.java new file mode 100644 index 000000000..72dbe97e6 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/AdminConfigCheckController.java @@ -0,0 +1,34 @@ +package cn.daxpay.open.payment.admin.controller.check; + +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.result.Result; +import cn.daxpay.open.payment.admin.check.service.AdminConfigCheckService; +import cn.daxpay.open.payment.check.model.ConfigCheckResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/// # 配置检查(运营端) +/// +/// 工作台「配置待完成」Widget 数据源。 +/// 不挂菜单权限码: 工作台为登录即达的页面, 任何已认证运营用户均可查看平台级配置完成度。 +@Tag(name = "配置检查(运营端)") +@Validated +@RestController +@RequestMapping("/admin/config-check") +@RequiredArgsConstructor +public class AdminConfigCheckController { + + private final AdminConfigCheckService adminConfigCheckService; + + /// 获取平台级未完成配置项列表(含分类汇总) + @Operation(summary = "获取平台级未完成配置项") + @GetMapping("/items") + public Result items() { + return Res.ok(adminConfigCheckService.check()); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/AdminConfigChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/AdminConfigChecker.java new file mode 100644 index 000000000..4e667433e --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/AdminConfigChecker.java @@ -0,0 +1,22 @@ +package cn.daxpay.open.payment.check.checker; + +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; + +/// # 运营端配置检查器接口 +/// +/// 每个实现类负责一个维度的"平台级配置是否已完成"检测。 +/// 实现类需标注 `@Component`, 由 [AdminConfigCheckService] 自动收集。 +/// 已完成配置时返回 `null`, 未完成时返回 [ConfigCheckItem]。 +/// +/// routeName 契约: [ConfigCheckItem#routeName] 必须等于**运营端前端菜单的 path 字段** +/// (前端 `menu.api.ts` 中 `route.name = menu.path`), 而非 PascalCase 组件名, +/// 否则前端 `router.hasRoute()` 无法命中。可在 `iam_perm_menu.sql` 查 path 真值。 +public interface AdminConfigChecker { + + /// 该检查器负责的分类 + ConfigCheckCategoryEnum getCategory(); + + /// 执行检测 + ConfigCheckItem check(); +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/MerchantConfigChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/MerchantConfigChecker.java new file mode 100644 index 000000000..6383b2e87 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/MerchantConfigChecker.java @@ -0,0 +1,22 @@ +package cn.daxpay.open.payment.check.checker; + +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; + +/// # 商户端配置检查器接口 +/// +/// 每个实现类负责一个维度的"商户级配置是否已完成"检测。 +/// 实现类需标注 `@Component`, 由 [MerchantConfigCheckService] 自动收集。 +/// 已完成配置时返回 `null`, 未完成时返回 [ConfigCheckItem]。 +/// +/// routeName 契约: [ConfigCheckItem#routeName] 必须等于**商户端前端菜单的 path 字段** +/// (商户端前端 `menu.api.ts` 中 `route.name = menu.path`)。 +/// TODO 商户端前端尚未开发, 现有实现下发的 PascalCase 组件名待商户端路由确定后统一对齐。 +public interface MerchantConfigChecker { + + /// 该检查器负责的分类 + ConfigCheckCategoryEnum getCategory(); + + /// 执行检测(按商户号隔离) + ConfigCheckItem check(String mchNo); +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/ChannelMerchantChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/ChannelMerchantChecker.java new file mode 100644 index 000000000..93ccb7a49 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/ChannelMerchantChecker.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.payment.check.checker.merchant; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager; +import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/// # 通道商户检查器 +/// +/// 检测商户下是否存在启用状态的通道商户。无启用通道商户则视为未配置。 +@Component +@RequiredArgsConstructor +public class ChannelMerchantChecker implements MerchantConfigChecker { + + private final ChannelMerchantManager channelMerchantManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.CHANNEL_MERCHANT; + } + + @Override + public ConfigCheckItem check(String mchNo) { + List channels = channelMerchantManager.findAllByMchNo(mchNo); + // 无通道商户 或 无启用的通道商户 => 告警 + long enabledCount = channels.stream() + .filter(ChannelMerchant::getEnable) + .count(); + if (enabledCount == 0) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.CHANNEL_MERCHANT, + ConfigCheckCategoryEnum.CHANNEL_MERCHANT.getCode(), + "configCheck.channelMerchant.title", + "configCheck.channelMerchant.description", + "ChannelMerchant" + ).setCount(channels.size()); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchAppChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchAppChecker.java new file mode 100644 index 000000000..56198de84 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchAppChecker.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.payment.check.checker.merchant; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; +import cn.daxpay.open.payment.merchant.entity.appinfo.MchAppInfo; +import cn.daxpay.open.platform.core.enums.merchant.MchAppStatusEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/// # 商户应用检查器 +/// +/// 检测当前商户下是否存在启用状态的应用。无启用应用则视为未配置。 +@Component +@RequiredArgsConstructor +public class MchAppChecker implements MerchantConfigChecker { + + private final MchAppInfoManager mchAppInfoManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.MCH_APP; + } + + @Override + public ConfigCheckItem check(String mchNo) { + List apps = mchAppInfoManager.findAllByMchNo(mchNo); + // 无任何应用 或 无启用状态的应用 => 告警 + long enabledCount = apps.stream() + .filter(a -> MchAppStatusEnum.ENABLE.getCode().equals(a.getStatus())) + .count(); + if (enabledCount == 0) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.MCH_APP, + ConfigCheckCategoryEnum.MCH_APP.getCode(), + "configCheck.mchApp.title", + "configCheck.mchApp.description", + "MchAppInfo" + ).setCount(apps.isEmpty() ? 0 : (int) (apps.size() - enabledCount)); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchCredentialChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchCredentialChecker.java new file mode 100644 index 000000000..dea338c91 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchCredentialChecker.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.payment.check.checker.merchant; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.merchant.dao.config.MerchantCredentialManager; +import cn.daxpay.open.payment.merchant.entity.config.MerchantCredential; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/// # 商户 API 凭证检查器 +/// +/// 检测商户通信凭证(公钥/密钥)是否已生成。任一为空视为未配置。 +@Component +@RequiredArgsConstructor +public class MchCredentialChecker implements MerchantConfigChecker { + + private final MerchantCredentialManager merchantCredentialManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.MCH_CREDENTIAL; + } + + @Override + public ConfigCheckItem check(String mchNo) { + Optional credential = merchantCredentialManager.findByMchNo(mchNo); + // 无凭证记录 或 公钥/密钥任一为空 => 告警 + boolean unconfigured = credential.isEmpty() + || StrUtil.isBlank(credential.get().getPublicKey()) + || StrUtil.isBlank(credential.get().getSecretKey()); + if (unconfigured) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.MCH_CREDENTIAL, + ConfigCheckCategoryEnum.MCH_CREDENTIAL.getCode(), + "configCheck.mchCredential.title", + "configCheck.mchCredential.description", + "MchCredential" + ); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchNotifyConfigChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchNotifyConfigChecker.java new file mode 100644 index 000000000..f879ddddd --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/MchNotifyConfigChecker.java @@ -0,0 +1,62 @@ +package cn.daxpay.open.payment.check.checker.merchant; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; +import cn.daxpay.open.payment.merchant.dao.config.MchAppNotifyConfigManager; +import cn.daxpay.open.payment.merchant.entity.appinfo.MchAppInfo; +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import cn.daxpay.open.platform.core.enums.merchant.MchAppStatusEnum; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/// # 商户通知配置检查器 +/// +/// 检测商户下启用的应用是否配置了有效的回调通知地址。 +/// 任一启用应用的 notifyUrl 为空或未启用 => 告警。 +@Component +@RequiredArgsConstructor +public class MchNotifyConfigChecker implements MerchantConfigChecker { + + private final MchAppInfoManager mchAppInfoManager; + private final MchAppNotifyConfigManager mchAppNotifyConfigManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.MCH_NOTIFY; + } + + @Override + public ConfigCheckItem check(String mchNo) { + List enabledApps = mchAppInfoManager.findAllByMchNo(mchNo).stream() + .filter(a -> MchAppStatusEnum.ENABLE.getCode().equals(a.getStatus())) + .toList(); + // 无启用应用时由 MchAppChecker 告警, 通知检查器跳过 + if (enabledApps.isEmpty()) { + return null; + } + // 统计启用应用中通知地址未配置或未启用的数量 + long missingCount = enabledApps.stream() + .filter(app -> { + MchAppNotifyConfig cfg = mchAppNotifyConfigManager.findByAppId(app.getAppId()).orElse(null); + return cfg == null + || StrUtil.isBlank(cfg.getNotifyUrl()) + || !Boolean.TRUE.equals(cfg.getStatus()); + }) + .count(); + if (missingCount > 0) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.MCH_NOTIFY, + ConfigCheckCategoryEnum.MCH_NOTIFY.getCode(), + "configCheck.mchNotify.title", + "configCheck.mchNotify.description", + "MchNotifyConfig" + ).setCount((int) missingCount); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/PayRouteChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/PayRouteChecker.java new file mode 100644 index 000000000..8f1ec6fec --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/checker/merchant/PayRouteChecker.java @@ -0,0 +1,56 @@ +package cn.daxpay.open.payment.check.checker.merchant; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; +import cn.daxpay.open.payment.merchant.entity.appinfo.MchAppInfo; +import cn.daxpay.open.payment.route.dao.strategy.PayRouteStrategyManager; +import cn.daxpay.open.payment.route.entity.strategy.PayRouteStrategy; +import cn.daxpay.open.platform.core.enums.merchant.MchAppStatusEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/// # 支付路由检查器 +/// +/// 检测商户下启用的应用是否配置了支付路由策略。 +/// 任一启用应用无路由策略记录 => 告警(深入子表校验由支付运行时承担, 配置态仅校验策略记录存在性)。 +@Component +@RequiredArgsConstructor +public class PayRouteChecker implements MerchantConfigChecker { + + private final MchAppInfoManager mchAppInfoManager; + private final PayRouteStrategyManager payRouteStrategyManager; + + @Override + public ConfigCheckCategoryEnum getCategory() { + return ConfigCheckCategoryEnum.PAY_ROUTE; + } + + @Override + public ConfigCheckItem check(String mchNo) { + List enabledApps = mchAppInfoManager.findAllByMchNo(mchNo).stream() + .filter(a -> MchAppStatusEnum.ENABLE.getCode().equals(a.getStatus())) + .toList(); + // 无启用应用时由 MchAppChecker 告警, 路由检查器跳过 + if (enabledApps.isEmpty()) { + return null; + } + // 统计启用应用中无路由策略记录的数量 + long missingCount = enabledApps.stream() + .filter(app -> payRouteStrategyManager.findByAppId(app.getAppId()).isEmpty()) + .count(); + if (missingCount > 0) { + return ConfigCheckItem.of( + ConfigCheckCategoryEnum.PAY_ROUTE, + ConfigCheckCategoryEnum.PAY_ROUTE.getCode(), + "configCheck.payRoute.title", + "configCheck.payRoute.description", + "PayRouteConfig" + ).setCount((int) missingCount); + } + return null; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckCategoryEnum.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckCategoryEnum.java new file mode 100644 index 000000000..b28019db3 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckCategoryEnum.java @@ -0,0 +1,37 @@ +package cn.daxpay.open.payment.check.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 配置检查分类 +/// +/// 标识一项"配置是否已完成"的维度, 运营端与商户端共用。 +/// `code` 同时作为前端 i18n key 后缀(`configCheck.category.{code}`)。 +@Getter +@RequiredArgsConstructor +public enum ConfigCheckCategoryEnum { + + // ===== 运营端(平台级)配置 ===== + /// 平台站点信息 + PLATFORM_WEBSITE("platformWebsite"), + /// 平台访问地址 + PLATFORM_URL("platformUrl"), + /// 对象存储 + PLATFORM_OSS("platformOss"), + /// 社交登录配置 + SOCIAL_LOGIN("socialLogin"), + + // ===== 商户端配置 ===== + /// 商户应用 + MCH_APP("mchApp"), + /// API 凭证 + MCH_CREDENTIAL("mchCredential"), + /// 通道商户 + CHANNEL_MERCHANT("channelMerchant"), + /// 支付路由 + PAY_ROUTE("payRoute"), + /// 通知配置 + MCH_NOTIFY("mchNotify"); + + private final String code; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckSeverityEnum.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckSeverityEnum.java new file mode 100644 index 000000000..58900296c --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/enums/ConfigCheckSeverityEnum.java @@ -0,0 +1,19 @@ +package cn.daxpay.open.payment.check.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 配置告警严重程度 +/// +/// 当前阶段统一为建议级(SUGGEST), 预留扩展位用于后续划分阻塞级。 +@Getter +@RequiredArgsConstructor +public enum ConfigCheckSeverityEnum { + + /// 建议级: 影响功能完整性, 不配不会立即阻断核心流程 + SUGGEST("suggest"), + /// 阻塞级: 不配置将导致核心能力不可用(预留) + BLOCKER("blocker"); + + private final String code; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckItem.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckItem.java new file mode 100644 index 000000000..e0746c00d --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckItem.java @@ -0,0 +1,50 @@ +package cn.daxpay.open.payment.check.model; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 单个配置检查结果项 +/// +/// 一条"未完成配置"记录。已完成的配置不产生本对象(返回 null)。 +/// i18n 由前端解析, 后端仅回传 key。 +/// `category`/`severity` 以 code 字符串传输, 避免枚举序列化为大写 name()。 +@Data +@Accessors(chain = true) +public class ConfigCheckItem { + + /// 所属分类 code(对应 [cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum#getCode]) + private String category; + + /// 唯一键(用于前端去重/定位, 如 `pay_product:alipay`、`mch_app`) + private String itemKey; + + /// 标题 i18n key(前端 `$t(titleKey)` 解析) + private String titleKey; + + /// 描述 i18n key(前端 `$t(descriptionKey)` 解析) + private String descriptionKey; + + /// 严重程度 code(对应 [cn.daxpay.open.payment.check.enums.ConfigCheckSeverityEnum#getCode]) + private String severity; + + /// 前端路由 name, 点击该项时跳转 + /// 必须等于前端菜单的 path 字段(运营端 menu.api.ts 中 `route.name = menu.path`), + /// 而非组件 defineOptions 的 PascalCase name, 否则 [org.vue.Router#hasRoute] 无法命中 + /// 跳转目标(如 `/system/config/platform`、`/payment/merchant/route`) + private String routeName; + + /// 列表型告警的未配置数量(单项型可留空) + /// 前端用于显示"N 项待配置", 并在跳转列表后聚焦这些条目 + private Integer count; + + public static ConfigCheckItem of(cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum category, + String itemKey, String titleKey, String descriptionKey, String routeName) { + return new ConfigCheckItem() + .setCategory(category.getCode()) + .setItemKey(itemKey) + .setTitleKey(titleKey) + .setDescriptionKey(descriptionKey) + .setRouteName(routeName) + .setSeverity(cn.daxpay.open.payment.check.enums.ConfigCheckSeverityEnum.SUGGEST.getCode()); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckResult.java new file mode 100644 index 000000000..67db9db41 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/model/ConfigCheckResult.java @@ -0,0 +1,33 @@ +package cn.daxpay.open.payment.check.model; + +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; +import java.util.Map; + +/// # 配置检查汇总结果 +/// +/// 聚合所有检查器的输出, 供工作台 Widget 渲染。 +/// `categoryCounts` 用于按分类聚合显示数量; `items` 为明细列表。 +@Data +@Accessors(chain = true) +public class ConfigCheckResult { + + /// 未配置明细列表 + private List items; + + /// 未配置总数 + private int totalCount; + + /// 按分类统计数量(key = category code, value = 数量) + private Map categoryCounts; + + public static ConfigCheckResult empty() { + return new ConfigCheckResult() + .setItems(List.of()) + .setTotalCount(0) + .setCategoryCounts(Map.of()); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/service/MerchantConfigCheckService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/service/MerchantConfigCheckService.java new file mode 100644 index 000000000..167e081b8 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/check/service/MerchantConfigCheckService.java @@ -0,0 +1,68 @@ +package cn.daxpay.open.payment.check.service; + +import cn.daxpay.open.payment.check.checker.MerchantConfigChecker; +import cn.daxpay.open.payment.check.enums.ConfigCheckCategoryEnum; +import cn.daxpay.open.payment.check.model.ConfigCheckItem; +import cn.daxpay.open.payment.check.model.ConfigCheckResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/// # 商户端配置检查聚合服务 +/// +/// 自动收集所有 [MerchantConfigChecker] 实现, 按商户号执行检测并聚合结果。 +/// 检测为纯计算无状态, 配置变更后立即生效(无缓存延迟)。 +/// 单个检查器抛异常时降级跳过, 不影响其他维度检测。 +@Slf4j +@Service +@RequiredArgsConstructor +public class MerchantConfigCheckService { + + private final List checkers; + + /// 检测指定商户的未完成配置项 + public ConfigCheckResult check(String mchNo) { + if (mchNo == null || mchNo.isBlank()) { + return ConfigCheckResult.empty(); + } + List items = new ArrayList<>(); + Map categoryCounts = new LinkedHashMap<>(); + for (MerchantConfigChecker checker : checkers) { + ConfigCheckItem item = safeCheck(checker, mchNo); + if (item != null) { + items.add(item); + categoryCounts.merge(item.getCategory(), 1, Integer::sum); + } + } + return new ConfigCheckResult() + .setItems(items) + .setTotalCount(items.size()) + .setCategoryCounts(categoryCounts); + } + + /// 单个检查器执行保护: 抛异常时记录日志并返回 null(视为该维度已配置或不可检测) + private ConfigCheckItem safeCheck(MerchantConfigChecker checker, String mchNo) { + try { + return checker.check(mchNo); + } catch (Exception e) { + log.warn("商户配置检查器执行失败 mchNo={} checker={} error={}", + mchNo, checker.getClass().getSimpleName(), e.getMessage()); + return null; + } + } + + /// 仅返回未配置总数(供 Header 角标等轻量场景, 避免重复聚合) + public int count(String mchNo) { + return check(mchNo).getTotalCount(); + } + + /// 返回支持的分类列表(供前端分类标签渲染) + public List supportedCategories() { + return checkers.stream().map(MerchantConfigChecker::getCategory).distinct().toList(); + } +} diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/check/MerchantConfigCheckController.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/check/MerchantConfigCheckController.java new file mode 100644 index 000000000..e8240572d --- /dev/null +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/check/MerchantConfigCheckController.java @@ -0,0 +1,37 @@ +package cn.daxpay.open.payment.merchant.controller.check; + +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.result.Result; +import cn.daxpay.open.payment.check.model.ConfigCheckResult; +import cn.daxpay.open.payment.check.service.MerchantConfigCheckService; +import cn.daxpay.open.payment.common.context.PaymentContext; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/// # 配置检查(商户端) +/// +/// 工作台「配置待完成」Widget 数据源。 +/// 商户号由 `MchContextLocalFilter` 从登录用户装载至 [PaymentContext], 此处读取后隔离查询。 +@Tag(name = "配置检查(商户端)") +@Validated +@RestController +@RequestMapping("/mch/config-check") +@RequiredArgsConstructor +public class MerchantConfigCheckController { + + private final PaymentContext paymentContext; + private final MerchantConfigCheckService merchantConfigCheckService; + + /// 获取当前商户的未完成配置项列表(含分类汇总) + @Operation(summary = "获取当前商户的未完成配置项") + @GetMapping("/items") + public Result items() { + String mchNo = paymentContext.getMchNo(); + return Res.ok(merchantConfigCheckService.check(mchNo)); + } +}