mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 14:36:00 +08:00
feat(check): 配置检查模块, 检测运营/商户未完成配置项
运营端(AdminConfigCheckService)聚合站点信息/访问地址/OSS/社交登录 4 个检查器; 商户端(MerchantConfigCheckService)聚合应用/凭证/通道商户/路由/通知 5 个检查器。 通过 AdminConfigChecker/MerchantConfigChecker 接口扩展, 检测纯计算无状态, 单项失败降级跳过。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<AdminConfigChecker> checkers;
|
||||
|
||||
/// 检测平台级未完成配置项
|
||||
public ConfigCheckResult check() {
|
||||
List<ConfigCheckItem> items = new ArrayList<>();
|
||||
Map<String, Integer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ConfigCheckResult> items() {
|
||||
return Res.ok(adminConfigCheckService.check());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<ChannelMerchant> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<MchAppInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<MerchantCredential> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<MchAppInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<MchAppInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<ConfigCheckItem> items;
|
||||
|
||||
/// 未配置总数
|
||||
private int totalCount;
|
||||
|
||||
/// 按分类统计数量(key = category code, value = 数量)
|
||||
private Map<String, Integer> categoryCounts;
|
||||
|
||||
public static ConfigCheckResult empty() {
|
||||
return new ConfigCheckResult()
|
||||
.setItems(List.of())
|
||||
.setTotalCount(0)
|
||||
.setCategoryCounts(Map.of());
|
||||
}
|
||||
}
|
||||
@@ -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<MerchantConfigChecker> checkers;
|
||||
|
||||
/// 检测指定商户的未完成配置项
|
||||
public ConfigCheckResult check(String mchNo) {
|
||||
if (mchNo == null || mchNo.isBlank()) {
|
||||
return ConfigCheckResult.empty();
|
||||
}
|
||||
List<ConfigCheckItem> items = new ArrayList<>();
|
||||
Map<String, Integer> 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<ConfigCheckCategoryEnum> supportedCategories() {
|
||||
return checkers.stream().map(MerchantConfigChecker::getCategory).distinct().toList();
|
||||
}
|
||||
}
|
||||
@@ -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<ConfigCheckResult> items() {
|
||||
String mchNo = paymentContext.getMchNo();
|
||||
return Res.ok(merchantConfigCheckService.check(mchNo));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user