# 产品商城 **Repository Path**: BuLiangShuai01033/product-mall ## Basic Information - **Project Name**: 产品商城 - **Description**: 主要用于学习微信支付功能,用户端和后台管理端分权限。 前端:Vue2+Element-ui+Axios 后端:jdk8+SpringBoot2.7.6+MD5加密+微信支付Native的apiV3+lombok+mybatis-plus+mysql - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2023-05-03 - **Last Updated**: 2023-06-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 产品商城(主要用于学习微信支付功能开发) ## 256位秘钥策略与JDK8的小Bug 如果使用JDK8运行后端时遇到报错解决:java.security.InvalidKeyException: Illegal key size(微信支付v3遇到的问题) 原因是因为微信支付256位秘钥策略可能会导致某些jdk的版本加密解密出现问题,首先观察你这个目录下的文件 ![image-20230317231340561](assets/image-20230317231340561.png) 根据文件内容做判断看下目录里面是有一个 policy 文件夹,还是有local_policy.jar 去官方下载JCE无限制权限策略文件,这里贴出jdk 8的国内地址 方便下载https://wwi.lanzoup.com/iXGs404zm1dg ![image-20230317231652843](assets/image-20230317231652843.png) 下载解压后,将以上两个文件拷贝覆盖到Java\jdk1.8.0_152\jre\lib\security目录中 ## 开发前需知 SpringBoot整合微信支付(Native最详细):https://blog.csdn.net/yueyue763184/article/details/129624617?spm=1001.2014.3001.5501 ## 一、项目系统介绍 ### 1、开发背景 微信支付是腾讯集团开发的具有支付功能的一种产品,基于智能手机微信客户端可以迅速、便捷实现在线支付。微信支付通过绑定银行卡完成实名制验证,从而为个人或企业提供安全性高、快捷性强、专业水平高的支付功能,不仅包括基础的收款能力,甚至可以提供运营能力以及资金结算解决方案,当然以上腾讯集团承诺均在保障安全性的前提下提供相关功能。用户首先需要通过实名认证,将一张用户名下的银行卡与微信支付进行绑定注册,只需要简单的几个步骤,一个安装微信软件的手机就会摇身变成一个“钱包”,用户在使用“钱包”时仅需要输入密码或指纹识别即可完成转移支付,支付过程便捷、高效。目前,微信适用于多个应用场景,可以使用微信支付来购物、旅游、就医、生活缴费等。 ### 2、系统功能介绍 ![image-20230429162435355](assets/image-20230429162435355.png) !!!注意:这里的用户端的"删除订单"和后台管理端的"删除订单"是有区别的,用户是没有彻底删除订单的权限的,只有管理员才有,所以要在数据库的订单表中添加一个字段来判断该订单是否被用户删除。 ### 3、数据库表设计 ![image-20230430141027269](assets/image-20230430141027269.png) 数据库表已经在sql文件夹中提供。 ### 4、技术栈和架构 前端:Vue2+Element-ui+Axios 后端:jdk8+SpringBoot2.7.6+MD5加密+微信支付Native的apiV3+lombok+mybatis-plus+mysql 系统架构图: ![image-20230503224436285](assets/image-20230503224436285.png) ## 二、后端开发 ### 1、对用户密码进行MD5加密 pom.xml ```xml cn.hutool hutool-all 5.7.20 ``` 登录和注册主要使用SecureUtil.md5()来加密 ```java @Resource private UserMapper userMapper; /** * 登录 * 先根据事务查询到用户名,再判断经MD5加密后的密码是否匹配 */ @Override public R login(User user) { // 创建查询包装器来根据用户名查询用户 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", user.getName()); User user1 = userMapper.selectOne(queryWrapper); System.out.println("根据name查询到:" + user1); if(user1 == null){ log.info("用户不存在"); return R.error().setMessage("用户不存在"); } // 获取查询到的用户的密码 String password1 = user1.getPassword(); // 加密前端传过来的password和上面的password1对比 System.out.println("加密前:" + user.getPassword()); String password = SecureUtil.md5(user.getPassword()); System.out.println("加密后:" + password); user.setId(user1.getId()); if(password1.equalsIgnoreCase(password)){ //字符串不分大小写比较 log.info(user.getName()+"登录成功"); return R.ok().setMessage("登录成功").data("user",user); } log.info("密码错误"); return R.error().setMessage("密码错误").data("user",user); } /** * 用户注册功能 * 将用户的密码进行MD5加密后存入数据库 * */ @Override public R signIn(User user) { // 创建查询包装器来根据用户名查询用户 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", user.getName()); User user1 = userMapper.selectOne(queryWrapper); System.out.println("根据name查询到:" + user1); if(user1 != null){ log.info("用户名已存在"); return R.error().setMessage("用户名已存在"); } String password = user.getPassword(); System.out.println("加密前:" + password); String md5 = SecureUtil.md5(password); System.out.println("加密后:" + md5); user.setPassword(md5); userMapper.insert(user); return R.ok().setMessage("用户注册成功").data("user", user); } ``` ### 2、User扫码支付、取消订单功能 #### (1)支付商户信息引入 wxpay.properties文件 ```yaml # 微信支付相关参数 # 商户号 wxpay.mch-id=1639864766 # 商户API证书序列号 wxpay.mch-serial-no=4FFB54B40DEAEF54819ED5CFC0A6C6E12B4A1767 # 商户私钥文件相对路径 wxpay.private-key-path=apiclient_key.pem # APIv3密钥 wxpay.api-v3-key=BuLiangYuWangLuoGongZuoShi123456 # APPID wxpay.appid=wx1cd564c7469238d3 # 微信服务器地址 wxpay.domain=https://api.mch.weixin.qq.com # 接收结果通知地址 # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置(有公网IP映射也行) wxpay.notify-domain=https://5d59-112-96-225-60.ngrok-free.app # APIv2密钥 wxpay.partnerKey: BuLiangYuWangLuoGongZuoShi123456 ``` #### (2)加载和配置微信支付相关功能 WxPayConfig读取加载以上信息和对微信支付进行相关配置 ```java @Configuration @PropertySource("classpath:wxpay.properties") //读取配置文件 @ConfigurationProperties(prefix="wxpay") //读取wxpay节点 @Data @Slf4j public class WxPayConfig { // 商户号 private String mchId; // 商户API证书序列号 private String mchSerialNo; // 商户私钥文件 private String privateKeyPath; // APIv3密钥 private String apiV3Key; // APPID private String appid; // 微信服务器地址 private String domain; // 接收结果通知地址 private String notifyDomain; // APIv2密钥 private String partnerKey; /** * 获取商户的私钥文件 * @param filename * @return */ public PrivateKey getPrivateKey(String filename){ try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私钥文件不存在", e); } } /** * 获取签名验证器 * @return */ @Bean public ScheduledUpdateCertificatesVerifier getVerifier(){ log.info("获取签名验证器"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //私钥签名对象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); //身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 使用定时更新的签名验证器,不需要传入证书 ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } /** * 获取http请求对象 * @param verifier * @return */ @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){ log.info("获取httpClient"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); return httpClient; } /** * 获取HttpClient,无需进行应答签名验证,跳过验签的流程 */ @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient(){ //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于构造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() //设置商户信息 .withMerchant(mchId, mchSerialNo, privateKey) //无需进行签名验证、通过withValidator((response) -> true)实现 .withValidator((response) -> true); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END =="); return httpClient; } } ``` #### (3)具体service业务实现 ```java import com.bls.productmall.config.WxPayConfig; import com.bls.productmall.entity.Order; import com.bls.productmall.enums.OrderStatus; import com.bls.productmall.enums.wxpay.WxApiType; import com.bls.productmall.enums.wxpay.WxNotifyType; import com.bls.productmall.enums.wxpay.WxTradeState; import com.bls.productmall.service.OrderService; import com.bls.productmall.service.PaymentService; import com.bls.productmall.service.WxPayService; import com.google.gson.Gson; import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Resource private WxPayConfig wxPayConfig; @Resource private CloseableHttpClient wxPayClient; @Resource private OrderService orderService; @Resource private PaymentService paymentService; @Resource private CloseableHttpClient wxPayNoSignClient; //无需应答签名 private final ReentrantLock lock = new ReentrantLock(); /** * 创建订单,调用Native apiV3支付接口 * 根据产品id创建订单 */ @Transactional(rollbackFor = Exception.class) @Override public Map nativePay(Long productId, Long userId) throws Exception { log.info("生成订单"); //生成订单 Order order = orderService.createOrderByProductIdAndUserId(productId, userId); String codeUrl = order.getCodeUrl(); if(order != null && !StringUtils.isEmpty(codeUrl)){ log.info("订单已存在,二维码已保存"); //返回二维码 Map map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", order.getOrderNo()); return map; } log.info("调用统一下单API"); //调用统一下单API HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); // 请求body参数 Gson gson = new Gson(); Map paramsMap = new HashMap(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); paramsMap.put("description", order.getTitle()); paramsMap.put("out_trade_no", order.getOrderNo()); paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map amountMap = new HashMap(); amountMap.put("total", order.getTotalFee()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity(jsonParams,"utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); } else { log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } //响应结果 Map resultMap = gson.fromJson(bodyAsString, HashMap.class); //二维码 codeUrl = resultMap.get("code_url"); //保存二维码 String orderNo = order.getOrderNo(); orderService.saveCodeUrl(orderNo, codeUrl); //返回二维码 Map map = new HashMap<>(); map.put("codeUrl", codeUrl); map.put("orderNo", order.getOrderNo()); return map; } finally { response.close(); } } /** * 处理订单 */ @Transactional(rollbackFor = Exception.class) @Override public void processOrder(Map bodyMap) throws GeneralSecurityException { log.info("处理订单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no"); /* 在对业务数据进行状态检查和处理之前, 要采用数据锁进行并发控制, 以避免函数重入造成的数据混乱 */ // 尝试获取锁:成功获取则立即返回true,获取失败则立即返回false。 // 不必一直等待锁的释放 if(lock.tryLock()){ try { //处理重复的通知 //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 String orderStatus = orderService.getOrderStatus(orderNo); if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){ return; } //模拟通知并发 try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //更新订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentService.createPayment(plainText); } finally { //要主动释放锁 lock.unlock(); } } } /** * 用户取消订单 */ @Override public void cancelOrder(String orderNo) throws Exception { //调用微信支付的关单接口 this.closeOrder(orderNo); //更新商户端的订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); } /** * 查询订单 * */ @Override public String queryOrder(String orderNo) throws Exception { log.info("查单接口调用 ===> {}", orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId()); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); } else { log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } return bodyAsString; } finally { response.close(); } } /** * 根据订单号查询微信支付查单接口,核实订单状态 * 如果订单已支付,则更新商户端订单状态,并记录支付日志 * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态 */ @Transactional(rollbackFor = Exception.class) @Override public void checkOrderStatus(String orderNo) throws Exception { log.warn("根据订单号核实订单状态 ===> {}", orderNo); //调用微信支付查单接口 String result = this.queryOrder(orderNo); Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端的订单状态 String tradeState = resultMap.get("trade_state"); //判断订单状态 if (WxTradeState.SUCCESS.getType().equals(tradeState)) { log.warn("核实订单已支付 ===> {}", orderNo); //如果确认订单已支付则更新本地订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); //记录支付日志 paymentService.createPayment(result); } if (WxTradeState.NOTPAY.getType().equals(tradeState)) { log.warn("核实订单未支付 ===> {}", orderNo); //如果订单未支付,则调用关单接口 this.closeOrder(orderNo); //更新本地订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); } } /** * 申请账单 */ @Override public String queryBill(String billDate, String type) throws Exception { log.warn("申请账单接口调用 {}", billDate); String url = ""; if("tradebill".equals(type)){ url = WxApiType.TRADE_BILLS.getType(); }else if("fundflowbill".equals(type)){ url = WxApiType.FUND_FLOW_BILLS.getType(); }else{ throw new RuntimeException("不支持的账单类型"); } url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(url); httpGet.addHeader("Accept", "application/json"); //使用wxPayClient发送请求得到响应 CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 申请账单返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString); } //获取账单下载地址 Gson gson = new Gson(); Map resultMap = gson.fromJson(bodyAsString, HashMap.class); return resultMap.get("download_url"); } finally { response.close(); } } /** * 关闭订单 */ private void closeOrder(String orderNo) throws Exception { log.info("关单接口的调用,订单号 ===> {}", orderNo); //创建远程请求对象 String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url); HttpPost httpPost = new HttpPost(url); //组装json请求体 Gson gson = new Gson(); Map paramsMap = new HashMap<>(); paramsMap.put("mchid", wxPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}", jsonParams); //将请求参数设置到请求对象中 StringEntity entity = new StringEntity(jsonParams,"utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功200"); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功204"); } else { log.info("Native下单失败,响应码 = " + statusCode); throw new IOException("request failed"); } } finally { response.close(); } } /** * 对称解密 */ private String decryptFromResource(Map bodyMap) throws GeneralSecurityException { log.info("密文解密"); //通知数据 Map resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); log.info("密文 ===> {}", ciphertext); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}", plainText); return plainText; } } ``` #### (4)前端请求controller层 ```java @CrossOrigin //跨域 @RestController @RequestMapping("/api/wx-pay") @Slf4j public class WxPayController { @Resource private WxPayService wxPayService; @Resource private Verifier verifier; /** * Native下单 */ @PostMapping("/native/{productId}/{userId}") public R nativePay(@PathVariable Long productId, @PathVariable Long userId) throws Exception { log.info("发起支付请求 v3"); //返回支付二维码连接和订单号 Map map = wxPayService.nativePay(productId, userId); return R.ok().setData(map); } /** * 支付通知 * 微信支付通过支付通知接口将用户支付成功消息通知给商户 */ @PostMapping("/native/notify") public String nativeNotify(HttpServletRequest request, HttpServletResponse response){ Gson gson = new Gson(); Map map = new HashMap<>();//应答对象 try { //处理通知参数 String body = HttpUtils.readData(request); Map bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String)bodyMap.get("id"); log.info("支付通知的id ===> {}", requestId); //签名的验证 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body); if(!wechatPay2ValidatorForRequest.validate(request)){ log.error("通知验签失败"); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return gson.toJson(map); } log.info("通知验签成功"); //处理订单 wxPayService.processOrder(bodyMap); //应答超时 //模拟接收微信端的重复通知 TimeUnit.SECONDS.sleep(5); //成功应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } catch (Exception e) { e.printStackTrace(); //失败应答 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "失败"); return gson.toJson(map); } } /** * 用户取消订单 * @param orderNo * @return * @throws Exception */ @PostMapping("/cancel/{orderNo}") public R cancel(@PathVariable String orderNo) throws Exception { log.info("取消订单"); wxPayService.cancelOrder(orderNo); return R.ok().setMessage("订单已取消"); } /** * 查询订单 * @param orderNo * @return * @throws Exception */ @GetMapping("/query/{orderNo}") public R queryOrder(@PathVariable String orderNo) throws Exception { log.info("查询订单"); String result = wxPayService.queryOrder(orderNo); return R.ok().setMessage("查询成功").data("result", result); } @GetMapping("/querybill/{billDate}/{type}") public R queryTradeBill( @PathVariable String billDate, @PathVariable String type) throws Exception { log.info("获取账单url"); String downloadUrl = wxPayService.queryBill(billDate, type); return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl); } } ``` ### 3、Admin退款、下载订单功能 #### (1) 引入支付商户信息 admin后端也和user后端一样,需要引入支付商户信息。 #### (2)退款service业务实现 ```java @Service @Slf4j public class RefundServiceImpl extends ServiceImpl implements RefundService { @Resource private WxPayConfig wxPayConfig; @Resource private OrderService orderService; @Resource private CloseableHttpClient wxPayClient; private final ReentrantLock lock = new ReentrantLock(); /** * 根据订单号创建退款订单 */ @Override public Refund createRefundByOrderNo(String orderNo, String reason) { //根据订单号获取订单信息 Order order = orderService.getOrderByOrderNo(orderNo); //根据订单号生成退款订单 Refund refund = new Refund(); refund.setOrderNo(orderNo);//订单编号 refund.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号 refund.setTotalFee(order.getTotalFee());//原订单金额(分) refund.setRefund(order.getTotalFee());//退款金额(分) refund.setReason(reason);//退款原因 //保存退款订单 baseMapper.insert(refund); return refund; } /** * 记录退款记录 */ @Override public void updateRefund(String content) { //将json字符串转换成Map Gson gson = new Gson(); Map resultMap = gson.fromJson(content, HashMap.class); //根据退款单编号修改退款单 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_no", resultMap.get("out_refund_no")); //设置要修改的字段 Refund refund = new Refund(); //微信支付退款单号 refund.setRefundId(resultMap.get("refund_id")); //查询退款和申请退款中的返回参数 if(resultMap.get("status") != null){ refund.setRefundStatus(resultMap.get("status"));//退款状态 refund.setContentReturn(content);//将全部响应结果存入数据库的content字段 } //退款回调中的回调参数 if(resultMap.get("refund_status") != null){ refund.setRefundStatus(resultMap.get("refund_status"));//退款状态 refund.setContentNotify(content);//将全部响应结果存入数据库的content字段 } //更新退款单 baseMapper.update(refund, queryWrapper); } /** * 找出申请退款超过minutes分钟并且未成功的退款单 * @param minutes * @return */ @Override public List getNoRefundOrderByDuration(int minutes) { //minutes分钟之前的时间 Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType()); queryWrapper.le("create_time", instant); List refundList = baseMapper.selectList(queryWrapper); return refundList; } /** * 退款(订单号、理由) */ @Transactional(rollbackFor = Exception.class) @Override public void refund(String orderNo, String reason) throws Exception { log.info("创建退款单记录"); //根据订单编号创建退款单 Refund refundsInfo = this.createRefundByOrderNo(orderNo, reason); log.info("调用退款API"); //调用统一下单API String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType()); HttpPost httpPost = new HttpPost(url); // 请求body参数 Gson gson = new Gson(); Map paramsMap = new HashMap(); paramsMap.put("out_trade_no", orderNo);//订单编号 paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号 paramsMap.put("reason",reason);//退款原因 paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址 Map amountMap = new HashMap(); amountMap.put("refund", refundsInfo.getRefund());//退款金额 amountMap.put("total", refundsInfo.getTotalFee());//原订单金额 amountMap.put("currency", "CNY");//退款币种 paramsMap.put("amount", amountMap); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" + jsonParams); StringEntity entity = new StringEntity(jsonParams,"utf-8"); entity.setContentType("application/json");//设置请求报文格式 httpPost.setEntity(entity);//将请求报文放入请求对象 httpPost.setHeader("Accept", "application/json");//设置响应报文格式 //完成签名并执行请求,并完成验签 CloseableHttpResponse response = wxPayClient.execute(httpPost); try { //解析响应结果 String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 退款返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString); } //更新订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING); //更新退款单 this.updateRefund(bodyAsString); } finally { response.close(); } } /** * 查询退款接口调用 */ @Override public String queryRefund(String refundNo) throws Exception { log.info("查询退款接口调用 ===> {}", refundNo); String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo); url = wxPayConfig.getDomain().concat(url); //创建远程Get 请求对象 HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { log.info("成功, 查询退款返回结果 = " + bodyAsString); } else if (statusCode == 204) { log.info("成功"); } else { throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString); } return bodyAsString; } finally { response.close(); } } /** * 根据退款单号核实退款单状态 */ @Transactional(rollbackFor = Exception.class) @Override public void checkRefundStatus(String refundNo) throws Exception { log.warn("根据退款单号核实退款单状态 ===> {}", refundNo); //调用查询退款单接口 String result = this.queryRefund(refundNo); //组装json请求体字符串 Gson gson = new Gson(); Map resultMap = gson.fromJson(result, HashMap.class); //获取微信支付端退款状态 String status = resultMap.get("status"); String orderNo = resultMap.get("out_trade_no"); if (WxRefundStatus.SUCCESS.getType().equals(status)) { log.warn("核实订单已退款成功 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 this.updateRefund(result); } if (WxRefundStatus.ABNORMAL.getType().equals(status)) { log.warn("核实订单退款异常 ===> {}", refundNo); //如果确认退款成功,则更新订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL); //更新退款单 this.updateRefund(result); } } /** * 处理退款单 */ @Transactional(rollbackFor = Exception.class) @Override public void processRefund(Map bodyMap) throws Exception { log.info("退款单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String)plainTextMap.get("out_trade_no"); if(lock.tryLock()){ try { String orderStatus = orderService.getOrderStatus(orderNo); if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) { return; } //更新订单状态 orderService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS); //更新退款单 this.updateRefund(plainText); } finally { //要主动释放锁 lock.unlock(); } } } /** * 对称解密 */ private String decryptFromResource(Map bodyMap) throws GeneralSecurityException { log.info("密文解密"); //通知数据 Map resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); log.info("密文 ===> {}", ciphertext); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}", plainText); return plainText; } } ``` #### (3)下载订单功能 也就是将mysql数据库表导出为excel表,具体参考:https://blog.csdn.net/yueyue763184/article/details/130470124?spm=1001.2014.3001.5501 ## 三、前端开发 使用的是Vue+Element-ui+axios ### 1、User端 ![image-20230503205818923](assets/image-20230503205818923.png) ![image-20230503205858573](assets/image-20230503205858573.png) 用axios发送订单请求 ```javascript // axios 发送ajax请求 import request from '@/utils/request' export default{ //Native下单 nativePay(productId, userId) { return request({ url: '/api/wx-pay/native/' + productId + "/" + userId, method: 'post' }) }, // 取消订单 cancel(orderNo) { return request({ url: '/api/wx-pay/cancel/' + orderNo, method: 'post' }) } } ``` 首页ui代码 ```html ``` ### 2、Admin端 ![image-20230503210055067](assets/image-20230503210055067.png) ![image-20230503210133222](assets/image-20230503210133222.png) 用axios发送订单请求 ```javascript import request from '@/utils/request' export default{ // 退款请求 refundsByOrderNo(orderNo) { return request({ url: '/api/wx-pay/refunds/'+ orderNo +'/管理员退款', method: 'get' }) }, } ``` 订单管理页面代码 ```html ``` 作者博客:https://blog.csdn.net/yueyue763184?type=lately !对此作品有任何异议请联系作者邮箱:2013994940@qq.com !对此作品有任何异议请联系作者邮箱:2013994940@qq.com !对此作品有任何异议请联系作者邮箱:2013994940@qq.com