微信支付之扫码支付Native支付 模式二

一.使用场景

1.福袋机上的屏幕是一个Android平板,相当于一个Android手机

2.需要给用户生成一张二维码,让用户扫描付款

3.得到用户付款的消息后,转动对应的电机,给用户掉落福袋

二.准备资料

1.研究微信支付文档 https://pay.weixin.qq.com/wiki/doc/api/index.html  根据我的需求,我选择了Native支付

2.模式二比较简单,比较容易实现成功,所已选择了模式二

3.拿着公司提供的一个邮箱在微信公众平台https://mp.weixin.qq.com/cgi-bin/loginpage?t=wxm2-login&lang=zh_CN申请一个账号,选择申请的这个公众号为服务号,把公司的相关信息和一些申请资料提交后,1-3天就申请好了.

4.拿到我们申请的公众号后,我们还要花300元进行微信认证,得1-3天就认证好了(可以得到一个AppID)

5.在微信公平平台的微信支付页面,点击 申请接入,发现需要一个商户号,所以我们 现在去微信商户平台https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F注册一个账户(注意:在电脑自带的IE浏览器上进行操作,因为后边要弄财富通,在谷歌浏览器上识别不到财富通),提交一些公司资料后,过1-3天就申请好了(可以自己设置一个32位的秘钥,可以得到一个商户号)

6.有了AppID,秘钥和商户号就可以进行开发了,即使没有后台,全做到Android都可以实现了

7.看微信支付文档有统一下单接口,查询订单接口,关闭订单接口.

三.开发

优快云橙子紫了博客主 https://blog.youkuaiyun.com/u013164584/article/details/78030481的这片帖子 封装的比较好

1.导入依赖

//网络
implementation 'org.lucee:httpcomponents-httpclient:4.5.6'
//二维码
implementation 'com.google.zxing:core:3.2.1'
implementation 'cn.bingoogolapple:bga-qrcodecore:1.1.7@aar'
implementation 'cn.bingoogolapple:bga-zxing:1.1.7@aar'

2.PayCommonUtil,该工具类中的方法包含:生成签名,判断签名是否正确,生成二维码,xml和map转换,随机字符串,请求下单等等

package com.wjbzg.wxtestdome.util;

import android.content.Context;
import android.content.res.AssetManager;
import android.support.annotation.NonNull;
import android.util.Log;

import org.apache.http.conn.ssl.SSLContexts;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * Created by ${szz} on 2019\6\5 0005
 */
public class PayCommonUtil {
    //判断签名是否正确
    public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {
        StringBuffer sb = new StringBuffer();
        Set es = packageParams.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            String v = (String) entry.getValue();
            if (!"sign".equals(k) && null != v && !"".equals(v)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + API_KEY);
        //算出摘要
        String mysign = MD5Encode(sb.toString(), characterEncoding).toLowerCase();
        String tenpaySign = ((String) packageParams.get("sign")).toLowerCase();
        return tenpaySign.equals(mysign);
    }

    /**
     * 生成签名
     * @param characterEncoding 字符编码
     * @param parameters
     * @return
     */
    public static String createSign(String characterEncoding, SortedMap<String, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v)
                    && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + Constent.VALUE_API_KEY);
        String sign = MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    public static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            resultString = byteArrayToHexString(md.digest(resultString
                    .getBytes("UTF-8")));
        } catch (Exception exception) {
        }
        return resultString;
    }

    //随机字符串生成
    public static String getRandomString(int length) { //length表示生成字符串的长度
        String base = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    //请求xml组装
    public static String getRequestXml(SortedMap<String, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        Set es = parameters.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            sb.append("<" + key + ">" + value + "</" + key + ">");
        }
        sb.append("</xml>");
        return sb.toString();
    }

    //请求方法
    public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");

            // 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
//            System.out.println("连接超时");
            Log.e("Tag","连接超时");
            ce.printStackTrace();
        } catch (Exception e) {
//            System.out.println("https请求异常");
            Log.e("Tag","https请求异常"+e);
            e.printStackTrace();
        }
        return null;
    }

//    public static String httpsRequest(Context context, String data) throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyManagementException, CertificateException, NoSuchProviderException {
//        String result = null;
//        // 证书密码(默认为商户ID)
//        String password = Constent.VALUE_MCH_ID;
//        // 实例化密钥库
//        KeyStore ks = KeyStore.getInstance("PKCS12");
//        // 获得密钥库文件流
//        AssetManager am = context.getResources().getAssets();
//        InputStream fis = am.open("apiclient_cert.p12");
//        // 加载密钥库
//        ks.load(fis, password.toCharArray());
//        // 关闭密钥库文件流
//        fis.close();
//        // 实例化密钥库
//        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
//        // 初始化密钥工厂
//        kmf.init(ks, password.toCharArray());
//        // 创建SSLContext
//        SSLContext sslContext = SSLContexts.custom()
//                .loadKeyMaterial(ks, Constent.VALUE_MCH_ID.toCharArray())       //加载证书密码,默认为商户ID
//                .build();
//        sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
//        // 获取SSLSocketFactory对象
//        SSLSocketFactory ssf = sslContext.getSocketFactory();
//        URL url = new URL(Constent.URL_TUIKUAN);
//        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
//        conn.setRequestMethod("POST");
//        //设置当前实例使用的SSLSocketFactory
//        conn.setSSLSocketFactory(ssf);
//        conn.setDoOutput(true);
//        conn.setDoInput(true);
//        conn.connect();
//        DataOutputStream out = new DataOutputStream(
//                conn.getOutputStream());
//        if (data != null)
//            out.writeBytes(data);
//        out.flush();
//        out.close();
//        //获取输入流
//        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
//        int code = conn.getResponseCode();
//        if (HttpsURLConnection.HTTP_OK == code) {
//            String temp = in.readLine();
//            while (temp != null) {
//                if (result != null)
//                    result += temp;
//                else
//                    result = temp;
//                temp = in.readLine();
//            }
//        }
//        return result;
//    }

    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.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 e) {
                e.printStackTrace();
            }
            return data;
        } catch (Exception ex) {
            ex.printStackTrace();
            throw ex;
        }

    }

    @NonNull
    public static SortedMap<Object, Object> getSortedMap(Map<String, String> map) {
        //过滤空 设置 TreeMap
        SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
        Iterator it = map.keySet().iterator();
        while (it.hasNext()) {
            String parameter = (String) it.next();
            String parameterValue = map.get(parameter);

            String v = "";
            if (null != parameterValue) {
                v = parameterValue.trim();
            }
            packageParams.put(parameter, v);
        }
        return packageParams;
    }
//---------------------
//    作者:橙子紫了
//    来源:优快云
//    原文:https://blog.youkuaiyun.com/u013164584/article/details/78030481
//    版权声明:本文为博主原创文章,转载请附上博文链接!
}

3.MainActivty

package com.wjbzg.wxtestdome;

import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;

import com.wjbzg.wxtestdome.util.Constent;
import com.wjbzg.wxtestdome.util.PayCommonUtil;

import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.json.JSONObject;

import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import cn.bingoogolapple.qrcode.core.BGAQRCodeUtil;
import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder;

public class MainActivity extends AppCompatActivity {
    //Native支付
    //统一下单:https://api.mch.weixin.qq.com/pay/unifiedorder
//    公众账号ID appid  
//    商户号 mch_id   
//    随机字符串  nonce_str 随机字符串,长度要求在32位以内。推荐随机数生成算法    秘钥 
//    签名 sign
//    商品描述	body
//    商户订单号	out_trade_no
//    标价金额	total_fee
//    终端IP	spbill_create_ip
//    通知地址	notify_url
//    交易类型	trade_type  NATIVE -Native支付
    ImageView img_ewm;
    String time;
    SortedMap<Object, Object> packageParams;
    Handler handler = new Handler();
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                updateUI(packageParams);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    Handler handler2 = new Handler();

    Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
            new Thread(new Runnable() {//每次都要开一个线程去查询订单情况,直到有用户支付成功的结果。
                @Override
                public void run() {
                    checkOrder();
                }
            }).start();
            handler2.postDelayed(runnable2, 5000);
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        createQRCode();//生成二维码

    }

    private void initView() {
        img_ewm = (ImageView) findViewById(R.id.img_ewm);
    }

    /**
     * 生成二维码
     */
    private void createQRCode() {
        new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... params) {
//                System.out.println("ds>>>  生成二维码成功");
                return QRCodeEncoder.syncEncodeQRCode(unifiedOrder(), BGAQRCodeUtil.dp2px(MainActivity.this, 150));
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                if (bitmap != null) {
                    Log.e("Tag","生成二维码成功");
                    img_ewm.setImageBitmap(bitmap);
                    handler2.post(runnable2);//在main中调用  查询订单
                } else {
//                    System.out.println("ds>>>  生成二维码失败");
                    Log.e("Tag","生成二维码失败");
                }
            }
        }.execute();
    }

    //1 统一下单
    public String unifiedOrder() {
        Log.e("Tag","****准备下单数据****");
        SortedMap<String, Object> parameterMap = new TreeMap<String, Object>();
        parameterMap.put(Constent.APPID, Constent.VALUE_APPID);//公众号ID
        parameterMap.put(Constent.MCH_ID, Constent.VALUE_MCH_ID);//商户号
        parameterMap.put(Constent.NONCE_STR, PayCommonUtil.getRandomString(32));//随机字符串
        parameterMap.put(Constent.BODY, "一瓶可乐");//商品描述
        parameterMap.put(Constent.SIGN_TYPE, Constent.MD);//签名类型
//        parameterMap.put(Constent.DETAIL, "");//商品详情
//        parameterMap.put(Constent.ATTACH, "欧亚国际分店");//附加数据
        time = System.currentTimeMillis() + "";
        parameterMap.put(Constent.OUT_TRADE_NO, time);//商户订单号
//        parameterMap.put(Constent.FEE_TYPE, Constent.CNY);//标价币种
        parameterMap.put(Constent.TOTAL_FEE, "1");//标价金额
        parameterMap.put(Constent.SPBILL_CREATE_IP, "127.0.0.1");//终端IP
        parameterMap.put(Constent.TIME_START, System.currentTimeMillis() + "");//交易起始时间
        //异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的,不能携带参数。
        parameterMap.put(Constent.NOTIFY_URL, "http://www.wjbzg.cn/");//通知地址(支付结果通知)
        parameterMap.put(Constent.TRADE_TYPE, Constent.NATIVE);//交易类型
//        parameterMap.put(Constent.PRODUCT_ID, "");//商品ID(和设备ID一起需知,扫码支付时必须传)
//        parameterMap.put(Constent.LIMIT_PAY, Constent.NO_CREDIT);//指定支付方式
        parameterMap.put(Constent.SIGN, PayCommonUtil.createSign(Constent.UTF, parameterMap));//签名
        final String requestXML = PayCommonUtil.getRequestXml(parameterMap);//将请求组装成xml形式
        Log.e("Tag","----下单的xml-----"+requestXML);
        String result = PayCommonUtil.httpsRequest(
                Constent.URL_TONGYI_XIADAN, Constent.POST,
                requestXML);//调用统一支付接口返回String类型字符串
        Log.e("Tag","----下单后微信后台返回的结果xml-----"+result);
        Map<String, String> map = null;
        try {
            map = PayCommonUtil.xmlToMap(result);//将返回的结果转为map形式
        } catch (Exception e) {
            Log.e("Tag","---------下单失败----------"+e);
            e.printStackTrace();
        }
        String string = map.toString();
        Log.e("Tag","---------返回的结果xml转成Map.toString----------"+string);
        String result_code = map.get("result_code");
        Log.e("Tag","****下单结果****"+result_code);
        if (result_code.equals("SUCCESS")){
            //下单成功
            String code_url = map.get("code_url");
            Log.e("Tag","二维码的url为:"+ code_url);
            return code_url;
        }else {
            return null;
        }
    }


    // 2 查询订单  每5秒查询一次订单
    public void checkOrder() {
        SortedMap<String, Object> parameterMap = new TreeMap<String, Object>();
        parameterMap.put(Constent.APPID, Constent.VALUE_APPID);//公众号ID
        parameterMap.put(Constent.MCH_ID, Constent.VALUE_MCH_ID);//商户号
        parameterMap.put(Constent.OUT_TRADE_NO, time);//商户订单号
        parameterMap.put(Constent.NONCE_STR, PayCommonUtil.getRandomString(32));//随机字符串
        parameterMap.put(Constent.SIGN, PayCommonUtil.createSign(Constent.UTF, parameterMap));//签名
        String requestXML = PayCommonUtil.getRequestXml(parameterMap);//将请求组装成xml形式
        Log.e("Tag","调用查询订单准备的xml:"+requestXML);
        String result = PayCommonUtil.httpsRequest(
                Constent.URL_CHAXUN_DINGDAN, Constent.POST,
                requestXML);//调用查询订单接口返回String类型字符串
        Log.e("Tag","调用查询订单接口返回String类型字符串:"+result);
        Map<String, String> map = null;
        try {
            if (result != null)
                map = PayCommonUtil.xmlToMap(result);//将返回的结果转为map形式
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map != null) {
            packageParams = PayCommonUtil.getSortedMap(map);
        }
        if (packageParams != null && PayCommonUtil.isTenpaySign(Constent.UTF, packageParams, Constent.VALUE_API_KEY)) {
            handler.post(runnable);//在新的线程去匹配返回的信息,做相应的UI变化
        } else {
            System.out.println("通知签名验证失败");
        }
    }


    private void updateUI(SortedMap<Object, Object> packageParams) throws InterruptedException {
        String result_code = (String) packageParams.get(Constent.RESULT_CODE);
        String return_code = (String) packageParams.get(Constent.RETURN_CODE);
        String trade_state = (String) packageParams.get(Constent.TRADE_STATE);
        String trade_state_desc = (String) packageParams.get(Constent.TRADE_STATE_DESC);
        String error_code = (String) packageParams.get(Constent.ERROR_CODE);
        Log.e("Tag","result_code=" + result_code + ", return_code=" + return_code + ", trade_state=" + trade_state);
        Log.e("Tag","trade_state_desc=" + trade_state_desc + ", error_code=" + error_code);
        if (result_code.equals(Constent.SUCCESS) && return_code.equals(Constent.SUCCESS) && trade_state.equals(Constent.SUCCESS)) {
            handler2.removeCallbacks(runnable2);
            //支付成功,做相关逻辑。
        } else if (Constent.PAYERROR.equals(packageParams.get(Constent.TRADE_STATE))) {
            handler2.removeCallbacks(runnable2);
            //支付失败,做相关逻辑。
        }
    }
}

3.Constent

package com.wjbzg.wxtestdome.util;

/**
 * Created by ${szz} on 2019\6\5 0005
 */
public class Constent {
    //秘钥
    public static final String VALUE_API_KEY = "";
    //商户ID  kv
    public static final String MCH_ID = "mch_id";
    public static final String VALUE_MCH_ID = "";
    //退款url
//    public static final String URL_TUIKUAN = ;
    //APPID kv
    public static final String APPID = "appid";
    public static final String VALUE_APPID = "";

    //随机字符串  k
    public static final String NONCE_STR = "nonce_str";
    //商品描述  k
    public static final String BODY = "body";
    //签名类型  kv
    public static final String SIGN_TYPE = "sign_type";
    public static final String MD = "MD5";
    //商品详情
//    public static final String DETAIL = ;
    //附加数据
    public static final String ATTACH = "ATTACH";
    //商户订单号   k
    public static final String OUT_TRADE_NO = "out_trade_no";
    //标价金额    k
    public static final String TOTAL_FEE = "total_fee";
    //终端IP    k
    public static final String SPBILL_CREATE_IP = "spbill_create_ip";
    //交易起始时间  k
    public static final String TIME_START = "time_start";
    //异步接收微信支付结果通知的回调地址 K
    public static final String NOTIFY_URL = "notify_url";
    //交易类型
    public static final String TRADE_TYPE = "trade_type";
    public static final String NATIVE = "NATIVE";

    public static final String SIGN = "sign";
    public static final String UTF = "UTF-8";
    public static final String URL_TONGYI_XIADAN = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    public static final String POST = "POST";
    public static final String CODE_URL = "CODE_URL";

    //2.查询订单接口
    public static final String URL_CHAXUN_DINGDAN = "https://api.mch.weixin.qq.com/pay/orderquery";
    public static final String RESULT_CODE = "return_code";
    public static final String RETURN_CODE = "result_code";
    public static final String TRADE_STATE = "trade_state";
    public static final String TRADE_STATE_DESC = "trade_state_desc";
    public static final String ERROR_CODE = "err_code";
    public static final String SUCCESS = "SUCCESS";

    public static final String PAYERROR = "PAYERROR";
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值