package com.dy.pipIrrSell.wechatpay; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.dy.common.aop.SsoAop; import com.dy.common.webUtil.BaseResponse; import com.dy.common.webUtil.BaseResponseUtils; import com.dy.common.webUtil.ResultCodeMsg; import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard; import com.dy.pipIrrGlobal.pojoSe.SeWebchatLogonState; import com.dy.pipIrrGlobal.voSe.VoClient; import com.dy.pipIrrSell.client.ClientSv; import com.dy.pipIrrSell.result.SellResultCode; import com.dy.pipIrrSell.util.AesUtil; import com.dy.pipIrrSell.util.PayHelper; import com.dy.pipIrrSell.util.RestTemplateUtil; import com.dy.pipIrrSell.virtualCard.VirtualCardSv; import com.dy.pipIrrSell.virtualCard.dto.DtoVirtualCard; import com.dy.pipIrrSell.wechatpay.dto.*; 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.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.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-03-06 13:49 * @LastEditTime 2024-03-06 13:49 * @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 ClientSv clientSv; private String privateCertFileName = PayInfo.privateCertFileName; private String appid = PayInfo.appid; private String mchid = PayInfo.mchid; private String schema = PayInfo.schema; private String signType = PayInfo.signType; private String description = PayInfo.description; private String loginUrl = PayInfo.loginUrl; private String notifyUrl = PayInfo.notifyUrl; private String grantType = PayInfo.grantType; private String refundUrl = PayInfo.refundUrl; // 平台证书公钥 private Map CERTIFICATE_MAP = new HashMap(); /** * 登录凭证校验 * @param appid 小程序 appId * @param secret 小程序 appSecret * @param js_code 临时登录凭证code * @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) @SsoAop() public BaseResponse getSessionId(@RequestParam("appid") String appid, @RequestParam("secret") String secret, @RequestParam("js_code") String js_code) throws Exception { Map queryParams = new HashMap<>(); queryParams.put("appid", appid); queryParams.put("secret", secret); queryParams.put("js_code", js_code); queryParams.put("grant_type", grantType); Map 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"); // 检验登录态 JSONObject checkSessionKey = payHelper.checkSessionKey(appid, secret, openid, sessionKey); if(checkSessionKey != null) { Integer errcode = checkSessionKey.getInteger("errcode"); String errmsg = checkSessionKey.getString("errmsg"); } // 重置登录态 JSONObject resetUserSessionKey = payHelper.resetUserSessionKey(appid, secret, openid, sessionKey); if(resetUserSessionKey != null) { Integer errcode = checkSessionKey.getInteger("errcode"); String errmsg = checkSessionKey.getString("errmsg"); String openid_New = checkSessionKey.getString("openid"); String sessionKey_New = checkSessionKey.getString("session_key"); } // 添加登录态记录 SeWebchatLogonState po = new SeWebchatLogonState(); po.setOpenId(openid); po.setSessionKey(sessionKey); Date createTime = new Date(); po.setCreateTime(createTime); Long id = paymentSv.insert(po); if(id == null || id <= 0) { return BaseResponseUtils.buildFail("登录态记录添加失败"); } String SessionId = String.valueOf(id); return BaseResponseUtils.buildSuccess(SessionId) ; } /** * 下载微信支付平台证书 测试完废除 * @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) @SsoAop() public BaseResponse 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 headers = new HashMap<>(); headers.put("Authorization", header); headers.put("Accept", "application/json"); JSONObject job_result = restTemplateUtil.getHeaders(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 */ @Operation(summary = "JSAPI下单", description = "JSAPI下单") @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 = "placeOrder") @Transactional(rollbackFor = Exception.class) @SsoAop() public BaseResponse 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、虚拟卡ID、充值金额 String sessionId = order.getSessionId(); Long virtualId = order.getVirtualId(); Integer rechargeAmount = order.getRechargeAmount(); String prepayId = ""; SeWebchatLogonState po = paymentSv.selectOne(Long.parseLong(sessionId)); String openid = po.getOpenId(); SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId); Long clientId = seVirtualCard.getClientId(); VoClient voClient = clientSv.getOneClient(clientId); String clientNum = voClient.getClientNum(); // 生成订单号并添加充值记录 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS"); String orderNumber = clientNum + dateFormat.format(new Date()); // 生成虚拟卡充值记录(部分字段) DtoVirtualCard virtualCard = new DtoVirtualCard(); virtualCard.setOrderNumber(orderNumber); virtualCard.setClientId(clientId); virtualCard.setVirtualId(virtualId); virtualCard.setRechargeAmount(rechargeAmount); BaseResponse result = virtualCardSv.insertVCRecharge(virtualCard); if(!result.getCode().equals("0001")) { return BaseResponseUtils.buildFail(SellResultCode.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", 1); 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 headers = new HashMap<>(); headers.put("Authorization", header); headers.put("Accept", "application/json"); headers.put("Content-Type", "application/json"); JSONObject job_result = restTemplateUtil.post(PayInfo.orderUrl, body, headers); if(job_result != null) { System.out.println(job_result.toString()); prepayId = job_result.getString("prepay_id"); } return BaseResponseUtils.buildSuccess(prepayId) ; } /** * 申请退款 * @param po 退款请求对象 * @param bindingResult * @return * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException * @throws IOException * @throws SignatureException * @throws InvalidKeyException */ @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 = "refunds") @Transactional(rollbackFor = Exception.class) @SsoAop() public BaseResponse refunds(@RequestBody @Valid Refund po, BindingResult bindingResult) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException { if(bindingResult != null && bindingResult.hasErrors()){ return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage()); } /** * 1. 判断交易时间是否超过一年 * 2. 判断退款总金额是否超过订单金额 * 3. 判断当前订单退款次数是否超过50次 * 4. 判断与该订单上次退款是否相隔1分钟 */ String tradeNo = po.getTradeNo(); String refundNo = po.getRefundNo(); Integer refund = po.getRefund(); if(refundNo == null || refundNo.length() <= 0) { // 新提退款申请,生成退款单号 //refundNo = generateRefundNo(tradeNo); } // 根据订单号获取总支付金额和总退款金额 Integer totalTradeAmount = 0; Integer totalRefundAmount = 0; //Integer totalTradeAmount = getTotalTradeAmount(tradeNo); //Integer totalRefundAmount = getTotalRefundAmount(tradeNo); if(totalRefundAmount > totalTradeAmount) { return BaseResponseUtils.buildFail(SellResultCode.TOTAL_REFUND_EXCEED_TRADE.getMessage()); } // 生成body RefundRequest.Amount amount = new RefundRequest.Amount(); amount.setRefund(refund); amount.setTotal(totalTradeAmount); amount.setCurrency("CNY"); RefundRequest refundRequest = new RefundRequest(); refundRequest.setOut_trade_no(tradeNo); refundRequest.setOut_refund_no(refundNo); refundRequest.setNotify_url(notifyUrl); refundRequest.setAmount(amount); // 生成header String nonceStr = payHelper.generateRandomString(); Long timestamp = System.currentTimeMillis() / 1000; String method = "POST"; String httpUrl = "/v3/refund/domestic/refunds"; String body = JSONObject.toJSONString(refundRequest); String header = schema + " " + payHelper.getToken(method, httpUrl, body, nonceStr, timestamp, privateCertFileName); Map headers = new HashMap<>(); headers.put("Authorization", header); headers.put("Accept", "application/json"); headers.put("Content-Type", "application/json"); JSONObject job_refundResponse = restTemplateUtil.post(PayInfo.orderUrl, body, headers); RefundResponse refundResponse = JSON.parseObject(job_refundResponse.toJSONString(), RefundResponse.class); String status = refundResponse.getStatus(); if(status != null && status.equals("SUCCESS")) { // 退款申请已受理 return BaseResponseUtils.buildSuccess(true) ; } else if(status != null && status.equals("PROCESSING")) { // 退款处理中 return BaseResponseUtils.buildFail(SellResultCode.PROCESSING.getMessage()); } else { // 退款异常 return BaseResponseUtils.buildError(SellResultCode.ABNORMAL.getMessage()); } } /** * 支付通知/退款结果通知 * @param headers * @param orderNotify * @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) @SsoAop() public JSONObject orderNotify(@RequestHeader HttpHeaders headers, @RequestBody OrderNotify orderNotify, HttpServletResponse response) throws IOException, GeneralSecurityException { JSONObject result = new JSONObject(); /** * 1.验签处理 * 从header中取出4个子参数,同时取出body * 验时间差,超过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)); String bodyStr = JSONObject.toJSONString(orderNotify); // 验时间戳,时间差大于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; } /** * 解密处理 * 1 */ String eventType = orderNotify.getEvent_type(); if(eventType != null && eventType.equals("TRANSACTION.SUCCESS")) { // 支付成功回调 /** * 支付成功的回调 * 取出通知数据对象,继而取出解密所需的associatedData和nonce,以及密文ciphertext * 解密ciphertext得到 */ OrderNotify.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 trade_state = job_resource.getString("trade_state"); Date success_time = job_resource.getDate("success_time"); // 更新虚拟卡表及充值表响应字段 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")) { // 退款成功后回调 } // 通知应答 response.setStatus(200); result.put("code", "SUCCESS"); result.put("message", "成功"); return 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) @SsoAop() public BaseResponse signAgain(@RequestParam("prepayId") String prepayId) throws Exception { // 获取随机串和时间戳,放在此处以保证 String appid = 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) ; } }