feat(check): 配置检查模块, 检测运营/商户未完成配置项

运营端(AdminConfigCheckService)聚合站点信息/访问地址/OSS/社交登录 4 个检查器;
商户端(MerchantConfigCheckService)聚合应用/凭证/通道商户/路由/通知 5 个检查器。
通过 AdminConfigChecker/MerchantConfigChecker 接口扩展, 检测纯计算无状态, 单项失败降级跳过。
This commit is contained in:
DaxPay Dev
2026-07-20 10:25:59 +08:00
parent 0e3236ae9c
commit a95947af5e
19 changed files with 819 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}