微信支付JSAPI下单

一、导入maven

<!--微信支付SDK-->
		<dependency>
			<groupId>com.github.wechatpay-apiv3</groupId>
			<artifactId>wechatpay-apache-httpclient</artifactId>
			<version>0.3.0</version>
		</dependency>

二、添加配置文件

package org.jeecg.config.wxpay;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.util.ResourceUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;


@Configuration
@PropertySource("classpath:application-dev.yml") //读取配置文件
@ConfigurationProperties(prefix="wxjzg") //读取wx.pay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@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;


    /**
     * 获取商户的私钥文件
     * @description:获取商户的私钥文件
     * @author: Chris
     * @date: 2021/12/29 20:50
     * @Param filename:
     * @return: java.security.PrivateKey
     */
    private PrivateKey getPrivateKey(String filename){
        try {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                    new ByteArrayInputStream(filename.getBytes("utf-8")));
            return merchantPrivateKey;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {


        //获取商户私钥
        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请求对象
     * @description:获取http请求对象
     * @author: Chris
     * @date: 2021/12/29 20:56
     * @Param verifier:
     * @return: org.apache.http.impl.client.CloseableHttpClient
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {


//        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));

        // 通过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;
    }
}

三、统一支付接口

controller层

package org.jeecg.modules.orderinfo.controller;

import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
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.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.modules.orderadmin.orderinfo.entity.OrderInfo;
import org.jeecg.modules.orderadmin.orderinfo.util.HttpUtils;
import org.jeecg.modules.orderadmin.orderinfo.util.WechatPay2ValidatorForRequest;
import org.jeecg.modules.orderinfo.entity.HyOrderInfo;
import org.jeecg.modules.orderinfo.service.IHyOrderInfoService;
import org.jeecg.modules.orderinfo.service.IPaymentService;
import org.jeecg.modules.orderinfo.xcxutil.AesUtil;
import org.jeecg.modules.orderinfo.xcxutil.PayUtil;
import org.jeecg.modules.orderinfo.xcxutil.QRCodeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.math.BigDecimal;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.text.SimpleDateFormat;
import java.util.*;
import org.apache.commons.codec.binary.Base64;


/**
 * @Description: 订单
 * @Author: jeecg-boot
 * @Date:   2021-12-15
 * @Version: V1.0
 */
@Api(tags="微信支付")
@RestController
@RequestMapping("/payment")
@Slf4j
public class PaymentController {

    @Autowired
    private CloseableHttpClient wxPayClient;

    /**
     * url
     */
    @Value(value = "${wxjzg.url}")
    private String url;

    /**
     * appid
     */
    @Value(value = "${wxjzg.appid}")
    private String appId;

    /**
     * mchid
     */
    @Value(value = "${wxjzg.mchid}")
    private String mchId;

    /**
     * key
     */
    @Value(value = "${wxjzg.apiV3Key}")
    private String key;

    /**
     * privateKeyPath
     */
    @Value(value = "${wxjzg.privateKeyPath}")
    private String privateKeyPath;

    /**
     * 生成链接地址配置
     */
    @Value(value = "${wxjzg.secret}")
    private String secret;

    @Autowired
    private IPaymentService paymentService;

    @Autowired
    private IHyOrderInfoService hyOrderInfoService;
    @Resource
    private Verifier verifier;

    private static String ENCODING  = "UTF-8";
    public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";


    /**
     * <p>统一下单入口</p>
     *
     * @param
     * @param
     * @throws Exception
     */
    @AutoLog(value = "统一下单入口")
    @ApiOperation(value="统一下单入口", notes="统一下单入口")
    @PostMapping(value = "/toPay")
    public Result<?> toPay(@RequestBody Map<String,String> map) throws Exception {
        String orderId = map.get("orderId");
        // 获取openid
        String openId = map.get("openId");
        // 获取订单下信息
        HyOrderInfo orderInfo = hyOrderInfoService.getById(orderId);
        if (orderInfo == null) {
            return Result.error("订单不存在");
        }

        if (orderInfo.getOrderTotalSum() == null || new BigDecimal(orderInfo.getOrderTotalSum()).floatValue() < 0) {
            return Result.error("订单有误,请确认!");
        }

        if (Integer.valueOf(orderInfo.getOrderState()) != 1 && Integer.valueOf(orderInfo.getOrderState()) != 7) {//1待付款
            String msg = Integer.valueOf(orderInfo.getOrderState()) > 1 ? "此订单已支付!" : "订单未提交,请确认!";
            return Result.error(msg);
        }
        //发起预支付
//        String prepayId = paymentService.xcxPayment(orderInfo.getOrderCode(), orderInfo.getOrderTotalSum(), openId);
        String prepayId = paymentService.xcxPayment(orderInfo, openId);
        // 封装要签名的数据
        Map<String,String> parameters = new LinkedHashMap<>();
        // appId
        parameters.put("appId",appId);
        // 时间戳
        Long timeStamp = PayUtil.getCurrentTimestampMs();
        parameters.put("timeStamp", timeStamp.toString());
        // 随机数
        String nonceStr = PayUtil.generateNonceStr();
        parameters.put("nonceStr",nonceStr);
        // 订单详情扩展字符串
        parameters.put("package","prepay_id=" + prepayId);
        // 签名串
        String signString = appId + "\n" + timeStamp.toString() + "\n" + nonceStr + "\n" + "prepay_id=" + prepayId + "\n" ;
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        // 使用商户私钥对待签名串进行SHA256 with RSA签名
        // 进行签名服务
        byte[] sign = sign(signString,privateKey);
        // Base64计算签名值
        String paySign = encodeBase64(sign);
//        String paySign = PayUtil.generateSignature(parameters,key);
        parameters.put("paySign", paySign);

        return Result.OK(parameters);
    }

    /**
     * 二进制数据编码为BASE64字符串
     * @param
     * @return
     */
    public static String encodeBase64(byte[] bytes) {
        return new String(Base64.encodeBase64(bytes));
    }

    /**
     * SHA256WithRSA签名
     * @param data
     * @param privateKey
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws InvalidKeyException
     * @throws SignatureException
     * @throws UnsupportedEncodingException
     */
    public static byte[] sign(String data, PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException,
            SignatureException, UnsupportedEncodingException {

        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);

        signature.initSign(privateKey);

        signature.update(data.getBytes(ENCODING));

        return signature.sign();
    }

    /**
     * 获取商户的私钥文件
     * @description:获取商户的私钥文件
     * @author: Chris
     * @date: 2021/12/29 20:50
     * @Param filename:
     * @return: java.security.PrivateKey
     */
    private PrivateKey getPrivateKey(String filename){
        try {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                    new ByteArrayInputStream(filename.getBytes("utf-8")));
            return merchantPrivateKey;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取openid
     */
    public String getOpenId(@RequestParam("code")String code) throws Exception{
        String urlPath = url;
        urlPath += "?appid=" + appId;
        urlPath += "&secret=" + secret;
        urlPath += "&js_code=" + code;
        urlPath += "&grant_type=authorization_code";
        urlPath += "&connect_redirect=1";
        String res = null;
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
//		get请求
        HttpGet httpGet = new HttpGet(urlPath);
        CloseableHttpResponse response = null;
//		配置信息
        RequestConfig requestConfig = RequestConfig.custom()
//				设置请求超时时间(毫秒)
                .setConnectTimeout(5000)
//				socket读写超时时间(单位毫秒)
                .setConnectionRequestTimeout(5000)
//				设置是否允许重定向(默认为true)
                .setSocketTimeout(5000)
//				将上面的配置信息 运用到这个Get请求里
                .setRedirectsEnabled(false).build();
//		由客户端执行(发送)Get请求
        httpGet.setConfig(requestConfig);
//		从响应模型中获取响应实体
        response = httpClient.execute(httpGet);
        HttpEntity responseEntity = response.getEntity();
        if (responseEntity != null){
            res = EntityUtils.toString(responseEntity);
        }
//		释放资源
        if (httpClient != null){
            httpClient.close();
        }
        if (response != null){
            response.close();
        }
        JSONObject jsonObject = JSON.parseObject(res);
        String openid = jsonObject.getString("openid");
        return jsonObject.getString("openid");
    }

    /**
     * 查询订单
     */
    @AutoLog(value = "查询订单")
    @ApiOperation(value="查询订单", notes="查询订单")
    @GetMapping(value = "/queryOrder")
    public Result<?> queryOrder (@RequestParam(name = "orderId") String orderId) throws IOException {
        HyOrderInfo hyOrderInfo = hyOrderInfoService.getById(orderId);
        String transactionId = hyOrderInfo.getTransactionId();
        String mchid = mchId;
        String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/" + transactionId + "?" + "mchid=" + mchid;
        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 && statusCode != 204){
                throw new RuntimeException("查询订单异常,响应码 = " + statusCode + ",查询订单返回结果 = "+ bodyAsString);
            }
            JSONObject json = JSONObject.parseObject(bodyAsString);
            // 交易状态
            String tradeState = json.getString("trade_state");
            if ("SUCCESS".equals(tradeState)){
                if (!"2".equals(hyOrderInfo.getOrderState())){
//                    生成6位随机数
                    String captcha = RandomUtil.randomNumbers(6);
                    // 封装二维码内容
                    String contents = "hyzj6666&" + orderId + captcha;
//                   生成base64加密二维码
                    hyOrderInfo.setQrCodeAddress(QRCodeUtils.creatRrCode(contents, 200,200));
                    hyOrderInfo.setOrderState("2");
                    hyOrderInfoService.updateById(hyOrderInfo);
                }
                return Result.OK(hyOrderInfo);
            }
            return Result.OK("");
        } finally {
            response.close();
        }
    }


    /**
     * 关闭订单
     */
    @AutoLog(value = "关闭订单")
    @ApiOperation(value="关闭订单", notes="关闭订单")
    @PostMapping(value = "/closeOrder")
    public Result<?> closeOrder (@RequestBody Map<String,String> map) throws IOException {
        String orderId = map.get("orderId");
        String mchid = mchId;
        // 请求的url
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + orderId + "/close");
        // 请求参数
        JSONObject body = new JSONObject();
        body.put("mchid", mchid);
        StringEntity entity = new StringEntity(String.valueOf(body),"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        //创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();
        //执行请求操作,并拿到结果(同步阻塞)
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
            } else if (statusCode == 204) {
//                System.out.println("success");
            } else {
                System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
//TODO:更改订单状态。status=6
        int i = hyOrderInfoService.updateRefundStateByOrderCode(orderId, "2");
        return Result.OK("取消成功");
    }

    @ApiOperation("支付通知")
    @PostMapping("/notify")
    public String notify(HttpServletRequest request, HttpServletResponse response){
        Gson gson = new Gson();
//        应答对象
        HashMap<String, Object> map = new HashMap<>();

        try {
//            处理通知参数
            String body = HttpUtils.readData(request);
            Map<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");

//            签名验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)){
//                失败应答
                response.setStatus(500);
                map.put("code","ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
//            处理订单
            hyOrderInfoService.processOrder(bodyMap);
            //成功应答
            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);
        }

    }


}

serviceimpl层

package org.jeecg.modules.orderinfo.service.impl;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.orderinfo.entity.HyOrderInfo;
import org.jeecg.modules.orderinfo.service.IPaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.math.BigDecimal;

@Service
public class PaymentServiceImpl implements IPaymentService {

    @Autowired
    private CloseableHttpClient wxPayClient;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * appid
     */
    @Value(value = "${wxjzg.appid}")
    private String appid;
    /**
     * mchid
     */
    @Value(value = "${wxjzg.mchId}")
    private String mchid;

    /**
     * key
     */
    @Value(value = "${wxjzg.key}")
    private String key;

    /**
     * 发起微信支付获取预支付交易会话标识prepayId
     */
    @Override
    public String xcxPayment(HyOrderInfo orderInfo, String openId) throws IOException {
        // 请求URL
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");

        // 请求body参数
        JSONObject jsonObject = new JSONObject();
        // 直连商户号
        jsonObject.put("mchid",mchid);
        // 商户订单号
        jsonObject.put("out_trade_no", orderInfo.getOrderCode());
        // 应用ID
        jsonObject.put("appid", appid);
        // 商品描述
        jsonObject.put("description","航院之家");
        // 通知地址
        jsonObject.put("notify_url", "http://hyzj.rzocean.com/hyzj/payment/notify");
        // 订单金额
        JSONObject amount = new JSONObject();
        amount.put("total", new BigDecimal(orderInfo.getOrderTotalSum()).multiply(BigDecimal.valueOf(100)).intValue());
        amount.put("currency", "CNY");
        jsonObject.put("amount",amount);
        // 用户标识
        JSONObject payer = new JSONObject();
        payer.put("openid",openId);
        jsonObject.put("payer", payer);

        StringEntity entity = new StringEntity(String.valueOf(jsonObject),"utf-8");
        entity.setContentType("application/json");
//        httpPost.setHeader("Content-type", "application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();

        //执行请求操作,并拿到结果(同步阻塞)
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
//                System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
            } else if (statusCode == 204) {
//                System.out.println("success");
            } else {
//                System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
                JSONObject json = JSONObject.parseObject(EntityUtils.toString(response.getEntity()));
                String message = (String) json.get("message");
                if (message != null){
                    throw new IOException(message);
                } else {
                    throw new IOException("支付失败");
                }
            }
        } finally {
            response.close();
        }
        //获取结果实体
        HttpEntity httpEntity = response.getEntity();
        String result = EntityUtils.toString(httpEntity, "UTF-8");
        JSONObject json = JSONObject.parseObject(result);
        String prepayId = json.get("prepay_id").toString();
//        redisUtil.del(CommonConstant.PREFIX_FACULTY_ORDER+orderInfo.getId());
        return prepayId;
    }
}

四、退款

controller

@Autowired
private Verifier verifier;



/**
     * 申请退款
     *
     * @description:微信申请退款API
     * @author: Chris
     * @date: 2021/12/29 9:14
     * @Param null:
     * @return: null
     */
    @ApiOperation("申请退款")
    @PostMapping("/refunds")
    public Result<?> orderRefund(@RequestBody RefundRecord refundRecord) throws Exception {
        orderInfoService.orderRefund(refundRecord);
        return Result.OK();
    }

    /**
     * 查询退款
     *
     * @description:微信查询退款API
     * @author: Chris
     * @date: 2021/12/29 14:52
     * @Param refundNo:
     * @return: org.jeecg.common.api.vo.Result<?>
     */
    @ApiOperation("查询退款")
    @GetMapping("/queryRefund")
    public Result<?> queryRefund(@RequestParam("refundNo") String refundNo) throws Exception {
        String result = orderInfoService.queryRefund(refundNo);
        return Result.OK("查询成功", result);
    }

    /**
     * 退款结果通知
     *
     * @description:退款状态改变后,微信会把相关退款结果发送给商户。
     * @author: Chris
     * @date: 2021/12/29 16:00
     * @Param request:
     * @Param response:
     * @return: java.lang.String
     */
    @PostMapping("/refunds/notify")
    public String refundsNotify(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");

//			签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            //处理退款单
            orderInfoService.processRefund(bodyMap);

            //成功应答
            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);
        }
    }

serviceimpl

/**
     * 微信申请退款接口
     *
     * @description:微信申请退款API
     * @author: Chris
     * @date: 2021/12/29 10:04
     * @Param orderRefundBO:
     * @return: int
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void orderRefund(RefundRecord refundRecord) throws Exception {
        // 根据单号查询订单状态
        OrderInfo orderInfo = orderInfoMapper.selectOne(new QueryWrapper<OrderInfo>().lambda()
                .eq(OrderInfo::getDelFlag, "1")
                .eq(OrderInfo::getOrderCode, refundRecord.getOutTradeNo()));
        if ("3".equals(orderInfo.getOrderState()) && "1".equals(orderInfo.getRefundState())) {
            // 根据单号查询记录是否已存在
            RefundRecord refundRecord1 = refundRecordService.getOne(new QueryWrapper<RefundRecord>().lambda()
                    .eq(RefundRecord::getOutTradeNo, refundRecord.getOutTradeNo()));


            if (refundRecord1 != null) {// 根据单号查询记录是否已存在,返回
                return;
            }
            RefundRecord Record = new RefundRecord();
            BeanUtils.copyProperties(refundRecord, Record);
            BigDecimal refund = new BigDecimal(Record.getRefund());
            BigDecimal divide = refund.divide(BigDecimal.valueOf(100));
            Record.setTotal(divide.toString());
            Record.setRefund(divide.toString());
            refundRecordService.save(Record);
        } else {
            return;
        }

//        1.获取返回参数存入退款表
//        refundRecord.setId(IdWorker.getIdStr());

//        2.调用微信申请退款API
        String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
        HttpPost httpPost = new HttpPost(url);
//        请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
//        订单编号
        paramsMap.put("out_trade_no", refundRecord.getOutTradeNo());
//        退款单编号
        paramsMap.put("out_refund_no", refundRecord.getOutTradeNo());
        // 通知地址
        paramsMap.put("notify_url", "http://hyzj.rzocean.com/hyzj/orderinfo/orderInfo/refunds/notify");

        Map amountMap = new HashMap();
//        退款金额
        amountMap.put("refund", new BigDecimal(refundRecord.getRefund()));
//        原订单金额
        amountMap.put("total", new BigDecimal(refundRecord.getRefund()));
//        退款币种
        amountMap.put("currency", "CNY");

        paramsMap.put("amount", amountMap);

//        参数转换为json
        String jsonParams = gson.toJson(paramsMap);
        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 && statusCode != 204) {
                throw new RuntimeException("退款异常,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);
            }
//        3.修改退款记录信息
            refundRecordService.updateRefund(bodyAsString);
            orderInfoMapper.updateRefundStateByOrderCode(refundRecord.getOutTradeNo(), "2");//2退款申请通过
        } finally {
            response.close();
        }
    }

    /**
     * @description:微信查询退款API
     * @author: Chris
     * @date: 2021/12/29 14:53
     * @Param refundNo:
     * @return: java.lang.String
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {
        String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/" + refundNo;
        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 && statusCode != 204) {
                throw new RuntimeException("查询退款异常,响应码 = " + statusCode + ",查询退款返回结果 = " + bodyAsString);
            }
            JSONObject jsonObject = JSONObject.parseObject(bodyAsString);
            // 查询退款信息
            List<RefundRecord> refundRecordList = refundRecordService.list(new QueryWrapper<RefundRecord>().lambda().eq(RefundRecord::getOutTradeNo, refundNo));
            refundRecordList.forEach(refundRecord -> {
                if (!refundRecord.getStatus().equals(jsonObject.get("status"))) {
                    refundRecord.setStatus((String) jsonObject.get("status"));
                    refundRecord.setSuccessTime(new Date());
                    refundRecordService.updateById(refundRecord);
                }
            });
            return bodyAsString;
        } finally {
            response.close();
        }
    }


    /**
     * @description:对称解密
     * @author: Chris
     * @date: 2021/12/29 21:17
     * @Param bodyMap:
     * @return: java.lang.String
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
//        通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//        数据密文
        String ciphertext = resourceMap.get("ciphertext");
//        随机串
        String nonce = resourceMap.get("nonce");
//        附加数据
        String associatedData = resourceMap.get("associated_data");

        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);
        return plainText;
    }

    /**
     * @description:处理退款
     * @author: Chris
     * @date: 2021/12/29 21:11
     * @Param bodyMap:
     * @return: void
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {
//        解密报文
        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 = refundRecordService.getOrderStatus(orderNo);
        if (!orderStatus.equals("PROCESSING")) {
            return;
        }
//        修改退款记录状态
        refundRecordService.updateRefund(plainText);
//            } finally {
//                主动释放锁
//                lock.unlock();
//            }
//        }

    }

五、相关工具类

AesUtil
package org.jeecg.modules.orderinfo.xcxutil;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AesUtil {

    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
            throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}
PayUtil
package org.jeecg.modules.orderinfo.xcxutil;

import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayConstants.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;


public class PayUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            PayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }


    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判断签名是否正确
     *
     * @param xmlStr XML格式数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, SignType.MD5);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, SignType.HMACSHA256);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(WXPayConstants.FIELD_SIGN)) {
                continue;
            }
            if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
        }
        sb.append("key=").append(key);
        if (SignType.MD5.equals(signType)) {
            return MD5(sb.toString()).toUpperCase();
        }
        else if (SignType.HMACSHA256.equals(signType)) {
            return HMACSHA256(sb.toString(), key);
        }
        else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }


    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }


    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 HMACSHA256
     * @param data 待处理数据
     * @param key 密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 日志
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

    /**
     * 获取当前时间戳,单位毫秒
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }

}
QRCodeUtils
package org.jeecg.modules.orderinfo.xcxutil;


import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import static com.google.zxing.client.j2se.MatrixToImageWriter.toBufferedImage;

public class QRCodeUtils {

    private static final String CHARSET = "utf-8";


    public static String creatRrCode(String contents, int width, int height) {
        String binary = null;
        Hashtable hints = new Hashtable();
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        try {
            BitMatrix bitMatrix = new MultiFormatWriter().encode(
                    contents, BarcodeFormat.QR_CODE, width, height, hints);
            // 1、读取文件转换为字节数组
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            BufferedImage image = toBufferedImage(bitMatrix);
            //转换成png格式的IO流
            ImageIO.write(image, "png", out);
            byte[] bytes = out.toByteArray();

            // 2、将字节数组转为二进制
            BASE64Encoder encoder = new BASE64Encoder();
            binary = encoder.encodeBuffer(bytes).trim();
        } catch (WriterException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "data:image/png;base64,"+binary;
    }

    /**
     * image流数据处理
     *
     * @author ianly
     */
    public static BufferedImage toBufferedImage(BitMatrix matrix) {
        int width = matrix.getWidth();
        int height = matrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        return image;
    }

    /**
     * 解析二维码解析,此方法是解析Base64格式二维码图片
     * baseStr:base64字符串,data:image/png;base64开头的
     */
    public static String deEncodeByBase64(String baseStr) {
        String content = null;
        BufferedImage image;
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] b=null;
        try {
            int i = baseStr.indexOf("data:image/png;base64,");
            baseStr = baseStr.substring(i+"data:image/png;base64,".length());//去掉base64图片的data:image/png;base64,部分才能转换为byte[]

            b = decoder.decodeBuffer(baseStr);//baseStr转byte[]
            ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(b);//byte[] 转BufferedImage
            image = ImageIO.read(byteArrayInputStream);
            LuminanceSource source = new BufferedImageLuminanceSource(image);
            Binarizer binarizer = new HybridBinarizer(source);
            BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
            Map<DecodeHintType, Object> hints = new HashMap<DecodeHintType, Object>();
            hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
            Result result = new MultiFormatReader().decode(binaryBitmap, hints);//解码
//            System.out.println("图片中内容:  ");
//            System.out.println("content: " + result.getText());
            content = result.getText();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return content;
    }

    /**
     * 解析二维码,此方法解析一个路径的二维码图片
     * path:图片路径
     */
    public static String deEncodeByPath(String path) {
        String content = null;
        BufferedImage image;
        try {
            image = ImageIO.read(new File(path));
            LuminanceSource source = new BufferedImageLuminanceSource(image);
            Binarizer binarizer = new HybridBinarizer(source);
            BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
            Map<DecodeHintType, Object> hints = new HashMap<DecodeHintType, Object>();
            hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
            Result result = new MultiFormatReader().decode(binaryBitmap, hints);//解码
//            System.out.println("图片中内容:  ");
//            System.out.println("content: " + result.getText());
            content = result.getText();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return content;
    }

/*    public static void main(String[] args) {
        String binary = QRCodeUtils.creatRrCode("https://blog.csdn.net/ianly123", 200,200);
        System.out.println(binary);
    }*/
}
WXPayXmlUtil
package org.jeecg.modules.orderinfo.xcxutil;

import org.w3c.dom.Document;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * 2018/7/3
 */
public final class WXPayXmlUtil {
    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值