feat(payment): 服务商支付产品绑定检查框架与微信ISV实现

新增产品级绑定检查通用接口(ProductBindingChecker), 微信ISV首发实现,
检查服务商商户号/API密钥/API证书/默认支付应用4项配置完整性
This commit is contained in:
daxpay
2026-07-30 13:31:45 +08:00
parent 339bd59586
commit d6d8c0a8e4
6 changed files with 275 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
package cn.daxpay.open.channel.wechat.service.isv;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvKeyConfigManager;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvKeyConfig;
import cn.daxpay.open.payment.common.check.checker.ProductBindingChecker;
import cn.daxpay.open.payment.common.check.model.ProductBindingCheckItem;
import cn.daxpay.open.payment.wx.service.platform.WxPlatformAppCapabilityService;
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
/// # 微信服务商产品绑定检查器
///
/// 检查微信服务商产品(`wechat_isv`)的4项关键配置完整性:
/// 1. 服务商商户号(wxMchId)
/// 2. API V3密钥(apiKeyV3)
/// 3. API证书(privateKey + certSerialNo)
/// 4. 默认支付应用(产品级平台应用能力绑定, 至少一条)
///
/// 检查为只读操作, 不产生写入副作用。
@Slf4j
@Component
@RequiredArgsConstructor
public class WechatIsvProductBindingChecker implements ProductBindingChecker {
private final WechatIsvKeyConfigManager wechatIsvKeyConfigManager;
private final WxPlatformAppCapabilityService wxPlatformAppCapabilityService;
@Override
public String getProduct() {
return ProductEnum.WECHAT_ISV.getCode();
}
@Override
public List<ProductBindingCheckItem> check() {
// 只读查询密钥配置, 不使用 findByProduct(有 upsert 副作用)
Optional<WechatIsvKeyConfig> configOpt = wechatIsvKeyConfigManager.findByProduct(getProduct());
WechatIsvKeyConfig config = configOpt.orElse(null);
boolean mchIdConfigured = config != null && StrUtil.isNotBlank(config.getWxMchId());
boolean apiKeyConfigured = config != null && StrUtil.isNotBlank(config.getApiKeyV3());
boolean certConfigured = config != null
&& StrUtil.isNotBlank(config.getPrivateKey())
&& StrUtil.isNotBlank(config.getCertSerialNo());
// 产品级默认应用: 至少绑定一个支付能力
boolean defaultAppConfigured = !wxPlatformAppCapabilityService.listByProduct(getProduct()).isEmpty();
return List.of(
// 服务商商户号
ProductBindingCheckItem.of(
"wechatIsv.mchId",
"productBindingCheck.wechatIsv.mchId.title",
"productBindingCheck.wechatIsv.mchId.description",
mchIdConfigured,
"openKeyConfig"
),
// API V3密钥
ProductBindingCheckItem.of(
"wechatIsv.apiKeyV3",
"productBindingCheck.wechatIsv.apiKeyV3.title",
"productBindingCheck.wechatIsv.apiKeyV3.description",
apiKeyConfigured,
"openKeyConfig"
),
// API证书(私钥 + 证书序列号)
ProductBindingCheckItem.of(
"wechatIsv.cert",
"productBindingCheck.wechatIsv.cert.title",
"productBindingCheck.wechatIsv.cert.description",
certConfigured,
"openKeyConfig"
),
// 默认支付应用(产品级平台应用能力绑定)
ProductBindingCheckItem.of(
"wechatIsv.defaultApp",
"productBindingCheck.wechatIsv.defaultApp.title",
"productBindingCheck.wechatIsv.defaultApp.description",
defaultAppConfigured,
"openPlatformCapability"
)
);
}
}

View File

@@ -0,0 +1,51 @@
package cn.daxpay.open.payment.admin.check.service;
import cn.daxpay.open.payment.admin.controller.check.ProductBindingCheckController;
import cn.daxpay.open.payment.common.check.checker.ProductBindingChecker;
import cn.daxpay.open.payment.common.check.model.ProductBindingCheckItem;
import cn.daxpay.open.payment.common.check.model.ProductBindingCheckResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/// # 产品级绑定检查聚合服务
///
/// 自动收集所有 [ProductBindingChecker] 实现, 按支付产品编码路由到对应检查器。
/// 单个检查器抛异常时降级返回空结果, 不影响其他产品。
///
/// 供运营端产品绑定检查 Controller 调用, 前端在进入服务商产品配置页时拉取检查结果。
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductBindingCheckService {
private final List<ProductBindingChecker> checkers;
/// 检查指定产品的配置绑定完整性
public ProductBindingCheckResult check(String product) {
ProductBindingChecker checker = checkers.stream()
.filter(c -> c.getProduct().equals(product))
.findFirst()
.orElse(null);
if (checker == null) {
// 无对应产品的检查器, 返回空结果
return ProductBindingCheckResult.empty(product);
}
try {
List<ProductBindingCheckItem> items = checker.check();
int configuredCount = (int) items.stream().filter(ProductBindingCheckItem::isConfigured).count();
return new ProductBindingCheckResult()
.setProduct(product)
.setItems(items)
.setConfiguredCount(configuredCount)
.setTotalCount(items.size())
.setAllConfigured(configuredCount == items.size());
} catch (Exception e) {
log.warn("产品绑定检查器执行失败 product={} checker={} error={}",
product, checker.getClass().getSimpleName(), e.getMessage());
return ProductBindingCheckResult.empty(product);
}
}
}

View File

@@ -0,0 +1,36 @@
package cn.daxpay.open.payment.admin.controller.check;
import cn.daxpay.open.payment.admin.check.service.ProductBindingCheckService;
import cn.daxpay.open.payment.common.check.model.ProductBindingCheckResult;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/// # 产品绑定检查(运营端)
///
/// 服务商产品配置页的数据源, 展示该产品各项关键配置的绑定状态。
/// 不挂菜单权限码: 与配置检查(运营端)一致, 任何已认证运营用户均可查看产品绑定完成度。
@Tag(name = "产品绑定检查(运营端)")
@Validated
@RestController
@RequestMapping("/admin/product-binding-check")
@RequiredArgsConstructor
public class ProductBindingCheckController {
private final ProductBindingCheckService productBindingCheckService;
/// 检查指定支付产品的配置绑定完整性
@Operation(summary = "检查产品绑定完整性")
@GetMapping("/check")
public Result<ProductBindingCheckResult> check(
@NotBlank(message = "{validation.field.product.notBlank}") String product) {
return Res.ok(productBindingCheckService.check(product));
}
}

View File

@@ -0,0 +1,24 @@
package cn.daxpay.open.payment.common.check.checker;
import cn.daxpay.open.payment.common.check.model.ProductBindingCheckItem;
import java.util.List;
/// # 产品级绑定检查器接口
///
/// 每个实现类负责一个"服务商支付产品"的配置完整性检测。
/// 实现类需标注 `@Component`, 由 [cn.daxpay.open.payment.admin.check.service.ProductBindingCheckService]
/// 自动收集, 按 [#getProduct] 路由到对应产品。
///
/// 与 [AdminConfigChecker] / [MerchantConfigChecker] 的区别:
/// - 前两者是"是否有未完成配置"(返回未完成项, 已完成返回 null)
/// - 本接口是"逐项报告配置状态"(返回全部检查项及其 configured 标记),
/// 供前端在产品配置页展示绑定进度。
public interface ProductBindingChecker {
/// 该检查器负责的支付产品编码(如 `wechat_isv`、`alipay_isv`)
String getProduct();
/// 执行绑定检查, 返回各项配置状态(包含已配置与未配置)
List<ProductBindingCheckItem> check();
}

View File

@@ -0,0 +1,38 @@
package cn.daxpay.open.payment.common.check.model;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 产品绑定检查单项结果
///
/// 描述一项产品级配置的检查结果(如"服务商商户号已配置"、"API证书未配置")。
/// i18n 由前端解析, 后端仅回传 key。
@Data
@Accessors(chain = true)
public class ProductBindingCheckItem {
/// 唯一键(用于前端定位, 如 `wechatIsv.mchId`)
private String itemKey;
/// 标题 i18n key(前端 `$t(titleKey)` 解析)
private String titleKey;
/// 描述 i18n key(前端 `$t(descriptionKey)` 解析)
private String descriptionKey;
/// 是否已配置
private boolean configured;
/// 前端操作标识(用于前端区分跳转目标, 如 `openKeyConfig`、`openPlatformCapability`)
private String action;
public static ProductBindingCheckItem of(String itemKey, String titleKey, String descriptionKey,
boolean configured, String action) {
return new ProductBindingCheckItem()
.setItemKey(itemKey)
.setTitleKey(titleKey)
.setDescriptionKey(descriptionKey)
.setConfigured(configured)
.setAction(action);
}
}

View File

@@ -0,0 +1,38 @@
package cn.daxpay.open.payment.common.check.model;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/// # 产品绑定检查汇总结果
///
/// 聚合单个产品的全部检查项, 供前端展示配置进度条与各项状态。
@Data
@Accessors(chain = true)
public class ProductBindingCheckResult {
/// 支付产品编码
private String product;
/// 检查明细列表(包含已配置与未配置)
private List<ProductBindingCheckItem> items;
/// 已配置项数
private int configuredCount;
/// 总检查项数
private int totalCount;
/// 是否全部已配置
private boolean allConfigured;
public static ProductBindingCheckResult empty(String product) {
return new ProductBindingCheckResult()
.setProduct(product)
.setItems(List.of())
.setConfiguredCount(0)
.setTotalCount(0)
.setAllConfigured(false);
}
}