package com.dy.pipIrrWechat.wechatpay;
|
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.dy.common.webUtil.BaseResponse;
|
import com.dy.common.webUtil.BaseResponseUtils;
|
import com.dy.common.webUtil.ResultCodeMsg;
|
import com.dy.pipIrrGlobal.pojoSe.*;
|
import com.dy.pipIrrGlobal.voSe.VoClient;
|
import com.dy.pipIrrWechat.result.WechatResultCode;
|
import com.dy.pipIrrWechat.util.AesUtil;
|
import com.dy.pipIrrWechat.util.PayHelper;
|
import com.dy.pipIrrWechat.util.RestTemplateUtil;
|
import com.dy.pipIrrWechat.virtualCard.VirtualCardSv;
|
import com.dy.pipIrrWechat.virtualCard.dto.DtoVirtualCard;
|
import com.dy.pipIrrWechat.virtualCard.enums.LastOperateENUM;
|
import com.dy.pipIrrWechat.virtualCard.enums.RefundItemStateENUM;
|
import com.dy.pipIrrWechat.wechatpay.dto.Code2Session;
|
import com.dy.pipIrrWechat.wechatpay.dto.DtoOrder;
|
import com.dy.pipIrrWechat.wechatpay.dto.NotifyResource;
|
import com.dy.pipIrrWechat.wechatpay.dto.OrderNotify;
|
import com.dy.pipIrrWechat.wechatpay.enums.RefundStatusENUM;
|
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.validation.Valid;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.MediaType;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.BindingResult;
|
import org.springframework.web.bind.annotation.*;
|
|
import javax.crypto.NoSuchPaddingException;
|
import java.io.BufferedReader;
|
import java.io.IOException;
|
import java.security.GeneralSecurityException;
|
import java.security.InvalidKeyException;
|
import java.security.NoSuchAlgorithmException;
|
import java.security.SignatureException;
|
import java.security.spec.InvalidKeySpecException;
|
import java.text.SimpleDateFormat;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.Objects;
|
|
/**
|
* @author ZhuBaoMin
|
* @date 2024-07-16 15:05
|
* @LastEditTime 2024-07-16 15:05
|
* @Description
|
*/
|
|
@Slf4j
|
@Tag(name = "微信支付管理", description = "微信支付各种操作")
|
@RestController
|
@RequestMapping(path="payment")
|
@RequiredArgsConstructor
|
public class PaymentCtrl {
|
private final PaymentSv paymentSv;
|
private final RestTemplateUtil restTemplateUtil;
|
private final PayHelper payHelper;
|
private final VirtualCardSv virtualCardSv;
|
private final String privateCertFileName = com.dy.pipIrrWechat.wechatpay.PayInfo.privateCertFileName;
|
private final String appid = com.dy.pipIrrWechat.wechatpay.PayInfo.appid;
|
private final String secret = com.dy.pipIrrWechat.wechatpay.PayInfo.secret;
|
private final String mchid = com.dy.pipIrrWechat.wechatpay.PayInfo.mchid;
|
private final String schema = com.dy.pipIrrWechat.wechatpay.PayInfo.schema;
|
private final String signType = com.dy.pipIrrWechat.wechatpay.PayInfo.signType;
|
private final String description = com.dy.pipIrrWechat.wechatpay.PayInfo.description;
|
private final String loginUrl = com.dy.pipIrrWechat.wechatpay.PayInfo.loginUrl;
|
private final String notifyUrl = com.dy.pipIrrWechat.wechatpay.PayInfo.notifyUrl;
|
private final String grantType = com.dy.pipIrrWechat.wechatpay.PayInfo.grantType;
|
|
// 平台证书公钥
|
private final Map CERTIFICATE_MAP = new HashMap();
|
|
/**
|
* 登录凭证校验,农户绑定账号逻辑包含登录凭证校验,此接口作废
|
* @param code2Session 登录凭证校验传入对象
|
* @param bindingResult
|
* @return
|
* @throws Exception
|
*/
|
@Operation(summary = "登录凭证校验", description = "登录凭证校验")
|
@ApiResponses(value = {
|
@ApiResponse(
|
responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
|
description = "操作结果:true:成功,false:失败(BaseResponse.content)",
|
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
|
schema = @Schema(implementation = Boolean.class))}
|
)
|
})
|
@PostMapping(path = "getSessionId")
|
@Transactional(rollbackFor = Exception.class)
|
public BaseResponse<Boolean> getSessionId(@RequestBody @Valid Code2Session code2Session, BindingResult bindingResult) throws Exception {
|
if(bindingResult != null && bindingResult.hasErrors()){
|
return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
|
}
|
|
String phoneNumber = code2Session.getPhoneNumber();
|
String jsCode = code2Session.getJs_code();
|
|
Map<String, Object> queryParams = new HashMap<>();
|
queryParams.put("appid", appid);
|
queryParams.put("secret", secret);
|
queryParams.put("js_code", jsCode);
|
queryParams.put("grant_type", grantType);
|
Map<String, String> headerParams = new HashMap<>();
|
JSONObject job = restTemplateUtil.get(loginUrl, queryParams, headerParams);
|
|
if(job.getLong("errcode") != null && job.getLong("errcode") >= -1) {
|
return BaseResponseUtils.buildFail("登录凭证校验失败");
|
}
|
|
String openid = job.getString("openid");
|
String sessionKey = job.getString("session_key");
|
|
Long clientId = paymentSv.getClientIdByPhone(phoneNumber);
|
String SessionId = "";
|
if(clientId != null) {
|
// 添加微信用户账户记录
|
SeOpenId seOpenId = new SeOpenId();
|
seOpenId.setClientId(clientId);
|
seOpenId.setOpenId(openid);
|
seOpenId.setSessionKey(sessionKey);
|
seOpenId.setCreateTime(new Date());
|
Long rec = paymentSv.addOpenId(seOpenId);
|
if(rec != null) {
|
SessionId = String.valueOf(rec);
|
}
|
return BaseResponseUtils.buildSuccess(SessionId);
|
|
} else {
|
return BaseResponseUtils.buildError(WechatResultCode.PHONE_NUMBER_IS_ERROR.getMessage());
|
}
|
}
|
|
/**
|
* 下载微信支付平台证书 测试完废除
|
* @return
|
* @throws Exception
|
*/
|
@Operation(summary = "下载平台证书", description = "下载平台证书")
|
@ApiResponses(value = {
|
@ApiResponse(
|
responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
|
description = "操作结果:true:成功,false:失败(BaseResponse.content)",
|
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
|
schema = @Schema(implementation = Boolean.class))}
|
)
|
})
|
@GetMapping(path = "certificates")
|
@Transactional(rollbackFor = Exception.class)
|
public BaseResponse<Boolean> certificates() throws Exception {
|
String method = "GET";
|
String httpUrl = "/v3/certificates";
|
String nonceStr = payHelper.generateRandomString();
|
Long timestamp = System.currentTimeMillis() / 1000;
|
|
String header = schema + " " + payHelper.getToken(method, httpUrl, "", nonceStr, timestamp, privateCertFileName);
|
|
Map<String, String> headers = new HashMap<>();
|
headers.put("Authorization", header);
|
headers.put("Accept", "application/json");
|
JSONObject job_result = restTemplateUtil.getHeaders(com.dy.pipIrrWechat.wechatpay.PayInfo.certificates,null, headers);
|
JSONObject job_headers = job_result.getJSONObject("headers");
|
String wechatpayNonce = job_headers.getJSONArray("Wechatpay-Nonce").getString(0);
|
String wechatpaySerial = job_headers.getJSONArray("Wechatpay-Serial").getString(0);
|
String wechatpaySignature = job_headers.getJSONArray("Wechatpay-Signature").getString(0);
|
String wechatpaySignatureType = job_headers.getJSONArray("Wechatpay-Signature-Type").getString(0);
|
String wechatpayTimestamp = job_headers.getJSONArray("Wechatpay-Timestamp").getString(0);
|
|
JSONObject job_body = job_result.getJSONObject("body");
|
|
// 构造验签名串
|
String signatureStr = payHelper.responseSign(wechatpayTimestamp, wechatpayNonce, job_body.toJSONString());
|
// 验证签名
|
Boolean valid = payHelper.responseSignVerify(wechatpaySerial, signatureStr, wechatpaySignature);
|
|
return BaseResponseUtils.buildSuccess();
|
}
|
|
/**
|
* JSAPI下单
|
* @param order 下单请求对象,包含需要传入的参数
|
* @param bindingResult
|
* @return 预支付交易会话标识(有效期2小时)
|
*/
|
@PostMapping(path = "placeOrder")
|
@Transactional(rollbackFor = Exception.class)
|
public BaseResponse<Boolean> placeOrder(@RequestBody @Valid DtoOrder order, BindingResult bindingResult) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException {
|
if(bindingResult != null && bindingResult.hasErrors()){
|
return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
|
}
|
|
// 接收参数:登录态ID、虚拟卡ID、充值金额(分)
|
Long sessionId = order.getSessionId();
|
Long virtualId = order.getVcId();
|
Integer rechargeAmount = order.getRechargeAmount();
|
|
String prepayId = "";
|
|
SeOpenId po = paymentSv.selectOne(sessionId);
|
if(po == null) {
|
return BaseResponseUtils.buildErrorMsg(WechatResultCode.SESSION_ID_ERROR.getMessage());
|
}
|
String openid = po.getOpenId();
|
|
SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId);
|
if(seVirtualCard == null) {
|
return BaseResponseUtils.buildErrorMsg(WechatResultCode.VIRTUAL_CARD_NOT_EXIST.getMessage());
|
}
|
Long clientId = seVirtualCard.getClientId();
|
|
VoClient voClient = paymentSv.getOneClient(clientId);
|
if(voClient == null) {
|
return BaseResponseUtils.buildErrorMsg(WechatResultCode.VIRTUAL_CARD_CLIENT_NOT_EXIST.getMessage());
|
}
|
String clientNum = voClient.getClientNum();
|
|
// 生成订单号并添加充值记录
|
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
|
String orderNumber = clientNum + dateFormat.format(new Date());
|
|
// 生成虚拟卡充值记录(部分字段)
|
DtoVirtualCard virtualCard = new DtoVirtualCard();
|
virtualCard.setVirtualId(virtualId);
|
virtualCard.setClientId(clientId);
|
virtualCard.setOrderNumber(orderNumber);
|
virtualCard.setRechargeAmount(rechargeAmount);
|
BaseResponse result = virtualCardSv.insertVCRecharge(virtualCard);
|
if(!result.getCode().equals("0001")) {
|
return BaseResponseUtils.buildErrorMsg(WechatResultCode.RECHARGE_ADD_FAIL.getMessage());
|
}
|
|
JSONObject job_body = new JSONObject();
|
job_body.put("appid", appid);
|
job_body.put("mchid", mchid);
|
job_body.put("description", description);
|
job_body.put("out_trade_no", orderNumber);
|
job_body.put("notify_url", notifyUrl);
|
|
//订单金额
|
JSONObject job_amount = new JSONObject();
|
job_amount.put("total", rechargeAmount);
|
job_amount.put("currency", "CNY");
|
job_body.put("amount", job_amount);
|
|
//支付者
|
JSONObject job_payer = new JSONObject();
|
job_payer.put("openid", openid);
|
job_body.put("payer", job_payer);
|
|
// 获取随机串和时间戳
|
String nonceStr = payHelper.generateRandomString();
|
Long timestamp = System.currentTimeMillis() / 1000;
|
|
String method = "POST";
|
String httpUrl = "/v3/pay/transactions/jsapi";
|
|
String body = job_body.toJSONString();
|
String header = schema + " " + payHelper.getToken(method, httpUrl, body, nonceStr, timestamp, privateCertFileName);
|
|
Map<String, String> headers = new HashMap<>();
|
headers.put("Authorization", header);
|
headers.put("Accept", "application/json");
|
headers.put("Content-Type", "application/json");
|
|
// 暂时注释掉,认证通过后再放开
|
JSONObject job_result = restTemplateUtil.post(com.dy.pipIrrWechat.wechatpay.PayInfo.orderUrl, body, headers);
|
if(job_result == null) {
|
return BaseResponseUtils.buildFail(WechatResultCode.RECHARGE_ADD_FAIL.getMessage());
|
}
|
|
return BaseResponseUtils.buildSuccess(job_result) ;
|
}
|
|
/**
|
* 再次签名
|
* @param prepayId 预支付交易会话标识
|
* @return 小程序调起支付参数
|
* @throws Exception
|
*/
|
@Operation(summary = "再次签名", description = "再次签名")
|
@ApiResponses(value = {
|
@ApiResponse(
|
responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
|
description = "操作结果:true:成功,false:失败(BaseResponse.content)",
|
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
|
schema = @Schema(implementation = Boolean.class))}
|
)
|
})
|
@GetMapping(path = "/signAgain")
|
@Transactional(rollbackFor = Exception.class)
|
public BaseResponse<JSONObject> signAgain(@RequestParam("prepayId") String prepayId) throws Exception {
|
|
// 获取随机串和时间戳,放在此处以保证
|
String appid = com.dy.pipIrrWechat.wechatpay.PayInfo.appid;
|
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
String nonceStr = payHelper.generateRandomString();
|
String pkg = "prepay_id=" + prepayId;
|
String message = payHelper.buildMessage_signAgain(appid, timeStamp, nonceStr, pkg);
|
String paySign = payHelper.sign(message.getBytes("utf-8"), privateCertFileName);
|
|
JSONObject job_result = new JSONObject();
|
job_result.put("timeStamp", timeStamp);
|
job_result.put("nonceStr", nonceStr);
|
job_result.put("package", pkg);
|
job_result.put("signType", signType);
|
job_result.put("paySign", paySign);
|
|
return BaseResponseUtils.buildSuccess(job_result) ;
|
}
|
|
/**
|
* 支付通知/退款结果通知
|
* @param headers
|
* @param request
|
* @param response
|
* @return
|
* @throws IOException
|
* @throws GeneralSecurityException
|
*/
|
@Operation(summary = "支付通知", description = "支付通知")
|
@ApiResponses(value = {
|
@ApiResponse(
|
responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
|
description = "操作结果:true:成功,false:失败(BaseResponse.content)",
|
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
|
schema = @Schema(implementation = Boolean.class))}
|
)
|
})
|
@PostMapping(path = "orderNotify", consumes = MediaType.APPLICATION_JSON_VALUE)
|
@Transactional(rollbackFor = Exception.class)
|
public JSONObject orderNotify(@RequestHeader HttpHeaders headers, HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
|
JSONObject result = new JSONObject();
|
|
/**
|
* 1.验签处理
|
* 从header中取出4个子参数
|
* 验时间差,超过5分钟的不处理
|
* 验证签名
|
* 验证书序列号,必须与某一个证书的序列号一致
|
*/
|
String wechatpayNonce = String.valueOf(headers.get("Wechatpay-Nonce").get(0));
|
String wechatpaySerial = String.valueOf(headers.get("Wechatpay-Serial").get(0));
|
String wechatpaySignature = String.valueOf(headers.get("Wechatpay-Signature").get(0));
|
String wechatpayTimestamp = String.valueOf(headers.get("Wechatpay-Timestamp").get(0));
|
|
// 获取body内容
|
BufferedReader reader = request.getReader();
|
StringBuilder stringBuilder = new StringBuilder();
|
String line;
|
while ((line = reader.readLine()) != null) {
|
stringBuilder.append(line);
|
}
|
String bodyStr = stringBuilder.toString();
|
|
// body转对象
|
OrderNotify orderNotify = JSON.parseObject(bodyStr, OrderNotify.class);
|
|
// 验时间戳,时间差大于5分钟的拒绝
|
Long timeDiff = (System.currentTimeMillis() / 1000 - Long.parseLong(wechatpayTimestamp))/60;
|
if(timeDiff > 5) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
|
// 构造验签名串
|
String signatureStr = payHelper.responseSign(wechatpayTimestamp, wechatpayNonce, bodyStr);
|
// 验证签名
|
Boolean valid = payHelper.responseSignVerify(wechatpaySerial, signatureStr, wechatpaySignature);
|
if(!valid) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
|
// 序列号验证要放在验签后,因为验签时可能会下载新的证书
|
boolean SerialIsValid = false;
|
for (String key : payHelper.CERTIFICATE_MAP.keySet()) {
|
if(key.equals(wechatpaySerial)) {
|
SerialIsValid = true;
|
}
|
}
|
if(!SerialIsValid) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
|
// 解密处理
|
String eventType = orderNotify.getEvent_type();
|
|
if(eventType != null && eventType.equals("TRANSACTION.SUCCESS")) {
|
// 支付成功回调
|
/**
|
* 支付成功的回调
|
* 取出通知数据对象,继而取出解密所需的associatedData和nonce,以及密文ciphertext
|
* 解密ciphertext得到
|
*/
|
NotifyResource notifyResource = orderNotify.getResource();
|
String associatedData = notifyResource.getAssociated_data();
|
String nonce = notifyResource.getNonce();
|
String ciphertext = notifyResource.getCiphertext();
|
|
String resource = AesUtil.decryptToString(com.dy.pipIrrWechat.wechatpay.PayInfo.key.getBytes("utf-8"), associatedData.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);
|
JSONObject job_resource = JSONObject.parseObject(resource);
|
|
// 解密后取出:商户订单号、微信支付订单号、交易状态、支付完成时间
|
String out_trade_no = job_resource.getString("out_trade_no");
|
String transaction_id = job_resource.getString("transaction_id");
|
String trade_state = job_resource.getString("trade_state");
|
Date success_time = job_resource.getDate("success_time");
|
|
// 如果当前订单状态为未支付状态,则更新虚拟卡表及充值表响应字段
|
SeVcRecharge seVcRecharge = virtualCardSv.getVCRechargeByorderNumber(out_trade_no);
|
if(seVcRecharge != null && seVcRecharge.getOrderState() == 1) {
|
BaseResponse result_ = virtualCardSv.updateVCRecharge(out_trade_no, success_time);
|
if(!result_.getCode().equals("0001")) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
}
|
} else if(eventType != null && eventType.equals("REFUND.SUCCESS")) {
|
// 退款成功后回调
|
/**
|
* 退款成功的回调
|
* 取出通知数据对象,继而取出解密所需的associatedData和nonce,以及密文ciphertext
|
* 解密ciphertext得到
|
*/
|
NotifyResource notifyResource = orderNotify.getResource();
|
String associatedData = notifyResource.getAssociated_data();
|
String nonce = notifyResource.getNonce();
|
String ciphertext = notifyResource.getCiphertext();
|
|
String resource = AesUtil.decryptToString(PayInfo.key.getBytes("utf-8"), associatedData.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);
|
JSONObject job_resource = JSONObject.parseObject(resource);
|
|
// 解密后取出:商户订单员、微信支付订单号、交易状态、支付完成时间
|
String out_trade_no = job_resource.getString("out_trade_no");
|
String transaction_id = job_resource.getString("transaction_id");
|
String out_refund_no = job_resource.getString("out_refund_no");
|
String refund_status = job_resource.getString("refund_status");
|
Date success_time = job_resource.getDate("success_time");
|
if(!refund_status.equals("SUCCESS")) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
|
// 更新虚拟卡表及充值表响应字段
|
SeVcRefundItem seVcRefundItem = new SeVcRefundItem();
|
seVcRefundItem.setRefundTime(success_time);
|
seVcRefundItem.setRefundStatus(RefundItemStateENUM.REFUNDED.getCode());
|
Integer rec = virtualCardSv.updateRefundItem(seVcRefundItem);
|
if(rec == null && rec <= 0) {
|
response.setStatus(500);
|
result.put("code", "FAIL");
|
result.put("message", "失败");
|
return result;
|
}
|
|
// 根据退款单号反查退款ID,根据退款ID获取退款状态是未退款的记录数量,如果是0则说明全部退款完成,更新退款表状态为已退款,将退款后金额更新到虚拟卡表
|
/**
|
* 根据退款通知接口返回的退款单号反查退款ID,查询该退款ID下未退款记录数量
|
* 如果结果为0,则该退款已经完成
|
* 1. 更新退款表状态为已退款
|
* 2. 将退款后余额更新到虚拟卡表中
|
*/
|
Integer noRefundedCount = virtualCardSv.getNoRefundedCount(out_refund_no);
|
if(noRefundedCount != null && noRefundedCount == 0) {
|
// 获取退款对象并修改退款状态
|
Long refundId = virtualCardSv.getRefundIdByRefundNumber(out_refund_no);
|
SeVcRefund seVcRefund = virtualCardSv.selectRefundByRefundId(refundId);
|
seVcRefund.setRefundStatus(RefundStatusENUM.REFUNDED.getCode());
|
virtualCardSv.updateRefund(seVcRefund);
|
|
// 获取虚拟卡对象并修改余额、最后操作、最后操作时间
|
Long vcId = seVcRefund.getVcId();
|
Double afterRefund = seVcRefund.getAfterRefund();
|
SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(vcId);
|
seVirtualCard.setMoney(afterRefund);
|
seVirtualCard.setLastOperate(LastOperateENUM.REFUND.getCode());
|
seVirtualCard.setLastOperateTime(new Date());
|
virtualCardSv.updateVirtualCard(seVirtualCard);
|
}
|
}
|
|
// 通知应答
|
response.setStatus(200);
|
result.put("code", "SUCCESS");
|
result.put("message", "成功");
|
return result;
|
}
|
}
|