1 Star 1 Fork 1

不凉帅 / 产品商城

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

产品商城(主要用于学习微信支付功能开发)

256位秘钥策略与JDK8的小Bug

如果使用JDK8运行后端时遇到报错解决:java.security.InvalidKeyException: Illegal key size(微信支付v3遇到的问题)

原因是因为微信支付256位秘钥策略可能会导致某些jdk的版本加密解密出现问题,首先观察你这个目录下的文件

image-20230317231340561

根据文件内容做判断看下目录里面是有一个 policy 文件夹,还是有local_policy.jar

去官方下载JCE无限制权限策略文件,这里贴出jdk 8的国内地址 方便下载https://wwi.lanzoup.com/iXGs404zm1dg

image-20230317231652843

下载解压后,将以上两个文件拷贝覆盖到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

!!!注意:这里的用户端的"删除订单"和后台管理端的"删除订单"是有区别的,用户是没有彻底删除订单的权限的,只有管理员才有,所以要在数据库的订单表中添加一个字段来判断该订单是否被用户删除。

3、数据库表设计

image-20230430141027269

数据库表已经在sql文件夹中提供。

4、技术栈和架构

前端:Vue2+Element-ui+Axios

后端:jdk8+SpringBoot2.7.6+MD5加密+微信支付Native的apiV3+lombok+mybatis-plus+mysql

系统架构图:

image-20230503224436285

二、后端开发

1、对用户密码进行MD5加密

pom.xml

<!-- hutool  -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.20</version>
</dependency>

登录和注册主要使用SecureUtil.md5()来加密

@Resource
    private UserMapper userMapper;

    /**
    * 登录
    * 先根据事务查询到用户名,再判断经MD5加密后的密码是否匹配
    */
    @Override
    public R login(User user) {
        // 创建查询包装器来根据用户名查询用户
        QueryWrapper<User> 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<User> 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文件

# 微信支付相关参数
# 商户号
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读取加载以上信息和对微信支付进行相关配置

@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业务实现

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<String, Object> 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<String, Object> 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<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            codeUrl = resultMap.get("code_url");
            //保存二维码
            String orderNo = order.getOrderNo();
            orderService.saveCodeUrl(orderNo, codeUrl);
            //返回二维码
            Map<String, Object> 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<String, Object> 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<String, String> 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<String, String> 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<String, String> 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<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String, String> 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层

@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<String, Object> 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<String, String> map = new HashMap<>();//应答对象
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> 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业务实现

@Service
@Slf4j
public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> 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<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号修改退款单
        QueryWrapper<Refund> 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<Refund> getNoRefundOrderByDuration(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        QueryWrapper<Refund> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
        queryWrapper.le("create_time", instant);
        List<Refund> 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<String, String> 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<String, Object> 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<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String, String> 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

image-20230503205858573

用axios发送订单请求

// 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代码

<template>
  <div class="bg-fa of">
    <!-- 公共头 -->
    <AppHeader :inputName="user.name"/>
    <!-- /公共头 -->

    <section id="index" class="container">
      <header class="comm-title">
        <h2 class="fl tac">
          <span class="c-333">课程列表</span>
        </h2>
      </header>
      <ul>
        <li v-for="product in productList" :key="product.id">
          <a :class="['orderBtn', {current:payOrder.productId === product.id}]"
             @click="selectItem(product.id)"
             href="javascript:void(0);">
            {{ product.title }}
            ¥{{ product.price / 100 }}
          </a>
        </li>
      </ul>

      <div class="PaymentChannel_payment-channel-panel">
        <h3 class="PaymentChannel_title">
          选择支付方式
        </h3>
        <div class="PaymentChannel_channel-options">
          <!-- 选择微信 -->
          <div :class="['ChannelOption_payment-channel-option', {current:payOrder.payType === 'wxpay'}]"
               @click="selectPayType('wxpay')">
            <div class="ChannelOption_channel-icon">
              <img src="../assets/img/wxpay.png" class="ChannelOption_icon">
            </div>
            <div class="ChannelOption_channel-info">
              <div class="ChannelOption_channel-label">
                <div class="ChannelOption_label">微信支付</div>
                <div class="ChannelOption_sub-label"></div>
                <div class="ChannelOption_check-option"></div>
              </div>
            </div>
          </div>

        </div>
      </div>

      <div class="payButtom">
        <el-button
            :disabled="payBtnDisabled"
            type="warning"
            round
            style="width: 180px;height: 44px;font-size: 18px;"
            @click="toPay()">
          确认支付
        </el-button>
      </div>
    </section>

    <!-- 微信支付二维码 -->
    <el-dialog
        :visible.sync="codeDialogVisible"
        :show-close="false"
        @close="closeDialog"
        width="350px"
        center>
      <qriously :value="codeUrl" :size="300"/>
      使用微信扫码支付
    </el-dialog>

    <!-- 公共底 -->
    <AppFooter/>
    <!-- /公共底 -->
  </div>
</template>

<script>
import productApi from '../api/product'
import wxPayApi from '../api/wxPay'
import orderInfoApi from '../api/orderInfo'
import AppHeader from '../components/AppHeader'
import selectUserByName from "../api/login"
import AppFooter from '../components/AppFooter'
import '../assets/css/reset.css'
import '../assets/css/theme.css'
import '../assets/css/global.css'

export default {
  components: {
    AppHeader, AppFooter
  },
  data() {
    return {
      payBtnDisabled: false, //确认支付按钮是否禁用
      codeDialogVisible: false, //微信支付二维码弹窗
      productList: [], //商品列表
      payOrder: { //订单信息
        productId: '', //商品id
        payType: 'wxpay' //支付方式
      },
      codeUrl: '', // 二维码
      orderNo: '', //订单号
      timer: null, // 定时器
      // 存储兄弟组件login传过来的user信息
      user: {
        id: null,
        name: "未登录"
      }
    }
  },
  mounted() {
    // 获取兄弟组件传的值
    this.$bus.$on("name", name => {
        this.user.name = name;
        // 根据name查询user信息
        selectUserByName.selectUserByName(this.user.name).then(r => {
          this.user.id = r.data.user.id
          // 将整个user对象存入浏览器缓存
          localStorage.setItem("user", JSON.stringify(this.user))
        })
    })
  },
  created() {
    //获取商品列表
    productApi.list().then(response => {
      this.productList = response.data.productList
      this.payOrder.productId = this.productList[0].id
    })
    // 20毫秒后读取浏览器缓存
    if(JSON.parse(localStorage.getItem('user')).id !== null){
      setTimeout(() => {
        this.user = JSON.parse(localStorage.getItem('user'))
      }, 20)
    }
  },
  methods: {
    //选择商品
    selectItem(productId) {
      this.payOrder.productId = productId
    },
    //选择支付方式
    selectPayType(type) {
      this.payOrder.payType = type
    },
    //确认支付
    toPay() {
      if (this.user.id === null) {
        this.$router.push("/login")
      } else {
        //禁用按钮,防止重复提交
        this.payBtnDisabled = true
        //微信支付
        if (this.payOrder.payType === 'wxpay') {
          //调用统一下单接口
          wxPayApi.nativePay(this.payOrder.productId, this.user.id).then(response => {
            this.codeUrl = response.data.codeUrl
            this.orderNo = response.data.orderNo
            //打开二维码弹窗
            this.codeDialogVisible = true
            //启动定时器
            this.timer = setInterval(() => {
              //查询订单是否支付成功
              this.queryOrderStatus()
            }, 3000)
          })
        }
      }
    },
    //关闭微信支付二维码对话框时让“确认支付”按钮可用
    closeDialog() {
      this.payBtnDisabled = false
      clearInterval(this.timer)  // 清除定时器
    },
    // 查询订单状态
    queryOrderStatus() {
      orderInfoApi.queryOrderStatus(this.orderNo).then(response => {
        if (response.code === 0) {
          clearInterval(this.timer)  // 清除定时器
          // 5毫秒后提示支付成功
          setTimeout(() => {
            this.payBtnDisabled = false
            this.codeDialogVisible = false
            this.$message.success("支付成功!")
          }, 5)
        }
      })
    }
  }
}
</script>

2、Admin端

image-20230503210055067

image-20230503210133222

用axios发送订单请求

import request from '@/utils/request'

export default{
    // 退款请求
    refundsByOrderNo(orderNo) {
        return request({
            url: '/api/wx-pay/refunds/'+ orderNo +'/管理员退款',
            method: 'get'
        })
    },
}

订单管理页面代码

<template>
  <div>
    <!--条件搜索区域-->
    <el-row>
      <el-col :span="24">
        <el-card header=" ">
          <span class="header2">订单列表</span>
          <el-form :inline="true" style="margin: 0 0 0 300px">
            <el-form-item label="用户名称:">
              <el-input
                  v-model="searchOrder.orderName"
                  placeholder="用户名"
                  clearable>
              </el-input>
            </el-form-item>
            <el-form-item>
              <el-button icon="el-icon-search" @click="search">搜索</el-button>
            </el-form-item>
            <el-form-item>
              <el-button  @click="exportExcel">
                <i class="el-icon-download"></i>下载所有订单信息
              </el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>

    <!--数据显示区域-->
    <el-row>
      <el-col :span="24">
        <el-card>
          <!-- 订单的详细信息 -->
          <el-table :data="orderData" style="width: 100%">
            <el-table-column
                width="40"
                prop="id"
                label="ID">
            </el-table-column>
            <el-table-column
                width="110"
                prop="userName"
                label="用户">
            </el-table-column>
            <el-table-column
                width="250"
                prop="orderNo"
                label="订单号">
            </el-table-column>
            <el-table-column
                width="175"
                prop="title"
                label="产品名称">
            </el-table-column>
            <el-table-column
                width="100"
                prop="totalFee"
                label="价格/¥">
            </el-table-column>
            <el-table-column
                width="200"
                prop="orderStatus"
                label="订单状态">
            </el-table-column>
            <el-table-column
                width="180"
                prop="updateTime"
                label="更新时间">
            </el-table-column>
            <el-table-column label="操作" width="210">
              <template slot-scope="scope">
                <el-popconfirm :title="('确定删除第'+ scope.row.id +'个订单吗?')" @confirm="remove(scope.row.id)">
                  <!--删除按钮-->
                  <el-button
                      icon="el-icon-close"
                      size="mini"
                      type="danger"
                      slot="reference">删除
                  </el-button>
                </el-popconfirm>
                <!--退款按钮-->
                <el-button
                    style="margin: 0 0 0 15px;"
                    v-if="scope.row.orderStatus==='支付成功'||scope.row.orderStatus==='支付成功,用户已删除'"
                    icon="el-icon-sort"
                    size="mini"
                    type="primary"
                    @click="refundsBtn(scope.row.id,scope.row.orderNo)"
                    slot="reference">退款
                </el-button>
                <!-- 退款时弹出的确认对话框 -->
                <el-dialog title="提示" :visible.sync="centerDialogVisible" width="30%" center>
                  <span style="margin-left: 25%">确定退款回第<span
                      style="color: red">{{ orderRefunds.id }}</span>号订单的用户吗?</span>
                  <span slot="footer" class="dialog-footer">
                    <el-button @click="centerDialogVisible = false">取 消</el-button>
                    <el-button type="primary" @click="refunds">确 定</el-button>
                  </span>
                </el-dialog>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>

    <!--分页-->
    <el-row class="page-row">
      <el-pagination
          class="el-pagination"
          layout="total, sizes, prev, pager, next, jumper"
          :page-sizes="[5, 10, 30, 50]"
          @size-change="sizeChange"
          @current-change="currentChange"
          @prev-click="currentChange"
          @next-click="currentChange"
          :current-page="current"
          :page-size="size"
          :total="total">
      </el-pagination>
    </el-row>

  </div>
</template>
<script>
import order from "../api/order"
import refunds from "../api/refunds"

export default {
  data() {
    return {
      adminName: "",
      // 是否显示提示框
      centerDialogVisible: false,
      // 退款时的数据
      orderRefunds: {
        id: null,
        orderNo: '',
      },
      // 搜索条件对象
      searchOrder: {
        orderName: '',
      },
      // 产品数据 (页面初始化时会将数据传进来)
      orderData: [],
      size: 5, // 每页的数据大小
      current: 1, //当前页数
      total: 0, // 总数量
    }
  },
  created() {
    this.getData()
    if(JSON.parse(localStorage.getItem('adminName')) === null){
      this.$router.push("/login")
    }else{
      setTimeout(() => {
        // 20毫秒后读取浏览器缓存
        this.adminName = JSON.parse(localStorage.getItem('adminName'))
      }, 20)
    }
  },
  methods: {
    // 下载订单
    exportExcel() {
      // 这里可以传入一些查询参数,我在这里传入了标题内容
      // 将标题内容作为导出的Excel文件名,此处的标题内容后期可动态改变
      let url = 'http://127.0.0.1:8051/api/order/exportExcel/订单信息'
      window.open(url)
    },
    // 点击退款按钮时保存值
    refundsBtn(id, orderNo) {
      this.orderRefunds.id = id
      this.orderRefunds.orderNo = orderNo
      this.centerDialogVisible = true
    },
    // 退款
    refunds() {
      refunds.refundsByOrderNo(this.orderRefunds.orderNo).then(r => {
        if (r.code === 0) {
          this.$message.success(r.message)
        }
        if (this.searchOrder.orderName !== '') {
          this.search()
        } else {
          this.getData()
        }
      })
      this.centerDialogVisible = false
    },
    // 根据搜索条件查询
    search() {
      if (this.searchOrder.orderName !== '') {
        order.getOrderByUserName(this.searchOrder.orderName, this.current, this.size).then(r => {
          let orderList = r.data.orderList
          this.total = r.total
          this.orderData = orderList
          // 将分转化为元
          for (let i = 0; i < orderList.length; i++) {
            this.orderData[i].totalFee = orderList[i].totalFee / 100
          }
        })
      } else {
        this.$message.error("搜索的用户名不能为空")
      }
    },
    // 删除
    remove(id) {
      order.deleteOrderByOrderId(id).then(r => {
        this.$message.success(r.message)
        if (this.searchOrder.orderName !== '') {
          this.search()
        } else {
          this.getData()
        }
      })
    },
    // 每页大小修改
    sizeChange(val) {
      this.size = val
      if (this.searchOrder.orderName !== '') {
        this.search()
      } else {
        this.getData()
      }
    },
    // 第几页
    currentChange(val) {
      this.current = val
      if (this.searchOrder.orderName !== '') {
        this.search()
      } else {
        this.getData()
      }
    },
    // 获取数据,并赋值给wordData
    getData() {
      order.getOrderPage(this.current, this.size).then(r => {
        let orderList = r.data.orderList
        this.total = r.total
        this.orderData = orderList
        // 将分转化为元
        for (let i = 0; i < orderList.length; i++) {
          this.orderData[i].totalFee = orderList[i].totalFee / 100
        }
      })
    }
  }
}
</script>

<style>
@import "~@/assets/css/ddgl.css";
</style>

作者博客:https://blog.csdn.net/yueyue763184?type=lately

!对此作品有任何异议请联系作者邮箱:2013994940@qq.com

!对此作品有任何异议请联系作者邮箱:2013994940@qq.com

!对此作品有任何异议请联系作者邮箱:2013994940@qq.com

空文件

简介

主要用于学习微信支付功能,用户端和后台管理端分权限。 前端:Vue2+Element-ui+Axios 后端:jdk8+SpringBoot2.7.6+MD5加密+微信支付Native的apiV3+lombok+mybatis-plus+mysql 展开 收起
Java 等 5 种语言
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/BuLiangShuai01033/product-mall.git
git@gitee.com:BuLiangShuai01033/product-mall.git
BuLiangShuai01033
product-mall
产品商城
master

搜索帮助