JAVA微信支付、微信退款开发。

1. 微信支付开发(JSAPI)


  • 原作者:小流至江河(https://blog.csdn.net/javaYouCome)
  • 原地址:https://blog.csdn.net/javaYouCome/article/details/79473743
  • 修改编写:fazcube(https://github.com/fazcube)
  • 修改时间:2021/1/14

1.1. 获取微信支付四大参数

首先要想支持微信支付,必须拥有两个账号,这两个账号一个不能少:

  • 微信公众已认证的服务号,并且需要开通微信支付功能(必须是企业才有资格申请)。
  • 微信商户平台账号;

此处是账号模板,请参考:

微信公众平台:账户:con*******om 登录密码 ******

公众APPID:wx15*********a8

APPSECEPT : c210***************892d7

微信商户平台:账户:149**********6742 登录密码:******

商户ID:14******42

API密钥:5d5************b35b

1.2. 配置平台

1.3. 开发流程

这里只展示后端代码

微信支付原理

调用官方文档的“统一下单”接口,之后将微信服务器返回的参数经过加工后,
返回到前端,就OK了。需要凑齐“统一下单”接口的所有参数即可。

所有参数解释请参考:官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

以下是所有参数:
微信支付参数

其中我们需要的(必填的)参数有以下11个:

参数备注
appidAPPID (已有)
mch_id商户ID (已有)
nonce_str随机字符串
sign签名
body所支付的名称
out_trade_no自己所提供的订单号,需要唯一
total_fee支付金额
spbill_create_ipIP地址
notify_url回调地址
trade_type支付类型
openid支付人的微信公众号对应的唯一标识

需要获取这11个参数然后调用微信的“统一下单”接口就可以了。

在这之前先从官网把公众号支付的sdk下载下来,如图
下载官方sdk

官网下载链接:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

将下载的zip文件解压,得到\src\main\java\com\github\wxpay\sdk里面的java文件放在自己项目当中(注意改一下文件头的导包)

将文件放好之后,我们就要开始取之前的11个值了,具体如下:

  1. appid APPID (已有)
  2. mch_id 商户ID (已有)
  3. nonce_str 随机字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
  4. sign 签名(最后处理sign签名);
  5. body 所支付的名称
  6. out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“20210114000001”
  7. total_fee 支付金额 单位:分
  8. spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”
  9. notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)调用这个回调api,地址要公网可以访问。
  10. trade_type 支付类型 公众号支付此处给“JSAPI”
  11. openid 支付人的微信公众号对应的唯一标识,每个人的openid在不同的公众号是不一样的

1.3.1. 获取openId

(By fazcube)

在开发微信支付的时候,肯定会有需要一个参数,就是openId,这是一个微信用户在一个小程序(公众号)的唯一标识。

先把需要的工具类准备好

import javax.servlet.http.HttpServletRequest;

/**
 * @Description: 获取ip
 * @Author: By fazcube
 * @Date: 2021/1/13
 */
public class IpUtil {

    public static String getIp(HttpServletRequest request){
        String ipAddress = request.getHeader("x-forwarded-for");
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)){
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)){
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)){
            ipAddress = request.getRemoteAddr();
        }
        if(ipAddress.indexOf(",")!=-1){
            String[] ips = ipAddress.split(",");
            ipAddress = ips[0].trim();
        }
        return ipAddress;
    }
}

发送请求工具

import org.json.JSONException;
import org.json.JSONObject;

import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;

/**
 * @Description: 发送请求类
 * @Author: By fazcube
 * @Date: 2020/11/17
 */
public class ClientUtil {

    private static String readAll(Reader rd) throws IOException {
        StringBuilder sb = new StringBuilder();
        int cp;
        while ((cp = rd.read()) != -1) {
            sb.append((char) cp);
        }
        return sb.toString();
    }

    /**
     * 发送post请求并且获得返回的json数据
     * @param url
     * @param body
     * @return
     * @throws IOException
     * @throws JSONException
     */
    public static JSONObject postRequestFromUrl(String url, String body) throws IOException, JSONException {
        URL realUrl = new URL(url);
        URLConnection conn = realUrl.openConnection();
        conn.setDoOutput(true);
        conn.setDoInput(true);
        PrintWriter out = new PrintWriter(conn.getOutputStream());
        out.print(body);
        out.flush();
        InputStream instream = conn.getInputStream();
        try {
            BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
            String jsonText = readAll(rd);
            JSONObject json = new JSONObject(jsonText);
            return json;
        } finally {
            instream.close();
        }
    }

    /**
     * 发送get请求并获得返回的json数据
     * @param url
     * @return
     * @throws IOException
     * @throws JSONException
     */
    public static JSONObject getRequestFromUrl(String url) throws IOException, JSONException {
        URL realUrl = new URL(url);
        URLConnection conn = realUrl.openConnection();
        InputStream instream = conn.getInputStream();
        try {
            BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
            String jsonText = readAll(rd);
            JSONObject json = new JSONObject(jsonText);
            return json;
        } finally {
            instream.close();
        }
    }

    /**
     * 发送post请求,且body参数为json字符串
     * @param url
     * @param body
     * @return
     * @throws IOException
     * @throws JSONException
     */
    public static JSONObject postOfJson(String url, String body) throws IOException, JSONException {
        URL realUrl = new URL(url);
        URLConnection conn = realUrl.openConnection();
        conn.setDoOutput(true);
        conn.setDoInput(true);
        conn.setRequestProperty("Content-Type", "application/json");
        PrintWriter out = new PrintWriter(conn.getOutputStream());
        out.print(body);
        out.flush();
        InputStream instream = conn.getInputStream();
        try {
            BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
            String jsonText = readAll(rd);
            JSONObject json = new JSONObject(jsonText);
            return json;
        } finally {
            instream.close();
        }
    }

    /**
     * 向指定URL发送GET方法的请求
     * @param url 发送请求的URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param) {
        String result = "";
        BufferedReader in = null;
        try {
            String urlNameString = url + "?" + param;
            System.out.println(urlNameString);
            URL realUrl = new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String, List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
            for (String key : map.keySet()) {
                System.out.println(key + "--->" + map.get(key));
            }
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url 发送请求的 URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return 所代表远程资源的响应结果
     */
    public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(param);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送 POST 请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }
}

获取openId

小程序(公众号)微信用户同意授权,前端将code传到后端,来换取openId

    @GetMapping(value = "/getOpenId")
    public String getOpenId(@RequestParam(name = "code") String code) throws IOException {
        //获取token
        String accessToken = getAccess_token();
        //如果获取token的时候失败了,会返回空字符串,判断获取token是否成功。
        if(accessToken.equals("")){
            return "获取token失败!";
        }
        //根据code获取微信用户openId
        //传入appid和app_secret
        JSONObject json = JSONObject.parseObject(ClientUtil.getRequestFromUrl(
                "https://api.weixin.qq.com/sns/jscode2session" +
                "?appid=appid" +
                "&secret=appSecret" +
                "&js_code=" + code +
                "&grant_type=authorization_code").toString());
        //这边使用的是alibaba的json依赖包
        /*
        <dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.72</version>
		</dependency>
        */
        if(json.containsKey("errcode")){
            return "获取openid失败!";
        }
        openId = json.getString("openid");
        return openId;
    }

    /**
     * 获取token,这边可以使用redis来设置token的有效时间(自行实现)
     * @return
     * @throws IOException
     */
    public String getAccess_token(){
        String access_token;
        JSONObject token = new JSONObject();
        try{
            //根据微信接口获取token
            token = JSONObject.parseObject(ClientUtil.getRequestFromUrl(
                    "https://api.weixin.qq.com/cgi-bin/token" +
                    "?grant_type=client_credential" +
                    "&appid=appid" +
                    "&secret=appSecret").toString());
            access_token = token.getString("access_token");
        }catch (IOException e){
            //获取失败的时候返回空字符串
            return "";
        }
        System.out.println("获取到的access_token------>" + this.access_token);
        return access_token;
    }

1.3.2. 发送支付请求

接下来写controller层

    @RequestMapping("/wxPayment")
    public Map<String,String> wxPayment(HttpServletRequest request) throws Exception{
        // 获取ip地址,使用的是自定义的获取ip地址方法
        String ip = IpUtil.getIp(request);

        // 统一下单,以下参数都要自己确认一遍
        Map<String,String> data = new HashMap<String,String>();
        data.put("appid", "wx91e********544f2e");//商家平台ID
        data.put("body", "xxx-套餐购买");//标题
        data.put("mch_id","mch_id");//商户ID
        data.put("out_trade_no", "20210114000001");//订单号
        data.put("nonce_str", WXPayUtil.generateNonceStr());
        data.put("total_fee", "1");//金额,单位是分
        data.put("spbill_create_ip", ip);
        data.put("notify_url", "https://www.xxx.com/wxPay/notify");//微信调取回调
        data.put("trade_type", "JSAPI");  // 支付类型
        data.put("openid","o6M8m6wuX*******15d0tocwMQ");//用户标识

        // 将以上10个参数传入,换取sign签名
        String sign = WXPayUtil.generateSignature(data, "api密钥");

        // 将sign签名put进去,凑齐11个参数
        data.put("sign", sign);

        // 将所有参数(map)转xml格式,这里使用的是微信的官方方法
        String xml = WXPayUtil.mapToXml(data);

        // 统一下单接口 https://api.mch.weixin.qq.com/pay/unifiedorder 这个是固定的使用这个接口
        String unifiedOrder_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";

        //输出一下xml文件测试一下参数拼接是否正常
        System.out.println("xml为:" + xml);

        //这边使用的是自定义工具类的发送请求方法
        String xmlStr = ClientUtil.sendPost(unifiedOrder_url,xml);

        System.out.println("xmlStr为:" + xmlStr);

        // **************以下内容是返回前端页面的json数据**************
        // 预支付id
        String prepay_id = "";

        Map<String, String> map = WXPayUtil.xmlToMap(xmlStr);
        //判断是否成功
        if(map.get("return_code").equals("SUCCESS")){
            prepay_id = (String) map.get("prepay_id");
        }else {
            System.out.println(map.get("return_msg").toString());
        }

        //将这个6个参数传给前端,固定的六个参数,前端要接收
        Map<String, String> payMap = new HashMap<String, String>();
        payMap.put("appId", WxData.APP_ID); //appId
        payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");//时间戳
        payMap.put("nonceStr", WXPayUtil.generateNonceStr()); //随机字符串
        payMap.put("signType", "MD5"); //固定写MD5就好
        payMap.put("package", "prepay_id="+prepay_id);//写法就是这样,提交格式如"prepay_id=***"

        //还是通过五个参数获取到paySign
        String paySign = WXPayUtil.generateSignature(payMap, "api密钥");

        //再将paySign这个参数put进去,凑齐六个参数
        payMap.put("paySign", paySign);

        return payMap;
    }

    /**
     * @Title: callBack
     * @Description: 支付完成的回调函数
     * @param:
     * @return:
     */
    @RequestMapping("/notify")
    public String callBack(HttpServletRequest request, HttpServletResponse response) {

        System.out.println("微信支付成功,微信发送的callback信息,可以开始修改订单信息");
        InputStream is = null;
        try {
            is = request.getInputStream();// 获取请求的流信息(这里是微信发的xml格式所以只能使用流来读)
            String xml = WXPayUtil.InputStream2String(is);// 流转换为String
            Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);// 将微信发的xml转map

            //System.out.println("微信返回给回调函数的信息为:"+xml);

            //支付成功进入
            if (notifyMap.get("result_code").equals("SUCCESS")) {

            // 告诉微信服务器收到信息了,不要在调用回调action了========这里很重要回复微信服务器信息用流发送一个xml即可
            response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
                // 商户订单号
                String ordersSn = notifyMap.get("out_trade_no");

                // 实际支付的订单金额:单位 分
                String amountpaid = notifyMap.get("total_fee");

                // 将分转换成元-实际支付金额:元
                BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);

                //下面做业务处理
                System.out.println("===notify===回调方法已经被调!!!");
                //比如修改下单时间、订单状态等等...


            }

            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

再回调的callBack方法中,WXPayUtil.InputStream2String(is)方法可能会报错,原因可能是微信有更好的方法替代,还可以在WXPayUtil类中添加该方法的实现,具体如下:

/**
     * fazcube
     * @param is
     * @return
     * @throws IOException
     */
    public static String InputStream2String(InputStream is) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = -1;
        while ((len = is.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }
        baos.close();
        is.close();

        byte[] lens = baos.toByteArray();
        String result = new String(lens,"UTF-8");//内容乱码处理
        return result;
    }

至此,后端代码结束。

2. 微信退款开发

微信退款开发微信官方地址
微信退款官方

微信退款和微信支付差不多,也是凑齐参数发送给微信。

微信申请退款需要双向证书!(重要)

微信双向证书可以在微信商户频台-》账户设置-》API安全中下载,解压之后为以下文件:
证书文件

JAVA只需要 apiclient_cert.p12 这个证书文件 。

双击apiclient.p12文件,安装一直下一步到输入私钥密码,密码为商户号(mch_id),一直下一步,直至提示导入成功,至此证书安装成功。

以下是后端代码:

    /**
     * 微信申请退款
     * @param jsonObject
     * @return
     * @throws Exception
     */
    @RequestMapping("/refund")
    public Result<?> wxRefund(@RequestBody JSONObject jsonObject) throws Exception {

        Map<String, String> params = new HashMap<>();
        
        //订单总金额,这个根据自身业务来。
        String orderPrice = "69900";
        String refundPrice = "69900";

        String outRefundNo = "你的退款号";//可以自定义一个类来随机生成退款单号等等
        String refundMark = "退款备注";

        params.put("appid", "appid");
        params.put("mch_id", "mch_id");
        //商户订单号
        params.put("out_trade_no", "orderId");
        //商户退款单号
        params.put("out_refund_no", outRefundNo);
        //总金额
        params.put("total_fee", orderPrice);
        //退款金额
        params.put("refund_fee", refundPrice);
        //退款原因
        params.put("refund_desc", refundMark);
        //退款结果回调地址
        /*params.put("notify_url", "");*/
        //随机字符串
        params.put("nonce_str", WXPayUtil.generateNonceStr());//使用微信随机字符串生成
        //生成sign
        String sign = WXPayUtil.generateSignature(params, WxData.API_SECRET);
        params.put("sign", sign);
        //微信申请退款接口
        String wx_refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund";
        
        String xmlStr = WXPayUtil.mapToXml(params);//转换成xml格式
        //发送双向证书请求给微信
        String resultXmlStr = CertUtil.doRefund(wx_refund_url,xmlStr);

        // 将返回的字符串转成Map集合
        Map<String, String> resultMap = WXPayUtil.xmlToMap(resultXmlStr);//转成map格式
        
        Map<String, String> map = new HashMap<>();
        if ("SUCCESS".equalsIgnoreCase(resultMap.get("result_code"))) {
            System.out.println("------申请退款成功,正在退款中----");
            //申请微信退款接口返回success,但是退款到账还需要时间;
            map.put("success", "REFUNDS");
        } else {
            map.put("success", resultMap.get("err_code_des"));
            System.out.println("------退款失败----{}" + resultMap.get("err_code_des"));
        }
        return Result.OK(map);
    }

微信退款的代码和微信支付的代码差不多,主要是退款需要微信双向证书绑定。


import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
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.springframework.core.io.ClassPathResource;

public class CertUtil {

    public static String doRefund(String url, String data) throws Exception {

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // 指定证书路径
        String path = "/cert/apiclient_cert.p12";
        ClassPathResource classPathResource = new ClassPathResource(path);

        //读取本机存放的PKCS12证书文件
        InputStream stream = classPathResource.getInputStream();
        String mchId = "你的商户id";
        try {
            //指定PKCS12的密码(商户ID)
            keyStore.load(stream, mchId.toCharArray());
        } finally {
            stream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
        //指定TLS版本
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext,new String[] { "TLSv1"},null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        //设置httpclient的SSLSocketFactory
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        try {
            HttpPost httpost = new HttpPost(url); // 设置响应头信息
            httpost.addHeader("Connection", "keep-alive");
            httpost.addHeader("Accept", "*/*");
            httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
            httpost.addHeader("Host", "api.mch.weixin.qq.com");
            httpost.addHeader("X-Requested-With", "XMLHttpRequest");
            httpost.addHeader("Cache-Control", "max-age=0");
            httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
            httpost.setEntity(new StringEntity(data, "UTF-8"));
            CloseableHttpResponse response = httpclient.execute(httpost);
        try {
            HttpEntity entity = response.getEntity();
            String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
            EntityUtils.consume(entity);
            return jsonStr;
        } finally {
            response.close();
        }
        } finally {
            httpclient.close();
        }
    }
}

在我的项目中,我将证书文件放置在模块的resources文件夹下面,具体项目结构如下:
结构

用以上的方法可以直接读取。

注意:maven打包转码问题!!!

程序运行时报错:
java.io.IOException: DerInputStream.getLength(): lengthTag=111, too big.
原因:maven打包时,会对文件进行转码,重新编码后会导致证书文件不可用。
解决:pom依赖中进行配置,让maven打包时过滤掉不需要转码的文件:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>p12</nonFilteredFileExtension>
                        <nonFilteredFileExtension>pem</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>

至此,微信退款接口开发完毕,如果项目有需求,还可以配置微信退款回调。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值