|
@@ -0,0 +1,267 @@
|
|
|
+package com.ylx.web.controller.massage;
|
|
|
+
|
|
|
+import cn.hutool.core.date.DatePattern;
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
+import cn.hutool.core.io.file.FileWriter;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.hutool.http.ContentType;
|
|
|
+import cn.hutool.json.JSONArray;
|
|
|
+import cn.hutool.json.JSONObject;
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
+import com.ijpay.core.IJPayHttpResponse;
|
|
|
+import com.ijpay.core.enums.AuthTypeEnum;
|
|
|
+import com.ijpay.core.enums.RequestMethodEnum;
|
|
|
+import com.ijpay.core.enums.SignType;
|
|
|
+import com.ijpay.core.enums.TradeType;
|
|
|
+import com.ijpay.core.kit.AesUtil;
|
|
|
+import com.ijpay.core.kit.HttpKit;
|
|
|
+import com.ijpay.core.kit.PayKit;
|
|
|
+import com.ijpay.core.kit.WxPayKit;
|
|
|
+import com.ijpay.core.utils.DateTimeZoneUtil;
|
|
|
+import com.ijpay.wxpay.WxPayApi;
|
|
|
+import com.ijpay.wxpay.enums.WxDomainEnum;
|
|
|
+import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
|
|
|
+import com.ijpay.wxpay.enums.v3.CertAlgorithmTypeEnum;
|
|
|
+import com.ijpay.wxpay.model.v3.Amount;
|
|
|
+import com.ijpay.wxpay.model.v3.Payer;
|
|
|
+import com.ijpay.wxpay.model.v3.UnifiedOrderModel;
|
|
|
+import com.ylx.common.config.WxPayConfig;
|
|
|
+import com.ylx.common.core.domain.R;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.cert.X509Certificate;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static com.ylx.common.constant.HttpStatus.SUCCESS;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author jianlong
|
|
|
+ * @date 2024-04-03 15:27
|
|
|
+ */
|
|
|
+
|
|
|
+@RestController
|
|
|
+@Slf4j
|
|
|
+public class PayController {
|
|
|
+
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private WxPayConfig wxPayProperties;
|
|
|
+
|
|
|
+ String serialNo;
|
|
|
+ String platSerialNo;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序微信支付的第一步,统一下单
|
|
|
+ */
|
|
|
+ @GetMapping("/pay")
|
|
|
+ public R createUnifiedOrder(@RequestParam(value = "openId") String openId) throws Exception {
|
|
|
+ String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
|
|
|
+ UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
|
|
|
+ .setAppid(wxPayProperties.getAppId())
|
|
|
+ .setMchid(wxPayProperties.getMchId())
|
|
|
+ //商品描述
|
|
|
+ .setDescription("充值")
|
|
|
+ //订单号
|
|
|
+ .setOut_trade_no(PayKit.generateStr())
|
|
|
+ //交易结束时间
|
|
|
+ .setTime_expire(timeExpire)
|
|
|
+ //附加数据
|
|
|
+ .setAttach("夜来香")
|
|
|
+ //通知地址异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
|
|
|
+ //示例值:https://www.weixin.qq.com/wxpay/pay.php
|
|
|
+ .setNotify_url(wxPayProperties.getNotifyUrl())
|
|
|
+ //支付金额以分为单位
|
|
|
+ .setAmount(new Amount().setTotal(1))
|
|
|
+ //交易人
|
|
|
+ .setPayer(new Payer().setOpenid(""));
|
|
|
+
|
|
|
+ log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
|
|
|
+ IJPayHttpResponse response = WxPayApi.v3(
|
|
|
+ RequestMethodEnum.POST,
|
|
|
+ WxDomainEnum.CHINA.toString(),
|
|
|
+ BasePayApiEnum.JS_API_PAY.toString(),
|
|
|
+ wxPayProperties.getMchId(),
|
|
|
+ getSerialNumber(),
|
|
|
+ null,
|
|
|
+ wxPayProperties.getCertKeyPath(),
|
|
|
+ JSONUtil.toJsonStr(unifiedOrderModel)
|
|
|
+ );
|
|
|
+
|
|
|
+ log.info("统一下单响应 {}", response);
|
|
|
+ // 根据证书序列号查询对应的证书来验证签名结果
|
|
|
+ boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
|
|
|
+ log.info("verifySignature: {}", verifySignature);
|
|
|
+ if (response.getStatus() == SUCCESS && verifySignature) {
|
|
|
+ String body = response.getBody();
|
|
|
+ JSONObject jsonObject = JSONUtil.parseObj(body);
|
|
|
+ String prepayId = jsonObject.getStr("prepay_id");
|
|
|
+ Map<String, String> map = WxPayKit.jsApiCreateSign(wxPayProperties.getAppId(), prepayId, wxPayProperties.getCertKeyPath());
|
|
|
+ log.info("唤起支付参数:{}", map);
|
|
|
+ return R.ok(JSONUtil.toJsonStr(map));
|
|
|
+ }
|
|
|
+ return R.ok(JSONUtil.toJsonStr(response));
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getSerialNumber() {
|
|
|
+ if (StrUtil.isEmpty(serialNo)) {
|
|
|
+ // 获取证书序列号
|
|
|
+ X509Certificate certificate = PayKit.getCertificate(wxPayProperties.getCertPath());
|
|
|
+ if (null != certificate) {
|
|
|
+ serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
|
|
|
+ // 提前两天检查证书是否有效
|
|
|
+ boolean isValid = PayKit.checkCertificateIsValid(certificate, wxPayProperties.getMchId(), -2);
|
|
|
+ log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN));
|
|
|
+ }
|
|
|
+// System.out.println("输出证书信息:\n" + certificate.toString());
|
|
|
+// // 输出关键信息,截取部分并进行标记
|
|
|
+// System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16));
|
|
|
+// System.out.println("版本号:" + certificate.getVersion());
|
|
|
+// System.out.println("签发者:" + certificate.getIssuerDN());
|
|
|
+// System.out.println("有效起始日期:" + certificate.getNotBefore());
|
|
|
+// System.out.println("有效终止日期:" + certificate.getNotAfter());
|
|
|
+// System.out.println("主体名:" + certificate.getSubjectDN());
|
|
|
+// System.out.println("签名算法:" + certificate.getSigAlgName());
|
|
|
+// System.out.println("签名:" + certificate.getSignature().toString());
|
|
|
+ }
|
|
|
+ System.out.println("serialNo:" + serialNo);
|
|
|
+ return serialNo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @RequestMapping(value = "/payNotify", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
|
|
|
+ @ResponseBody
|
|
|
+ public void payNotify(HttpServletRequest request, HttpServletResponse response) {
|
|
|
+ Map<String, String> map = new HashMap<>(12);
|
|
|
+ try {
|
|
|
+ String timestamp = request.getHeader("Wechatpay-Timestamp");
|
|
|
+ String nonce = request.getHeader("Wechatpay-Nonce");
|
|
|
+ String serialNo = request.getHeader("Wechatpay-Serial");
|
|
|
+ String signature = request.getHeader("Wechatpay-Signature");
|
|
|
+
|
|
|
+ log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
|
|
|
+ String result = HttpKit.readData(request);
|
|
|
+ log.info("支付通知密文 {}", result);
|
|
|
+
|
|
|
+ // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
|
|
|
+ String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp,
|
|
|
+ wxPayProperties.getMchKey(), wxPayProperties.getPlatFormPath());
|
|
|
+
|
|
|
+ log.info("支付通知明文 {}", plainText);
|
|
|
+
|
|
|
+ if (StrUtil.isNotEmpty(plainText)) {
|
|
|
+ response.setStatus(200);
|
|
|
+ map.put("code", "SUCCESS");
|
|
|
+ map.put("message", "SUCCESS");
|
|
|
+ } else {
|
|
|
+ response.setStatus(500);
|
|
|
+ map.put("code", "ERROR");
|
|
|
+ map.put("message", "签名错误");
|
|
|
+ }
|
|
|
+ response.setHeader("Content-type", ContentType.JSON.toString());
|
|
|
+ response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
|
|
|
+ response.flushBuffer();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("系统异常", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @RequestMapping("/get")
|
|
|
+ @ResponseBody
|
|
|
+ public String v3Get() {
|
|
|
+ // 获取平台证书列表
|
|
|
+ try {
|
|
|
+ IJPayHttpResponse response = WxPayApi.v3(
|
|
|
+ RequestMethodEnum.GET,
|
|
|
+ WxDomainEnum.CHINA.toString(),
|
|
|
+ CertAlgorithmTypeEnum.getCertSuffixUrl(CertAlgorithmTypeEnum.SM2.getCode()),
|
|
|
+ wxPayProperties.getMchId(),
|
|
|
+ getSerialNumber(),
|
|
|
+ null,
|
|
|
+ wxPayProperties.getCertKeyPath(),
|
|
|
+ "",
|
|
|
+ AuthTypeEnum.SM2.getCode()
|
|
|
+ );
|
|
|
+ Map<String, List<String>> headers = response.getHeaders();
|
|
|
+ log.info("请求头: {}", headers);
|
|
|
+ String timestamp = response.getHeader("Wechatpay-Timestamp");
|
|
|
+ String nonceStr = response.getHeader("Wechatpay-Nonce");
|
|
|
+ String serialNumber = response.getHeader("Wechatpay-Serial");
|
|
|
+ String signature = response.getHeader("Wechatpay-Signature");
|
|
|
+
|
|
|
+ String body = response.getBody();
|
|
|
+ int status = response.getStatus();
|
|
|
+
|
|
|
+ log.info("serialNumber: {}", serialNumber);
|
|
|
+ log.info("status: {}", status);
|
|
|
+ log.info("body: {}", body);
|
|
|
+ int isOk = 200;
|
|
|
+ if (status == isOk) {
|
|
|
+ JSONObject jsonObject = JSONUtil.parseObj(body);
|
|
|
+ JSONArray dataArray = jsonObject.getJSONArray("data");
|
|
|
+ // 默认认为只有一个平台证书
|
|
|
+ JSONObject encryptObject = dataArray.getJSONObject(0);
|
|
|
+ JSONObject encryptCertificate = encryptObject.getJSONObject("encrypt_certificate");
|
|
|
+ String associatedData = encryptCertificate.getStr("associated_data");
|
|
|
+ String cipherText = encryptCertificate.getStr("ciphertext");
|
|
|
+ String nonce = encryptCertificate.getStr("nonce");
|
|
|
+ String algorithm = encryptCertificate.getStr("algorithm");
|
|
|
+ String serialNo = encryptObject.getStr("serial_no");
|
|
|
+ final String platSerialNo = savePlatformCert(associatedData, nonce, cipherText, algorithm, wxPayProperties.getPlatFormPath());
|
|
|
+ log.info("平台证书序列号: {} serialNo: {}", platSerialNo, serialNo);
|
|
|
+ // 根据证书序列号查询对应的证书来验证签名结果
|
|
|
+ boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
|
|
|
+ log.info("verifySignature:{}", verifySignature);
|
|
|
+ }
|
|
|
+ return body;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取平台证书列表异常", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String savePlatformCert(String associatedData, String nonce, String cipherText, String algorithm, String certPath) {
|
|
|
+ try {
|
|
|
+ String key3 = wxPayProperties.getMchKey();
|
|
|
+ String publicKey;
|
|
|
+ if (StrUtil.equals(algorithm, AuthTypeEnum.SM2.getPlatformCertAlgorithm())) {
|
|
|
+ publicKey = PayKit.sm4DecryptToString(key3, cipherText, nonce, associatedData);
|
|
|
+ } else {
|
|
|
+ AesUtil aesUtil = new AesUtil(wxPayProperties.getMchKey().getBytes(StandardCharsets.UTF_8));
|
|
|
+ // 平台证书密文解密
|
|
|
+ // encrypt_certificate 中的 associated_data nonce ciphertext
|
|
|
+ publicKey = aesUtil.decryptToString(
|
|
|
+ associatedData.getBytes(StandardCharsets.UTF_8),
|
|
|
+ nonce.getBytes(StandardCharsets.UTF_8),
|
|
|
+ cipherText
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotEmpty(publicKey)) {
|
|
|
+ // 保存证书
|
|
|
+ FileWriter writer = new FileWriter(certPath);
|
|
|
+ writer.write(publicKey);
|
|
|
+ // 获取平台证书序列号
|
|
|
+ X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes()));
|
|
|
+ return certificate.getSerialNumber().toString(16).toUpperCase();
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("保存平台证书异常", e);
|
|
|
+ return e.getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+}
|