生产消费者线程池的可视化实现V1

目标:

实现一个可视化界面,能够监控以及调整生产者,消费者线程池的参数系统

1.生产者:

        -用户输入参数:线程:任务数量,每个任务生产的时间

        -可视化数据:已经生产的任务总量,任务生产消耗的时间

2.消费者:(初步设为三个步骤)

        -线程池A/B/C:

        -用户输入参数:核心线程数量,最大线程数,非核心线程的存活时间,阻塞队列(工作队列)容量,每个任务消耗的时间。

        -可视化数据:每个线程池已经完成的任务量,工作队列的剩余任务数量,当前激活线程数量。

-总体可视化数据:系统的运行时间,已完成的任务总量,预计完成时间

-主要功能按钮:

        -启动系统,停止系统,生产者停止

1.界面UI

利用JFrame实现界面的开发,在需要显示的“结果”的地方利用标签实现后续的显示。

package Monitor.V1;
import javax.swing.*;
import java.awt.*;

public class MonitorUI extends JFrame implements DATE {
    public MonitorUI() {
        setTitle("生产消费监管");
        setSize(1200, 500);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(null);
        MyActionListener mal = new MyActionListener();
        JLabel producer = new JLabel("生产者:");
        producer.setBounds(25, 15, 50, 20);
        add(producer);
        for (int i = 0; i < PRODUCER_TEXT.length; i++) {
            JLabel jl = new JLabel(PRODUCER_TEXT[i]);
            jl.setBounds(100 + i * 220, 15, 150, 20);
            add(jl);
        }
        for (int i = 0; i < PRODUCER_FIELD.length; i++) {
            JTextField jtf = new JTextField();
            jtf.setBounds(250 + 225 * i, 15, 50, 20);
            add(jtf);
            PRODUCER_FIELD[i] = jtf;
        }
        for (int i = 0; i < 2; i++) {
            JLabel jl = new JLabel();
            jl.setBounds(250 + 225 + 200 + 200 * i, 15, 150, 20);
            jl.setText("结果");
            OUT_JLABEL[i] = jl;
            add(jl);
        }
        //消费者线程ABC
        JLabel consumer = new JLabel("消费者:");
        consumer.setBounds(25, 15 + 30, 50, 20);
        add(consumer);
        //A线程池:
        JLabel threadPoolA = new JLabel("线程池A:");
        threadPoolA.setBounds(25, 15 + 30 + 25, 50, 20);
        add(threadPoolA);

        for (int i = 0; i < CONSUMER_TEXT.length; i++) {
            JLabel jlA = new JLabel(CONSUMER_TEXT[i]);
            jlA.setBounds(75 + i * 220, 15 + 30 + 25, 160, 20);
            JTextField jtfA = new JTextField();
            jtfA.setBounds(175 + i * 220, 15 + 30 + 25, 80, 20);
            //将输入框添加到数组中
            CONSUMER_FILED[0][i] = jtfA;
            add(jlA);
            add(jtfA);
        }
        for (int i = 0; i < CONSUMER_OUT_TEXT.length; i++) {
            JLabel jlA = new JLabel(CONSUMER_OUT_TEXT[i]);
            jlA.setBounds(75 + i * 220, 15 + 30 + 25 + 30, 160, 20);
            JLabel jlA1 = new JLabel("结果");
            jlA1.setBounds(230 + i * 220, 15 + 30 + 25 + 30, 160, 20);
            THREAD_POOL_JLABEL[0][i] = jlA1;
            add(jlA1);
            add(jlA);
        }
        //B线程池
        JLabel threadPoolB = new JLabel("线程池B:");
        threadPoolB.setBounds(25, 15 + 30 + 25 + 30 + 30, 50, 20);
        add(threadPoolB);
        for (int i = 0; i < CONSUMER_TEXT.length; i++) {
            JLabel jlB = new JLabel(CONSUMER_TEXT[i]);
            jlB.setBounds(75 + i * 220, 15 + 30 + 25 + 30 + 30, 160, 20);
            JTextField jtfB = new JTextField();
            jtfB.setBounds(175 + i * 220, 15 + 30 + 25 + 30 + 30, 80, 20);
            CONSUMER_FILED[1][i] = jtfB;
            add(jlB);
            add(jtfB);
        }
        for (int i = 0; i < CONSUMER_OUT_TEXT.length; i++) {
            JLabel jlA = new JLabel(CONSUMER_OUT_TEXT[i]);
            jlA.setBounds(75 + i * 220, 15 + 30 + 25 + 30 + 30 + 30, 160, 20);
            JLabel jlA1 = new JLabel("结果");
            jlA1.setBounds(230 + i * 220, 15 + 30 + 25 + 30 + 30 + 30, 160, 20);
            THREAD_POOL_JLABEL[1][i] = jlA1;
            add(jlA1);
            add(jlA);
        }
        //线程池C
        JLabel threadPoolC = new JLabel("线程池C:");
        threadPoolC.setBounds(25, 15 + 30 + 25 + 30 + 30 + 30 + 30, 50, 20);
        add(threadPoolC);
        for (int i = 0; i < CONSUMER_TEXT.length; i++) {
            JLabel jlC = new JLabel(CONSUMER_TEXT[i]);
            jlC.setBounds(75 + i * 220, 15 + 30 + 25 + 30 + 30 + 30 + 30, 160, 20);
            JTextField jtfC = new JTextField();
            jtfC.setBounds(175 + i * 220, 15 + 30 + 25 + 30 + 30 + 30 + 30, 80, 20);
            CONSUMER_FILED[2][i] = jtfC;
            add(jlC);
            add(jtfC);
        }
        for (int i = 0; i < CONSUMER_OUT_TEXT.length; i++) {
            JLabel jlA = new JLabel(CONSUMER_OUT_TEXT[i]);
            jlA.setBounds(75 + i * 220, 15 + 30 + 25 + 30 + 30 + 30+30+30, 160, 20);
            JLabel jlA2 = new JLabel("结果");
            jlA2.setBounds(230 + i * 220, 15 + 30 + 25 + 30 + 30 + 30+30+30, 160, 20);
            THREAD_POOL_JLABEL[2][i] = jlA2;
            add(jlA2);
            add(jlA);
        }

        //结果
        for (int i = 0; i < RESULTS_TEXT.length; i++) {
            JLabel jlR = new JLabel(RESULTS_TEXT[i]);
            jlR.setBounds(75 + i * 220, 15 + 30 + 25 + 30 + 30 + 30 + 30 + 30+30+30, 160, 20);
            JLabel out = new JLabel("结果");
            out.setBounds(200 + i * 220, 15 + 30 + 25 + 30 + 30 + 30 + 30 + 30+30+30, 100, 20);
            OUT_JLABEL[2 + i] = out;
            add(jlR);
            add(out);
        }
        //按钮
        for (int i = 0; i < BUTTON_TEXT.length; i++) {
            JButton jb = new JButton(BUTTON_TEXT[i]);
            jb.setBounds(300 + i * 200, 15 + 30 + 25 + 30 + 30 + 30 + 30 + 30 + 30+30+30, 100, 20);
            jb.addActionListener(mal);
            add(jb);
        }
        PRODUCER_FIELD[0].setText("500");
        PRODUCER_FIELD[1].setText("300");

        CONSUMER_FILED[0][0].setText("2");
        CONSUMER_FILED[0][1].setText("5");
        CONSUMER_FILED[0][2].setText("300");
        CONSUMER_FILED[0][3].setText("100");
        CONSUMER_FILED[0][4].setText("1000");

        CONSUMER_FILED[1][0].setText("2");
        CONSUMER_FILED[1][1].setText("10");
        CONSUMER_FILED[1][2].setText("300");
        CONSUMER_FILED[1][3].setText("30");
        CONSUMER_FILED[1][4].setText("1500");

        CONSUMER_FILED[2][0].setText("2");
        CONSUMER_FILED[2][1].setText("15");
        CONSUMER_FILED[2][2].setText("300");
        CONSUMER_FILED[2][3].setText("30");
        CONSUMER_FILED[2][4].setText("2000");
        setVisible(true);

    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
    }

    public static void main(String[] args) {
        new MonitorUI();
    }
}

这里减少测试时的输入,提前设置好输入框的文本。

2.常用的数据以及文本

定义了一个接口,接口里的数据都是常量,不比修改的,可以放在接口里面。并且一个类可以实现多个接口,不影响使用。

package Monitor.V1;

import javax.swing.*;
public interface DATE {
    String[] PRODUCER_TEXT = {"任务数量:", "每个任务的生产时间(ms):", "已经生产的任务数量:", "已生产总时间(ms):"};
    String[] CONSUMER_TEXT = {"核心线程数量:","最大线程数:","非核心存活时间:","工作队列容量:","消耗时间(ms):"};
    String[] CONSUMER_OUT_TEXT = {"已完成任务量:","工作队列剩余任务数量:","当前激活线程数量"};
    String[] RESULTS_TEXT = {"系统运行时间:","已完成任务总量:","预计完成时间(ms):"};
    String[] BUTTON_TEXT = {"启动系统","停止系统","生产者停止"};
    //生产者的输入框
    JTextField[] PRODUCER_FIELD = new JTextField[2];
    //生产者获取的数据
     int[] PRODUCER_IN = new int[2];
    //消费者的输入框
    JTextField[][] CONSUMER_FILED = new JTextField[3][5];
    //消费者获取的数据
    int[][] CONSUMER_IN = new int[3][5];
    //显示标签
    JLabel[] OUT_JLABEL = new JLabel[5];
    //线程池结果标签
    JLabel[][] THREAD_POOL_JLABEL = new JLabel[3][3];

    //记录数据
    int[] NUM = new int[5] ;
    int[][] SUM = new int[3][3];
}

里面有按钮的文本,以及输入框等。在UI中加入新的标签以及输入框时将其放入对应的数组中,方便调用。

3.按钮的监听

创建类实现ActionListener接口的抽象方法。

获取按钮文本进行区分是哪一个按钮。

package Monitor.V1;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyActionListener implements ActionListener, DATE {
 
    @Override
    public void actionPerformed(ActionEvent e) {
        //获取文本
        String ae = e.getActionCommand();
        if (ae.equals(BUTTON_TEXT[0])) {//启动系统
            
        } else if (ae.equals(BUTTON_TEXT[1])) {//停止系统
           
        } else if (ae.equals(BUTTON_TEXT[2])) {//停止生产者
            
        }
    }

3.1启动系统

启动系统第一步就要获取用户所输入的文本。

 //生产者的数据
            for (int i = 0; i < PRODUCER_FIELD.length; i++) {
                PRODUCER_IN[i] = Integer.parseInt(PRODUCER_FIELD[i].getText());
            }
            //消费者的数据
            for (int i = 0; i < CONSUMER_FILED.length; i++) {
                for (int j = 0; j < CONSUMER_FILED[i].length; j++) {
                    CONSUMER_IN[i][j] = Integer.parseInt(CONSUMER_FILED[i][j].getText());
                }
            }
            //启动线程
            startThread();

获取后接着启动系统的线程,我将线程写在一个方法里面进行调用。

注:

这里学到了一个将字符串转化为整数的方法。Integer.patseInt(String s,int redix),s为想转化的字符串,redix代表转化的进制,不写则默认为十进制。

startThread();方法:

现将创建线程池需要的阻塞队列,以及线程池声明在类中。

 ArrayBlockingQueue<Runnable> queue1;
    ArrayBlockingQueue<Runnable> queue2;
    ArrayBlockingQueue<Runnable> queue3;
    ThreadPoolExecutor threadPoolExecutorA;
    ThreadPoolExecutor threadPoolExecutorB;
    ThreadPoolExecutor threadPoolExecutorC;
    static int numA = 0, numB = 0, numC = 0;
    static boolean isProduce = true;
    static boolean isUpdate = true;

    public void closeProduce() {
        isProduce = false;
    }

    public void closeUpdate() {
        isUpdate = false;
    }

使用numA来记录A线程池所完成的个数,numB记录B完成个数,numC记录线程池C完成的个数。isproduce,以及isupdate为控制生产者线程和更新标签线程的退出项。

在获取到用户所输入的文本后接着new相关队列以及线程池

    public void startThread() {
        queue1 = new ArrayBlockingQueue<>(CONSUMER_IN[0][3]);
        queue2 = new ArrayBlockingQueue<>(CONSUMER_IN[1][3]);
        queue3 = new ArrayBlockingQueue<>(CONSUMER_IN[2][3]);
        threadPoolExecutorA = new ThreadPoolExecutor(
                CONSUMER_IN[0][0],
                CONSUMER_IN[0][1],
                CONSUMER_IN[0][2],
                TimeUnit.MICROSECONDS,
                queue1
        );
        threadPoolExecutorB = new ThreadPoolExecutor(
                CONSUMER_IN[1][0],
                CONSUMER_IN[1][1],
                CONSUMER_IN[1][2],
                TimeUnit.MICROSECONDS,
                queue2
        );
        threadPoolExecutorC = new ThreadPoolExecutor(
                CONSUMER_IN[2][0],
                CONSUMER_IN[2][1],
                CONSUMER_IN[2][2],
                TimeUnit.MICROSECONDS,
                queue3
        );
        ProducerRunnable pr = new ProducerRunnable(threadPoolExecutorA, threadPoolExecutorB, threadPoolExecutorC);
        Thread producer = new Thread(pr);
        producer.start();
        TextRunnable tr = new TextRunnable(queue1, queue2, queue3, threadPoolExecutorA, threadPoolExecutorB, threadPoolExecutorC);
        Thread thread = new Thread(tr);
        thread.start();
    }

同时创建好生产者的线程,以及ABC三步的线程

package Monitor.V1;

import java.util.concurrent.ThreadPoolExecutor;

public class ProducerRunnable implements Runnable,DATE{
    ThreadPoolExecutor threadPoolExecutorA;
    ThreadPoolExecutor threadPoolExecutorB;
    ThreadPoolExecutor threadPoolExecutorC;

    public ProducerRunnable(ThreadPoolExecutor threadPoolExecutorA, ThreadPoolExecutor threadPoolExecutorB, ThreadPoolExecutor threadPoolExecutorC) {
        this.threadPoolExecutorA = threadPoolExecutorA;
        this.threadPoolExecutorB = threadPoolExecutorB;
        this.threadPoolExecutorC = threadPoolExecutorC;
    }


    int num = PRODUCER_IN[0];
    int OneTime = PRODUCER_IN[1];
    private int FinishNum ;
    private int FinishTime;
    public synchronized void addFinishTime(){
        FinishTime += OneTime;
    }
    public synchronized void addFinishNum(){
        FinishNum += 1;
    }

    @Override
    public void run() {
        //控制生产
            //根据输入的数据进行设置
            for (int i = 0;i < num;i++){
                try {
                    Thread.sleep(OneTime);
                    addFinishNum();
                    addFinishTime();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(!MyActionListener.isProduce){
                    break;
                }
                //生成任务
                threadPoolExecutorA.execute(new ConsumerRunnableA(threadPoolExecutorB,threadPoolExecutorC));
                //记录数量与时间
                NUM[0] = FinishNum;
                NUM[1] = FinishTime;

            }
          
    }
}
package Monitor.V1;

import java.util.concurrent.ThreadPoolExecutor;

public class ConsumerRunnableA implements Runnable, DATE {
    //记录任务量
    //消耗时间
    int time = CONSUMER_IN[0][4];
    ThreadPoolExecutor threadPoolExecutorB;
    ThreadPoolExecutor threadPoolExecutorC;

    public ConsumerRunnableA(ThreadPoolExecutor threadPoolExecutorB, ThreadPoolExecutor threadPoolExecutorC) {
        this.threadPoolExecutorB = threadPoolExecutorB;
        this.threadPoolExecutorC = threadPoolExecutorC;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //生成任务传到B
        threadPoolExecutorB.execute(new ConsumerRunnableB(threadPoolExecutorC));
        MyActionListener.numA++;
        SUM[0][0] = MyActionListener.numA;
    }
}
package Monitor.V1;

import java.util.concurrent.ThreadPoolExecutor;

public class ConsumerRunnableB implements Runnable,DATE{

    ThreadPoolExecutor threadPoolExecutorC;

    public ConsumerRunnableB(ThreadPoolExecutor threadPoolExecutorC) {
        this.threadPoolExecutorC = threadPoolExecutorC;
    }

    int time = CONSUMER_IN[1][4];
    @Override
    public void run() {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //生成任务传到C
        threadPoolExecutorC.execute(new ConsumerRunnableC());
        MyActionListener.numB++;
        SUM[1][0] = MyActionListener.numB;
    }
}
package Monitor.V1;

public class ConsumerRunnableC implements Runnable,DATE{
    int time = CONSUMER_IN[2][4];
    @Override
    public void run() {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //生成任务传到完成队列中
        MyActionListener.numC++;
        NUM[3] = MyActionListener.numC;
        SUM[2][0] = MyActionListener.numC;
    }
}

这里生产者生产后直接通过线程池的execute()方法提交给线程池新任务。与之前的线程池的建议使用的原理一致,不再赘述。同时需要完成后将对应的任务书加1,并且赋值给DATE中记录数据的数组中相对于的位置。

在这个启动线程的方法中还有一个TextRunnable类线程负责将记录数据的数组写入显示标签中以及计算预计完成时间(大概的计算)。最后设置标签即可。

package Monitor.V1;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;

public class TextRunnable implements Runnable,DATE{
    ArrayBlockingQueue<Runnable> queue1;
    ArrayBlockingQueue<Runnable> queue2;
    ArrayBlockingQueue<Runnable> queue3;
    ThreadPoolExecutor threadPoolExecutorA;
    ThreadPoolExecutor threadPoolExecutorB;
    ThreadPoolExecutor threadPoolExecutorC;


    public TextRunnable(ArrayBlockingQueue<Runnable> queue1, ArrayBlockingQueue<Runnable> queue2, ArrayBlockingQueue<Runnable> queue3, ThreadPoolExecutor threadPoolExecutorA, ThreadPoolExecutor threadPoolExecutorB, ThreadPoolExecutor threadPoolExecutorC) {
        this.queue1 = queue1;
        this.queue2 = queue2;
        this.queue3 = queue3;
        this.threadPoolExecutorA = threadPoolExecutorA;
        this.threadPoolExecutorB = threadPoolExecutorB;
        this.threadPoolExecutorC = threadPoolExecutorC;
    }

    int time = 0;
    int needTime = 0;
    public void addTime(){
        time++;
    }
    @Override
    public void run() {
        while(MyActionListener.isUpdate){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //获取文本
            SUM[0][1] = queue1.size();
            SUM[0][2] = threadPoolExecutorA.getPoolSize();//激活线程数
            SUM[1][1] = queue2.size();
            SUM[1][2] = threadPoolExecutorB.getPoolSize();
            SUM[2][1] = queue3.size();
            SUM[2][2] = threadPoolExecutorC.getPoolSize();
            addTime();
            NUM[2] = time;
            //预计完成时间
            int size1 = (threadPoolExecutorA.getPoolSize()+threadPoolExecutorB.getPoolSize()+threadPoolExecutorC.getPoolSize())/3;
            int size2 = (threadPoolExecutorC.getPoolSize()+threadPoolExecutorB.getPoolSize())/2;
            int size3 = threadPoolExecutorC.getPoolSize();
            if(size1!=0){
                if(MyActionListener.isProduce){
                   // int lowTime = SUM[0][1]*(CONSUMER_IN[0][4]+CONSUMER_IN[1][4]+CONSUMER_IN[2][4])/size+SUM[1][1]*(CONSUMER_IN[1][4]+CONSUMER_IN[2][4])/(threadPoolExecutorC.getPoolSize()+threadPoolExecutorB.getPoolSize())+SUM[2][1]*CONSUMER_IN[2][4]/threadPoolExecutorC.getPoolSize();

                    needTime = ((PRODUCER_IN[0]-NUM[0])*(PRODUCER_IN[1]+CONSUMER_IN[0][4]+CONSUMER_IN[1][4]+CONSUMER_IN[2][4]))/size1;
                }else {
                    int lowTime = SUM[0][1]*(CONSUMER_IN[0][4]+CONSUMER_IN[1][4]+CONSUMER_IN[2][4])/size1+SUM[1][1]*(CONSUMER_IN[1][4]+CONSUMER_IN[2][4])/size2+SUM[2][1]*CONSUMER_IN[2][4]/size3;
                    needTime = lowTime;
                }

            }

            NUM[4] = needTime;
            //设置标签
            for (int i = 0; i < OUT_JLABEL.length; i++) {
                //转为字符串
                OUT_JLABEL[i].setText(String.valueOf(NUM[i]));
            }
            for (int i = 0; i < SUM.length; i++) {
                for (int j = 0; j < SUM[i].length; j++) {
                    THREAD_POOL_JLABEL[i][j].setText(String.valueOf(SUM[i][j]));
                }
            }
        }

    }
}

循环条件为isUpdate 为true,为false结束线程。到此可以实现了系统的启动。

3.2生产者停止

先实现生产者停止。

只需让isProduce为false即可退出生产者的线程,实现该功能。

closeProduce();

3.3停止系统

停止系统需让生产者先停止生产,接着线程池完成各自内容后就可以结束,最后掌管显示标签的线程也停止即可。

线程池的停止可以调用方法shutdown();它可以让线程池停止接受任务,并且完成工作队列任务后停止。因为我们有三个线程池从A到B到C依次执行,如果直接调用各自shutdown()方法会导致部分工作的缺少。当我们A将他所做的完成后应该放在B中,但此时B的工作队列已经停止接受,就会丢失。B到C同理。因此需要将A彻底关闭后再关闭B,最后关闭C实现整个流水线的完成。

public void closeThreadPool() {
        threadPoolExecutorA.shutdown();
        //等待A关闭
        while (threadPoolExecutorA.getPoolSize() != 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("A线程池中运行线程数量" + threadPoolExecutorA.getPoolSize());
        }
        threadPoolExecutorB.shutdown();
        //等待B关闭
        while (threadPoolExecutorB.getPoolSize() != 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("B线程池中运行线程数量" + threadPoolExecutorB.getPoolSize());
        }
        threadPoolExecutorC.shutdown();
        while (threadPoolExecutorC.getPoolSize() != 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("C线程池中运行线程数量" + threadPoolExecutorC.getPoolSize());
        }
    }

这里等A关闭时进入循环,核心线程关闭后再关闭线程池B依次类推。但如果我们之间将此方法放入停止系统的关闭内容中会导致界面一直等待他结束完成后才显示。

因此利用线程,将关闭放入线程中,自己执行。最后可以实现。

System.out.println("正在停止系统");
            //生产者不能生产
            closeProduce();
            //线程池关闭
            Thread th = new Thread(new Runnable() {
                @Override
                public void run() {
                    closeThreadPool();
                    //Text显示线程关闭
                    closeUpdate();
                    System.out.println("系统已关闭");
                }
            });
            th.start();

结果:

按下停止系统后:

按下生产者停止后:

不足:

再次按下启动后不能启动,不够灵活。

下一版预期:按下启动后重新启动,实现任务接口,能够实现传进来一个任务去完成这个任务的流程。

解决如果生产太快导致 无法提交的问题。

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值