简介:
本篇文章为多租户场景
在当今数字经济时代,许多企业都面临着处理多租户支付的挑战。多租户系统是指一种架构,其中单个实例的软件服务多个租户,每个租户的数据通常被隔离,以确保安全性和数据隐私。而在这种环境下,实现支付功能需要特别注意数据隔离、安全性和可扩展性等方面的考量。
本文将探讨如何利用JAVA编程语言实现多租户支付系统。我们将介绍一种简单而强大的解决方案,该方案能够轻松处理多租户环境下的支付需求,并且具有良好的可扩展性和安全性。
首先,我们将深入了解多租户系统的特点以及为何需要专门的支付解决方案。然后,我们将讨论设计和实施多租户支付系统的关键考虑因素,包括数据隔离、安全认证、支付通道管理等。接着,我们将介绍一些常用的JAVA支付库和框架,以及它们在多租户环境下的应用实践。
通过本文的阅读,读者将了解到如何利用JAVA技术栈构建一个稳健、高效的多租户支付系统,并且能够灵活地适应不同规模和需求的业务场景。同时,我们还将分享一些实用的技巧和最佳实践,帮助读者在实践中避免常见的问题和挑战。
无论您是正在构建多租户支付系统的开发者,还是对多租户系统和支付技术感兴趣的技术爱好者,本文都将为您提供有价值的见解和指导,助您在支付领域取得更大的成功。
pom依赖
微信支付pom
代码部分:
判断是否存在该支付类型(根据service Bean名称)
String BASE_NAME = "PayStrategy";
/**
* 获取支付凭证
*/
static Map<String, Object> ObtainCertificate(String orderSerial, PaymentBody paymentBody, PaymentConfigProperties paymentConfigProperties, HttpServletResponse response) {
// 支付类型与付款方式
String paymentType = paymentBody.getPaymentType();
String appName = paymentConfigProperties.getAppName();
String beanName = appName + BASE_NAME;
if (!SpringUtils.containsBean(beanName)) {
throw new ResultException("支付类型不正确!");
}
IPayStrategy instance = SpringUtils.getBean(beanName);
instance.validate(paymentBody);
return instance.ObtainCertificate(paymentType, orderSerial, paymentBody, paymentConfigProperties, response);
}
service
注⚠️:多支付渠道情境下需要配置路由Bean名称
@Service("wechatV3" + IPayStrategy.BASE_NAME)
传入paymentType(支付渠道)
PaymentBody中 为订单信息 以及租户信息和应用信息
@Override
public Map<String, Object> ObtainCertificate(String paymentType, String orderSerial, PaymentBody paymentBody, PaymentConfigProperties paymentConfigProperties, HttpServletResponse response) {
switch (paymentType) {
case Const.APP:
return wxPay(paymentBody, paymentConfigProperties,WXPayConstants.V3_APP_API,Const.APP);
case Const.JSAPI:
return wxPay(paymentBody, paymentConfigProperties,WXPayConstants.V3_JSAPI_API,Const.JSAPI);
default:
log.error("不支持的支付类型:{}", paymentType);
throw new ResultException("不支持的支付类型");
}
}
impl
public Map<String, Object> wxPay(PaymentBody paymentBody, PaymentConfigProperties paymentConfigProperties,String api,String type) {
Map<String,Object> map =new HashMap<>();
//支付总金额
BigDecimal totalPrice = BigDecimal.ZERO;
totalPrice = totalPrice.add(BigDecimal.valueOf(paymentBody.getActualPrice()).divide(BigDecimal.valueOf(100)));
//转换金额保留两位小数点
Integer money=new BigDecimal(String.valueOf(totalPrice)).movePointRight(2).intValue();
try {
//验证证书
CloseableHttpClient httpClient = wxPayV3Util.checkSign(paymentConfigProperties);
//app下单
HttpPost httpPost = new HttpPost(api);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", paymentConfigProperties.getMchId())
.put("appid", paymentConfigProperties.getAppId())
.put("description",paymentBody.getDescription())
.put("notify_url", paymentConfigProperties.getNotifyUrl())//回调
.put("out_trade_no", paymentBody.getOrderNo());
// 如果为JSAPI支付
if (type.equals(Const.JSAPI)){
if (StringUtils.isBlank(paymentBody.getOpenId())){
throw new IllegalStateException("openId不能为空");
}
rootNode.putObject("payer").put("openid", paymentBody.getOpenId());
}
rootNode.putObject("amount")
.put("total",paymentBody.getActualPrice());
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//获取返回状态
int statusCode = response.getStatusLine().getStatusCode();
log.info("JSAPI成功->{}->{}",statusCode,response);
if (statusCode == 200) { //处理成功
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
JSONObject object = JSONObject.parseObject(result);
//获取预付单
String prepayId = object.getString("prepay_id");
//生成签名
Long timestamp = System.currentTimeMillis() / 1000;
//随机字符串 这个是微信支付maven自带的 也可以用其它的
//这个是v2支付依赖自带的工具包 可以去掉了
//String nonceStr = WXPayUtil.generateNonceStr();
//该方法org.apache.commons.lang3.RandomStringUtils依赖自带随机生成字符串 RandomStringUtils.randomAlphanumeric(32) 代表生成32位
String nonceStr = RandomStringUtils.randomAlphanumeric(32);
//生成带签名支付信息
String paySign = wxPayV3Util.appPaySign(String.valueOf(timestamp), nonceStr, prepayId, paymentConfigProperties);
Map<String, String> param = new HashMap<>();
map.put("apply", paymentBody.getApply());
param.put("appid", paymentConfigProperties.getAppId());
param.put("partnerid", paymentConfigProperties.getMchId());
param.put("prepayid", prepayId);
param.put("package", "Sign=WXPay");
param.put("noncestr", nonceStr);
param.put("timestamp", String.valueOf(timestamp));
param.put("sign", paySign);
map.put("code",200);
map.put("message", "下单成功");
map.put("data", param);
return map;
}else {
map.put("code", statusCode);
map.put("message", "下单失败");
map.put("data", EntityUtils.toString(response.getEntity(), "UTF-8")); // 提取响应体信息
return map;
}
} catch (Exception e) {
log.error("微信预支付id获取失败{}->{}",e.getMessage(),e);
e.printStackTrace();
}
return map;
}
获取预支付id
注⚠️:JSAPI拉起支付时 需要传入 用户的openid
获取方式在我的另一个文章 获取openid
WxPayV3Util
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
public class WxPayV3Util {
/**
* 证书验证
* 自动更新的签名验证器
*/
public CloseableHttpClient checkSign(PaymentConfigProperties paymentConfigProperties) throws IOException {
//验签
CloseableHttpClient httpClient = null;
PrivateKey merchantPrivateKey = getPrivateKey(paymentConfigProperties);
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(paymentConfigProperties.getMchId(), paymentConfigProperties.getMchSerialNo(), merchantPrivateKey)
.withValidator(new WechatPay2Validator(getVerifier(paymentConfigProperties)))
.build();
return httpClient;
}
/**
* 保存微信平台证书
*/
private final ConcurrentHashMap<String, AutoUpdateCertificatesVerifier> verifierMap = new ConcurrentHashMap<>();
/**
* 功能描述:获取平台证书,自动更新
* 注意:这个方法内置了平台证书的获取和返回值解密
*/
public AutoUpdateCertificatesVerifier getVerifier(PaymentConfigProperties properties) {
String mchSerialNo = properties.getMchSerialNo();
AutoUpdateCertificatesVerifier verifier = null;
if (verifierMap.isEmpty() || !verifierMap.containsKey(mchSerialNo)) {
verifierMap.clear();
try {
//传入证书
PrivateKey privateKey = getPrivateKey(properties);
//刷新
PrivateKeySigner signer = new PrivateKeySigner(mchSerialNo, privateKey);
WechatPay2Credentials credentials = new WechatPay2Credentials(properties.getMchId(), signer);
verifier = new AutoUpdateCertificatesVerifier(credentials
, properties.getApiV3Key().getBytes("utf-8"));
verifierMap.put(verifier.getValidCertificate().getSerialNumber()+"", verifier);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
verifier = verifierMap.get(mchSerialNo);
}
return verifier;
}
/**
* app生成带签名支付信息
*
* @param timestamp 时间戳
* @param nonceStr 随机数
* @param prepayId 预付单
* @return 支付信息
* @throws Exception
*/
public String appPaySign(String timestamp, String nonceStr, String prepayId,PaymentConfigProperties paymentConfigProperties) throws Exception {
//上传私钥
PrivateKey privateKey = getPrivateKey(paymentConfigProperties);
String signatureStr = Stream.of(paymentConfigProperties.getAppId(), timestamp, nonceStr, prepayId)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 小程序及其它支付生成带签名支付信息
*
* @param timestamp 时间戳
* @param nonceStr 随机数
* @param prepayId 预付单
* @return 支付信息
* @throws Exception
*/
public String jsApiPaySign(String timestamp, String nonceStr, String prepayId,PaymentConfigProperties paymentConfigProperties) throws Exception {
//上传私钥
PrivateKey privateKey = getPrivateKey(paymentConfigProperties);
String signatureStr = Stream.of(paymentConfigProperties.getAppId(), timestamp, nonceStr, "prepay_id="+prepayId)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 获取私钥。
* String filename 私钥文件路径 (required)
* @return 私钥对象
*/
public PrivateKey getPrivateKey(PaymentConfigProperties properties) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(properties.getPrivateKeyPath())), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
}
PaymentConfigProperties
import lombok.Data;
import lombok.ToString;
/**
* 支付配置
*/
@Data
@ToString
public class PaymentConfigProperties {
/**
* 是否启用
*/
private Boolean enabled;
/**
* 应用配置名称(alipay,wechat,yinsheng)
*/
private String appName;
/**
* 支持的付款类型
*/
private String paymentType;
/**
* 微信公众号或者小程序等的appid
*/
private String appId;
/**
* 微信支付商户号
*/
private String mchId;
/**
* 公钥
*/
private String publicKey;
/**
* 微信支付商户密钥,支付宝私钥
*/
private String privateKey;
private String privateKeyPath;
/**
* 服务商模式下的子商户公众账号ID
* 如果是普通模式,请不要配置这两个参数,最好从配置文件中移除相关项
*/
private String subAppId;
/**
* 服务商模式下的子商户号
* 如果是普通模式,请不要配置这两个参数,最好从配置文件中移除相关项
*/
private String subMchId;
/**
* 微信:p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
* 支付宝:应用公钥证书文件本地路径。
*/
private String keyPath;
/**
* 支付成功回调地址
*/
private String notifyUrl;
/**
* 支付成功返回地址
*/
private String returnUrl;
/**
* 退款成功回调地址
*/
private String refundUrl;
//-----------支付宝-----------
/**
* 支付宝证书
*/
private String appCertPath;
/**
* 支付宝证书
*/
private String aliPayCertPath;
/**
* 支付宝证书
*/
private String aliPayRootCertPath;
/**
*
*/
private String domain;
/**
* 支付宝服务地址
*/
private String serverUrl;
/**
* 图标
*/
private String icon;
/**
* 平台证书序列号
*/
private String mchSerialNo;
/**
* API V3密钥 "c3Gmjlc5A7fds7uJNUsReVEbs37BlmWL";
*/
private String apiV3Key;
/**
* 银盛私钥
*/
private String ysPrivateCerPath;
/**
* 银盛公钥
*/
private String ysPublicCerPath;
/**
* 银盛环境
*/
private String ysEnv;
/**
* 银盛私钥密码
*/
private String ysPrivateCerPwd;
/**
* 银盛发起方
*/
private String srcMerchantNo;
/**
* 银盛收款方
*/
private String payeeMerchantNo;
/**
* 银盛支付回调地址
*/
private String ysNotifyUrl;
/**
* 银盛支付回调地址
*/
private String ysRefundUrl;
/**
* v2 微信 商户key
*/
private String mchKey;
/** 拉卡拉 银联商户号 */
private String lklMerchantNo;
/** 拉卡拉 订单有效期 */
private int lklOrderEffMinutes = 15;
/** 拉卡拉 订单支付成功后商户接收订单通知的地址 */
private String lklOrderCreateNotifyUrl;
}
PayProperties
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* payment 配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "payment")
public class PayProperties {
/**
* 支付租户
*/
private Map<String, Map<String, PaymentConfigProperties>> tenement;
}
Const
public class Const {
public static final String NATIVE = "NATIVE";//原生扫码支付
public static final String APP = "APP";//APP支付
public static final String JSAPI = "JSAPI";//公众号支付/小程序支付
public static final String M_WEB = "MWEB";//H5支付
public static final String MICRO_PAY = "MICROPAY";//刷卡支付
public static final String PC = "PC";//PC支付
public static final String WAP = "WAP";//网页支付
}
到这里 就可以正常的获取到微信的预支付id了