| New file | 
|  |  |  | 
|---|
|  |  |  | 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.cert.WxCertUtil; | 
|---|
|  |  |  | import com.dy.pipIrrGlobal.pojoSe.*; | 
|---|
|  |  |  | 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.virtualCard.enums.LastOperateENUM; | 
|---|
|  |  |  | import com.dy.pipIrrSell.virtualCard.enums.RefundItemStateENUM; | 
|---|
|  |  |  | import com.dy.pipIrrSell.wallet.enums.RefundStatusENUM; | 
|---|
|  |  |  | 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.HttpServletRequest; | 
|---|
|  |  |  | import jakarta.servlet.http.HttpServletResponse; | 
|---|
|  |  |  | import jakarta.validation.Valid; | 
|---|
|  |  |  | import lombok.RequiredArgsConstructor; | 
|---|
|  |  |  | import lombok.extern.slf4j.Slf4j; | 
|---|
|  |  |  | import org.springframework.core.io.ResourceLoader; | 
|---|
|  |  |  | 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.*; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * @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 ResourceLoader resourceLoader; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | private final PaymentSv paymentSv; | 
|---|
|  |  |  | private final RestTemplateUtil restTemplateUtil; | 
|---|
|  |  |  | private final PayHelper payHelper; | 
|---|
|  |  |  | private final VirtualCardSv virtualCardSv; | 
|---|
|  |  |  | private final ClientSv clientSv; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | //private final String privateCertFileName = PayInfo.privateCertFileName; | 
|---|
|  |  |  | private final String appid = PayInfo.appid; | 
|---|
|  |  |  | private final String secret = PayInfo.secret; | 
|---|
|  |  |  | private final String mchid = PayInfo.mchid; | 
|---|
|  |  |  | private final String schema = PayInfo.schema; | 
|---|
|  |  |  | private final String signType = PayInfo.signType; | 
|---|
|  |  |  | private final String description = PayInfo.description; | 
|---|
|  |  |  | private final String loginUrl = PayInfo.loginUrl; | 
|---|
|  |  |  | private final String notifyUrl = PayInfo.notifyUrl; | 
|---|
|  |  |  | private final String grantType = 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) | 
|---|
|  |  |  | @SsoAop() | 
|---|
|  |  |  | 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 = clientSv.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 = clientSv.addOpenId(seOpenId); | 
|---|
|  |  |  | if(rec != null) { | 
|---|
|  |  |  | SessionId = String.valueOf(rec); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | return BaseResponseUtils.buildSuccess(SessionId); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | return BaseResponseUtils.buildError(SellResultCode.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) | 
|---|
|  |  |  | @SsoAop() | 
|---|
|  |  |  | public BaseResponse<Boolean> certificates() throws Exception { | 
|---|
|  |  |  | String method = "GET"; | 
|---|
|  |  |  | String httpUrl = "/v3/certificates"; | 
|---|
|  |  |  | String nonceStr = payHelper.generateRandomString(); | 
|---|
|  |  |  | Long timestamp = System.currentTimeMillis() / 1000; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | byte[] keyPemBs = WxCertUtil.getKey_pemBytes(resourceLoader) ; | 
|---|
|  |  |  | String header = schema + " " + payHelper.getToken(method, httpUrl, "", nonceStr, timestamp, keyPemBs); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | Map<String, String> 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, keyPemBs); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | return BaseResponseUtils.buildSuccess(); | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | /** | 
|---|
|  |  |  | * JSAPI下单 | 
|---|
|  |  |  | * @param order 下单请求对象,包含需要传入的参数 | 
|---|
|  |  |  | * @param bindingResult | 
|---|
|  |  |  | * @return 预支付交易会话标识(有效期2小时) | 
|---|
|  |  |  | */ | 
|---|
|  |  |  | @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<Boolean> placeOrder(@RequestBody @Valid DtoOrder order, BindingResult bindingResult) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, Exception { | 
|---|
|  |  |  | 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); | 
|---|
|  |  |  | 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(order.getRechargeAmount()); | 
|---|
|  |  |  | 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", (int)(order.getRechargeAmount()*100)); | 
|---|
|  |  |  | 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, WxCertUtil.getKey_pemBytes(resourceLoader)); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | 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(PayInfo.orderUrl, body, headers); | 
|---|
|  |  |  | if(job_result == null) { | 
|---|
|  |  |  | return BaseResponseUtils.buildFail(SellResultCode.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) | 
|---|
|  |  |  | @SsoAop() | 
|---|
|  |  |  | public BaseResponse<JSONObject> 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"), WxCertUtil.getKey_pemBytes(resourceLoader)); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | 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) | 
|---|
|  |  |  | @SsoAop() | 
|---|
|  |  |  | public JSONObject orderNotify(@RequestHeader HttpHeaders headers, HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException, Exception { | 
|---|
|  |  |  | 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); | 
|---|
|  |  |  | byte[] keyPemBs = WxCertUtil.getKey_pemBytes(resourceLoader) ; | 
|---|
|  |  |  | // 验证签名 | 
|---|
|  |  |  | Boolean valid = payHelper.responseSignVerify(wechatpaySerial, signatureStr, wechatpaySignature, keyPemBs); | 
|---|
|  |  |  | 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(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; | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | } | 
|---|