🎨 #1733 微信支付服务商配置优化,增加服务商合单支付接口

* art:微信服务商配置优化

* new:jsapi合单支付

* new:合单支付

Co-authored-by: 曾浩 <epdcgsi@dingtalk.com>
This commit is contained in:
cloudX 2020-08-29 21:20:21 +08:00 committed by GitHub
parent 425b08245a
commit 6c490e3295
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 688 additions and 11 deletions

View File

@ -7,6 +7,10 @@ import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 二级商户进件 查询申请状态结果响应
*
*/
@Data
@NoArgsConstructor
public class ApplymentsStatusResult implements Serializable {

View File

@ -0,0 +1,455 @@
package com.github.binarywang.wxpay.bean.ecommerce;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 合单支付 对象
*/
@Data
@NoArgsConstructor
public class CombineTransactionsRequest implements Serializable {
/**
* <pre>
* 字段名合单商户appid
* 变量名combine_appid
* 是否必填
* 类型string(32)
* 描述
* 合单发起方的appid
* 示例值wxd678efh567hg6787
* </pre>
*/
@SerializedName(value = "combine_appid")
private String combineAppid;
/**
* <pre>
* 字段名合单商户号
* 变量名combine_mchid
* 是否必填
* 类型string(32)
* 描述
* 合单发起方商户号
* 示例值1900000109
* </pre>
*/
@SerializedName(value = "combine_mchid")
private String combineMchid;
/**
* <pre>
* 字段名合单商户订单号
* 变量名combine_out_trade_no
* 是否必填
* 类型string(32)
* 描述
* 合单支付总订单号要求32个字符内只能是数字大小写字母_-|*@ 且在同一个商户号下唯一
* 示例值P20150806125346
* </pre>
*/
@SerializedName(value = "combine_out_trade_no")
private String combineOutTradeNo;
/**
* <pre>
* 字段名+场景信息
* 变量名scene_info
* 是否必填
* 类型object
* 描述支付场景信息描述
* </pre>
*/
@SerializedName(value = "scene_info")
private SceneInfo sceneInfo;
/**
* <pre>
* 字段名+子单信息
* 变量名sub_orders
* 是否必填
* 类型array
* 描述
* 最多支持子单条数50
*
* </pre>
*/
@SerializedName(value = "sub_orders")
private List<SubOrders> subOrders;
/**
* <pre>
* 字段名+支付者
* 变量名combine_payer_info
* 是否必填(JSAPI必填)
* 类型object
* 描述支付者信息
* </pre>
*/
@SerializedName(value = "combine_payer_info")
private CombinePayerInfo combinePayerInfo;
/**
* <pre>
* 字段名交易起始时间
* 变量名time_start
* 是否必填
* 类型string(14)
* 描述
* 订单生成时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONEYYYY-MM-DD表示年月日T出现在字符串中表示time元素的开头HH:mm:ss表示时分秒TIMEZONE表示时区+08:00表示东八区时间领先UTC 8小时即北京时间例如2015-05-20T13:29:35+08:00表示北京时间2015年5月20日 13点29分35秒
* 示例值2019-12-31T15:59:60+08:00
* </pre>
*/
@SerializedName(value = "time_start")
private String timeStart;
/**
* <pre>
* 字段名交易结束时间
* 变量名time_expire
* 是否必填
* 类型string(14)
* 描述
* 订单失效时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONEYYYY-MM-DD表示年月日T出现在字符串中表示time元素的开头HH:mm:ss表示时分秒TIMEZONE表示时区+08:00表示东八区时间领先UTC 8小时即北京时间例如2015-05-20T13:29:35+08:00表示北京时间2015年5月20日 13点29分35秒
* 示例值2019-12-31T15:59:60+08:00
* </pre>
*/
@SerializedName(value = "time_expire")
private String timeExpire;
/**
* <pre>
* 字段名通知地址
* 变量名notify_url
* 是否必填
* 类型string(256)
* 描述
* 接收微信支付异步通知回调地址通知url必须为直接可访问的URL不能携带参数
* 格式: URL
* 示例值https://yourapp.com/notify
* </pre>
*/
@SerializedName(value = "notify_url")
private String notifyUrl;
@Data
@NoArgsConstructor
public static class SceneInfo implements Serializable {
/**
* <pre>
* 字段名商户端设备号
* 变量名device_id
* 是否必填
* 类型string(16)
* 描述
* 终端设备号门店号或收银设备ID
* 特殊规则长度最小7个字节
* 示例值POS1:1
* </pre>
*/
@SerializedName(value = "device_id")
private String deviceId;
/**
* <pre>
* 字段名用户终端IP
* 变量名payer_client_ip
* 是否必填
* 类型string(45)
* 描述
* 用户端实际ip
* 格式: ip(ipv4+ipv6)
* 示例值14.17.22.32
* </pre>
*/
@SerializedName(value = "payer_client_ip")
private String payerClientIp;
/**
* <pre>
* 字段名H5场景信息
* 变量名h5_info
* 是否必填(H5支付必填)
* 类型object
* 描述
* H5场景信息
* </pre>
*/
@SerializedName(value = "h5_info")
private H5Info h5Info;
}
@Data
@NoArgsConstructor
public static class SubOrders implements Serializable {
/**
* <pre>
* 字段名子单商户号
* 变量名mchid
* 是否必填
* 类型string(32)
* 描述
* 子单发起方商户号必须与发起方appid有绑定关系
* 示例值1900000109
* 此处一般填写服务商商户号
* </pre>
*/
@SerializedName(value = "mchid")
private String mchid;
/**
* <pre>
* 字段名附加信息
* 变量名attach
* 是否必填
* 类型string(128)
* 描述
* 附加数据在查询API和支付通知中原样返回可作为自定义参数使用
* 示例值深圳分店
* </pre>
*/
@SerializedName(value = "attach")
private String attach;
/**
* <pre>
* 字段名+订单金额
* 变量名amount
* 是否必填
* 类型object
* 描述
* </pre>
*/
@SerializedName(value = "amount")
private Amount amount;
/**
* <pre>
* 字段名子单商户订单号
* 变量名out_trade_no
* 是否必填
* 类型string(32)
* 描述
* 商户系统内部订单号要求32个字符内只能是数字大小写字母_-|*@ 且在同一个商户号下唯一
* 特殊规则最小字符长度为6
* 示例值20150806125346
* </pre>
*/
@SerializedName(value = "out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名二级商户号
* 变量名sub_mchid
* 是否必填
* 类型string(32)
* 描述
* 二级商户商户号由微信支付生成并下发
* 注意仅适用于电商平台 服务商
* 示例值1900000109
* </pre>
*/
@SerializedName(value = "sub_mchid")
private String subMchid;
/**
* <pre>
* 字段名商品描述
* 变量名description
* 是否必填
* 类型string(128)
* 描述
* 商品简单描述需传入应用市场上的APP名字-实际商品名称例如天天爱消除-游戏充值
* 示例值腾讯充值中心-QQ会员充值
* </pre>
*/
@SerializedName(value = "description")
private String description;
/**
* <pre>
* 字段名+结算信息
* 变量名settle_info
* 是否必填
* 类型Object
* 描述结算信息
* </pre>
*/
@SerializedName(value = "settle_info")
private SettleInfo settleInfo;
}
@Data
@NoArgsConstructor
public static class CombinePayerInfo implements Serializable {
/**
* <pre>
* 字段名用户标识
* 变量名openid
* 是否必填
* 类型string(128)
* 描述
* 使用合单appid获取的对应用户openid是用户在商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName(value = "openid")
private String openid;
}
@Data
@NoArgsConstructor
public static class Amount implements Serializable {
/**
* <pre>
* 字段名标价金额
* 变量名total_amount
* 是否必填
* 类型int64
* 描述
* 子单金额单位为分
* 示例值100
* </pre>
*/
@SerializedName(value = "total_amount")
private Integer totalAmount;
/**
* <pre>
* 字段名标价币种
* 变量名currency
* 是否必填
* 类型string(8)
* 描述
* 符合ISO 4217标准的三位字母代码人民币CNY
* 示例值CNY
* </pre>
*/
@SerializedName(value = "currency")
private String currency;
}
@Data
@NoArgsConstructor
public static class SettleInfo implements Serializable{
/**
* <pre>
* 字段名是否指定分账
* 变量名profit_sharing
* 是否必填
* 类型bool
* 描述
* 是否分账与外层profit_sharing同时存在时以本字段为准
* true
* false
* 示例值true
* </pre>
*/
@SerializedName(value = "profit_sharing")
private Boolean profitSharing;
/**
* <pre>
* 字段名补差金额
* 变量名subsidy_amount
* 是否必填
* 类型int64
* 描述
* SettleInfo.profit_sharing为true时该金额才生效
* 示例值10
* </pre>
*/
@SerializedName(value = "subsidy_amount")
private Integer subsidyAmount;
}
@Data
@NoArgsConstructor
public static class H5Info implements Serializable {
/**
* <pre>
* 字段名场景类型
* 变量名type
* 是否必填
* 类型string(32)
* 描述
* 场景类型枚举值
* iOSIOS移动应用
* Android安卓移动应用
* WapWAP网站应用
* 示例值iOS
* </pre>
*/
@SerializedName(value = "type")
private String type;
/**
* <pre>
* 字段名应用名称
* 变量名app_name
* 是否必填
* 类型string(64)
* 描述
* 应用名称
* 示例值王者荣耀
* </pre>
*/
@SerializedName(value = "app_name")
private String appName;
/**
* <pre>
* 字段名网站URL
* 变量名app_url
* 是否必填
* 类型string(128)
* 描述
* 网站URL
* 示例值https://pay.qq.com
* </pre>
*/
@SerializedName(value = "app_url")
private String appUrl;
/**
* <pre>
* 字段名iOS平台BundleID
* 变量名bundle_id
* 是否必填
* 类型string(128)
* 描述
* iOS平台BundleID
* 示例值com.tencent.wzryiOS
* </pre>
*/
@SerializedName(value = "bundle_id")
private String bundleId;
/**
* <pre>
* 字段名Android平台PackageName
* 变量名package_name
* 是否必填
* 类型string(128)
* 描述
* Android平台PackageName
* 示例值com.tencent.tmgp.sgame
* </pre>
*/
@SerializedName(value = "package_name")
private String packageName;
}
}

View File

@ -0,0 +1,119 @@
package com.github.binarywang.wxpay.bean.ecommerce;
import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.v3.util.AesUtils;
import com.github.binarywang.wxpay.v3.util.SignUtils;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
/**
* 合单支付 JSAPI支付结果响应
*/
@Data
@NoArgsConstructor
public class CombineTransactionsResult implements Serializable {
/**
* <pre>
* 字段名预支付交易会话标识 APP支付JSAPI支付 会返回
* 变量名prepay_id
* 是否必填
* 类型string(64)
* 描述
* 数字和字母微信生成的预支付会话标识用于后续接口调用使用
* 示例值wx201410272009395522657a690389285100
* </pre>
*/
@SerializedName("prepay_id")
private String prepayId;
/**
* <pre>
* 字段名支付跳转链接 H5支付 会返回
* 变量名h5_url
* 是否必填
* 类型string(512)
* 描述
* 支付跳转链接
* 示例值https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2016121516420242444321ca0631331346&package=1405458241
* </pre>
*/
@SerializedName("h5_url")
private String h5Url;
/**
* <pre>
* 字段名二维码链接 NATIVE支付 会返回
* 变量名h5_url
* 是否必填
* 类型string(512)
* 描述
* 二维码链接
* 示例值weixin://pay.weixin.qq.com/bizpayurl/up?pr=NwY5Mz9&groupid=00
* </pre>
*/
@SerializedName("code_url")
private String codeUrl;
@Data
@Accessors(chain = true)
public static class JsapiResult implements Serializable {
private String appId;
private String timeStamp;
private String nonceStr;
private String packageValue;
private String signType;
private String paySign;
private String getSignStr(){
return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue);
}
}
@Data
@Accessors(chain = true)
public static class AppResult implements Serializable {
private String appid;
private String partnerid;
private String prepayid;
private String packageValue;
private String noncestr;
private String timestamp;
}
public <T> T getPayInfo(TradeTypeEnum tradeType, String appId, String mchId, PrivateKey privateKey){
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = SignUtils.genRandomStr();
switch (tradeType){
case JSAPI:
JsapiResult jsapiResult = new JsapiResult();
jsapiResult.setAppId(appId).setTimeStamp(timestamp)
.setPackageValue("prepay_id=" + this.prepayId).setNonceStr(nonceStr)
//签名类型默认为RSA仅支持RSA
.setSignType("RSA").setPaySign(SignUtils.sign(jsapiResult.getSignStr(), privateKey));
return (T) jsapiResult;
case H5:
return (T) this.h5Url;
case APP:
AppResult appResult = new AppResult();
appResult.setAppid(appId).setPrepayid(this.prepayId).setPartnerid(mchId)
.setNoncestr(nonceStr).setTimestamp(timestamp)
//暂填写固定值Sign=WXPay
.setPackageValue("Sign=WXPay");
return (T) appResult;
case NATIVE:
return (T) this.codeUrl;
}
return null;
}
}

View File

@ -0,0 +1,27 @@
package com.github.binarywang.wxpay.bean.ecommerce.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付方式
*/
@Getter
@AllArgsConstructor
public enum TradeTypeEnum {
APP("/v3/combine-transactions/app","/v3/pay/partner/transactions/app"),
JSAPI("/v3/combine-transactions/jsapi","/v3/pay/partner/transactions/jsapi"),
NATIVE("/v3/combine-transactions/native","/v3/pay/partner/transactions/native"),
H5("/v3/combine-transactions/h5","/v3/pay/partner/transactions/h5")
;
/**
* 合单url
*/
private String combineUrl;
/**
* 单独下单url
*/
private String partnerUrl;
}

View File

@ -19,6 +19,7 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
/**
@ -229,7 +230,7 @@ public class WxPayConfig {
public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
String privateKeyPath = this.getPrivateKeyPath();
String privateCertPath = this.getPrivateCertPath();
String certSerialNo = this.getCertSerialNo();
String serialNo = this.getCertSerialNo();
String apiV3Key = this.getApiV3Key();
if (StringUtils.isBlank(privateKeyPath)) {
throw new WxPayException("请确保privateKeyPath已设置");
@ -237,9 +238,9 @@ public class WxPayConfig {
if (StringUtils.isBlank(privateCertPath)) {
throw new WxPayException("请确保privateCertPath已设置");
}
if (StringUtils.isBlank(certSerialNo)) {
throw new WxPayException("请确保certSerialNo证书序列号已设置");
}
// if (StringUtils.isBlank(certSerialNo)) {
// throw new WxPayException("请确保certSerialNo证书序列号已设置");
// }
if (StringUtils.isBlank(apiV3Key)) {
throw new WxPayException("请确保apiV3Key值已设置");
}
@ -248,6 +249,10 @@ public class WxPayConfig {
InputStream certInputStream = this.loadConfigInputStream(privateCertPath);
try {
PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream);
X509Certificate certificate = PemUtils.loadCertificate(certInputStream);
if(StringUtils.isBlank(serialNo)){
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
@ -255,7 +260,7 @@ public class WxPayConfig {
CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create()
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withWechatpay(Collections.singletonList(PemUtils.loadCertificate(certInputStream)))
.withWechatpay(Collections.singletonList(certificate))
.withValidator(new WxPayValidator(verifier))
.build();
this.apiV3HttpClient = httpClient;

View File

@ -1,8 +1,7 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsRequest;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsResult;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsStatusResult;
import com.github.binarywang.wxpay.bean.ecommerce.*;
import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
/**
@ -55,4 +54,16 @@ public interface EcommerceService {
*/
ApplymentsStatusResult queryApplyStatusByOutRequestNo(String outRequestNo) throws WxPayException;
/**
* <pre>
* 合单下单-JS支付API.
* 请求URLhttps://api.mch.weixin.qq.com/v3/combine-transactions/jsapi
* 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter3_2.shtml
* </pre>
*
* @param request 请求对象
* @return 预支付交易会话标识, 数字和字母微信生成的预支付会话标识用于后续接口调用使用
*/
<T> T combineTransactions(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException;
}

View File

@ -1,8 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsRequest;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsResult;
import com.github.binarywang.wxpay.bean.ecommerce.ApplymentsStatusResult;
import com.github.binarywang.wxpay.bean.ecommerce.*;
import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.EcommerceService;
import com.github.binarywang.wxpay.service.WxPayService;
@ -10,6 +9,7 @@ import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.net.URI;
@ -41,5 +41,14 @@ public class EcommerceServiceImpl implements EcommerceService {
return GSON.fromJson(result, ApplymentsStatusResult.class);
}
@Override
public <T> T combineTransactions(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
String url = this.payService.getPayBaseUrl() + tradeType.getCombineUrl();
String response = this.payService.postV3(url, GSON.toJson(request));
CombineTransactionsResult result = GSON.fromJson(response, CombineTransactionsResult.class);
return result.getPayInfo(tradeType, request.getCombineAppid(),
request.getCombineMchid(), payService.getConfig().getPrivateKey());
}
}

View File

@ -0,0 +1,47 @@
package com.github.binarywang.wxpay.v3.util;
import java.security.*;
import java.util.Base64;
import java.util.Random;
public class SignUtils {
public static String sign(String string, PrivateKey privateKey){
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(string.getBytes());
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名计算失败", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的私钥", e);
}
}
/**
* 随机生成32位字符串.
*/
public static String genRandomStr(){
return genRandomStr(32);
}
/**
* 生成随机字符串
* @param length 字符串长度
* @return
*/
public static String genRandomStr(int length) {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}