pdu短信发送

1、负责信息发送的类

package com.hgsoft.gsm;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import javax.comm.CommPortIdentifier;
import javax.comm.SerialPort;
import javax.comm.SerialPortEvent;
import javax.comm.SerialPortEventListener;
import javax.comm.UnsupportedCommOperationException;
/**
* 单例类
* @author chenzebin
*
*/
public class GSMSender implements SerialPortEventListener {

private static String portName; //串口号
private static GSMSender msgSender;
private String ctrlZ=new String(new char[]{0x1A,0x1D}); //ctrl+z
@SuppressWarnings("rawtypes")
private Enumeration portList;
private CommPortIdentifier portId;
private SerialPort serialPort;
private OutputStream outputStream;
private BufferedReader br;
private static String repMsg=""; //每次发出指令后的终端返回的实时信息
private static String lastCommand = "";
private boolean flag = true;
private static List<String> msgList = new ArrayList<String>(0);//存放已接收的短信内容
private static List<String> indexList=new ArrayList<String>();//存放被listMsg()方法读过的短信的序号
private String firstStr="0011000D91"; //00占位代表SMSC的位置
//11:基本参数(TP-MTI/VFP) 发送,TP-VP用相对格式
//00:消息基准值(TP-MR)
//0D:目标地址数字个数 共13个十进制数(不包括91和‘F’)
//91:目标地址格式(TON/NPI) 用国际格式号码(在前面加‘+’)

private String middleStr="00088F"; //00:协议标识(TP-PID) 是普通GSM类型,点到点方式
//08 用户信息编码方式(TP-DCS) UCS2编码
//00 有效期(TP-VP) 5分钟 相应的有效期00 to 8F (VP+1)*5 分钟
//90 to A7 12小时+(VP-143)*30分钟
//A8 to C4 (VP-166)*1天
//C5 to FF (VP-192)*1 周
//完整的发送例子如:firstr 手机号码编码 middleStr 短信内容
// 0011000D91 685110489083F6 000800064F 60597D0021
static {

String configPath=SendMsgTh.class.getClassLoader().getResource("").getPath();
String pathTemp = configPath.substring(0,configPath.length()-1);
configPath = pathTemp.substring(0,pathTemp.lastIndexOf("/")+1);
Properties pp=System.getProperties();

try {
pp.load(new FileInputStream(new File(configPath+"gsm/config.proerties")));
String temp=null;
if((temp=pp.getProperty("portName"))!=null)
portName=temp;
else
portName="COM1";

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static GSMSender getInstance(){
if(msgSender==null)msgSender=new GSMSender(portName);
return msgSender;
}

private GSMSender(String portName){
init(portName);
}

public void init(String portName) {
portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
portId = (CommPortIdentifier) portList.nextElement();
if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {

if (portId.getName().equals(portName)) {

// if (portId.getName().equals("/dev/term/a")) {
try {
serialPort = (SerialPort) portId.open("SimpleWriteApp",
2000);
serialPort.addEventListener(this);
serialPort.notifyOnDataAvailable(true);

} catch (Exception e) {
e.printStackTrace();
}
try {
outputStream = serialPort.getOutputStream();
InputStream inputStream = serialPort.getInputStream();
br = new BufferedReader(new InputStreamReader(
inputStream));
} catch (IOException e) {
e.printStackTrace();
}
try {
serialPort.setSerialPortParams(9600,
SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
} catch (UnsupportedCommOperationException e) {
e.printStackTrace();
}
}

}
}

}
/**
*
* @param line
* @param isBlock 发送信息后是否阻塞
* @throws InterruptedException
*/
public synchronized void sendMsg(String line,boolean isBlock) throws InterruptedException{

System.out.println("send:"+line);
try {
Thread.sleep(1000);
outputStream.write(line.getBytes());
outputStream.flush();
if(isBlock)
this.wait();
} catch (IOException e) {
e.printStackTrace();
}
}



@SuppressWarnings("unused")
private String readReturnInfo(){
String str=null;
try {
while((str=br.readLine())!=null){
System.out.println("rece: "+str);
if(str.equals("OK")||str.equals("ERROR"))break;
}
} catch (IOException e) {
e.printStackTrace();
}
return str;
}
/**
* 调用此函数发作息
* @param context 发送的内容
* @param tele 改送的电话
* @throws InterruptedException
*/
public void sendMsg(String tele,String context) throws InterruptedException{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//完整的短信编码为:0011000D91 685110489083F6 000800 064F60597D0021
String msg=firstStr+EncodeTele(tele)+middleStr+parseContext(context);
int msgNum=(msg.length()-2)/2;//去掉最前面的两人个补位符
//sendMsg("AT+CMGF=0\r\n");
setLastCommand("AT+CMGS=");
sendMsg("AT+CMGS="+msgNum+"\r\n",false);
sendMsg(msg,false);
sendMsg(ctrlZ,true);//加入此句完成发送,测试时不要加入,用回车换行代替
//sendMsg(msg+"\r\n",false); //测试行,让短信发送失败
}

/**
* 将十进行制转成二位显示的十六进制
* @param i
* @return
*/
private String changeHexStr(int i){
String temp="00"+Integer.toHexString(i);
return temp.substring(temp.length()-2,temp.length());
}
/**
* 对手机号码进行编码
* @param tele
* @return
*/
private String EncodeTele(String tele) {
if (!(tele.substring(0, 2).equals("86"))) {
tele = "86" + tele; // 检查当前接收手机号是否按标准格式书写,不是,就补上“86”
}
char [] chars=tele.toCharArray();
if(chars.length%2!=0){
for(int i=0;i<chars.length-1;i+=2){
if(chars[i]==chars[i+1])continue;
swap(chars,i,i+1);
}
Character c=chars[chars.length-1];
chars[chars.length-1]='F';
return new String(chars).concat(c.toString());
}else {
for(int i=0;i<chars.length;i+=2){
if(chars[i]==chars[i+1])continue;
swap(chars,i,i+1);
}
return new String(chars);
}
}

private String parseContext(String context) {
String temp=EncodeUCS2(context);
String numString=changeHexStr(temp.length()/2);
return numString+temp;
}



private void swap(char chars[],int i,int j){
char temp=chars[i];
chars[i]=chars[j];
chars[j]=temp;

}


/**
* UCS2解码
*
* @param src
* UCS2 源串
* @return 解码后的UTF-16BE字符串
*/
private String DecodeUCS2(String src) {
byte[] bytes = new byte[src.length() / 2];

for (int i = 0; i < src.length(); i += 2) {
bytes[i / 2] = (byte) (Integer
.parseInt(src.substring(i, i + 2), 16));
}
String reValue = "";
try {
reValue = new String(bytes, "UTF-16BE");
} catch (Exception e) {
e.printStackTrace();
}
return reValue;

}

/**
* UCS2编码
*
* @param src
* UTF-16BE编码的源串
* @return 编码后的UCS2串
*/
private String EncodeUCS2(String src) {

byte[] bytes = null;
try {
bytes = src.getBytes("UTF-16BE");
} catch (Exception e) {
e.printStackTrace();
}

StringBuffer reValue = new StringBuffer();
StringBuffer tem = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
tem.delete(0, tem.length());
tem.append(Integer.toHexString(bytes[i] & 0xFF));
if(tem.length()==1){
tem.insert(0, '0');
}
reValue.append(tem);
}
return reValue.toString().toUpperCase();
}

/**
* 7bit解码
*/
private static String decode7bit(String src) {
String result = null;
int[] b;
String temp = null;
byte srcAscii;
byte left = 0;
if (src != null && src.length() % 2 == 0) {
result = "";
b = new int[src.length() / 2];
temp = src + "0";
for (int i = 0, j = 0, k = 0; i < temp.length() - 2; i += 2, j++) {
b[j] = Integer.parseInt(temp.substring(i, i + 2), 16);

k = j % 7;
srcAscii = (byte) (((b[j] << k) & 0x7F) | left);
result += (char) srcAscii;
left = (byte) (b[j] >>> (7 - k));
if (k == 6) {
result += (char) left;
left = 0;
}
if (j == src.length() / 2)
result += (char) left;
}
}
return result;
}

public List<String> listMsg() throws InterruptedException{
sendMsg("AT+CMGF=0\r\n",true);
setLastCommand("AT+CMGL=4");
sendMsg("AT+CMGL=4\r\n",true);
return msgList;
}
/**
* 对短信进行解码
* @param oneMsg
* @return
*/
public String parseMsg(String oneMsg) {

String str=oneMsg.substring(20,22);
//05说明发送电话号码只有5位数,信息内容从50位开始
//默认按13位电话号码截取,信息内容从58位开始
if(str.equals("05")){
if("00".equals(oneMsg.substring(32,34)))
return DecodeTime(oneMsg.substring(34,46))+","+DecodeTele(oneMsg.substring(24, 30))+decode7bit(oneMsg.substring(50,oneMsg.length()));
else
return DecodeTime(oneMsg.substring(34,46))+","+DecodeTele(oneMsg.substring(24, 30))+DecodeUCS2(oneMsg.substring(50,oneMsg.length()));
}
else{
if("00".equals(oneMsg.substring(40,42)))
return DecodeTime(oneMsg.substring(42,54))+","+DecodeTele(oneMsg.substring(24,38))+decode7bit(oneMsg.substring(58,oneMsg.length()));
else
return DecodeTime(oneMsg.substring(42,54))+","+DecodeTele(oneMsg.substring(24,38))+DecodeUCS2(oneMsg.substring(58,oneMsg.length()));
}

}
/**
* 解析日期
* @param date 含十二位字符的字符串
* @return
*/
public String DecodeTime(String time){
char [] chars=time.toCharArray();
for(int i=0;i<chars.length;i+=2){
if(chars[i]==chars[i+1])continue;
swap(chars,i,i+1);
}
String temp = new String(chars);
String newTime="20"+temp.substring(0,2)+"-"+temp.substring(2,4)+"-"+temp.substring(4,6)+" "+temp.substring(6,8)
+":"+temp.substring(8,10)+":"+temp.substring(10,12);
return newTime;
}
/**
* 对手机号码进行解码
* @param tele
* @return
*/
public String DecodeTele(String tele) {
//找出倒数第二位
char c=tele.charAt(tele.length()-2);
char [] chars=tele.toCharArray();
for(int i=0;i<chars.length;i+=2){
if(chars[i]==chars[i+1])continue;
swap(chars,i,i+1);
}
if(c=='F'){//判断倒数第二位是否是F,如果是将','号放在数组最后一位,不是则加在字符串最后
chars[chars.length-1]=',';
return new String(chars);
}
else return new String(chars)+",";
}
/**
* 删除被listMsg()方法读过的短信
* @throws InterruptedException
*/
public void deleteMsg() throws InterruptedException{
for(String index:indexList){
sendMsg("AT+CMGD="+index+"\r\n",true);
}
indexList.clear();//清空列表数据
}

public String getRepMsg(){
return repMsg;
}

public List<String> getMsgList(){
return msgList;
}
@SuppressWarnings("static-access")
@Override
/**
* 串口监听函数
*/
public synchronized void serialEvent(SerialPortEvent ev) {
System.out.println("exec event");
flag = true;
while (flag) {
if (ev.getEventType() == ev.DATA_AVAILABLE){
try {
if (lastCommand.equals("AT+CMGL=4")) {//AT+CMGL=4 列出所有短信
readrepMsg();
// 以0891开头说明是信息
if (repMsg.startsWith("0891")) {//089168310820000...
msgList.add(parseMsg(repMsg).trim());
}else if(repMsg.startsWith("+CMGL")){//+CMGL: 2,1,,26 2为短信序号
indexList.add(repMsg.substring(7,8));
} else
outEvent();
}
else {
readrepMsg();
outEvent();
}
} catch (Exception e) {
e.printStackTrace();
}

}
}
this.notify();
}

private void readrepMsg(){
String s;
try {
s = br.readLine();
repMsg = s == null ? "" : s;
System.out.println("response is:" + repMsg);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

private void outEvent(){
if(repMsg.equals("OK")||repMsg.equals("ERROR")){
setLastCommand("");
flag =false;
}
}


private void setLastCommand(String command){
lastCommand = command;
}

public void destroy(){
try {
if(outputStream!=null)
outputStream.close();
if(br!=null)
br.close();
if(serialPort!=null)
serialPort.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

public static void main(String[] args) {

//char bufs[] = { 0x1A, 0x1D };
//String s2 = new String(bufs);

//MSGSender cd = new MSGSender("COM1");

//GSMSender cd=GSMSender.getInstance();
// text发送方式
// cd.sendMsg("AT+CMGF=1\r\n");
// cd.sendMsg("AT+CMGS=15018409386\r\n");
// cd.sendMsg("hello,World!11");
// //cd.sendMsg(s2);//ctrl+z
// cd.readMsg();

// pdu发送方式
// cd.sendMsg("AT+CMGF=0\r\n");
// cd.sendMsg("AT+CMGS=21\r\n");
// cd.sendMsg("0011000D91685110489083F6000800064F60597D0021");
// cd.sendMsg(s2);
// cd.readMsg();
//

// at+cmgl=4,列出所有信息
// cd.sendMsg("AT+CMGF=0\r\n");
// cd.sendMsg("AT+CMGL=4\r\n");
// cd.readMsg();



//测试
// cd.sendMsg("8615018409386","你好!");
// cd.sendMsg("AT+CMGF=0\r\n");
// List<String> list=new ArrayList<String>();
// list.add("8615018409386,你中奖了!");
// list.add("8615018409386,今晚有美食吃!");
// for(int i=0;i<list.size();i++){
// System.out.println("index:"+i);
// String[] strs=list.get(i).split(",");
// cd.sendMsg(strs[0], strs[1]);
// while(true){
// if(cd.repMsg.equals("OK")||cd.repMsg.equals("ERROR"))break;
// else {
// try {
// Thread.sleep(20000);
// break;
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
// }

//
// long s=System.currentTimeMillis();
// List list=cd.listMsg();
// System.out.println(list.size());
// System.out.println(System.currentTimeMillis()-s);

//测试短信删除
// cd.deleteAllMsg();
}


}



2、短信发送线程

package com.hgsoft.gsm;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 此线程负责发送短信
* @author chenzebin
*
*/
public class SendMsgTh extends Thread{
private GSMSender msgSender;
private static File toSendFile; //准备发送信息的文件
private static File sendingFile ;//正在发送信息的文件
private static File sendNotSuccessFile ;//储存发送未成功的信息的文件
private static File historyFile ;//储存发送成功的信息的文件
//private Thread receTh = new Thread(new ReceMsgTh(msgSender));//读取短信列表线程
private static PrintWriter historyPw;
private static PrintWriter sendNotSuccessPw;
private static DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private BufferedReader br;
private boolean flag = true;


/**
* @param args
*/
public static void main(String[] args) {
Thread MsgTh=new SendMsgTh(null);
MsgTh.start();
}

public SendMsgTh(GSMSender msgSender){
if(msgSender==null)this.msgSender=GSMSender.getInstance();
else
this.msgSender=msgSender;
init();
}

private void init(){
String configPath=SendMsgTh.class.getClassLoader().getResource("").getPath();
String pathTemp = configPath.substring(0,configPath.length()-1);
configPath = pathTemp.substring(0,pathTemp.lastIndexOf("/")+1);
File configFile=new File(configPath+"gsm/config.proerties");
boolean flag=true;
while(flag){//直到找到config.properties,程序才继续执行
if(configFile.exists()){
try {

toSendFile = new File(configPath + "gsm/ready.txt"); // 准备发送信息的文件
sendingFile = new File(configPath + "gsm/sending.txt");// 正在发送信息的文件
sendNotSuccessFile = new File(configPath + "gsm/fail.txt");// 储存发送未成功的信息的文件
historyFile = new File(configPath + "gsm/success.txt");// 储存发送成功的信息的文件

historyPw=new PrintWriter(new FileWriter(historyFile,true));
sendNotSuccessPw=new PrintWriter(new FileWriter(sendNotSuccessFile,true));
flag=false;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
else {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("找不到配置文件:"+configPath+"config.properties!");
}
}
}

@Override
public void run() {

try {
//receTh.start();//启动读取信息表列线程
//boolean flag = true;
while (flag) {
System.out.println("search ready.txt.......");
Thread.sleep(10000);//线程每间隔10秒重新扫描toSend.txt文件
synchronized(msgSender){//利用静态变量与接收线程进行同步
if(sendingFile.exists()){//判断是否存在sending.txt,是的话重发
//一般是因为短信未发完非法关闭系统会出现此情况)
sendingMsgs();
}

else if (toSendFile.exists()) {//判断toSend.txt是否存在
if(toSendFile.renameTo(sendingFile)){//改名成功,则发送短信
sendingMsgs();
}

}
else//不存在则继续扫描
continue;
}
}
} catch (InterruptedException e) {
//System.out.println("等待发送文件............");
//e.printStackTrace();
System.out.println("信息发送线程中断.....");
flag = false;
}
}

/**
* 将文件中的信息逐行读出发送
* @throws InterruptedException
*/
private void sendingMsgs() throws InterruptedException{
msgSender.sendMsg("AT+CMGF=0\r\n",true);
try{
br=new BufferedReader(new InputStreamReader(new FileInputStream(sendingFile)));
String msg=null;
while((msg=br.readLine())!=null&&!msg.trim().equals("")){
boolean flag=true;
String [] strs=parseLine(msg);
String tele=strs[0];
String context=strs[1];
while(flag){
if((!"".equals(tele))&&(!"".equals(context))){
//发送信息
if(context.length()<=70){//判断是否小于70个字符,是则直接改送
sendMsg(tele,context);
flag=false;
}

else {//否则先发送前面70个字符
sendMsg(tele,context.substring(0,70));
context=context.substring(70,context.length());
}
}
}
}
if(br!=null)
br.close();//关闭sending.tx的流,之后将文件delete
sendingFile.delete();
}catch (Exception e){
e.printStackTrace();
}

}
/**
* 解析从文件中读取的短信内容
* @param msg 短信想关信息 信息格式为:8613888264553,你中奖了,赶快去拿奖吧!
* @return 字符串数组 strs strs[0]:手机号码 , strs[1]:短信内容
*/
private String[] parseLine(String msg) {
String[] strs=new String[]{"",""};
try{
int index=msg.indexOf(",");
strs[0]=msg.substring(0,index);
strs[1]=msg.substring(index+1,msg.length());
}catch (Exception e){
return strs;
}
return strs;
}

/**
* 发送单条短信
* @param tele 电话号码
* @param context 信息内容
* @throws InterruptedException
*/
private void sendMsg(String tele,String context) throws InterruptedException{
Date date=new Date();
msgSender.sendMsg(tele, context);//此方法返回时说明,已经成功获取串口返回信息
//终端返回OK或ERROR才继续发下条信息

if(msgSender.getRepMsg().equals("OK")){//发送成功则写入history.txt
historyPw.println(tele+","+context);
historyPw.flush();
System.out.println(sdf.format(date)+","+tele+","+context+"发送成功!");
}
else if(msgSender.getRepMsg().equals("ERROR")){//发送失败则写入sendNotSuccess.txt
sendNotSuccessPw.println(tele+","+context);
sendNotSuccessPw.flush();
System.out.println(sdf.format(date)+","+tele+","+context+"发送失败!");
}
try {
Thread.sleep(3000);//等3秒发下条短信
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

@Override
public void destroy(){
try{
this.interrupt();
if(historyPw!=null)historyPw.close();
if(sendNotSuccessPw!=null)sendNotSuccessPw.close();
if(br!=null)br.close();
}catch(Exception e){
e.printStackTrace();
}

}

}



3、负责读取信息列表的线程

package com.hgsoft.gsm;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* 此线程类为自动读取信息列表,默认为半时读取一次
* @author chenzebin
*
*/
public class ReceMsgTh extends Thread{
//private static String path;
private GSMSender msgSender;
private static File receMsgFile;
private static File tempFile;
private PrintWriter pw;
private List<String> msgList=new ArrayList<String>();
private static long sleepTime;
private boolean flag=true;
public static void main(String args[]){
Thread th=new ReceMsgTh(null);
th.start();
}
public ReceMsgTh(GSMSender msgSender){
if(msgSender==null)this.msgSender=GSMSender.getInstance();
else
this.msgSender=msgSender;
init();
}

private void init(){
String configPath=SendMsgTh.class.getClassLoader().getResource("").getPath();
String pathTemp = configPath.substring(0,configPath.length()-1);
configPath = pathTemp.substring(0,pathTemp.lastIndexOf("/")+1);
Properties pp=System.getProperties();
try {
pp.load(new FileInputStream(new File(configPath+"gsm/config.proerties")));
String temp=null;
if((temp=pp.getProperty("receThSleepTime"))!=null){
sleepTime=Long.parseLong(temp+"000");
}
else
sleepTime=1800000;
receMsgFile=new File(configPath+"gsm/received.txt");
tempFile=new File(configPath+"gsm/temp.txt");


} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(flag){
try {
synchronized(msgSender){
if(receMsgFile.exists()){
if(receMsgFile.renameTo(tempFile))
receMsg(true);

}
else receMsg(false);
msgSender.deleteMsg();//将读过的短信删除
}

Thread.sleep(sleepTime);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("信息接收线程中断.......");
flag = false;
}
}
}
/**
* 将信息列表写入文件
* @param flag 文件是否追加
* @throws InterruptedException
*/
public void receMsg(boolean flag) throws InterruptedException{
try{
pw=new PrintWriter(new OutputStreamWriter(new FileOutputStream(tempFile,flag)));
msgList=msgSender.listMsg();
for (int i = 0; i < msgList.size(); i++) {
pw.println(msgList.get(i));
pw.flush();
}
msgList.clear();//读完信息要信息列表中的信息清空,防止下次重复出现已读过的信息
pw.close();
tempFile.renameTo(receMsgFile);
System.out.println("列表读取完毕");
}catch(IOException e){
e.printStackTrace();
}
}

@Override
public void destroy(){
this.interrupt();
if(pw!=null)pw.close();

}


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值