mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 23:01:58 +08:00
feat(payment): 服务商支付产品绑定检查框架与微信ISV实现
新增产品级绑定检查通用接口(ProductBindingChecker), 微信ISV首发实现, 检查服务商商户号/API密钥/API证书/默认支付应用4项配置完整性
This commit is contained in:
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user