feat: 支付宝通道商户应用管理(ISV/直连)拆分重构

This commit is contained in:
DaxPay Dev
2026-06-14 13:01:13 +08:00
parent a7ca2130fc
commit 78cfc9f024
25 changed files with 494 additions and 215 deletions

View File

@@ -0,0 +1,75 @@
-- ----------------------------
-- 支付宝服务商通道商户绑定表
-- 一条记录代表"子商户挂靠在某个服务商应用下"的授权关系
-- ----------------------------
DROP TABLE IF EXISTS mch_alipay_isv_channel_merchant;
CREATE TABLE mch_alipay_isv_channel_merchant (
id int8 NOT NULL,
mch_no varchar(32) COLLATE pg_catalog."default",
channel_mch_no varchar(64) COLLATE pg_catalog."default",
product varchar(32) COLLATE pg_catalog."default",
app_id int8,
alipay_user_id varchar(32) COLLATE pg_catalog."default",
app_auth_token varchar(128) COLLATE pg_catalog."default",
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 DEFAULT 0,
deleted bool NOT NULL DEFAULT false
);
COMMENT ON TABLE mch_alipay_isv_channel_merchant IS '支付宝服务商通道商户绑定表';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.id IS '主键';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.mch_no IS '商户号';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.channel_mch_no IS '通道商户号(AISV+雪花)';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.product IS '所属支付产品';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.app_id IS '关联服务商应用ID(系统主键, 指向alipay_isv_app.id)';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.alipay_user_id IS '子商户支付宝识别码(2088开头)';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.app_auth_token IS '应用授权令牌(会过期/刷新)';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.creator IS '创建者ID';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.create_time IS '创建时间';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.last_modifier IS '最后修改ID';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.version IS '版本号';
COMMENT ON COLUMN mch_alipay_isv_channel_merchant.deleted IS '删除标志';
ALTER TABLE mch_alipay_isv_channel_merchant ADD CONSTRAINT pk_mch_alipay_isv_channel_merchant PRIMARY KEY (id);
-- 同一服务商应用下子商户不重复
ALTER TABLE mch_alipay_isv_channel_merchant ADD CONSTRAINT uk_isv_app_alipay_user UNIQUE (app_id, alipay_user_id);
-- ----------------------------
-- 支付宝直连通道商户绑定表
-- 一个商户PID对应一个channelMchNo, 商户的多个应用共享此绑定
-- ----------------------------
DROP TABLE IF EXISTS mch_alipay_direct_channel_merchant;
CREATE TABLE mch_alipay_direct_channel_merchant (
id int8 NOT NULL,
mch_no varchar(32) COLLATE pg_catalog."default",
channel_mch_no varchar(64) COLLATE pg_catalog."default",
product varchar(32) COLLATE pg_catalog."default",
alipay_user_id varchar(32) COLLATE pg_catalog."default",
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 DEFAULT 0,
deleted bool NOT NULL DEFAULT false
);
COMMENT ON TABLE mch_alipay_direct_channel_merchant IS '支付宝直连通道商户绑定表';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.id IS '主键';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.mch_no IS '商户号';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.channel_mch_no IS '通道商户号(系统生成雪花号)';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.product IS '所属支付产品';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.alipay_user_id IS '支付宝商家唯一识别码(2088开头)';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.creator IS '创建者ID';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.create_time IS '创建时间';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.last_modifier IS '最后修改ID';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.version IS '版本号';
COMMENT ON COLUMN mch_alipay_direct_channel_merchant.deleted IS '删除标志';
ALTER TABLE mch_alipay_direct_channel_merchant ADD CONSTRAINT pk_mch_alipay_direct_channel_merchant PRIMARY KEY (id);
-- 直连一PID一channelMchNo: 同一商户下同一alipayUserId唯一
ALTER TABLE mch_alipay_direct_channel_merchant ADD CONSTRAINT uk_direct_mch_alipay_user UNIQUE (mch_no, alipay_user_id);

View File

@@ -0,0 +1,45 @@
package org.dromara.daxpay.channel.alipay.controller.mch;
import org.dromara.daxpay.platform.core.annotation.PermCode;
import org.dromara.daxpay.platform.core.rest.Res;
import org.dromara.daxpay.platform.core.rest.result.Result;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayDirectChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.result.config.AlipayDirectChannelMerchantResult;
import org.dromara.daxpay.channel.alipay.service.mch.AlipayDirectChannelMerchantService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@PermCode(menuCode = "payment:merchant:channelMerchant")
@Validated
@Tag(name = "支付宝直连通道商户管理")
@RestController
@RequestMapping("/admin/alipay/direct-channel-merchant")
@RequiredArgsConstructor
public class AlipayDirectChannelMerchantController {
private final AlipayDirectChannelMerchantService alipayDirectChannelMerchantService;
@PermCode(code = "view", nameCn = "商户通道商户查看", nameEn = "Merchant Channel Merchant View")
@Operation(summary = "根据通道商户号查询支付宝直连通道商户配置")
@GetMapping("/find-by-channel-mch-no")
public Result<AlipayDirectChannelMerchantResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayDirectChannelMerchantService.findByChannelMchNo(channelMchNo));
}
@PermCode(code = "add", nameCn = "商户通道商户新增", nameEn = "Merchant Channel Merchant Add")
@Operation(summary = "创建支付宝直连通道商户")
@PostMapping("/create")
public Result<Void> create(@RequestBody @Validated AlipayDirectChannelMerchantCreateParam param) {
alipayDirectChannelMerchantService.create(param);
return Res.ok();
}
}

View File

@@ -3,10 +3,9 @@ package org.dromara.daxpay.channel.alipay.controller.mch;
import org.dromara.daxpay.platform.core.annotation.PermCode;
import org.dromara.daxpay.platform.core.rest.Res;
import org.dromara.daxpay.platform.core.rest.result.Result;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayDirectChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayIsvChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.result.config.AlipayChannelMerchantResult;
import org.dromara.daxpay.channel.alipay.service.mch.AlipayChannelMerchantService;
import org.dromara.daxpay.channel.alipay.result.config.AlipayIsvChannelMerchantResult;
import org.dromara.daxpay.channel.alipay.service.mch.AlipayIsvChannelMerchantService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
@@ -20,34 +19,27 @@ import org.springframework.web.bind.annotation.RestController;
@PermCode(menuCode = "payment:merchant:channelMerchant")
@Validated
@Tag(name = "支付宝通道商户管理")
@Tag(name = "支付宝服务商通道商户管理")
@RestController
@RequestMapping("/admin/alipay/channel-merchant")
@RequestMapping("/admin/alipay/isv-channel-merchant")
@RequiredArgsConstructor
public class AlipayChannelMerchantController {
public class AlipayIsvChannelMerchantController {
private final AlipayChannelMerchantService alipayChannelMerchantService;
private final AlipayIsvChannelMerchantService alipayIsvChannelMerchantService;
@PermCode(code = "view", nameCn = "商户通道商户查看", nameEn = "Merchant Channel Merchant View")
@Operation(summary = "根据通道商户号查询支付宝通道商户配置")
@Operation(summary = "根据通道商户号查询支付宝服务商通道商户配置")
@GetMapping("/find-by-channel-mch-no")
public Result<AlipayChannelMerchantResult> findByChannelMchNo(
public Result<AlipayIsvChannelMerchantResult> findByChannelMchNo(
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(alipayChannelMerchantService.findByChannelMchNo(channelMchNo));
return Res.ok(alipayIsvChannelMerchantService.findByChannelMchNo(channelMchNo));
}
@PermCode(code = "add", nameCn = "商户通道商户新增", nameEn = "Merchant Channel Merchant Add")
@Operation(summary = "创建支付宝服务商通道商户")
@PostMapping("/create")
public Result<Void> create(@RequestBody @Validated AlipayIsvChannelMerchantCreateParam param) {
alipayChannelMerchantService.create(param);
alipayIsvChannelMerchantService.create(param);
return Res.ok();
}
@PermCode(code = "add", nameCn = "商户通道商户新增", nameEn = "Merchant Channel Merchant Add")
@Operation(summary = "创建支付宝直连通道商户")
@PostMapping("/create-direct")
public Result<Long> createDirect(@RequestBody @Validated AlipayDirectChannelMerchantCreateParam param) {
return Res.ok(alipayChannelMerchantService.createDirect(param));
}
}

View File

@@ -1,14 +0,0 @@
package org.dromara.daxpay.channel.alipay.convert;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayChannelMerchant;
import org.dromara.daxpay.channel.alipay.result.config.AlipayChannelMerchantResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AlipayChannelMerchantConvert {
AlipayChannelMerchantConvert CONVERT = Mappers.getMapper(AlipayChannelMerchantConvert.class);
AlipayChannelMerchantResult toResult(AlipayChannelMerchant entity);
}

View File

@@ -0,0 +1,14 @@
package org.dromara.daxpay.channel.alipay.convert;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayDirectChannelMerchant;
import org.dromara.daxpay.channel.alipay.result.config.AlipayDirectChannelMerchantResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AlipayDirectChannelMerchantConvert {
AlipayDirectChannelMerchantConvert CONVERT = Mappers.getMapper(AlipayDirectChannelMerchantConvert.class);
AlipayDirectChannelMerchantResult toResult(AlipayDirectChannelMerchant entity);
}

View File

@@ -0,0 +1,17 @@
package org.dromara.daxpay.channel.alipay.convert;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayIsvChannelMerchant;
import org.dromara.daxpay.channel.alipay.result.config.AlipayIsvChannelMerchantResult;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AlipayIsvChannelMerchantConvert {
AlipayIsvChannelMerchantConvert CONVERT = Mappers.getMapper(AlipayIsvChannelMerchantConvert.class);
/// 实体 appId(Long) 映射到结果 isvAppId(Long), 避免与父类 MchTradeBaseResult.appId(String) 冲突
@Mapping(source = "appId", target = "isvAppId")
AlipayIsvChannelMerchantResult toResult(AlipayIsvChannelMerchant entity);
}

View File

@@ -1,17 +0,0 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayChannelMerchant;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
@Repository
public class AlipayChannelMerchantManager extends BaseManager<AlipayChannelMerchantMapper, AlipayChannelMerchant> {
/// 校验同一支付宝服务商应用下是否已绑定该子商户号
public boolean existsByIsvAppIdAndAlipayUserId(String isvAppId, String alipayUserId) {
return lambdaQuery()
.eq(AlipayChannelMerchant::getIsvAppId, isvAppId)
.eq(AlipayChannelMerchant::getAlipayUserId, alipayUserId)
.exists();
}
}

View File

@@ -1,9 +0,0 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayChannelMerchant;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AlipayChannelMerchantMapper extends MPJBaseMapper<AlipayChannelMerchant> {
}

View File

@@ -0,0 +1,17 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayDirectChannelMerchant;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
@Repository
public class AlipayDirectChannelMerchantManager extends BaseManager<AlipayDirectChannelMerchantMapper, AlipayDirectChannelMerchant> {
/// 校验同一商户下是否已绑定该支付宝商户PID
public boolean existsByMchNoAndAlipayUserId(String mchNo, String alipayUserId) {
return lambdaQuery()
.eq(AlipayDirectChannelMerchant::getMchNo, mchNo)
.eq(AlipayDirectChannelMerchant::getAlipayUserId, alipayUserId)
.exists();
}
}

View File

@@ -0,0 +1,11 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayDirectChannelMerchant;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 支付宝直连通道商户绑定
///
@Mapper
public interface AlipayDirectChannelMerchantMapper extends MPJBaseMapper<AlipayDirectChannelMerchant> {
}

View File

@@ -0,0 +1,17 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayIsvChannelMerchant;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
@Repository
public class AlipayIsvChannelMerchantManager extends BaseManager<AlipayIsvChannelMerchantMapper, AlipayIsvChannelMerchant> {
/// 校验同一服务商应用下是否已绑定该子商户
public boolean existsByAppIdAndAlipayUserId(Long appId, String alipayUserId) {
return lambdaQuery()
.eq(AlipayIsvChannelMerchant::getAppId, appId)
.eq(AlipayIsvChannelMerchant::getAlipayUserId, alipayUserId)
.exists();
}
}

View File

@@ -0,0 +1,11 @@
package org.dromara.daxpay.channel.alipay.dao.config;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayIsvChannelMerchant;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 支付宝服务商通道商户绑定
///
@Mapper
public interface AlipayIsvChannelMerchantMapper extends MPJBaseMapper<AlipayIsvChannelMerchant> {
}

View File

@@ -1,40 +0,0 @@
package org.dromara.daxpay.channel.alipay.entity.config;
import org.dromara.daxpay.platform.common.mybatisplus.function.ToResult;
import org.dromara.daxpay.channel.alipay.convert.AlipayChannelMerchantConvert;
import org.dromara.daxpay.channel.alipay.result.config.AlipayChannelMerchantResult;
import org.dromara.daxpay.payment.common.entity.merchant.MchBaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝通道商户配置
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "mch_alipay_channel_merchant", autoResultMap = true)
public class AlipayChannelMerchant extends MchBaseEntity implements ToResult<AlipayChannelMerchantResult> {
/// 通道商户号
private String channelMchNo;
/// 所属支付产品
private String product;
/// 支付宝服务商应用ID
private String isvAppId;
/// 支付宝商家用户唯一识别码(2088开头)
private String alipayUserId;
/// 应用授权令牌,服务商代商户调用接口的凭据
private String appAuthToken;
/// 转换
@Override
public AlipayChannelMerchantResult toResult() {
return AlipayChannelMerchantConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,37 @@
package org.dromara.daxpay.channel.alipay.entity.config;
import org.dromara.daxpay.channel.alipay.convert.AlipayDirectChannelMerchantConvert;
import org.dromara.daxpay.channel.alipay.result.config.AlipayDirectChannelMerchantResult;
import org.dromara.daxpay.payment.common.entity.merchant.MchBaseEntity;
import org.dromara.daxpay.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝直连通道商户绑定
///
/// 一个商户PID对应一个channelMchNo, 商户的多个应用共享此绑定。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "mch_alipay_direct_channel_merchant", autoResultMap = true)
public class AlipayDirectChannelMerchant extends MchBaseEntity implements ToResult<AlipayDirectChannelMerchantResult> {
/// 通道商户号(系统生成雪花号)
private String channelMchNo;
/// 所属支付产品
/// @see org.dromara.daxpay.platform.core.enums.pay.channel.ProductEnum
private String product;
/// 支付宝商家唯一识别码(2088开头的16位数字)
private String alipayUserId;
/// 转换
@Override
public AlipayDirectChannelMerchantResult toResult() {
return AlipayDirectChannelMerchantConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,44 @@
package org.dromara.daxpay.channel.alipay.entity.config;
import org.dromara.daxpay.channel.alipay.convert.AlipayIsvChannelMerchantConvert;
import org.dromara.daxpay.channel.alipay.result.config.AlipayIsvChannelMerchantResult;
import org.dromara.daxpay.payment.common.entity.merchant.MchBaseEntity;
import org.dromara.daxpay.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝服务商通道商户绑定
///
/// 一条记录代表"子商户挂靠在某个服务商应用下"的授权关系。
/// 同一子商户挂不同应用 = 不同行(不同 channelMchNo)。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "mch_alipay_isv_channel_merchant", autoResultMap = true)
public class AlipayIsvChannelMerchant extends MchBaseEntity implements ToResult<AlipayIsvChannelMerchantResult> {
/// 通道商户号(AISV+雪花)
private String channelMchNo;
/// 所属支付产品
/// @see org.dromara.daxpay.platform.core.enums.pay.channel.ProductEnum
private String product;
/// 关联服务商应用ID(系统主键, 指向 alipay_isv_app.id)
private Long appId;
/// 子商户支付宝识别码(2088开头的16位数字)
private String alipayUserId;
/// 应用授权令牌(服务商代子商户调用接口的凭据, 会过期/刷新)
private String appAuthToken;
/// 转换
@Override
public AlipayIsvChannelMerchantResult toResult() {
return AlipayIsvChannelMerchantConvert.CONVERT.toResult(this);
}
}

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.alipay.param.mch;
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;
@@ -25,17 +26,17 @@ public class AlipayIsvChannelMerchantCreateParam {
@NotBlank(message = "{validation.field.product.notBlank}")
private String product;
/// 支付宝服务商应用ID
@Schema(description = "支付宝服务商应用ID")
@NotBlank(message = "{validation.field.isvAppId.notBlank}")
private String isvAppId;
/// 服务商应用ID(系统主键, 指向 alipay_isv_app.id)
@Schema(description = "服务商应用ID")
@NotNull(message = "{validation.field.appId.notNull}")
private Long appId;
/// 支付宝商家用户ID(2088开头)
@Schema(description = "支付宝商家用户ID(2088开头)")
/// 子商户支付宝用户ID(2088开头)
@Schema(description = "子商户支付宝用户ID(2088开头)")
@NotBlank(message = "{validation.field.alipayUserId.notBlank}")
private String alipayUserId;
/// 应用授权令牌,服务商代商户调用接口的凭据
/// 应用授权令牌,服务商代商户调用接口的凭据
@Schema(description = "应用授权令牌")
@NotBlank(message = "{validation.field.appAuthToken.notBlank}")
private String appAuthToken;

View File

@@ -6,13 +6,13 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝通道商户配置结果
/// # 支付宝直连通道商户绑定结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付宝通道商户配置结果")
public class AlipayChannelMerchantResult extends MchTradeBaseResult {
@Schema(title = "支付宝直连通道商户绑定结果")
public class AlipayDirectChannelMerchantResult extends MchTradeBaseResult {
@Schema(description = "通道商户号")
private String channelMchNo;
@@ -20,12 +20,6 @@ public class AlipayChannelMerchantResult extends MchTradeBaseResult {
@Schema(description = "所属支付产品")
private String product;
@Schema(description = "支付宝服务商应用IDaliAppId")
private String isvAppId;
@Schema(description = "支付宝商家唯一识别码(2088开头的16位数字)")
@Schema(description = "支付宝商家唯一识别码(2088开头)")
private String alipayUserId;
@Schema(description = "应用授权令牌")
private String appAuthToken;
}

View File

@@ -0,0 +1,32 @@
package org.dromara.daxpay.channel.alipay.result.config;
import org.dromara.daxpay.payment.common.result.MchTradeBaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付宝服务商通道商户绑定结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付宝服务商通道商户绑定结果")
public class AlipayIsvChannelMerchantResult extends MchTradeBaseResult {
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "所属支付产品")
private String product;
/// 注意: 此处用 isvAppId(Long) 而非 appId, 避免与父类 MchTradeBaseResult.appId(String) 字段冲突
@Schema(description = "关联服务商应用ID(系统主键)")
private Long isvAppId;
@Schema(description = "子商户支付宝识别码(2088开头)")
private String alipayUserId;
@Schema(description = "应用授权令牌")
private String appAuthToken;
}

View File

@@ -1,98 +0,0 @@
package org.dromara.daxpay.channel.alipay.service.mch;
import org.dromara.daxpay.channel.alipay.dao.app.AlipayIsvAppManager;
import org.dromara.daxpay.channel.alipay.dao.config.AlipayChannelMerchantManager;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayChannelMerchant;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayDirectChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayIsvChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.result.config.AlipayChannelMerchantResult;
import org.dromara.daxpay.payment.channel.dao.mch.ChannelMerchantManager;
import org.dromara.daxpay.payment.channel.entity.mch.ChannelMerchant;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.enums.channel.ChannelMerchantSourceEnum;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayChannelMerchantService {
private final ChannelMerchantManager channelMerchantManager;
private final AlipayChannelMerchantManager alipayChannelMerchantManager;
private final AlipayIsvAppManager alipayIsvAppManager;
/// 创建支付宝服务商通道商户
@Transactional(rollbackFor = Exception.class)
public void create(AlipayIsvChannelMerchantCreateParam param) {
var isvApp = alipayIsvAppManager.findByAliAppId(param.getIsvAppId())
.orElseThrow(() -> new DataNotExistException("error.channel.alipay.appNotFound"));
// 校验同一应用下子商户号不重复
if (alipayChannelMerchantManager.existsByIsvAppIdAndAlipayUserId(
isvApp.getAliAppId(), param.getAlipayUserId())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.alipay.subMerchantDuplicate");
}
// 生成通道商户号:前缀 AISV + 雪花ID无分隔符符合 TradeNoGenerateUtil 约定)
String channelMchNo = "AISV" + IdUtil.getSnowflakeNextId();
// 写通用通道商户主表
ChannelMerchant channelMerchant = new ChannelMerchant();
channelMerchant.setId(IdUtil.getSnowflakeNextId());
channelMerchant.setMchNo(param.getMchNo());
channelMerchant.setChannelMchNo(channelMchNo);
channelMerchant.setChannelMerchantName(param.getChannelMerchantName());
channelMerchant.setProduct(param.getProduct());
channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode());
channelMerchant.setEnable(true);
channelMerchantManager.save(channelMerchant);
// 写支付宝扩展表(含服务商专属字段)
AlipayChannelMerchant alipayChannelMerchant = new AlipayChannelMerchant();
alipayChannelMerchant.setId(IdUtil.getSnowflakeNextId());
alipayChannelMerchant.setMchNo(param.getMchNo());
alipayChannelMerchant.setChannelMchNo(channelMchNo);
alipayChannelMerchant.setProduct(param.getProduct());
alipayChannelMerchant.setIsvAppId(isvApp.getAliAppId());
alipayChannelMerchant.setAlipayUserId(param.getAlipayUserId());
alipayChannelMerchant.setAppAuthToken(param.getAppAuthToken());
alipayChannelMerchantManager.save(alipayChannelMerchant);
}
/// 创建支付宝直连通道商户
@Transactional(rollbackFor = Exception.class)
public Long createDirect(AlipayDirectChannelMerchantCreateParam param) {
String channelMchNo = param.getAlipayUserId();
ChannelMerchant channelMerchant = new ChannelMerchant();
long channelMerchantId = IdUtil.getSnowflakeNextId();
channelMerchant.setId(channelMerchantId);
channelMerchant.setMchNo(param.getMchNo());
channelMerchant.setChannelMchNo(channelMchNo);
channelMerchant.setChannelMerchantName(param.getChannelMerchantName());
channelMerchant.setProduct(param.getProduct());
channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode());
channelMerchant.setEnable(true);
channelMerchantManager.save(channelMerchant);
AlipayChannelMerchant alipayChannelMerchant = new AlipayChannelMerchant();
alipayChannelMerchant.setId(IdUtil.getSnowflakeNextId());
alipayChannelMerchant.setMchNo(param.getMchNo());
alipayChannelMerchant.setChannelMchNo(channelMchNo);
alipayChannelMerchant.setProduct(param.getProduct());
alipayChannelMerchant.setAlipayUserId(param.getAlipayUserId());
alipayChannelMerchantManager.save(alipayChannelMerchant);
return channelMerchantId;
}
public AlipayChannelMerchantResult findByChannelMchNo(String channelMchNo) {
return alipayChannelMerchantManager.lambdaQuery()
.eq(AlipayChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt()
.map(AlipayChannelMerchant::toResult)
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
}
}

View File

@@ -0,0 +1,69 @@
package org.dromara.daxpay.channel.alipay.service.mch;
import org.dromara.daxpay.channel.alipay.dao.config.AlipayDirectChannelMerchantManager;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayDirectChannelMerchant;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayDirectChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.result.config.AlipayDirectChannelMerchantResult;
import org.dromara.daxpay.payment.channel.dao.mch.ChannelMerchantManager;
import org.dromara.daxpay.payment.channel.entity.mch.ChannelMerchant;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.enums.channel.ChannelMerchantSourceEnum;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/// # 支付宝直连通道商户管理
///
/// 一个商户PID对应一个channelMchNo, 商户的多个应用共享此绑定。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayDirectChannelMerchantService {
private final ChannelMerchantManager channelMerchantManager;
private final AlipayDirectChannelMerchantManager alipayDirectChannelMerchantManager;
/// 创建支付宝直连通道商户
@Transactional(rollbackFor = Exception.class)
public void create(AlipayDirectChannelMerchantCreateParam param) {
// 校验同一商户下 PID 不重复
if (alipayDirectChannelMerchantManager.existsByMchNoAndAlipayUserId(
param.getMchNo(), param.getAlipayUserId())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.alipay.directMchDuplicate");
}
// 生成通道商户号(雪花号, 不再使用 alipayUserId)
String channelMchNo = String.valueOf(IdUtil.getSnowflakeNextId());
// 写通用通道商户主表
ChannelMerchant channelMerchant = new ChannelMerchant();
channelMerchant.setId(IdUtil.getSnowflakeNextId());
channelMerchant.setMchNo(param.getMchNo());
channelMerchant.setChannelMchNo(channelMchNo);
channelMerchant.setChannelMerchantName(param.getChannelMerchantName());
channelMerchant.setProduct(param.getProduct());
channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode());
channelMerchant.setEnable(true);
channelMerchantManager.save(channelMerchant);
// 写直连绑定表(alipayUserId 作为业务字段, 不参与关联)
AlipayDirectChannelMerchant entity = new AlipayDirectChannelMerchant();
entity.setId(IdUtil.getSnowflakeNextId());
entity.setMchNo(param.getMchNo());
entity.setChannelMchNo(channelMchNo);
entity.setProduct(param.getProduct());
entity.setAlipayUserId(param.getAlipayUserId());
alipayDirectChannelMerchantManager.save(entity);
}
/// 根据通道商户号查询直连通道商户配置
public AlipayDirectChannelMerchantResult findByChannelMchNo(String channelMchNo) {
return alipayDirectChannelMerchantManager.lambdaQuery()
.eq(AlipayDirectChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt()
.map(AlipayDirectChannelMerchant::toResult)
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
}
}

View File

@@ -0,0 +1,77 @@
package org.dromara.daxpay.channel.alipay.service.mch;
import org.dromara.daxpay.channel.alipay.dao.app.AlipayIsvAppManager;
import org.dromara.daxpay.channel.alipay.dao.config.AlipayIsvChannelMerchantManager;
import org.dromara.daxpay.channel.alipay.entity.app.AlipayIsvApp;
import org.dromara.daxpay.channel.alipay.entity.config.AlipayIsvChannelMerchant;
import org.dromara.daxpay.channel.alipay.param.mch.AlipayIsvChannelMerchantCreateParam;
import org.dromara.daxpay.channel.alipay.result.config.AlipayIsvChannelMerchantResult;
import org.dromara.daxpay.payment.channel.dao.mch.ChannelMerchantManager;
import org.dromara.daxpay.payment.channel.entity.mch.ChannelMerchant;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.enums.channel.ChannelMerchantSourceEnum;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/// # 支付宝服务商通道商户管理
///
/// 一条记录代表"子商户挂靠在某个服务商应用下"的授权关系。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayIsvChannelMerchantService {
private final ChannelMerchantManager channelMerchantManager;
private final AlipayIsvChannelMerchantManager alipayIsvChannelMerchantManager;
private final AlipayIsvAppManager alipayIsvAppManager;
/// 创建支付宝服务商通道商户
@Transactional(rollbackFor = Exception.class)
public void create(AlipayIsvChannelMerchantCreateParam param) {
// 校验服务商应用存在(用 appId 系统主键查询)
AlipayIsvApp isvApp = alipayIsvAppManager.findById(param.getAppId())
.orElseThrow(() -> new DataNotExistException("error.channel.alipay.appNotFound"));
// 校验同一应用下子商户号不重复
if (alipayIsvChannelMerchantManager.existsByAppIdAndAlipayUserId(
param.getAppId(), param.getAlipayUserId())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.alipay.subMerchantDuplicate");
}
// 生成通道商户号:前缀 AISV + 雪花ID(无分隔符, 符合 TradeNoGenerateUtil 约定)
String channelMchNo = "AISV" + IdUtil.getSnowflakeNextId();
// 写通用通道商户主表
ChannelMerchant channelMerchant = new ChannelMerchant();
channelMerchant.setId(IdUtil.getSnowflakeNextId());
channelMerchant.setMchNo(param.getMchNo());
channelMerchant.setChannelMchNo(channelMchNo);
channelMerchant.setChannelMerchantName(param.getChannelMerchantName());
channelMerchant.setProduct(param.getProduct());
channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode());
channelMerchant.setEnable(true);
channelMerchantManager.save(channelMerchant);
// 写服务商绑定表(含挂靠关系专属字段)
AlipayIsvChannelMerchant entity = new AlipayIsvChannelMerchant();
entity.setId(IdUtil.getSnowflakeNextId());
entity.setMchNo(param.getMchNo());
entity.setChannelMchNo(channelMchNo);
entity.setProduct(param.getProduct());
entity.setAppId(isvApp.getId());
entity.setAlipayUserId(param.getAlipayUserId());
entity.setAppAuthToken(param.getAppAuthToken());
alipayIsvChannelMerchantManager.save(entity);
}
/// 根据通道商户号查询服务商通道商户配置
public AlipayIsvChannelMerchantResult findByChannelMchNo(String channelMchNo) {
return alipayIsvChannelMerchantManager.lambdaQuery()
.eq(AlipayIsvChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt()
.map(AlipayIsvChannelMerchant::toResult)
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
}
}

View File

@@ -37,5 +37,6 @@
"appIdDuplicate": "Application ID already exists under the same service provider",
"appNotFound": "Alipay ISV application not found",
"subMerchantDuplicate": "This sub-merchant already exists under the Alipay application",
"directMchDuplicate": "The Alipay merchant already exists under this merchant",
"userIdTypeInvalid": "Invalid user ID type"
}

View File

@@ -60,7 +60,8 @@
},
"appId": {
"notBlank": "Merchant app ID cannot be blank",
"size": "Merchant app ID cannot exceed 32 characters"
"size": "Merchant app ID cannot exceed 32 characters",
"notNull": "Application ID cannot be null"
},
"subMchId": {
"notBlank": "WeChat sub-merchant ID cannot be blank"

View File

@@ -37,5 +37,6 @@
"appIdDuplicate": "同一服务商下应用ID已存在",
"appNotFound": "支付宝服务商应用不存在",
"subMerchantDuplicate": "该支付宝应用下已存在此子商户号",
"directMchDuplicate": "同一商户下该支付宝商户已存在",
"userIdTypeInvalid": "用户标识类型不合法"
}

View File

@@ -60,7 +60,8 @@
},
"appId": {
"notBlank": "商户AppId不可为空",
"size": "应用号不可超过32位"
"size": "应用号不可超过32位",
"notNull": "应用ID不可为空"
},
"subMchId": {
"notBlank": "微信子商户号不可为空"