mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 07:25:39 +08:00
feat(adapay): 新增服务商密钥配置与微信应用绑定支持, 微信应用绑定能力收窄下沉产品策略声明
This commit is contained in:
@@ -27,6 +27,17 @@ MAXVALUE 9223372036854775807
|
||||
START 1
|
||||
CACHE 1;
|
||||
|
||||
-- ----------------------------
|
||||
-- Sequence structure for adapay_isv_key_config_id_seq
|
||||
-- ----------------------------
|
||||
DROP SEQUENCE IF EXISTS "public"."adapay_isv_key_config_id_seq";
|
||||
CREATE SEQUENCE "public"."adapay_isv_key_config_id_seq"
|
||||
INCREMENT 1
|
||||
MINVALUE 1
|
||||
MAXVALUE 9223372036854775807
|
||||
START 1
|
||||
CACHE 1;
|
||||
|
||||
-- ----------------------------
|
||||
-- Sequence structure for alipay_direct_app_capability_id_seq
|
||||
-- ----------------------------
|
||||
@@ -122,12 +133,12 @@ CREATE TABLE "public"."adapay_direct_key_config" (
|
||||
"last_modifier" int8,
|
||||
"last_modified_time" timestamptz(6),
|
||||
"version" int4 DEFAULT 0,
|
||||
"deleted" bool DEFAULT false
|
||||
"deleted" bool DEFAULT false,
|
||||
"sandbox" bool DEFAULT false
|
||||
)
|
||||
;
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."mch_no" IS '商户号';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."channel_mch_no" IS '通道商户号(创建时录入不可修改)';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."merchant_no" IS 'Adapay 商户号(创建时录入不可修改)';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."adapay_app_id" IS 'Adapay 应用ID(app_id)';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."api_key" IS 'Adapay API Key(请求头Authorization, 加密存储)';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."private_key" IS '商户RSA私钥(PKCS#8 Base64, 请求签名, 加密存储)';
|
||||
@@ -138,8 +149,42 @@ COMMENT ON COLUMN "public"."adapay_direct_key_config"."last_modifier" IS '最后
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."last_modified_time" IS '最后修改时间';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."version" IS '版本号(乐观锁)';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."deleted" IS '删除标志';
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."sandbox" IS '是否沙箱环境';
|
||||
COMMENT ON TABLE "public"."adapay_direct_key_config" IS 'Adapay 直连密钥配置';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for adapay_isv_key_config
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS "public"."adapay_isv_key_config";
|
||||
CREATE TABLE "public"."adapay_isv_key_config" (
|
||||
"id" int8 NOT NULL DEFAULT nextval('adapay_isv_key_config_id_seq'::regclass),
|
||||
"isv_no" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"api_key" text COLLATE "pg_catalog"."default",
|
||||
"private_key" text COLLATE "pg_catalog"."default",
|
||||
"public_key" text COLLATE "pg_catalog"."default",
|
||||
"creator" int8,
|
||||
"create_time" timestamptz(6),
|
||||
"last_modifier" int8,
|
||||
"last_modified_time" timestamptz(6),
|
||||
"version" int4 DEFAULT 0,
|
||||
"deleted" bool DEFAULT false,
|
||||
"sandbox" bool DEFAULT false
|
||||
)
|
||||
;
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."id" IS '主键';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."isv_no" IS '服务商号(平台在汇付的服务商/主体编号)';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."api_key" IS 'Adapay 交易密钥(请求头Authorization, 加密存储)';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."private_key" IS '商户RSA私钥(PKCS#8 Base64, 请求签名, 加密存储)';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."public_key" IS 'Adapay 平台公钥(X509 Base64, 响应验签, 加密存储; 为空使用全局默认)';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."creator" IS '创建者ID';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."create_time" IS '创建时间';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."last_modifier" IS '最后修改者ID';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."last_modified_time" IS '最后修改时间';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."version" IS '版本号(乐观锁)';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."deleted" IS '删除标志';
|
||||
COMMENT ON COLUMN "public"."adapay_isv_key_config"."sandbox" IS '是否沙箱环境';
|
||||
COMMENT ON TABLE "public"."adapay_isv_key_config" IS 'Adapay 服务商密钥配置';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for alipay_direct_app
|
||||
-- ----------------------------
|
||||
@@ -4118,6 +4163,13 @@ ALTER SEQUENCE "public"."adapay_direct_key_config_id_seq"
|
||||
OWNED BY "public"."adapay_direct_key_config"."id";
|
||||
SELECT setval('"public"."adapay_direct_key_config_id_seq"', 1, false);
|
||||
|
||||
-- ----------------------------
|
||||
-- Alter sequences owned by
|
||||
-- ----------------------------
|
||||
ALTER SEQUENCE "public"."adapay_isv_key_config_id_seq"
|
||||
OWNED BY "public"."adapay_isv_key_config"."id";
|
||||
SELECT setval('"public"."adapay_isv_key_config_id_seq"', 1, false);
|
||||
|
||||
-- ----------------------------
|
||||
-- Alter sequences owned by
|
||||
-- ----------------------------
|
||||
@@ -4167,11 +4219,33 @@ ALTER SEQUENCE "public"."pay_sync_record_id_seq"
|
||||
OWNED BY "public"."pay_sync_record"."id";
|
||||
SELECT setval('"public"."pay_sync_record_id_seq"', 1, false);
|
||||
|
||||
-- ----------------------------
|
||||
-- Indexes structure for table adapay_direct_key_config
|
||||
-- ----------------------------
|
||||
CREATE UNIQUE INDEX "uk_adapay_direct_key_config_sandbox" ON "public"."adapay_direct_key_config" USING btree (
|
||||
"channel_mch_no" "pg_catalog"."text_ops" ASC NULLS LAST,
|
||||
"sandbox" "pg_catalog"."bool_ops" ASC NULLS LAST
|
||||
) WHERE deleted = false;
|
||||
COMMENT ON INDEX "public"."uk_adapay_direct_key_config_sandbox" IS '同一通道商户同一环境密钥唯一';
|
||||
|
||||
-- ----------------------------
|
||||
-- Indexes structure for table adapay_isv_key_config
|
||||
-- ----------------------------
|
||||
CREATE UNIQUE INDEX "uk_adapay_isv_key_config_sandbox" ON "public"."adapay_isv_key_config" USING btree (
|
||||
"sandbox" "pg_catalog"."bool_ops" ASC NULLS LAST
|
||||
) WHERE deleted = false;
|
||||
COMMENT ON INDEX "public"."uk_adapay_isv_key_config_sandbox" IS '同一环境服务商配置唯一(平台为唯一服务商)';
|
||||
|
||||
-- ----------------------------
|
||||
-- Primary Key structure for table adapay_direct_key_config
|
||||
-- ----------------------------
|
||||
ALTER TABLE "public"."adapay_direct_key_config" ADD CONSTRAINT "adapay_direct_key_config_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- ----------------------------
|
||||
-- Primary Key structure for table adapay_isv_key_config
|
||||
-- ----------------------------
|
||||
ALTER TABLE "public"."adapay_isv_key_config" ADD CONSTRAINT "adapay_isv_key_config_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- ----------------------------
|
||||
-- Primary Key structure for table alipay_direct_app
|
||||
-- ----------------------------
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
-- ============================
|
||||
-- 修正 adapay 产品沙箱支持标志
|
||||
-- 种子数据 pay_md_product.sandbox=false 与 AdapayDirectProductStrategy.isSandbox()=true 声明不一致,
|
||||
-- 导致通道商户列表"环境状态"列显示为"-"。ChannelMerchantService 已改为统一读取产品策略, 此处同步修正存量数据。
|
||||
-- ============================
|
||||
UPDATE pay_md_product SET sandbox = true WHERE code = 'ada_pay';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- ========================================
|
||||
-- adapay_direct_key_config 直连密钥配置结构调整
|
||||
-- 开发阶段: Adapay 商户身份由 app_id + apiKey + RSA 私钥承载
|
||||
-- 无需独立商户号字段, 移除 merchant_no; 新增 sandbox 沙箱环境列与唯一索引
|
||||
-- ========================================
|
||||
|
||||
-- 1. 删除 Adapay 商户号列(身份由 app_id 承载; 该列旧版仅存商户身份快照, 直接删除)
|
||||
ALTER TABLE "public"."adapay_direct_key_config" DROP COLUMN IF EXISTS "merchant_no";
|
||||
|
||||
-- 2. 新增沙箱环境列(默认非沙箱, 双环境并存: 同一通道商户同一环境密钥唯一)
|
||||
ALTER TABLE "public"."adapay_direct_key_config" ADD COLUMN IF NOT EXISTS "sandbox" bool DEFAULT false;
|
||||
COMMENT ON COLUMN "public"."adapay_direct_key_config"."sandbox" IS '是否沙箱环境';
|
||||
|
||||
-- 3. 唯一索引: 同一通道商户同一环境密钥唯一(存量数据存在重复时建索引会失败, 需先清理)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "uk_adapay_direct_key_config_sandbox" ON "public"."adapay_direct_key_config" USING btree (
|
||||
"channel_mch_no" "pg_catalog"."text_ops" ASC NULLS LAST,
|
||||
"sandbox" "pg_catalog"."bool_ops" ASC NULLS LAST
|
||||
) WHERE deleted = false;
|
||||
COMMENT ON INDEX "public"."uk_adapay_direct_key_config_sandbox" IS '同一通道商户同一环境密钥唯一';
|
||||
|
||||
@@ -22,6 +22,9 @@ public class AdapaySdkCredential {
|
||||
/// Adapay 平台公钥(X509 Base64, 响应验签; 为空时子应用用全局默认公钥)
|
||||
private String publicKey;
|
||||
|
||||
/// 微信应用 AppId(微信 JSAPI/APP/小程序支付上送 wx_app_id, 其余场景为空)
|
||||
private String wxAppId;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ public class AdapayPayReq {
|
||||
/// 用户标识(微信 openId / 支付宝 buyerId, JSAPI/小程序必填)
|
||||
private String openId;
|
||||
|
||||
/// 微信应用 AppId(微信 JSAPI/APP/小程序支付上送 wx_app_id)
|
||||
private String wxAppId;
|
||||
|
||||
/// 付款码(条码支付必填)
|
||||
private String authCode;
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.channel.adapay.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.convert.isv.AdapayIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.adapay.param.isv.AdapayIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.adapay.result.isv.AdapayIsvKeyConfigResult;
|
||||
import cn.daxpay.open.channel.adapay.service.isv.AdapayIsvKeyConfigService;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.code.PermCodes;
|
||||
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.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// # Adapay 服务商密钥配置
|
||||
///
|
||||
/// 平台为唯一服务商, 同一环境(生产/沙箱)仅一条配置, 与通道商户直连密钥配置并存。
|
||||
@PermCode(menuCode = PermCodes.Payment.Isv.MENU)
|
||||
@Validated
|
||||
@Tag(name = "Adapay 服务商密钥配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/adapay/isv-key-config")
|
||||
@RequiredArgsConstructor
|
||||
public class AdapayIsvKeyConfigController {
|
||||
|
||||
private final AdapayIsvKeyConfigService adapayIsvKeyConfigService;
|
||||
|
||||
@PermCode(code = PermCodes.Action.VIEW)
|
||||
@Operation(summary = "查询Adapay 服务商密钥配置")
|
||||
@GetMapping("/find-config")
|
||||
public Result<AdapayIsvKeyConfigResult> findConfig(
|
||||
@NotNull(message = "{validation.field.sandbox.notNull}") Boolean sandbox) {
|
||||
return Res.ok(AdapayIsvKeyConfigConvert.CONVERT.toResult(adapayIsvKeyConfigService.findBySandbox(sandbox)));
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.MANAGE)
|
||||
@Operation(summary = "保存Adapay 服务商密钥配置")
|
||||
@PostMapping("/save-config")
|
||||
public Result<Void> saveConfig(@RequestBody @Validated AdapayIsvKeyConfigParam param) {
|
||||
adapayIsvKeyConfigService.saveConfig(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.daxpay.open.channel.adapay.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.entity.isv.AdapayIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.adapay.param.isv.AdapayIsvKeyConfigParam;
|
||||
import cn.daxpay.open.channel.adapay.result.isv.AdapayIsvKeyConfigResult;
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # Adapay 服务商密钥配置转换
|
||||
@Mapper
|
||||
public interface AdapayIsvKeyConfigConvert {
|
||||
|
||||
AdapayIsvKeyConfigConvert CONVERT = Mappers.getMapper(AdapayIsvKeyConfigConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
AdapayIsvKeyConfigResult toResult(AdapayIsvKeyConfig entity);
|
||||
|
||||
/// 转换为实体
|
||||
AdapayIsvKeyConfig toEntity(AdapayIsvKeyConfigParam param);
|
||||
|
||||
/// 更新源数据到实体(空值不覆盖, 密钥为空时保留原值)
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void copy(AdapayIsvKeyConfigParam param, @MappingTarget AdapayIsvKeyConfig entity);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.daxpay.open.channel.adapay.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.entity.isv.AdapayIsvKeyConfig;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/// # Adapay 服务商密钥配置 Manager
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AdapayIsvKeyConfigManager extends BaseManager<AdapayIsvKeyConfigMapper, AdapayIsvKeyConfig> {
|
||||
|
||||
/// 根据沙箱环境查询(平台为唯一服务商, 同一环境仅一条配置)
|
||||
public Optional<AdapayIsvKeyConfig> findBySandbox(boolean sandbox) {
|
||||
return lambdaQuery()
|
||||
.eq(AdapayIsvKeyConfig::getSandbox, sandbox)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.daxpay.open.channel.adapay.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.entity.isv.AdapayIsvKeyConfig;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # Adapay 服务商密钥配置 Mapper
|
||||
@Mapper
|
||||
public interface AdapayIsvKeyConfigMapper extends MPJBaseMapper<AdapayIsvKeyConfig> {
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import lombok.experimental.Accessors;
|
||||
/// 直连商户维度的通道配置, 含Adapay 支付应用 ID 与签名密钥, 敏感字段(apiKey/privateKey/publicKey)加密存储。
|
||||
/// channelMchNo(通道商户号) 创建时录入不可修改, adapayAppId/apiKey/privateKey/publicKey 由密钥配置维护。
|
||||
/// Adapay 签名算法: SHA1withRSA, 私钥签名 / 平台公钥验签, 无需证书。
|
||||
/// Adapay 商户身份由 app_id + apiKey(Authorization) + RSA 私钥签名承载, 无需单独的商户号字段。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@@ -27,10 +28,6 @@ public class AdapayDirectKeyConfig extends MchBaseEntity implements ToResult<Ada
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String channelMchNo;
|
||||
|
||||
/// Adapay 商户号(创建时录入不可修改)
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String merchantNo;
|
||||
|
||||
/// Adapay 支付应用 ID(app_id)
|
||||
private String adapayAppId;
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.daxpay.open.channel.adapay.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.convert.isv.AdapayIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.adapay.result.isv.AdapayIsvKeyConfigResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # Adapay 服务商密钥配置
|
||||
///
|
||||
/// Adapay(汇付天下)为服务商性质通道: 平台作为唯一服务商, 按环境(生产/沙箱)各保存一份服务商密钥。
|
||||
/// 与通道商户直连配置([cn.daxpay.open.channel.adapay.entity.direct.AdapayDirectKeyConfig])并存:
|
||||
/// 直连配置承载每个通道商户独立的支付应用 ID 与签名密钥, 服务商配置承载平台服务商主体身份,
|
||||
/// 供服务商台账 / 进件等场景使用。
|
||||
///
|
||||
/// 签名算法: SHA1withRSA, 私钥签名 / 平台公钥验签, 无需证书。
|
||||
/// 敏感字段(apiKey/privateKey/publicKey)通过 [DataEncryptTypeHandler] 加密入库。
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "adapay_isv_key_config", autoResultMap = true)
|
||||
public class AdapayIsvKeyConfig extends MpBaseEntity implements ToResult<AdapayIsvKeyConfigResult> {
|
||||
|
||||
/// 服务商号(平台在汇付的服务商/主体编号)
|
||||
private String isvNo;
|
||||
|
||||
/// Adapay 交易密钥(请求头 Authorization, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String apiKey;
|
||||
|
||||
/// 商户 RSA 私钥(PKCS#8 Base64, 请求签名, 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String privateKey;
|
||||
|
||||
/// Adapay 平台公钥(X509 Base64, 响应验签, 加密存储; 为空时使用全局默认公钥)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String publicKey;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public AdapayIsvKeyConfigResult toResult() {
|
||||
return AdapayIsvKeyConfigConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,4 @@ public class AdapayDirectChannelMerchantCreateParam {
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
@Schema(description = "所属支付产品")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "{validation.field.merchantNo.notBlank}")
|
||||
@Schema(description = "Adapay 商户号")
|
||||
private String merchantNo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.daxpay.open.channel.adapay.param.isv;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # Adapay 服务商密钥配置保存参数
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "Adapay 服务商密钥配置保存参数")
|
||||
public class AdapayIsvKeyConfigParam {
|
||||
|
||||
@NotBlank(message = "{validation.field.isvNo.notBlank}")
|
||||
@Schema(description = "服务商号(平台在汇付的服务商/主体编号)")
|
||||
private String isvNo;
|
||||
|
||||
@Schema(description = "Adapay 交易密钥(加密存储)")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "商户RSA私钥(PKCS#8 Base64, 加密存储)")
|
||||
private String privateKey;
|
||||
|
||||
@Schema(description = "Adapay 平台公钥(X509 Base64, 加密存储; 为空使用全局默认)")
|
||||
private String publicKey;
|
||||
|
||||
@NotNull(message = "{validation.field.sandbox.notNull}")
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -15,9 +15,6 @@ public class AdapayDirectKeyConfigResult {
|
||||
/// 是否沙箱环境
|
||||
private Boolean sandbox;
|
||||
|
||||
/// Adapay 商户号
|
||||
private String merchantNo;
|
||||
|
||||
/// Adapay 支付应用 ID
|
||||
private String adapayAppId;
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.daxpay.open.channel.adapay.result.isv;
|
||||
|
||||
import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo;
|
||||
import cn.daxpay.open.platform.core.result.BaseResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # Adapay 服务商密钥配置结果
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "Adapay 服务商密钥配置")
|
||||
public class AdapayIsvKeyConfigResult extends BaseResult {
|
||||
|
||||
/// 服务商号(平台在汇付的服务商/主体编号)
|
||||
@Schema(description = "服务商号")
|
||||
private String isvNo;
|
||||
|
||||
/// Adapay 交易密钥(脱敏返回)
|
||||
@SensitiveInfo(front = 6, end = 6)
|
||||
@Schema(description = "Adapay 交易密钥(脱敏返回)")
|
||||
private String apiKey;
|
||||
|
||||
/// 商户 RSA 私钥(脱敏返回)
|
||||
@SensitiveInfo(front = 24, end = 24)
|
||||
@Schema(description = "商户RSA私钥(脱敏返回)")
|
||||
private String privateKey;
|
||||
|
||||
/// Adapay 平台公钥(脱敏返回)
|
||||
@SensitiveInfo(front = 24, end = 24)
|
||||
@Schema(description = "Adapay 平台公钥(脱敏返回)")
|
||||
private String publicKey;
|
||||
|
||||
/// 是否沙箱环境
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private Boolean sandbox;
|
||||
}
|
||||
@@ -53,10 +53,10 @@ public class AdapayDirectChannelMerchantService {
|
||||
boolean sandbox = payProductConfigManager.isSandboxActive(param.getProduct());
|
||||
channelMerchant.setSandbox(sandbox);
|
||||
channelMerchantManager.save(channelMerchant);
|
||||
// 写Adapay 直连配置(merchantNo 创建时录入不可修改, adapayAppId/apiKey/privateKey/publicKey 由密钥配置单独维护)
|
||||
// 写Adapay 直连配置(adapayAppId/apiKey/privateKey/publicKey 由密钥配置单独维护)
|
||||
var keyConfig = new AdapayDirectKeyConfig()
|
||||
.setChannelMchNo(channelMchNo)
|
||||
.setMerchantNo(param.getMerchantNo());
|
||||
.setSandbox(sandbox);
|
||||
keyConfig.setMchNo(param.getMchNo());
|
||||
adapayDirectKeyConfigManager.save(keyConfig);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,13 @@ package cn.daxpay.open.channel.adapay.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.client.credential.AdapaySdkCredential;
|
||||
import cn.daxpay.open.channel.adapay.entity.direct.AdapayDirectKeyConfig;
|
||||
import cn.daxpay.open.channel.adapay.strategy.product.AdapayDirectProductStrategy;
|
||||
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
|
||||
import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppFacade;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppView;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -18,7 +23,10 @@ import org.springframework.stereotype.Service;
|
||||
/// 沙箱标识直接读通道商户固化的 [ChannelMerchant#isSandbox]
|
||||
/// (创建时按当时产品 activeEnv 写入, 不随产品切换改变), 据此选择对应环境的密钥与网关地址。
|
||||
///
|
||||
/// Adapay 为聚合支付, 不区分应用能力(capability 参数保留但不使用)。
|
||||
/// Adapay 为聚合支付, 不区分应用能力(capability 参数保留但不使用);
|
||||
/// 微信 JSAPI/小程序支付需上送 wx_app_id, 通过 [WxAppFacade] 解析
|
||||
/// (商户档优先, 平台档兜底), 需要解析的能力范围由产品策略
|
||||
/// [AdapayDirectProductStrategy#wxAppRequiredCapabilities] 声明。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,12 +34,14 @@ public class AdapayDirectConfigAssembler {
|
||||
|
||||
private final AdapayDirectKeyConfigService keyConfigService;
|
||||
private final ChannelMerchantManager channelMerchantManager;
|
||||
private final WxAppFacade wxAppFacade;
|
||||
private final AdapayDirectProductStrategy productStrategy;
|
||||
|
||||
/// 组装直连商户的通道调用凭证(下发给子应用)
|
||||
///
|
||||
/// @param mchNo 商户号(定位通用通道商户主表)
|
||||
/// @param channelMchNo 通道商户号(定位直连配置)
|
||||
/// @param capability 支付能力编码(Adapay 不使用, 保留对齐签名)
|
||||
/// @param capability 支付能力编码(产品策略声明需要绑定微信应用的能力时解析微信应用, 其余能力不使用)
|
||||
/// @return Adapay SDK 凭证
|
||||
public AdapaySdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
|
||||
// 1. 通用通道商户主表(取 sandbox 固化标识)
|
||||
@@ -50,6 +60,14 @@ public class AdapayDirectConfigAssembler {
|
||||
credential.setPrivateKey(keyConfig.getPrivateKey());
|
||||
credential.setPublicKey(keyConfig.getPublicKey());
|
||||
credential.setSandbox(sandbox);
|
||||
|
||||
// 5. 产品策略声明需要绑定微信应用的能力(JSAPI/小程序)需上送 wx_app_id, 解析商户/平台级微信应用
|
||||
// (sync/close/refund 场景 capability 为空, 跳过解析不影响凭证复用)
|
||||
PayCapabilityEnum cap = PayCapabilityEnum.findByCode(capability);
|
||||
if (cap != null && productStrategy.wxAppRequiredCapabilities().contains(cap)) {
|
||||
WxAppView app = wxAppFacade.resolve(mchNo, channelMchNo, capability, null, ProductEnum.ADA_PAY.getCode());
|
||||
credential.setWxAppId(app.wxAppId());
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.daxpay.open.channel.adapay.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.adapay.convert.isv.AdapayIsvKeyConfigConvert;
|
||||
import cn.daxpay.open.channel.adapay.dao.isv.AdapayIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.adapay.entity.isv.AdapayIsvKeyConfig;
|
||||
import cn.daxpay.open.channel.adapay.param.isv.AdapayIsvKeyConfigParam;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// # Adapay 服务商密钥配置
|
||||
///
|
||||
/// 管理服务商密钥配置, 查询时不存在则创建默认记录(平台为唯一服务商, 同一环境仅一条)。
|
||||
/// 服务商配置与通道商户直连密钥配置([cn.daxpay.open.channel.adapay.entity.direct.AdapayDirectKeyConfig])并存,
|
||||
/// 本配置承载平台服务商主体身份(服务商号/交易密钥/签名密钥), 供服务商台账与进件等场景使用。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AdapayIsvKeyConfigService {
|
||||
|
||||
private final AdapayIsvKeyConfigManager adapayIsvKeyConfigManager;
|
||||
|
||||
/// 根据沙箱环境查询密钥配置, 不存在则创建默认记录
|
||||
///
|
||||
/// @param sandbox 沙箱标志(生产/沙箱双环境并存, 按环境分别存一份服务商配置)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AdapayIsvKeyConfig findBySandbox(boolean sandbox) {
|
||||
var existing = adapayIsvKeyConfigManager.findBySandbox(sandbox);
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
var config = new AdapayIsvKeyConfig().setSandbox(sandbox);
|
||||
adapayIsvKeyConfigManager.save(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 保存服务商密钥配置(更新, 空值不覆盖)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(AdapayIsvKeyConfigParam param) {
|
||||
boolean sandbox = Boolean.TRUE.equals(param.getSandbox());
|
||||
var config = this.findBySandbox(sandbox);
|
||||
AdapayIsvKeyConfigConvert.CONVERT.copy(param, config);
|
||||
adapayIsvKeyConfigManager.updateById(config);
|
||||
}
|
||||
|
||||
/// 台账/进件场景查询服务商密钥(必填校验, 不创建记录)
|
||||
///
|
||||
/// 记录不存在或关键字段(isvNo/apiKey/privateKey)任一为空时 fail-fast,
|
||||
/// 避免空凭证下发到子应用后子应用才发现问题。
|
||||
public AdapayIsvKeyConfig getForPay(boolean sandbox) {
|
||||
AdapayIsvKeyConfig config = adapayIsvKeyConfigManager.findBySandbox(sandbox)
|
||||
// Adapay: 服务商密钥未配置
|
||||
.orElseThrow(() -> new BizInfoException("error.channel.adapay.isvKeyNotConfigured"));
|
||||
if (StrUtil.hasBlank(config.getIsvNo(), config.getApiKey(), config.getPrivateKey())) {
|
||||
throw new BizInfoException("error.channel.adapay.isvKeyNotConfigured");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ public class AdapayPayService {
|
||||
req.setAuthCode(payParam.getAuthCode());
|
||||
req.setClientIp(payParam.getClientIp());
|
||||
req.setNotifyUrl(this.buildNotifyUrl(order, payParam.getChannelMchNo()));
|
||||
// 微信 JSAPI/APP/小程序支付上送 wx_app_id(由配置组装阶段解析, 其余场景为空)
|
||||
req.setWxAppId(credential.getWxAppId());
|
||||
req.setCredential(credential);
|
||||
|
||||
// 调用子应用
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/// # Adapay 直连支付产品策略
|
||||
///
|
||||
@@ -88,4 +89,11 @@ public class AdapayDirectProductStrategy extends AbsProductStrategy {
|
||||
public Map<PayMethodEnum, List<PayCapabilityEnum>> methodCapabilityMapping() {
|
||||
return METHOD_CAP_MAP;
|
||||
}
|
||||
|
||||
/// Adapay 聚合通道: 微信侧仅 JSAPI/小程序支付需要 appid(上送 wx_app_id),
|
||||
/// 其余微信能力(扫码/H5/APP/付款码)无需绑定应用, 收窄弹窗候选与支付链路的解析范围
|
||||
@Override
|
||||
public Set<PayCapabilityEnum> wxAppRequiredCapabilities() {
|
||||
return Set.of(PayCapabilityEnum.WECHAT_JSAPI, PayCapabilityEnum.WECHAT_MINI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,18 @@ import cn.daxpay.open.channel.leshua.client.enums.LeshuaPayBodyType;
|
||||
import cn.daxpay.open.channel.leshua.client.enums.LeshuaPayMethod;
|
||||
import cn.daxpay.open.channel.leshua.client.req.LeshuaPayReq;
|
||||
import cn.daxpay.open.channel.leshua.client.resp.LeshuaPayResp;
|
||||
import cn.daxpay.open.channel.leshua.strategy.product.LeshuaProductStrategy;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.strategy.ProductStrategySupport;
|
||||
import cn.daxpay.open.payment.trade.runtime.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppFacade;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppView;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.unipay.PayBodyTypeEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.system.service.config.infra.PlatformUrlConfigService;
|
||||
@@ -33,6 +38,8 @@ public class LeshuaPayService {
|
||||
|
||||
private final LeshuaChannelClient leshuaChannelClient;
|
||||
private final PlatformUrlConfigService platformUrlConfigService;
|
||||
private final WxAppFacade wxAppFacade;
|
||||
private final LeshuaProductStrategy productStrategy;
|
||||
|
||||
/// 执行乐刷支付
|
||||
public PayTradeResultBo pay(PayTrade order, NormalPayParam payParam, LeshuaSdkCredential credential) {
|
||||
@@ -56,6 +63,15 @@ public class LeshuaPayService {
|
||||
req.setClientIp(payParam.getClientIp());
|
||||
req.setNotifyUrl(this.buildNotifyUrl(order, payParam.getChannelMchNo()));
|
||||
req.setCredential(credential);
|
||||
// 微信 JSAPI/小程序支付需 appid(乐刷 get_tdcode WXZF 场景, 拉起微信收银台);
|
||||
// 通过微信开放应用门面解析商户/平台级微信应用(通道能力绑 → 产品级默认绑 → 平台应用推导),
|
||||
// 付款码支付据 authCode 自动识别底层渠道, 无需 appid
|
||||
if (isWechatJsapiOrMini(payParam.getMethod())) {
|
||||
WxAppView wxApp = wxAppFacade.resolve(
|
||||
order.getMchNo(), payParam.getChannelMchNo(),
|
||||
payParam.getCapability(), payParam.getChannelAppId(), ProductEnum.LESHUA_PAY.getCode());
|
||||
req.setWxAppId(wxApp.wxAppId());
|
||||
}
|
||||
|
||||
// 调用子应用
|
||||
DaxResult<LeshuaPayResp> result = leshuaChannelClient.pay(req);
|
||||
@@ -78,6 +94,14 @@ public class LeshuaPayService {
|
||||
base, order.getMchNo(), channelMchNo);
|
||||
}
|
||||
|
||||
/// 是否微信 JSAPI/小程序支付(乐刷 get_tdcode WXZF, 需要 appid 拉起微信收银台;
|
||||
/// 判定范围由产品策略 [LeshuaProductStrategy#wxAppRequiredCapabilities] 声明)
|
||||
private boolean isWechatJsapiOrMini(String methodCode) {
|
||||
PayMethodEnum method = PayMethodEnum.findByCode(methodCode);
|
||||
return ProductStrategySupport.capabilitiesForMethod(productStrategy, method).stream()
|
||||
.anyMatch(productStrategy.wxAppRequiredCapabilities()::contains);
|
||||
}
|
||||
|
||||
/// 平台支付方式([PayMethodEnum] code) → 乐刷三要素
|
||||
private static MethodMapping mapMethod(String methodCode) {
|
||||
PayMethodEnum m = PayMethodEnum.findByCode(methodCode);
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/// # 乐刷支付产品策略
|
||||
///
|
||||
@@ -72,4 +73,11 @@ public class LeshuaProductStrategy extends AbsProductStrategy {
|
||||
public Map<PayMethodEnum, List<PayCapabilityEnum>> methodCapabilityMapping() {
|
||||
return METHOD_CAP_MAP;
|
||||
}
|
||||
|
||||
/// 乐刷聚合服务商: 微信侧仅 JSAPI/小程序支付需要 appid(拉起微信收银台),
|
||||
/// 其余微信能力(扫码/付款码等)无需绑定应用, 收窄弹窗候选与支付链路的解析范围
|
||||
@Override
|
||||
public Set<PayCapabilityEnum> wxAppRequiredCapabilities() {
|
||||
return Set.of(PayCapabilityEnum.WECHAT_JSAPI, PayCapabilityEnum.WECHAT_MINI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/// # 产品策略抽象类
|
||||
///
|
||||
@@ -32,6 +33,14 @@ public abstract class AbsProductStrategy implements PaymentStrategy {
|
||||
/// 目录支付方式 → 本产品在该方式下可用的支付能力列表(发版契约,对齐 PayMethodEnum / PayCapabilityEnum)
|
||||
public abstract Map<PayMethodEnum, List<PayCapabilityEnum>> methodCapabilityMapping();
|
||||
|
||||
/// 需要绑定微信应用(解析 wxAppId)的支付能力集
|
||||
///
|
||||
/// 返回 null 表示该产品所有微信支付能力均可绑定应用(不限制);
|
||||
/// 聚合类产品(如 Adapay/乐刷)仅部分微信能力需要 appid, 覆写返回收窄子集,
|
||||
/// 弹窗候选([WxPlatformAppCapabilityService#listSupportedCapabilities])与
|
||||
/// 支付链路(通道 Assembler/PayService)统一读取此声明。
|
||||
public Set<PayCapabilityEnum> wxAppRequiredCapabilities() { return null; }
|
||||
|
||||
/// 基础模式通道路由可用的支付渠道列表(各产品策略必须显式声明)
|
||||
public abstract List<PayProviderEnum> supportedPayProviders();
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ package cn.daxpay.open.payment.wx.service;
|
||||
|
||||
import cn.daxpay.open.payment.wx.dao.channel.WxChannelAppCapabilityManager;
|
||||
import cn.daxpay.open.payment.wx.dao.merchant.WxMchAppManager;
|
||||
import cn.daxpay.open.payment.wx.dao.platform.WxPlatformAppCapabilityManager;
|
||||
import cn.daxpay.open.payment.wx.dao.platform.WxPlatformAppManager;
|
||||
import cn.daxpay.open.payment.auth.core.AppScopeEnum;
|
||||
import cn.daxpay.open.payment.wx.entity.merchant.WxMchApp;
|
||||
import cn.daxpay.open.payment.wx.entity.platform.WxPlatformApp;
|
||||
import cn.daxpay.open.payment.wx.entity.platform.WxPlatformAppCapability;
|
||||
|
||||
import cn.daxpay.open.payment.wx.enums.WxAppTypeEnum;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppFacade;
|
||||
import cn.daxpay.open.payment.wx.facade.WxAppView;
|
||||
import cn.daxpay.open.payment.wx.facade.WxIsvAppPair;
|
||||
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
@@ -21,7 +21,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 微信开放应用解析服务
|
||||
///
|
||||
@@ -35,6 +35,7 @@ public class WxAppResolveService implements WxAppFacade {
|
||||
private final WxPlatformAppManager wxPlatformAppManager;
|
||||
private final WxMchAppManager wxMchAppManager;
|
||||
private final WxChannelAppCapabilityManager wxChannelAppCapabilityManager;
|
||||
private final WxPlatformAppCapabilityManager wxPlatformAppCapabilityManager;
|
||||
|
||||
/// 按档位与主键加载应用视图(含 Auth)
|
||||
@Override
|
||||
@@ -59,7 +60,7 @@ public class WxAppResolveService implements WxAppFacade {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.payment.wx.scopeNotExist");
|
||||
}
|
||||
|
||||
/// 解析单应用:显式 channelAppId → 通道能力绑 → appType 推导(平台应用唯一命中)
|
||||
/// 解析单应用:显式 channelAppId → 通道能力绑 → 产品级平台默认绑
|
||||
///
|
||||
/// 直连产品(WECHAT_PAY)禁止回退到平台档应用: OAuth 拿到的 openId 必须与
|
||||
/// 直连商户号同主体, 使用平台应用会导致 appid 与 mchid 主体不一致。
|
||||
@@ -87,11 +88,11 @@ public class WxAppResolveService implements WxAppFacade {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. appType 推导:要求该类型平台应用唯一命中(仅非直连)
|
||||
// 3. 产品级平台默认能力绑(wx_platform_app_capability 按 (product, capability) 取;仅非直连)
|
||||
if (!direct) {
|
||||
WxAppView platformFallback = this.resolvePlatformFallback(capability);
|
||||
if (platformFallback != null) {
|
||||
return platformFallback;
|
||||
WxAppView productDefault = this.resolveProductDefault(product, capability);
|
||||
if (productDefault != null) {
|
||||
return productDefault;
|
||||
}
|
||||
}
|
||||
// 微信: 直连商户未配置商户档应用 / 未配置该能力对应的应用
|
||||
@@ -122,9 +123,9 @@ public class WxAppResolveService implements WxAppFacade {
|
||||
merchant = this.getById(AppScopeEnum.MERCHANT, merchantBind.get().getWxAppRefId());
|
||||
}
|
||||
}
|
||||
// 平台侧回退:appType 推导(平台应用唯一命中)
|
||||
// 平台侧回退:产品级平台默认绑(按 product)
|
||||
if (platform == null) {
|
||||
platform = this.resolvePlatformFallback(capability);
|
||||
platform = this.resolveProductDefault(product, capability);
|
||||
}
|
||||
// channelAppId 命中某侧时按命中侧覆盖
|
||||
if (StrUtil.isNotBlank(channelAppId)) {
|
||||
@@ -177,31 +178,22 @@ public class WxAppResolveService implements WxAppFacade {
|
||||
throw new DataNotExistException("error.payment.wx.channelAppIdNotFound", channelAppId);
|
||||
}
|
||||
|
||||
/// 平台档 appType 推导兜底:按兼容类型优先级遍历, 首个唯一命中的平台应用返回
|
||||
/// 产品级平台默认能力绑兜底:按 (product, capability) 查 wx_platform_app_capability
|
||||
///
|
||||
/// - 某类型恰好 1 个:返回该应用
|
||||
/// - 某类型多个:直接报错(不猜测, 要求显式配置能力绑定)
|
||||
/// - 某类型无:跳过, 继续下一个兼容类型
|
||||
/// - 全部兼容类型均未唯一命中:返回 null, 由调用方走最终报错
|
||||
private WxAppView resolvePlatformFallback(String capability) {
|
||||
if (StrUtil.isBlank(capability)) {
|
||||
/// 供服务商/聚合类产品(如 leshua_pay)在通道商户未绑平台应用时, 回退到运营端配置的产品默认平台应用;
|
||||
/// product 为空(如 UMS 传 null)或未配置绑定返回 null, 由调用方报错。
|
||||
private WxAppView resolveProductDefault(String product, String capability) {
|
||||
if (StrUtil.hasBlank(product, capability)) {
|
||||
return null;
|
||||
}
|
||||
// 遍历兼容 appType(按优先级), 首个唯一命中即返回; 某类型存在多个则报错, 避免猜测
|
||||
for (String appType : WxAppTypeEnum.resolveCompatibleAppTypes(capability)) {
|
||||
List<WxPlatformApp> apps = wxPlatformAppManager.listByAppType(appType);
|
||||
if (apps.size() == 1) {
|
||||
return this.toPlatformView(apps.getFirst());
|
||||
}
|
||||
if (apps.size() > 1) {
|
||||
// 该类型存在多个平台应用, 要求显式配置能力绑定
|
||||
WxAppTypeEnum typeEnum = WxAppTypeEnum.findByCode(appType);
|
||||
String appTypeLabel = typeEnum != null ? I18nUtil.getEnumName(typeEnum) : appType;
|
||||
throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE,
|
||||
"error.payment.wx.appNotUnique", appTypeLabel);
|
||||
}
|
||||
Optional<WxPlatformAppCapability> rel = wxPlatformAppCapabilityManager.findByProductAndCapability(product, capability);
|
||||
if (rel.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
WxPlatformApp app = wxPlatformAppManager.findById(rel.get().getWxPlatformAppId())
|
||||
// 微信: 平台应用不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.payment.wx.appNotFound"));
|
||||
return this.toPlatformView(app);
|
||||
}
|
||||
|
||||
/// 平台应用 → View
|
||||
|
||||
@@ -2,6 +2,8 @@ package cn.daxpay.open.payment.wx.service.platform;
|
||||
|
||||
import cn.daxpay.open.payment.masterdata.dao.capability.PayProductCapabilityManager;
|
||||
import cn.daxpay.open.payment.masterdata.entity.product.PayProductCapability;
|
||||
import cn.daxpay.open.payment.strategy.PaymentStrategyFactory;
|
||||
import cn.daxpay.open.payment.strategy.product.AbsProductStrategy;
|
||||
import cn.daxpay.open.payment.wx.convert.platform.WxPlatformAppCapabilityConvert;
|
||||
import cn.daxpay.open.payment.wx.dao.platform.WxPlatformAppCapabilityManager;
|
||||
import cn.daxpay.open.payment.wx.dao.platform.WxPlatformAppManager;
|
||||
@@ -35,7 +37,7 @@ import java.util.stream.Collectors;
|
||||
/// # 平台微信应用默认能力绑定
|
||||
///
|
||||
/// 按支付产品管理「支付能力 → 平台微信应用」绑定。
|
||||
/// 支付时通过 [WxAppResolveService] 解析:通道绑 > 本产品默认绑 > appType 推导。
|
||||
/// 支付时通过 [WxAppResolveService] 解析:通道绑 > 本产品默认绑。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -117,13 +119,26 @@ public class WxPlatformAppCapabilityService {
|
||||
}
|
||||
|
||||
/// 按支付产品查询可绑定的能力候选(pay_md_product_capability 白名单)
|
||||
///
|
||||
/// 仅返回「微信相关能力」(WxAppTypeEnum 有应用类型映射的能力), 避免聚合类产品
|
||||
/// (如乐刷)把支付宝/云闪付等非微信能力带入微信应用绑定弹窗渲染空下拉;
|
||||
/// 是否进一步收窄由产品策略声明决定([AbsProductStrategy#wxAppRequiredCapabilities]):
|
||||
/// 返回 null 表示全部微信能力可绑定, 返回子集则仅子集可绑定(如乐刷/Adapay 聚合产品
|
||||
/// 微信侧仅 JSAPI/小程序支付需要 appid)。
|
||||
public List<WxCapabilityOption> listSupportedCapabilities(String product) {
|
||||
if (StrUtil.isBlank(product)) {
|
||||
return List.of();
|
||||
}
|
||||
// 产品策略声明: 需要绑定微信应用的能力子集(null 表示不限制, 全部微信能力可绑定)
|
||||
Set<PayCapabilityEnum> wxAppRequired = PaymentStrategyFactory
|
||||
.findOptionallyByProduct(product, AbsProductStrategy.class)
|
||||
.map(AbsProductStrategy::wxAppRequiredCapabilities)
|
||||
.orElse(null);
|
||||
return payProductCapabilityManager.listByProduct(product).stream()
|
||||
.map(PayProductCapability::getCapabilityCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.filter(this::isWechatCapability)
|
||||
.filter(code -> wxAppRequired == null || wxAppRequired.contains(PayCapabilityEnum.findByCode(code)))
|
||||
.distinct()
|
||||
.map(code -> {
|
||||
PayCapabilityEnum cap = PayCapabilityEnum.findByCode(code);
|
||||
@@ -133,6 +148,11 @@ public class WxPlatformAppCapabilityService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 是否为微信相关能力(有微信应用类型映射的能力)
|
||||
private boolean isWechatCapability(String capability) {
|
||||
return !WxAppTypeEnum.resolveCompatibleAppTypes(capability).isEmpty();
|
||||
}
|
||||
|
||||
/// 填充关联结果的应用展示字段
|
||||
private WxPlatformAppCapabilityResult fillResult(WxPlatformAppCapability rel, WxPlatformApp app) {
|
||||
WxPlatformAppCapabilityResult result = WxPlatformAppCapabilityConvert.CONVERT.toResult(rel);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Adapay payment error: {0}",
|
||||
"closeFailed": "Adapay close order error: {0}",
|
||||
"isvKeyNotConfigured": "Adapay ISV key not configured",
|
||||
"unsupportedPayMethod": "Unsupported payment method for Adapay: {0}"
|
||||
}
|
||||
|
||||
@@ -679,6 +679,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "Merchant AppId cannot be blank"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "ISV number cannot be blank"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId cannot be blank"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Kesalahan pembayaran Adapay: {0}",
|
||||
"closeFailed": "Kesalahan menutup pesanan Adapay: {0}",
|
||||
"isvKeyNotConfigured": "Kunci ISV Adapay belum dikonfigurasi",
|
||||
"unsupportedPayMethod": "Metode pembayaran tidak didukung untuk Adapay: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "AppId Penjual tidak boleh kosong"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "Nomor ISV wajib diisi"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId tidak boleh kosong"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Adapay支払エラー: {0}",
|
||||
"closeFailed": "Adapay注文クローズエラー: {0}",
|
||||
"isvKeyNotConfigured": "Adapay サービス事業者キーが設定されていません",
|
||||
"unsupportedPayMethod": "Adapayでサポートされていない支払方法: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "販売者の AppId を空白にすることはできません"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "サービス事業者番号は必須です"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appIdは必須です"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Adapay 결제 오류: {0}",
|
||||
"closeFailed": "Adapay 주문 종료 오류: {0}",
|
||||
"isvKeyNotConfigured": "Adapay ISV 키가 설정되지 않았습니다",
|
||||
"unsupportedPayMethod": "Adapay에서 지원하지 않는 결제 방식: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "판매자 앱 ID는 비워둘 수 없습니다."
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "ISV 번호는 필수입니다"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId는 필수입니다"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Ralat pembayaran Adapay: {0}",
|
||||
"closeFailed": "Ralat tutup pesanan Adapay: {0}",
|
||||
"isvKeyNotConfigured": "Kunci ISV Adapay belum dikonfigurasi",
|
||||
"unsupportedPayMethod": "Kaedah pembayaran tidak disokong untuk Adapay: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "Merchant AppId tidak boleh kosong"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "Nombor ISV wajib diisi"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId tidak boleh kosong"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "ข้อผิดพลาดการชำระเงิน Adapay: {0}",
|
||||
"closeFailed": "ข้อผิดพลาดการปิดคำสั่ง Adapay: {0}",
|
||||
"isvKeyNotConfigured": "ยังไม่ได้กำหนดค่าคีย์ ISV ของ Adapay",
|
||||
"unsupportedPayMethod": "วิธีการชำระเงินไม่ได้รับการสนับสนุนสำหรับ Adapay: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "Merchant AppId ไม่สามารถเว้นว่างได้"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "ต้องระบุหมายเลข ISV"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId ต้องไม่ว่าง"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "Lỗi thanh toán Adapay: {0}",
|
||||
"closeFailed": "Lỗi đóng đơn hàng Adapay: {0}",
|
||||
"isvKeyNotConfigured": "Chưa cấu hình khóa ISV Adapay",
|
||||
"unsupportedPayMethod": "Phương thức thanh toán không được hỗ trợ cho Adapay: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "AppId người bán không được để trống"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "Số ISV không được để trống"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "Sand appId không được để trống"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "汇付支付异常: {0}",
|
||||
"closeFailed": "汇付关闭订单异常: {0}",
|
||||
"isvKeyNotConfigured": "汇付服务商密钥未配置",
|
||||
"unsupportedPayMethod": "汇付不支持的支付方式: {0}"
|
||||
}
|
||||
|
||||
@@ -679,6 +679,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "商户AppId不可为空"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "服务商号不可为空"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "杉德 appId 不能为空"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "匯付支付異常: {0}",
|
||||
"closeFailed": "匯付關閉訂單異常: {0}",
|
||||
"isvKeyNotConfigured": "匯付服務商密鑰未設定",
|
||||
"unsupportedPayMethod": "匯付不支援的支付方式: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "商户AppId不可為空"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "服務商號不可為空"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "杉德 appId 不能為空"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"payFailed": "匯付支付異常: {0}",
|
||||
"closeFailed": "匯付關閉訂單異常: {0}",
|
||||
"isvKeyNotConfigured": "匯付服務商密鑰未設定",
|
||||
"unsupportedPayMethod": "匯付不支援的支付方式: {0}"
|
||||
}
|
||||
|
||||
@@ -675,6 +675,9 @@
|
||||
"dougongAppId": {
|
||||
"notBlank": "商戶AppId不可為空"
|
||||
},
|
||||
"isvNo": {
|
||||
"notBlank": "服務商號不可為空"
|
||||
},
|
||||
"sandAppId": {
|
||||
"notBlank": "杉德 appId 不能為空"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user