From d6d8c0a8e4412e5a052ceafca260fd42bd477e25 Mon Sep 17 00:00:00 2001 From: daxpay Date: Thu, 30 Jul 2026 13:31:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E6=9C=8D=E5=8A=A1=E5=95=86?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BA=A7=E5=93=81=E7=BB=91=E5=AE=9A=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=A1=86=E6=9E=B6=E4=B8=8E=E5=BE=AE=E4=BF=A1ISV?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增产品级绑定检查通用接口(ProductBindingChecker), 微信ISV首发实现, 检查服务商商户号/API密钥/API证书/默认支付应用4项配置完整性 --- .../isv/WechatIsvProductBindingChecker.java | 88 +++++++++++++++++++ .../service/ProductBindingCheckService.java | 51 +++++++++++ .../check/ProductBindingCheckController.java | 36 ++++++++ .../check/checker/ProductBindingChecker.java | 24 +++++ .../check/model/ProductBindingCheckItem.java | 38 ++++++++ .../model/ProductBindingCheckResult.java | 38 ++++++++ 6 files changed, 275 insertions(+) create mode 100644 daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvProductBindingChecker.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/ProductBindingCheckService.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/ProductBindingCheckController.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/checker/ProductBindingChecker.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckItem.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckResult.java diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvProductBindingChecker.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvProductBindingChecker.java new file mode 100644 index 000000000..36119bac1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvProductBindingChecker.java @@ -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 check() { + // 只读查询密钥配置, 不使用 findByProduct(有 upsert 副作用) + Optional 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" + ) + ); + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/ProductBindingCheckService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/ProductBindingCheckService.java new file mode 100644 index 000000000..9dd0b86a4 --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/check/service/ProductBindingCheckService.java @@ -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 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 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); + } + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/ProductBindingCheckController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/ProductBindingCheckController.java new file mode 100644 index 000000000..bf0fccaab --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/check/ProductBindingCheckController.java @@ -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 check( + @NotBlank(message = "{validation.field.product.notBlank}") String product) { + return Res.ok(productBindingCheckService.check(product)); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/checker/ProductBindingChecker.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/checker/ProductBindingChecker.java new file mode 100644 index 000000000..6444157a4 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/checker/ProductBindingChecker.java @@ -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 check(); +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckItem.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckItem.java new file mode 100644 index 000000000..9482080ae --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckItem.java @@ -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); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckResult.java new file mode 100644 index 000000000..220ffd6ef --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/check/model/ProductBindingCheckResult.java @@ -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 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); + } +}