Commit aae49d8f authored by Administrator's avatar Administrator

Merge remote-tracking branch 'origin/master'

parents 54476527 e003ed79
......@@ -129,7 +129,11 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcmail-jdk15</artifactId>
<version>1.45</version>
</dependency>
<!--cache-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
......
package cn.quant.baa.pay.acquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.web.*;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Created by Administrator on 2021/9/1 0001.
*/
public interface Acquirer {
Object code();
void pay();
void refund();
void check();
JsonNode pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity);
JsonNode refund(RefundRequestData refundRequestData);
JsonNode checkPay(CheckPayRequestData checkPayRequestData);
JsonNode checkRefund(CheckRefundRequestData checkRefundRequestData);
JsonNode close(CloseRequestData closeRequestData);
}
......@@ -42,6 +42,12 @@ public class AcquirerProperties implements Serializable{
private String payCertKey;
private String payRootCertNo;
private String payRootCertKey;
private String productCode;
//feature
private AccessCode accessCode;
......@@ -119,6 +125,30 @@ public class AcquirerProperties implements Serializable{
this.name = name;
}
public String getPayRootCertNo() {
return payRootCertNo;
}
public void setPayRootCertNo(String payRootCertNo) {
this.payRootCertNo = payRootCertNo;
}
public String getPayRootCertKey() {
return payRootCertKey;
}
public void setPayRootCertKey(String payRootCertKey) {
this.payRootCertKey = payRootCertKey;
}
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public String getSecretKey() {
return secretKey;
}
......@@ -315,6 +345,7 @@ public class AcquirerProperties implements Serializable{
sb.append(", secretType='").append(secretType).append('\'');
sb.append(", signType='").append(signType).append('\'');
sb.append(", payCertNo='").append(payCertNo).append('\'');
sb.append(", payRootCertNo='").append(payRootCertNo).append('\'');
sb.append(", accessCode=").append(accessCode);
sb.append(", accessType=").append(accessType);
sb.append(", accessMethod='").append(accessMethod).append('\'');
......
package cn.quant.baa.pay.acquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.web.*;
import cn.quant.spring.NotSupportedException;
import cn.quant.spring.security.Base64Cipher;
import cn.quant.spring.security.CharacterCipher;
import cn.quant.spring.util.RandomSequencer;
import com.fasterxml.jackson.databind.JsonNode;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
......@@ -62,17 +65,27 @@ public class MerchantAcquirer implements Acquirer {
}
@Override
public void pay() {
throw new NotSupportedException();
public JsonNode pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity) {
return acquirers.get(payRequestData.getChanId()).pay(payRequestData, payHistoryEntity);
}
@Override
public void refund() {
throw new NotSupportedException();
public JsonNode refund(RefundRequestData refundRequestData) {
return acquirers.get(refundRequestData.getChanId()).refund(refundRequestData);
}
@Override
public void check() {
throw new NotSupportedException();
public JsonNode checkPay(CheckPayRequestData checkPayRequestData) {
return acquirers.get(checkPayRequestData.getChanId()).checkPay(checkPayRequestData);
}
@Override
public JsonNode checkRefund(CheckRefundRequestData checkRefundRequestData) {
return acquirers.get(checkRefundRequestData.getChanId()).checkRefund(checkRefundRequestData);
}
@Override
public JsonNode close(CloseRequestData closeRequestData) {
return acquirers.get(closeRequestData.getChanId()).close(closeRequestData);
}
}
package cn.quant.baa.pay.acquirer.alipay;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.web.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Mono;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Administrator on 2021/8/31 0031.
*/
public class AlipayMerchantAcquirer extends MerchantAcquirer {
private WebClient webClient;
private String signType = "RSA2";
private String charset = "utf-8";
private static BouncyCastleProvider provider;
static {
provider = new BouncyCastleProvider();
Security.addProvider(provider);
}
/**
* 商户私钥
*/
PrivateKey privateKey;
/**
* 支付宝公钥
*/
X509Certificate payPublicKey;
/**
* 应用公钥
*/
X509Certificate payCertKey;
/**
* 支付宝根证书
*/
X509Certificate[] payRootCertKey;
ObjectMapper objectMapper = new ObjectMapper();
/**
* 支付初始化
* 1. 初始化私钥
* 2. 初始化平台证书
* 3. 初始化webClient
*
* @param properties
* @return
* @throws Exception
*/
@Override
public MerchantAcquirer init(AcquirerProperties properties) throws Exception {
// super.init(properties);
this.properties = properties;
privateKey = getPrivateKey(properties.getPrivateKey());
payPublicKey = loadCertificate(properties.getPayPublicKey());
payCertKey = loadCertificate(properties.getPayCertKey());
payRootCertKey = loadCertificates(properties.getPayRootCertKey());
this.webClient = WebClient.builder()
.baseUrl(properties.getDomain())
.defaultHeader("Accept", "application/json")
.defaultHeader("User-Agent", "QuantGroup PayCenter Java Api")
.defaultHeader("Content-Type", "application/x-www-form-urlencoded;charset=" + charset)
.build();
return this;
}
@Override
public Object code() {
return this.properties.getMchChanId().toString();
}
@Override
public JsonNode pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", payRequestData.getOutTradeNo());
bodyNode.put("total_amount", payRequestData.getAmount());
bodyNode.put("subject", payRequestData.getSubject());
bodyNode.put("body", payRequestData.getSubject());
bodyNode.put("product_code", properties.getProductCode());
switch (properties.getAccessCode()) {
case APP:
return doAppExecute(bodyNode);
case H5:
case WEB:
return doWebExecute(bodyNode);
default:
}
return doExecute(bodyNode);
}
@Override
public JsonNode checkPay(CheckPayRequestData checkPayRequestData) {
// String outTradeNo = "11111111218";
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", checkPayRequestData.getOutTradeNo());
return doExecute(bodyNode);
}
@Override
public JsonNode checkRefund(CheckRefundRequestData checkRefundRequestData) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", checkRefundRequestData.getOutTradeNo());
bodyNode.put("out_request_no", checkRefundRequestData.getOutRefundNo());
return doExecute(bodyNode);
}
@Override
public JsonNode close(CloseRequestData closeRequestData) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", closeRequestData.getOutTradeNo());
return doExecute(bodyNode);
}
@Override
public JsonNode refund(RefundRequestData refundRequestData) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", refundRequestData.getOutTradeNo());
bodyNode.put("refund_amount", refundRequestData.getRefundAmount());
bodyNode.put("refund_reason", refundRequestData.getRefundReason());
bodyNode.put("out_request_no", refundRequestData.getOutRefundNo());
return doExecute(bodyNode);
}
/**
* App支付发起请求
*
* @param bodyNode
* @return
*/
private JsonNode doAppExecute(ObjectNode bodyNode) {
ObjectNode params = buildParams(bodyNode);
TreeMap<String, String> sortedParams = getSortedMap(params);
ObjectNode body = objectMapper.createObjectNode();
body.put("data", buildQuery(sortedParams));
return body;
}
/**
* web支付发起请求
* alipay.trade.wap.pay
* alipay.trade.page.pay
*
* @param bodyNode
* @return
*/
private JsonNode doWebExecute(ObjectNode bodyNode) {
ObjectNode params = buildParams(bodyNode);
TreeMap<String, String> sortedParams = getSortedMap(params);
String url = String.format("%s%s?%s", properties.getDomain(), properties.getPayAccess(), buildQuery(sortedParams));
ObjectNode body = objectMapper.createObjectNode();
body.put("data", url);
return body;
}
/**
* 发起请求
*
* @param bodyNode
* @return
*/
private JsonNode doExecute(ObjectNode bodyNode) {
ObjectNode params = buildParams(bodyNode);
params.remove("biz_content");
Map<String, String> sortedParams = objectMapper.convertValue(params, new TypeReference<Map<String, String>>(){});
URI uri = URI.create(String.format("%s%s?%s", properties.getDomain(), properties.getPayAccess(), buildQuery(sortedParams)));
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("biz_content", objectMapper.writeValueAsString(bodyNode));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Mono<ClientResponse> mono = webClient.post()
.uri(uri)
.body(BodyInserters.fromFormData(formData))
.exchange();
ClientResponse response = mono.block();
JsonNode bodyJsonNode = objectMapper.createObjectNode();
if (response != null) {
Mono<String> resultMono = response.bodyToMono(String.class);
String bodyRes = resultMono.block();
Pattern pattern = Pattern.compile("[a-zA-Z_0-9]*_response"); //处理支付宝不同的返回格式,统一为response
Matcher matcher = pattern.matcher(bodyRes);
String body = matcher.replaceAll("response");
try {
bodyJsonNode = objectMapper.readTree(body);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
if (response.statusCode().value() >= 200 && response.statusCode().value() < 300) {
//支付宝公钥号
String certSN = bodyJsonNode.get("alipay_cert_sn").asText();
if (!getPublicKeyCertSN().equals(certSN)) {
// TODO: 证书可能过期了,需要处理支付宝公钥过期的情况
throw new RuntimeException("支付宝公钥过期");
}
//支付宝签名
String signature = bodyJsonNode.get("sign").asText();
//签名信息
//这里的斜杠替换是因为json解析之后自动去掉了转义符/,签名的时候需要再给加上
String signText = bodyJsonNode.get("response").toString().replace("/", "\\/");
if (verify(signText, signature)) {
return bodyJsonNode;
}
// TODO: 签名校验失败,做异常处理
throw new RuntimeException("签名校验失败");
} else {
// TODO: 返回异常,做异常处理
throw new RuntimeException("返回异常,做异常处理");
}
}
return bodyJsonNode;
}
public ObjectNode buildParams(ObjectNode bizContent) {
ObjectNode paramNode = objectMapper.createObjectNode();
paramNode.put("method", properties.getAccessMethod());
// paramNode.put("method", "alipay.trade.query");
paramNode.put("version", properties.getVersion());
paramNode.put("app_id", properties.getPayAcctId());
paramNode.put("sign_type", signType);
paramNode.put("notify_url", bizContent.get("notify_url"));
paramNode.put("return_url", bizContent.get("return_url"));
paramNode.put("charset", charset);
paramNode.put("app_cert_sn", properties.getPayCertNo());
paramNode.put("alipay_root_cert_sn", properties.getPayRootCertNo());
bizContent.remove("notify_url");
bizContent.remove("return_url");
Long timestamp = System.currentTimeMillis();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("GMT+8"));
paramNode.put("timestamp", df.format(new Date(timestamp)));
paramNode.put("format", "json");
// paramNode.put("auth_token", "");
try {
paramNode.put("biz_content", objectMapper.writeValueAsString(bizContent));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
TreeMap<String, String> sortedParams = getSortedMap(paramNode);
String signContent = getSignContent(sortedParams);
paramNode.put("sign", sign(signContent));
return paramNode;
}
public String buildQuery(Map<String, String> params) {
if (params == null || params.isEmpty()) {
return null;
}
StringBuilder query = new StringBuilder();
Set<Map.Entry<String, String>> entries = params.entrySet();
boolean hasParam = false;
for (Map.Entry<String, String> entry : entries) {
String name = entry.getKey();
String value = entry.getValue();
// 忽略参数名或参数值为空的参数
if (areNotEmpty(name, value)) {
if (hasParam) {
query.append("&");
} else {
hasParam = true;
}
query.append(name).append("=").append(UriUtils.encode(value, charset));
}
}
return query.toString();
}
public TreeMap<String, String> getSortedMap(ObjectNode paramNode) {
return objectMapper.convertValue(paramNode, new TypeReference<TreeMap<String, String>>(){});
}
/**
* @param sortedParams
* @return
*/
public String getSignContent(TreeMap<String, String> sortedParams) {
StringBuilder content = new StringBuilder();
List<String> keys = new ArrayList<String>(sortedParams.keySet());
Collections.sort(keys);
int index = 0;
for (String key : keys) {
String value = sortedParams.get(key);
if (areNotEmpty(key, value)) {
content.append(index == 0 ? "" : "&").append(key).append("=").append(value);
index++;
}
}
return content.toString();
}
/**
* 检查指定的字符串是否为空。
* <ul>
* <li>SysUtils.isEmpty(null) = true</li>
* <li>SysUtils.isEmpty("") = true</li>
* <li>SysUtils.isEmpty(" ") = true</li>
* <li>SysUtils.isEmpty("abc") = false</li>
* </ul>
*
* @param value 待检查的字符串
* @return true/false
*/
public static boolean isEmpty(String value) {
int strLen;
if (value == null || (strLen = value.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(value.charAt(i)) == false)) {
return false;
}
}
return true;
}
/**
* 检查指定的字符串列表是否不为空。
*/
public static boolean areNotEmpty(String... values) {
boolean result = true;
if (values == null || values.length == 0) {
result = false;
} else {
for (String value : values) {
result &= !isEmpty(value);
}
}
return result;
}
/**
* 签名
*
* @param signText 需要签名的字符串
* @return
*/
public String sign(String signText) {
try {
Signature sign = Signature.getInstance(properties.getSignType());
sign.initSign(this.privateKey);
sign.update(signText.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);
}
}
/**
* 获取证书。
*
* @param certString 证书内容
* @return X509证书
*/
public X509Certificate[] loadCertificates(String certString) {
try {
ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X509", provider);
Collection<? extends Certificate> certs = cf.generateCertificates(fis);
return (X509Certificate[]) certs.toArray(new X509Certificate[certs.size()]);
} catch (CertificateExpiredException var3) {
throw new RuntimeException("证书已过期", var3);
} catch (CertificateNotYetValidException var4) {
throw new RuntimeException("证书尚未生效", var4);
} catch (CertificateException var5) {
throw new RuntimeException("无效的证书", var5);
}
}
/**
* 获取证书。
*
* @param certString 证书内容
* @return X509证书
*/
public X509Certificate loadCertificate(String certString) {
try {
ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException var3) {
throw new RuntimeException("证书已过期", var3);
} catch (CertificateNotYetValidException var4) {
throw new RuntimeException("证书尚未生效", var4);
} catch (CertificateException var5) {
throw new RuntimeException("无效的证书", var5);
}
}
/**
* RSA验签名检查
*
* @param content 待签名数据
* @param signature 签名值
* @return 布尔值
*/
public boolean verify(String content, String signature) {
try {
Signature sign = Signature.getInstance(properties.getSignType());
sign.initVerify(payPublicKey);
sign.update(content.getBytes());
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
/**
* 获取私钥。
*
* @param certString 私钥文件内容(required)
* @return 私钥对象
*/
public PrivateKey getPrivateKey(String certString) {
try {
String privateKey = certString
.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("无效的密钥格式");
}
}
/**
* 获取公钥。
*
* @param certString 公钥文件内容(required)
* @return 私钥对象
*/
public static PublicKey getPublicKey(String certString) throws IOException {
try {
String publicKey = certString
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(
new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 获取支付宝私钥证书号
* @return
*/
public String getPublicKeyCertSN() {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update((payPublicKey.getIssuerX500Principal().getName() + payPublicKey.getSerialNumber()).getBytes());
String certSN = new BigInteger(1, md.digest()).toString(16);
//BigInteger会把0省略掉,需补全至32位
certSN = fillMD5(certSN);
return certSN;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private String fillMD5(String md5) {
return md5.length() == 32 ? md5 : fillMD5("0" + md5);
}
}
\ No newline at end of file
......@@ -2,15 +2,81 @@ package cn.quant.baa.pay.acquirer.weixin;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.web.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
/**
* Created by Administrator on 2021/8/31 0031.
*/
public class WeiXinMerchantAcquirer extends MerchantAcquirer {
private WebClient webClient;
/**
* 微信签名格式定义
*/
String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
/**
* 微信签名前缀
*/
String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";
/**
* 商户私钥
*/
PrivateKey privateKey;
/**
* 平台证书
*/
X509Certificate payCerKey;
ObjectMapper objectMapper = new ObjectMapper();
/**
* 支付初始化
* 1. 初始化私钥
* 2. 初始化平台证书
* 3. 初始化webClient
*
* @param properties
* @return
* @throws Exception
*/
@Override
public MerchantAcquirer init(AcquirerProperties properties) throws Exception {
super.init(properties);
privateKey = getPrivateKey(properties.getPrivateKey());
payCerKey = loadCertificate(properties.getPayCertKey());
this.webClient = WebClient.builder().baseUrl(properties.getDomain())
.defaultHeader("Accept", "application/json")
.defaultHeader("User-Agent", "QuantGroup PayCenter Java Api")
.build();
return this;
}
......@@ -18,4 +84,283 @@ public class WeiXinMerchantAcquirer extends MerchantAcquirer {
public Object code() {
return this.properties.getMchChanId().toString();
}
@Override
public JsonNode pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity) {
ObjectNode bodyNode = objectMapper.createObjectNode();
// 转换金额为分
BigInteger amount = new BigDecimal(payRequestData.getAmount()).multiply(new BigDecimal(100)).toBigInteger();
bodyNode.put("mchid", properties.getPayAcctId())
.put("appid", properties.getPayAppId())
.put("attach", payRequestData.getAttach())
.put("description", payRequestData.getSubject())
.put("notify_url", payRequestData.getNotifyUrl())
.put("out_trade_no", payRequestData.getOutTradeNo());
bodyNode.putObject("amount")
.put("total", amount);
ObjectNode sceneInfo = objectMapper.createObjectNode()
.put("payer_client_ip", "127.0.0.1");
switch (properties.getAccessCode()) {
case H5:
sceneInfo.putObject("h5_info").put("type", "Wap");
break;
case JS:
bodyNode.putObject("payer")
.put("openid", "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o");
break;
case APP:
default:
}
bodyNode.set("scene_info", sceneInfo);
String payAccess = properties.getPayAccess();
return doExecute(payAccess, bodyNode);
}
@Override
public JsonNode checkPay(CheckPayRequestData checkPayRequestData) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("mchid", properties.getPayAcctId());
String payAccess = properties.getPayAccess().replace("{out_trade_no}", checkPayRequestData.getOutTradeNo());
return doExecute(payAccess, bodyNode);
}
@Override
public JsonNode checkRefund(CheckRefundRequestData checkRefundRequestData) {
String payAccess = properties.getPayAccess().replace("{out_refund_no}", checkRefundRequestData.getOutRefundNo());
return doExecute(payAccess, null);
}
@Override
public JsonNode close(CloseRequestData closeRequestData) {
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("mchid", properties.getPayAcctId());
String payAccess = properties.getPayAccess().replace("{out_trade_no}", closeRequestData.getOutTradeNo());
return doExecute(payAccess, bodyNode);
}
@Override
public JsonNode refund(RefundRequestData refundRequestData) {
// 转换金额为分
BigInteger amount = new BigDecimal(refundRequestData.getRefundAmount()).multiply(new BigDecimal(100)).toBigInteger();
ObjectNode bodyNode = objectMapper.createObjectNode();
bodyNode.put("out_trade_no", refundRequestData.getOutTradeNo());
bodyNode.put("out_refund_no", refundRequestData.getOutRefundNo());
bodyNode.put("reason", refundRequestData.getRefundReason());
// bodyNode.put("notify_url", "");
bodyNode.putObject("amount")
.put("refund", amount)
.put("total", amount)
.put("currency", "CNY");
String payAccess = properties.getPayAccess();
return doExecute(payAccess, bodyNode);
}
/**
*
* @param payAccess
* @param bodyNode
* @return
*/
private JsonNode doExecute(String payAccess, ObjectNode bodyNode) {
String method = properties.getAccessMethod();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(payAccess);
String requestBody = "";
// 处理GET请求
if (HttpMethod.GET.name().equals(method)) {
if (null != bodyNode) {
bodyNode.fields().forEachRemaining((entry) -> {
uriComponentsBuilder.queryParam(entry.getKey(), entry.getValue().asText());
});
}
} else {
// 非GET请求的时候处理requestBody
try {
if (null != bodyNode) {
requestBody = objectMapper.writeValueAsString(bodyNode);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
String uri = uriComponentsBuilder.build().toString();
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = UUID.randomUUID().toString().replace("-", "");
// 代签名字符串
String signText = joining("\n", method, uri, String.valueOf(timestamp), nonceStr, requestBody);
String sign = sign(signText);
String token = String.format(TOKEN_PATTERN, properties.getPayAcctId(), nonceStr, timestamp, properties.getPayCertNo(), sign);
Mono<ClientResponse> mono = webClient.method(HttpMethod.resolve(method))
.uri(uri)
.header("Authorization", SCHEMA.concat(token))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.exchange();
ClientResponse response = mono.block();
JsonNode bodyJsonNode = objectMapper.createObjectNode();
if (response != null) {
Mono<String> resultMono = response.bodyToMono(String.class);
String body = resultMono.block();
if (null == body) {
body = "";
}
try {
bodyJsonNode = objectMapper.readTree(body);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
if (response.statusCode().value() >= 200 && response.statusCode().value() < 300) {
//Request-ID
String requestId = response.headers().asHttpHeaders().getFirst("Request-ID");
//当前使用的微信平台证书序列号
String serial = response.headers().asHttpHeaders().getFirst("Wechatpay-Serial");
//微信服务器的时间戳
String wxTimestamp = response.headers().asHttpHeaders().getFirst("Wechatpay-Timestamp");
//微信服务器提供的随机串
String nonce = response.headers().asHttpHeaders().getFirst("Wechatpay-Nonce");
//微信平台签名
String signature = response.headers().asHttpHeaders().getFirst("Wechatpay-Signature");
//签名信息
String wsSignText = joining("\n", wxTimestamp, nonce, body);
if (verify(wsSignText, signature)) {
return bodyJsonNode;
}
// TODO: 签名校验失败,做异常处理
throw new RuntimeException("签名校验失败");
} else {
return bodyJsonNode;
// TODO: 返回异常,做异常处理
// throw new RuntimeException("返回异常,做异常处理");
}
}
return bodyJsonNode;
}
/**
* 签名
*
* @param signText 需要签名的字符串
* @return
*/
public String sign(String signText) {
try {
Signature sign = Signature.getInstance(properties.getSignType());
sign.initSign(this.privateKey);
sign.update(signText.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);
}
}
/**
* 获取证书。
*
* @param certString 证书内容
* @return X509证书
*/
public X509Certificate loadCertificate(String certString) {
try {
ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException var3) {
throw new RuntimeException("证书已过期", var3);
} catch (CertificateNotYetValidException var4) {
throw new RuntimeException("证书尚未生效", var4);
} catch (CertificateException var5) {
throw new RuntimeException("无效的证书", var5);
}
}
/**
* RSA验签名检查
*
* @param content 待签名数据
* @param signature 签名值
* @return 布尔值
*/
public boolean verify(String content, String signature) {
try {
Signature sign = Signature.getInstance(properties.getSignType());
sign.initVerify(payCerKey);
sign.update(content.getBytes());
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
/**
* 获取私钥。
*
* @param certString 私钥文件内容(required)
* @return 私钥对象
*/
public PrivateKey getPrivateKey(String certString) {
try {
String privateKey = certString
.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("无效的密钥格式");
}
}
/**
* 获取公钥。
*
* @param certString 公钥文件内容(required)
* @return 私钥对象
*/
public static PublicKey getPublicKey(String certString) throws IOException {
try {
String publicKey = certString
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(
new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 字符串数组拼接为字符串
*
* @param separator 分隔符
* @param str 字符数组
* @return 字符串
*/
public String joining(String separator, String... str) {
StringBuilder builder = new StringBuilder();
for (String s : str) {
builder.append(s).append(separator);
}
return builder.toString();
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import cn.quant.baa.pay.acquirer.AcquirerConfiguration;
import cn.quant.baa.pay.acquirer.AcquirerConfigurer;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.acquirer.alipay.AlipayMerchantAcquirer;
import cn.quant.baa.pay.acquirer.weixin.WeiXinMerchantAcquirer;
import cn.quant.baa.pay.jpa.entity.*;
import cn.quant.baa.pay.jpa.repository.*;
......@@ -12,10 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.List;
......@@ -90,6 +87,10 @@ public class ServerConfiguration {
WeiXinMerchantAcquirer acquirer = new WeiXinMerchantAcquirer();
acquirer.init(property);
merchantAcquirer.register(acquirer);
} else if ("ALIP".equals(property.getPayChanCode())) {
AlipayMerchantAcquirer acquirer = new AlipayMerchantAcquirer();
acquirer.init(property);
merchantAcquirer.register(acquirer);
}
}
return merchantAcquirer;
......
......@@ -5,6 +5,11 @@ package cn.quant.baa.pay.dict;
*/
public enum AccessCode {
REFUND("退款"),
CHKRFD("退款查询"),
CHKPAY("支付查询"),
CLOSE("关闭订单"),
WEB("电脑网站支付"),
APP("应用支付"),
H5("H5页支付"),
JS("网页、小程序支付");
......
......@@ -44,13 +44,18 @@ public class PayAccountEntity extends OptimisticEntity implements Serializable {
@Column(name = "PAY_PUBLIC_KEY", nullable = false, length = 255)
private String payPublicKey;
@Column(name = "PAY_CERT_NO", nullable = true, length = 64)
private String payCertNo;
@Column(name = "PAY_CERT_KEY", nullable = true, length = 2000)
private String payCertKey;
@Column(name = "PAY_ROOT_CERT_NO", nullable = true, length = 64)
private String payRootCertNo;
@Column(name = "PAY_ROOT_CERT_KEY", nullable = true, length = 4000)
private String payRootCertKey;
public PayAccountIds getIds() {
return ids;
}
......@@ -139,6 +144,22 @@ public class PayAccountEntity extends OptimisticEntity implements Serializable {
this.payCertKey = payCertKey;
}
public String getPayRootCertNo() {
return payRootCertNo;
}
public void setPayRootCertNo(String payRootCertNo) {
this.payRootCertNo = payRootCertNo;
}
public String getPayRootCertKey() {
return payRootCertKey;
}
public void setPayRootCertKey(String payRootCertKey) {
this.payRootCertKey = payRootCertKey;
}
@Override
public String persistenceKey() {
return ids.persistenceKey();
......
......@@ -32,6 +32,9 @@ public class PayFeatureEntity extends DescriptionEntity implements Serializable
@Column(name = "PAY_ACCESS", nullable = false, length = 255)
private String payAccess;
@Column(name = "PRODUCT_CODE", nullable = false, length = 255)
private String productCode;
@Column(name = "REFUND_ACCESS", nullable = false, length = 255)
private String refundAccess;
......@@ -78,6 +81,14 @@ public class PayFeatureEntity extends DescriptionEntity implements Serializable
this.payAccess = payAccess;
}
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public String getRefundAccess() {
return refundAccess;
}
......
......@@ -14,4 +14,6 @@ import org.springframework.stereotype.Repository;
@Repository
public interface PayHistoryRepository extends JpaRepository<PayHistoryEntity, PayHistoryIds> {
// @Query("")
// int counAAAt(PayHistoryIds ids);
}
package cn.quant.baa.pay.model.web;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/7
* Time: 下午4:31
* Description: No Description
*/
public class CheckPayRequestData implements Serializable {
private static final long serialVersionUID = -2386424808660622542L;
/**
* 支付通道ID
*/
@NotNull(message = "ILLEGAL_REQ_CHAN_ID")
private String chanId;
/**
* 商户订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_TRADE_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_TRADE_NO")
private String outTradeNo;
public String getChanId() {
return chanId;
}
public void setChanId(String chanId) {
this.chanId = chanId;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
}
package cn.quant.baa.pay.model.web;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/8
* Time: 上午11:05
* Description: No Description
*/
public class CheckRefundRequestData implements Serializable {
private static final long serialVersionUID = 4652001398076281697L;
/**
* 支付通道ID
*/
@NotNull(message = "ILLEGAL_REQ_CHAN_ID")
private String chanId;
/**
* 退款订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_REFUND_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_REFUND_NO")
private String outRefundNo;
/**
* 商户订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_TRADE_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_TRADE_NO")
private String outTradeNo;
public String getChanId() {
return chanId;
}
public void setChanId(String chanId) {
this.chanId = chanId;
}
public String getOutRefundNo() {
return outRefundNo;
}
public void setOutRefundNo(String outRefundNo) {
this.outRefundNo = outRefundNo;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
}
package cn.quant.baa.pay.model.web;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/8
* Time: 上午11:28
* Description: No Description
*/
public class CloseRequestData implements Serializable {
private static final long serialVersionUID = 8489060627679269541L;
/**
* 支付通道ID
*/
@NotNull(message = "ILLEGAL_REQ_CHAN_ID")
private String chanId;
/**
* 商户订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_TRADE_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_TRADE_NO")
private String outTradeNo;
public String getChanId() {
return chanId;
}
public void setChanId(String chanId) {
this.chanId = chanId;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
}
package cn.quant.baa.pay.model.web;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/8
* Time: 上午11:30
* Description: No Description
*/
public class RefundRequestData implements Serializable {
private static final long serialVersionUID = -3619831737213449288L;
/**
* 支付通道ID
*/
@NotNull(message = "ILLEGAL_REQ_CHAN_ID")
private String chanId;
/**
* 退款订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_REFUND_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_REFUND_NO")
private String outRefundNo;
/**
* 商户订单号
*/
@Size(min = 6, max = 64, message = "ILLEGAL_REQ_OUT_TRADE_NO")
@NotNull(message = "ILLEGAL_REQ_OUT_TRADE_NO")
private String outTradeNo;
/**
* 退款原因
*/
@NotNull(message = "ILLEGAL_REQ_REFUND_REASON")
private String refundReason;
/**
* 退款金额
*/
@NotNull(message = "ILLEGAL_REQ_REFUND_AMOUNT")
private String refundAmount;
public String getChanId() {
return chanId;
}
public void setChanId(String chanId) {
this.chanId = chanId;
}
public String getOutRefundNo() {
return outRefundNo;
}
public void setOutRefundNo(String outRefundNo) {
this.outRefundNo = outRefundNo;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getRefundReason() {
return refundReason;
}
public void setRefundReason(String refundReason) {
this.refundReason = refundReason;
}
public String getRefundAmount() {
return refundAmount;
}
public void setRefundAmount(String refundAmount) {
this.refundAmount = refundAmount;
}
}
......@@ -67,6 +67,10 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
......@@ -81,6 +85,7 @@
<include>**/*.xml</include>
<include>**/*.properties</include>
<include>**/*.txt</include>
<include>**/*.html</include>
</includes>
</resource>
<resource>
......
package cn.quant.baa.pay.rest;
import cn.quant.baa.pay.annotation.BusinessMapping;
import cn.quant.baa.pay.context.TransactionSession;
import cn.quant.baa.pay.model.BusinessRequest;
import cn.quant.baa.pay.model.web.CheckPayRequestData;
import cn.quant.baa.pay.model.web.CheckRefundRequestData;
import cn.quant.baa.pay.model.web.PayRequestData;
import cn.quant.baa.pay.service.TransactionService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
* Author: Lipeng Liu
* Date: 2021/9/9
* Time: 上午9:51
* Description: No Description
*/
@Controller
//@RestController
@RequestMapping("pay")
public class PayTestController {
ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private TransactionService transactionService;
@GetMapping("")
public String getPay(Model model) {
return "pay/index";
}
@RequestMapping("h5")
public String getH5(Model model) {
return "pay/h5";
}
@RequestMapping("checkPay")
public String checkPay(CheckPayRequestData data, Model model, HttpServletRequest servletRequest) {
String res = "";
if (servletRequest.getMethod().equals("POST")) {
res = transactionService.checkPay(data).toString();
}
model.addAttribute("res", res);
return "pay/checkPay";
}
@RequestMapping("checkRefund")
public String checkRefund(CheckRefundRequestData data, Model model, HttpServletRequest servletRequest) {
String res = "";
if (servletRequest.getMethod().equals("POST")) {
res = transactionService.checkRefund(data).toString();
}
model.addAttribute("res", res);
return "pay/checkRefund";
}
@PostMapping("goPay")
@BusinessMapping(session = 1)
@ResponseBody
public JsonNode goPay(@RequestBody(required = false) BusinessRequest<PayRequestData> requestData) {
PayRequestData data = requestData.getData();
String str = "{\"subject\":\"测试订单1\",\"mchId\":\"wx2f44c7fe7b08458d\",\"chanId\":\"75772285618946307\",\"outTradeNo\":\"11111111223\",\"originalAmount\":\"110.00\",\"amount\":0.01,\"discounts\":\"10.00\",\"notifyUrl\":\"http://127.0.0.1:8080/notifyUrl\",\"buyerId\":\"777777\",\"attach\":\"AAAA-BBBB-1111-2222\",\"creditAmount\":\"10.00\",\"cashAmount\":\"10\",\"goodsDetail\":[{\"goodsNo\":\"123123\",\"goodsId\":\"11111\",\"goodsName\":\"商品1\",\"quantity\":2,\"price\":\"10.00\",\"discounts\":\"2.5\",\"amount\":\"17.5\",\"attach\":\"---\",\"creditAmount\":123,\"cashAmount\":123}]}";
try {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
PayRequestData payRequestData = objectMapper.readValue(str, PayRequestData.class);
String[] temp = data.getChanId().split("_");
payRequestData.setChanId(temp[0]);
payRequestData.setMchId(temp[1]);
payRequestData.setOutTradeNo(data.getOutTradeNo());
payRequestData.setSubject(data.getSubject());
payRequestData.setAmount(data.getAmount());
payRequestData.setNotifyUrl("http://127.0.0.1:8080/notifyUrl");
JsonNode res = transactionService.pay(payRequestData);
return res;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return objectMapper.createObjectNode();
}
}
\ No newline at end of file
......@@ -3,8 +3,9 @@ package cn.quant.baa.pay.rest;
import cn.quant.baa.pay.annotation.BusinessMapping;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.BusinessRequest;
import cn.quant.baa.pay.model.web.PayRequestData;
import cn.quant.baa.pay.model.web.*;
import cn.quant.baa.pay.service.TransactionService;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
......@@ -22,11 +23,61 @@ public class TransactionController extends BusinessController {
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/pay")
public void pay(@RequestBody BusinessRequest<PayRequestData> request) {
public JsonNode pay(@RequestBody BusinessRequest<PayRequestData> request) {
PayRequestData data = request.getData();
transactionService.pay(data);
JsonNode res = transactionService.pay(data);
System.currentTimeMillis();
return res;
}
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/checkPay")
public JsonNode checkPay(@RequestBody BusinessRequest<CheckPayRequestData> request) {
CheckPayRequestData data = request.getData();
JsonNode res = transactionService.checkPay(data);
System.currentTimeMillis();
return res;
}
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/checkRefund")
public JsonNode checkRefund(@RequestBody BusinessRequest<CheckRefundRequestData> request) {
CheckRefundRequestData data = request.getData();
JsonNode res = transactionService.checkRefund(data);
System.currentTimeMillis();
return res;
}
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/close")
public JsonNode close(@RequestBody BusinessRequest<CloseRequestData> request) {
CloseRequestData data = request.getData();
JsonNode res = transactionService.close(data);
System.currentTimeMillis();
return res;
}
@ResponseBody
@BusinessMapping(session = 1)
@PostMapping("/refund")
public JsonNode refund(@RequestBody BusinessRequest<RefundRequestData> request) {
RefundRequestData data = request.getData();
JsonNode res = transactionService.refund(data);
System.currentTimeMillis();
return res;
}
}
......@@ -5,10 +5,10 @@ import cn.quant.baa.pay.acquirer.AcquirerConfiguration;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.context.TransactionSession;
import cn.quant.baa.pay.jpa.entity.*;
import cn.quant.baa.pay.model.web.*;
import cn.quant.baa.pay.util.AssertUtils;
import cn.quant.baa.pay.model.web.GoodsDetail;
import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.model.web.PayRequestData;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -105,15 +105,33 @@ public class TransactionService extends BusinessService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void pay(PayRequestData data) {
public JsonNode pay(PayRequestData data) {
TransactionSession session = payHistory(data);
session.getProperty(PayHistoryEntity.class, PayHistoryEntity.class);
PayHistoryEntity payHistoryEntity = session.getProperty(PayHistoryEntity.class, PayHistoryEntity.class);
System.currentTimeMillis();
// acquirer.pay(history, data);
return acquirer.pay(data, payHistoryEntity);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JsonNode checkPay(CheckPayRequestData data) {
return acquirer.checkPay(data);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JsonNode checkRefund(CheckRefundRequestData data) {
return acquirer.checkRefund(data);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JsonNode close(CloseRequestData data) {
return acquirer.close(data);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JsonNode refund(RefundRequestData data) {
return acquirer.refund(data);
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="CN">
<head>
<title>交易查询</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
*{
margin:0;
padding:0;
}
ul,ol{
list-style:none;
}
body{
font-family: "Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;
}
.hidden{
display:none;
}
.new-btn-login-sp{
padding: 1px;
display: inline-block;
width: 75%;
}
.new-btn-login {
background-color: #02aaf1;
color: #FFFFFF;
font-weight: bold;
border: none;
width: 100%;
height: 30px;
border-radius: 5px;
font-size: 16px;
}
#main{
width:100%;
margin:0 auto;
font-size:14px;
}
.red-star{
color:#f00;
width:10px;
display:inline-block;
}
.null-star{
color:#fff;
}
.content{
margin-top:5px;
}
.content dt{
width:100px;
display:inline-block;
float: left;
margin-left: 20px;
color: #666;
font-size: 13px;
margin-top: 8px;
}
.content dd{
margin-left:120px;
margin-bottom:5px;
}
.content dd input, select {
width: 85%;
height: 28px;
border: 0;
-webkit-border-radius: 0;
-webkit-appearance: none;
}
#foot{
margin-top:10px;
position: absolute;
bottom: 15px;
width: 100%;
}
.foot-ul{
width: 100%;
}
.foot-ul li {
width: 100%;
text-align:center;
color: #666;
}
.note-help {
color: #999999;
font-size: 12px;
line-height: 130%;
margin-top: 5px;
width: 100%;
display: block;
}
#btn-dd{
margin: 20px;
text-align: center;
}
.foot-ul{
width: 100%;
}
.one_line{
display: block;
height: 1px;
border: 0;
border-top: 1px solid #eeeeee;
width: 100%;
margin-left: 20px;
}
.am-header {
display: -webkit-box;
display: -ms-flexbox;
display: box;
width: 100%;
position: relative;
padding: 7px 0;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
background: #1D222D;
height: 50px;
text-align: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
box-pack: center;
-webkit-box-align: center;
-ms-flex-align: center;
box-align: center;
}
.am-header h1 {
-webkit-box-flex: 1;
-ms-flex: 1;
box-flex: 1;
line-height: 18px;
text-align: center;
font-size: 18px;
font-weight: 300;
color: #fff;
}
</style>
</head>
<body text=#000000 bgColor="#ffffff" leftMargin=0 topMargin=4>
<header class="am-header">
<h1>交易查询</h1>
</header>
<div id="main">
<form name=alipayment action='' method=post>
<div id="body" style="clear:left">
<dl class="content">
<dt>支付方式
</dt>
<dd>
<select name="chanId">
<option value="75772285618946310">微信</option>
<option value="75772285618946309">支付宝</option>
</select>
</dd>
<hr class="one_line">
<dt>商户订单号
</dt>
<dd>
<input id="WIDout_trade_no" name="outTradeNo" value="11111111218"/>
</dd>
<hr class="one_line">
<dt></dt>
<dd>
<span style="line-height: 28px; color:red;">[[${res}]]</span>
</dd>
<dd id="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" type="submit" style="text-align:center;">确 认</button>
</span>
<span class="note-help">如果您点击“确认”按钮,即表示您同意该次的执行操作。</span>
</dd>
</dl>
</div>
</form>
<div id="foot">
<ul class="foot-ul">
<li>
QUANTGROUP.CN
</li>
</ul>
</div>
</div>
</body>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>退款查询</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
*{
margin:0;
padding:0;
}
ul,ol{
list-style:none;
}
body{
font-family: "Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;
}
.hidden{
display:none;
}
.new-btn-login-sp{
padding: 1px;
display: inline-block;
width: 75%;
}
.new-btn-login {
background-color: #02aaf1;
color: #FFFFFF;
font-weight: bold;
border: none;
width: 100%;
height: 30px;
border-radius: 5px;
font-size: 16px;
}
#main{
width:100%;
margin:0 auto;
font-size:14px;
}
.red-star{
color:#f00;
width:10px;
display:inline-block;
}
.null-star{
color:#fff;
}
.content{
margin-top:5px;
}
.content dt{
width:100px;
display:inline-block;
float: left;
margin-left: 20px;
color: #666;
font-size: 13px;
margin-top: 8px;
}
.content dd{
margin-left:120px;
margin-bottom:5px;
}
.content dd input, select {
width: 85%;
height: 28px;
border: 0;
-webkit-border-radius: 0;
-webkit-appearance: none;
}
#foot{
margin-top:10px;
position: absolute;
bottom: 15px;
width: 100%;
}
.foot-ul{
width: 100%;
}
.foot-ul li {
width: 100%;
text-align:center;
color: #666;
}
.note-help {
color: #999999;
font-size: 12px;
line-height: 130%;
margin-top: 5px;
width: 100%;
display: block;
}
#btn-dd{
margin: 20px;
text-align: center;
}
.foot-ul{
width: 100%;
}
.one_line{
display: block;
height: 1px;
border: 0;
border-top: 1px solid #eeeeee;
width: 100%;
margin-left: 20px;
}
.am-header {
display: -webkit-box;
display: -ms-flexbox;
display: box;
width: 100%;
position: relative;
padding: 7px 0;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
background: #1D222D;
height: 50px;
text-align: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
box-pack: center;
-webkit-box-align: center;
-ms-flex-align: center;
box-align: center;
}
.am-header h1 {
-webkit-box-flex: 1;
-ms-flex: 1;
box-flex: 1;
line-height: 18px;
text-align: center;
font-size: 18px;
font-weight: 300;
color: #fff;
}
</style>
</head>
<body text=#000000 bgColor="#ffffff" leftMargin=0 topMargin=4>
<header class="am-header">
<h1>交易退款查询</h1>
</header>
<div id="main">
<form name=alipayment action='' method=post>
<div id="body" style="clear:left">
<dl class="content">
<dt>支付方式
</dt>
<dd>
<select name="chanId">
<option value="75772285618946311">支付宝</option>
<option value="75772285618946312">微信</option>
</select>
</dd>
<hr class="one_line">
<dt>退款请求号:</dt>
<dd>
<input id="WIDout_request_no" name="outRefundNo" value="11111111218"/>
</dd>
<dt>商户订单号:</dt>
<dd>
<input id="WIDout_trade_no" name="outTradeNo" value="11111111218"/>
</dd>
<hr class="one_line">
<dt></dt>
<dd>
<span style="line-height: 28px; color:red;">[[${res}]]</span>
</dd>
<dd id="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" type="submit" style="text-align:center;">确 认</button>
</span>
<span class="note-help">如果您点击“确认”按钮,即表示您同意该次的执行操作。</span>
</dd>
</dl>
</div>
</form>
<div id="foot">
<ul class="foot-ul">
<li>
QUANTGROUP.CN
</li>
</ul>
</div>
</div>
</body>
\ No newline at end of file
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="CN">
<head>
<title>手机网站支付接口</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery.serializeJSON/3.2.1/jquery.serializejson.min.js"></script>
<!-- <meta http-equiv="refresh" content="url=[[${payUrl}]]">-->
<style>
* {
margin: 0;
padding: 0;
}
ul, ol {
list-style: none;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
}
.hidden {
display: none;
}
.new-btn-login-sp {
padding: 1px;
display: inline-block;
width: 75%;
}
.new-btn-login {
background-color: #02aaf1;
color: #FFFFFF;
font-weight: bold;
border: none;
width: 100%;
height: 30px;
border-radius: 5px;
font-size: 16px;
}
#main {
width: 100%;
margin: 0 auto;
font-size: 14px;
}
.red-star {
color: #f00;
width: 10px;
display: inline-block;
}
.null-star {
color: #fff;
}
.content {
margin-top: 5px;
}
.content dt {
width: 100px;
display: inline-block;
float: left;
margin-left: 20px;
color: #666;
font-size: 13px;
margin-top: 8px;
}
.content dd {
margin-left: 120px;
margin-bottom: 5px;
}
.content dd input, .content dd select {
width: 85%;
height: 28px;
border: 0;
-webkit-border-radius: 0;
-webkit-appearance: none;
}
#foot {
margin-top: 10px;
position: absolute;
bottom: 15px;
width: 100%;
}
.foot-ul {
width: 100%;
}
.foot-ul li {
width: 100%;
text-align: center;
color: #666;
}
.note-help {
color: #999999;
font-size: 12px;
line-height: 130%;
margin-top: 5px;
width: 100%;
display: block;
}
#btn-dd {
margin: 20px;
text-align: center;
}
.foot-ul {
width: 100%;
}
.one_line {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #eeeeee;
width: 100%;
margin-left: 20px;
}
.am-header {
display: -webkit-box;
display: -ms-flexbox;
display: box;
width: 100%;
position: relative;
padding: 7px 0;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
background: #1D222D;
height: 50px;
text-align: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
box-pack: center;
-webkit-box-align: center;
-ms-flex-align: center;
box-align: center;
}
.am-header h1 {
-webkit-box-flex: 1;
-ms-flex: 1;
box-flex: 1;
line-height: 18px;
text-align: center;
font-size: 18px;
font-weight: 300;
color: #fff;
}
</style>
</head>
<body text=#000000 bgColor="#ffffff" leftMargin=0 topMargin=4>
<header class="am-header">
<h1>手机网站支付接口</h1>
</header>
<div id="main">
<form name=payment action='' method=post target="_blank" enctype="application/x-www-form-urlencoded" th:object="${requestData}">
<div id="body" style="clear:left">
<dl class="content">
<dt>支付方式
</dt>
<dd>
<select name="data[chan_id]">
<option value="75772285618946307_wx2f44c7fe7b08458d">微信</option>
<option value="75772285618946317_wxd678efh567hg6787">支付宝</option>
</select>
</dd>
<hr class="one_line">
<dt>商户订单号
</dt>
<dd>
<input id="outTradeNo" name="data[out_trade_no]"/>
</dd>
<hr class="one_line">
<dt>订单名称
</dt>
<dd>
<input id="subject" name="data[subject]"/>
</dd>
<hr class="one_line">
<dt>付款金额
</dt>
<dd>
<input id="amount" name="data[amount]"/>
</dd>
<hr class="one_line">
<dt>商品描述:</dt>
<dd>
<input id="body" name="data[body]"/>
</dd>
<hr class="one_line">
<dt></dt>
<dd id="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" type="button" style="text-align:center;">确 认</button>
</span>
<span class="note-help">如果您点击“确认”按钮,即表示您同意该次的执行操作。</span>
</dd>
</dl>
</div>
</form>
<div id="foot">
<ul class="foot-ul">
<li>
QUANTGROUP.CN
</li>
</ul>
</div>
</div>
</body>
<script language="javascript">
$(".new-btn-login").click(function() {
$.ajax({
type: "post",
url: '/pay/goPay',
async: false, // 使用同步方式
// 1 需要使用JSON.stringify 否则格式为 a=2&b=3&now=14...
// 2 需要强制类型转换,否则格式为 {"a":"2","b":"3"}
data: JSON.stringify($('form').serializeJSON()),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data) {
if (data.hasOwnProperty("h5_url")) {
window.location.href = data.h5_url;
}
if (data.hasOwnProperty("data")) {
window.location.href = data.data;
}
} // 注意不要在此行增加逗号
});
});
function GetDateNow() {
var vNow = new Date();
var sNow = "";
sNow += String(vNow.getFullYear());
sNow += String(vNow.getMonth() + 1);
sNow += String(vNow.getDate());
sNow += String(vNow.getHours());
sNow += String(vNow.getMinutes());
sNow += String(vNow.getSeconds());
sNow += String(vNow.getMilliseconds());
document.getElementById("outTradeNo").value = sNow;
document.getElementById("subject").value = "测试";
document.getElementById("amount").value = "0.01";
document.getElementById("body").value = "购买测试商品0.01元";
}
GetDateNow();
</script>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="CN">
<head>
<title>支付中心API</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
*{
margin:0;
padding:0;
}
ul,ol{
list-style:none;
}
body{
font-family: "Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;
}
.hidden{
display:none;
}
.new-btn-login-sp{
padding: 1px;
display: inline-block;
width: 75%;
}
.new-btn-login {
background-color: #02aaf1;
color: #FFFFFF;
font-weight: bold;
border: none;
width: 100%;
height: 30px;
border-radius: 5px;
font-size: 16px;
}
#main{
width:100%;
margin:0 auto;
font-size:14px;
}
.red-star{
color:#f00;
width:10px;
display:inline-block;
}
.null-star{
color:#fff;
}
.content{
margin-top:5px;
}
.content dt{
width:100px;
display:inline-block;
float: left;
margin-left: 20px;
color: #666;
font-size: 13px;
margin-top: 8px;
}
.content dd{
margin-left:120px;
margin-bottom:5px;
}
.content dd input {
width: 85%;
height: 28px;
border: 0;
-webkit-border-radius: 0;
-webkit-appearance: none;
}
#foot{
margin-top:10px;
position: absolute;
bottom: 15px;
width: 100%;
}
.foot-ul{
width: 100%;
}
.foot-ul li {
width: 100%;
text-align:center;
color: #666;
}
.note-help {
color: #999999;
font-size: 12px;
line-height: 130%;
margin-top: 5px;
width: 100%;
display: block;
}
.btn-dd{
margin: 20px;
text-align: center;
}
.foot-ul{
width: 100%;
}
.one_line{
display: block;
height: 1px;
border: 0;
border-top: 1px solid #eeeeee;
width: 100%;
margin-left: 20px;
}
.am-header {
display: -webkit-box;
display: -ms-flexbox;
display: box;
width: 100%;
position: relative;
padding: 7px 0;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
background: #1D222D;
height: 50px;
text-align: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
box-pack: center;
-webkit-box-align: center;
-ms-flex-align: center;
box-align: center;
}
.am-header h1 {
-webkit-box-flex: 1;
-ms-flex: 1;
box-flex: 1;
line-height: 18px;
text-align: center;
font-size: 18px;
font-weight: 300;
color: #fff;
}
</style>
</head>
<body text=#000000 bgColor="#ffffff" leftMargin=0 topMargin=4>
<header class="am-header">
<h1>支付中心接口</h1>
</header>
<div id="main">
<div id="body" style="clear:left">
<dl class="content">
<dt></dt>
<dd class="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" style="text-align:center;" onclick="window.open('/pay/h5')">手机网站支付</button>
</span>
</dd>
<dt></dt>
<dd class="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" style="text-align:center;" onclick="window.open('/pay/checkPay')">订单查询</button>
</span>
</dd>
<dt></dt>
<dd class="btn-dd">
<span class="new-btn-login-sp">
<button class="new-btn-login" style="text-align:center;" onclick="window.open('/pay/checkRefund')">退款查询</button>
</span>
</dd>
</dl>
</div>
<div id="foot">
<ul class="foot-ul">
<li>
QUANTGROUP.CN
</li>
</ul>
</div>
</div>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment