使用HttpURLConnection.setChunkedStreamingMode(10*1024);的时候,不同的参数导致文件上传损坏...

bug修改之后的代码:

 

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.lubansoft.test.upload.po.UploadRecord;
import com.lubansoft.test.upload.po.UploadResult;

public class FileUpload {

	/**
	 * 接收上传的参数和文件
	 */
	public UploadResult upload(HttpServletRequest request,HttpServletResponse response){
		UploadResult uploadResult = new UploadResult();
		try {
			InputStream in = new BufferedInputStream(request.getInputStream(),10240);
			String requestHeader = readLine(in);
			String line = null;
			Map<String,String> param = new HashMap<String, String>();
			Map<String,String> fileUploadResults = new HashMap<String, String>();
			uploadResult.setParams(param);
			uploadResult.setFileUploadResults(fileUploadResults);
			/**
			 * 判断是否是文件/参数
			 */
			if("------parameters------".equals(requestHeader)){
				/**
				 * 上传参数的格式:
				 *  第一行: ------parameters------
				 *  第二行:"parameterLength=" + parameterLength
 			     * 	第三行: key1=value1&key2=value2........
				 */
				line = readLine(in);
				int length = 0;
				if(line != null){
					if("parameterLength".equals(line.split("=")[0])){
						length = Integer.parseInt(line.split("=")[1]);
					}
				}
				byte[] b = null;
				if(length != 0){
					b = new byte[length];
					in.read(b, 0, length);
					line = new String(b,0,length,"utf-8");
				}
				if(line != null){
					String[] params = line.split("&");
					if(params!=null && params.length>0){
						for(String keyValue : params){
							param.put(keyValue.split("=")[0], keyValue.split("=")[1]);
						}
					}
				}
				System.out.println(param);
				/**
				 *读取最后的一个换行符:\n
				 */
				readLine(in);
				if("------file begin------".equals(line=readLine(in))){
					startTimerAndUploadFile(request, in, fileUploadResults);
				}
			}else if("------file begin------".equals(requestHeader)){
				startTimerAndUploadFile(request, in, fileUploadResults);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				response.getWriter().write("upload complete !");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return uploadResult;
	}
	
	/**
	 * 启动定时器,开始上传文件
	 */
	public void startTimerAndUploadFile(HttpServletRequest request,InputStream in,Map<String,String> fileUploadResults){
		Timer timer = new Timer();
		UploadRateTimerTask task = new UploadRateTimerTask(request);
		long interval = FileUploadUtil.getTimerInfo(request) != null ? Long.parseLong(FileUploadUtil.getTimerInfo(request)[0]) : 1000;
		timer.schedule(task, 0 , interval);
		saveFile(in, request, task,fileUploadResults);
		timer.cancel();
	}
	
	//保存文件
	public void saveFile(InputStream in ,HttpServletRequest request,UploadRateTimerTask task,Map<String,String> fileUploadResults){
		String line=null;
		RandomAccessFile raf = null;
		UploadRecord uploadRecord = null;
		String fileMD5_client = null;
		String fileMD5_server = null;
		int blockSize = FileUploadUtil.getBlockSize();
		try {
			task.setTotal_uploadSize(0);
			//先读到该文件的信息头“fileName=ddd&fileSize=xxx&uploadSize=aaa”
			line = readLine(in);
			if(line != null){
					uploadRecord = FileUploadUtil.getUploadRecoreFromString(line);
					fileMD5_client = uploadRecord.getFileMD5();
					HttpSession session = request.getSession();
					String folder = session.getServletContext().getRealPath(UploadConfigConstants.UPLOAD_FILE); 
					String filepath = folder+File.separator +  FileUploadUtil.getFileNameByUploadRecord(uploadRecord);
					File file = new File(filepath);
					if(! file.getParentFile().exists()){
						file.getParentFile().mkdirs();
					}
					if(! file.exists()){
						file.createNewFile();
					}
					/**
					 * 先将properties文件放到临时文件夹下(tmep)
					 */
					String config_path = session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT) + File.separator +
												"temp/" + FileUploadUtil.getConfigFileNameByUploadRecord(uploadRecord);
					File config_file = new File(config_path);
					if(! config_file.getParentFile().exists()){
						config_file.getParentFile().mkdirs();
					}
					if (! config_file.exists()) {
						config_file.createNewFile();
					}
					raf = new RandomAccessFile(file, "rwd");
					raf.setLength(uploadRecord.getFileSize());
					//将读取文件的指针移动到断点位置
					raf.seek(uploadRecord.getUploadSize());
					//计算出从断点位置到文件结尾,还剩下的文件大小
					long leftSize = uploadRecord.getFileSize()-uploadRecord.getUploadSize();
					//本次上传,上传完成的文件大小
					long completeSize = 0l;
					/**
					 * 接下来是文件的内容
					 */
					int n=-1;
					byte[] b = new byte[2048];
					int addSize = 0;
					/**
					 * 在上传文件的时候,再去判断一下是否已经有程序在上传该文件
					 * 作为确认,防止万一
					 */
					boolean isStarted = false;
					if(FileUploadUtil.isUploadStarted(request, uploadRecord)){
						isStarted = true;
					}
					/**
					 * 为定时器动态绑定uploadRecord对象
					 */
					task.setUploadRecord(uploadRecord);
					/**
					 * 判断文件从断点开始,还有多大文件需要传输,如果太小(小于 1 k )就要特殊处理
					 * 因为这时候不能使用 in.read(b) ,而只能使用 in.read(b,0,leftSize)
					 */
					if((completeSize + b.length) <= leftSize){
						while((n=in.read(b)) != -1){
							if(! isStarted){
								raf.write(b,0,n);
								completeSize += n;
								addSize += n;
								task.setTotal_uploadSize(completeSize);
								if(addSize >= blockSize){
									FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
									addSize = 0;
								}
							}
							/**
							 * 最后一部分特殊处理
							 */
							if((completeSize + b.length) >= leftSize){
								int num = -1;
								while(true){
									/**
									 * num表示最后一部分还剩下多少,也就是应该读取的大小
									 * n表示真正读取到的部分是多少,也就是实际上读取到的大小
									 * n和num可能并不相同,一直到leftSize == completeSize才说明最后一部分处理完成
									 */
									num = (int)(leftSize-completeSize);
									n = in.read(b, 0, num);
									completeSize += n;
									if(! isStarted){
										raf.write(b,0,n);
										addSize += n;
										task.setTotal_uploadSize(completeSize);
										if(addSize >= blockSize){
											FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
											addSize = 0;
										}
									}
									/**
									 * 如果leftSize == completeSize说明最后一部分处理完成
									 */
									if(leftSize == completeSize){
										raf.close();
										FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
										addSize = 0;
										//完成一个文件的上传,将该文件的完成百分比改为100%
										task.setTaskComplete();
										//完成一个文件的上传,将该文件的isStopped改为stopped
										FileUploadUtil.setConfigFileStoped(request, uploadRecord);
										fileMD5_server = MD5Util.getFileMD5String(file);
										if(fileMD5_server != null && fileMD5_client != null){
											if(fileMD5_server.equals(fileMD5_client)){
												fileUploadResults.put(file.getName(), "OK");
												/**
												 * 文件上传完成并且校验成功,则将其配置文件转移到已上传成功的文件夹
												 */
												FileUploadUtil.MoveFile(config_file, session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT));
											}
										}else{
											/**
											 * 文件上传完成,但是文件已损坏
											 */
											fileUploadResults.put(file.getName(), "DAMAGED");
										}
										readLine(in);
										line = readLine(in);
										if("------file begin------".equals(line)){
											saveFile(in, request,task,fileUploadResults);
										}
										break;
									}
								}
							}
						}
					}else{//判断文件从断点开始,如果剩余文件太小(小于 1 k )就要特殊处理

						int num = -1;
						while(true){
							num = (int)(leftSize-completeSize);
							n = in.read(b, 0, num);
							completeSize += n;
							if(! isStarted){
								raf.write(b,0,n);
								addSize += n;
								task.setTotal_uploadSize(completeSize);
								if(addSize >= blockSize){
									FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
									addSize = 0;
								}
							}
							if(leftSize == completeSize){
								raf.close();
								FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
								addSize = 0;
								//完成一个文件的上传,将该文件的完成百分比改为100%
								task.setTaskComplete();
								//完成一个文件的上传,将该文件的isStopped改为stopped
								FileUploadUtil.setConfigFileStoped(request, uploadRecord);
								fileMD5_server = MD5Util.getFileMD5String(file);
								if(fileMD5_server != null && fileMD5_client != null){
									if(fileMD5_server.equals(fileMD5_client)){
										fileUploadResults.put(file.getName(), "OK");
										/**
										 * 文件上传完成并且校验成功,则将其配置文件转移到已上传成功的文件夹
										 */
										FileUploadUtil.MoveFile(config_file, session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT));
									}
								}else{
									/**
									 * 文件上传完成,但是文件已损坏
									 */
									fileUploadResults.put(file.getName(), "DAMAGED");
								}
								readLine(in);
								line = readLine(in);
								if("------file begin------".equals(line)){
									saveFile(in, request,task,fileUploadResults);
								}
								break;
							}
						}
					
					}
					/**
					 * 文件上传完成,将状态修改为 “stoped”
					 */
					if(uploadRecord != null){
						FileUploadUtil.setConfigFileStoped(request, uploadRecord);
					}
				}
//			}
		} catch (IOException e) {
			/**
			 * 上传过程中出现异常,将状态修改为 “stoped”
			 */
			e.printStackTrace();
		}finally{
			/**
			 * 上传过程中出现异常,将状态修改为 “stoped”
			 */
			if(uploadRecord != null){
				FileUploadUtil.setConfigFileStoped(request, uploadRecord);
			}
			if(raf != null){
				try {
					raf.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				raf = null;
			}
		}
	}
	
	/**
	 * 读取一行内容
	 */
	private String readLine(InputStream in) {
		return FileUploadUtil.readLine(in);
	}
}

 

之前的代码:

 

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.lubansoft.test.upload.po.UploadRecord;
import com.lubansoft.test.upload.po.UploadResult;

public class FileUpload {

	/**
	 * 接收上传的参数和文件
	 */
	public UploadResult upload(HttpServletRequest request,HttpServletResponse response){
		UploadResult uploadResult = new UploadResult();
		try {
			InputStream in = new BufferedInputStream(request.getInputStream(),10240);
			String requestHeader = readLine(in);
			String line = null;
			Map<String,String> param = new HashMap<String, String>();
			Map<String,String> fileUploadResults = new HashMap<String, String>();
			uploadResult.setParams(param);
			uploadResult.setFileUploadResults(fileUploadResults);
			/**
			 * 判断是否是文件/参数
			 */
			if("------parameters------".equals(requestHeader)){
				/**
				 * 上传参数的格式:
				 *  第一行: ------parameters------
				 *  第二行:"parameterLength=" + parameterLength
 			     * 	第三行: key1=value1&key2=value2........
				 */
				line = readLine(in);
				int length = 0;
				if(line != null){
					if("parameterLength".equals(line.split("=")[0])){
						length = Integer.parseInt(line.split("=")[1]);
					}
				}
				byte[] b = null;
				if(length != 0){
					b = new byte[length];
					in.read(b, 0, length);
					line = new String(b,0,length,"utf-8");
				}
				if(line != null){
					String[] params = line.split("&");
					if(params!=null && params.length>0){
						for(String keyValue : params){
							param.put(keyValue.split("=")[0], keyValue.split("=")[1]);
						}
					}
				}
				System.out.println(param);
				/**
				 *读取最后的一个换行符:\n
				 */
				readLine(in);
				if("------file begin------".equals(line=readLine(in))){
					startTimerAndUploadFile(request, in, fileUploadResults);
				}
			}else if("------file begin------".equals(requestHeader)){
				startTimerAndUploadFile(request, in, fileUploadResults);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				response.getWriter().write("upload complete !");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return uploadResult;
	}
	
	/**
	 * 启动定时器,开始上传文件
	 */
	public void startTimerAndUploadFile(HttpServletRequest request,InputStream in,Map<String,String> fileUploadResults){
		Timer timer = new Timer();
		UploadRateTimerTask task = new UploadRateTimerTask(request);
		long interval = FileUploadUtil.getTimerInfo(request) != null ? Long.parseLong(FileUploadUtil.getTimerInfo(request)[0]) : 1000;
		timer.schedule(task, 0 , interval);
		saveFile(in, request, task,fileUploadResults);
		timer.cancel();
	}
	
	//保存文件
	public void saveFile(InputStream in ,HttpServletRequest request,UploadRateTimerTask task,Map<String,String> fileUploadResults){
		String line=null;
//		String fileName=null;
//		String fileSize = null;
//		String uploadSize=null;
		RandomAccessFile raf = null;
		UploadRecord uploadRecord = null;
		String fileMD5_client = null;
		String fileMD5_server = null;
		int blockSize = FileUploadUtil.getBlockSize();
		try {
			task.setTotal_uploadSize(0);
			//先读到该文件的信息头“fileName=ddd&fileSize=xxx&uploadSize=aaa”
			line = readLine(in);
			if(line != null){
				/*String[] info = line.split("&");
				if(info != null && info.length>0){
					for(String s: info){
						if("fileName".equals(s.split("=")[0])){
							fileName = s.split("=")[1];
						}else if("fileSize".equals(s.split("=")[0])){
							fileSize = s.split("=")[1];
						}else if("uploadSize".equals(s.split("=")[0])){
							uploadSize = s.split("=")[1];
						}else if("fileMD5".equals(s.split("=")[0])){
							fileMD5_client = s.split("=")[1];
						}
					}*/
					uploadRecord = FileUploadUtil.getUploadRecoreFromString(line);
					fileMD5_client = uploadRecord.getFileMD5();
					HttpSession session = request.getSession();
					String folder = session.getServletContext().getRealPath(UploadConfigConstants.UPLOAD_FILE); 
					String filepath = folder+File.separator +  FileUploadUtil.getFileNameByUploadRecord(uploadRecord);
					File file = new File(filepath);
					if(! file.getParentFile().exists()){
						file.getParentFile().mkdirs();
					}
					if(! file.exists()){
						file.createNewFile();
					}
					/**
					 * 先将properties文件放到临时文件夹下(tmep)
					 */
					String config_path = session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT) + File.separator +
												"temp/" + FileUploadUtil.getConfigFileNameByUploadRecord(uploadRecord);
					File config_file = new File(config_path);
					if(! config_file.getParentFile().exists()){
						config_file.getParentFile().mkdirs();
					}
					if (! config_file.exists()) {
						config_file.createNewFile();
					}
					raf = new RandomAccessFile(file, "rwd");
//					if(file.length() != Long.parseLong(fileSize)){
//						raf.setLength(Long.parseLong(fileSize));
//					}
					raf.setLength(uploadRecord.getFileSize());
					//将读取文件的指针移动到断点位置
					raf.seek(uploadRecord.getUploadSize());
					//计算出从断点位置到文件结尾,还剩下的文件大小
					long leftSize = uploadRecord.getFileSize()-uploadRecord.getUploadSize();
					//本次上传,上传完成的文件大小
					long completeSize = 0l;
					/**
					 * 接下来是文件的内容
					 */
					int n=-1;
					byte[] b = new byte[2048];
					int total = 0;
					int addSize = 0;
					/*uploadRecord = new UploadRecord();
					uploadRecord.setFileName(uploadRecord.getFileName());
					uploadRecord.setFileSize(uploadRecord.getFileSize());
					uploadRecord.setIp(request.getRemoteAddr());
					uploadRecord.setUploadSize(Long.parseLong(uploadSize));*/
					/**
					 * 在上传文件的时候,再去判断一下是否已经有程序在上传该文件
					 * 作为确认,防止万一
					 */
					boolean isStarted = false;
					if(FileUploadUtil.isUploadStarted(request, uploadRecord)){
						isStarted = true;
					}
					/**
					 * 为定时器动态绑定uploadRecord对象
					 */
					task.setUploadRecord(uploadRecord);
					/**
					 * 判断文件从断点开始,还有多大文件需要传输,如果太小(小于 1 k )就要特殊处理
					 * 因为这时候不能使用 in.read(b) ,而只能使用 in.read(b,0,leftSize)
					 */
					if((completeSize + b.length) <= leftSize){
						while((n=in.read(b)) != -1){
							if(! isStarted){
								raf.write(b,0,n);
								total += n;
								completeSize += n;
								addSize += n;
								task.setTotal_uploadSize(completeSize);
								if(addSize >= blockSize){
									FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
									addSize = 0;
								}
							}
							//判断是否已经到达最后一部分
							if((completeSize + b.length) >= leftSize){
								n = (int)(leftSize-completeSize);
                                                                问题就出在这里,因为客户端使用的是conn.setChunkedStreamingMode(10*1024);也就是块的概念,每次传输给服务器10*1024大小的内容,那么就可能会出现一个文件的最后一部分,被分在了两块里面,也就是分两次上传,这种情况下,in.read(b,0,n)就不能读取到n个字节的内容,而是少于n个,因此,这里需要获取到真正读取到的字节数:realNum = in.read(b, 0, n);然后判断(n-realNum)的大小,如果大于0,说明还没有读取完成,就需要循环,所以正确的方式应该如下:
/**
							 * 最后一部分特殊处理
							 */
							if((completeSize + b.length) >= leftSize){
								int num = -1;
								while(true){
									/**
									 * num表示最后一部分还剩下多少
									 * n表示真正读取到的部分是多少
									 * n和num可能并不相同
									 */
									num = (int)(leftSize-completeSize);
									n = in.read(b, 0, num);
									completeSize += n;
									if(! isStarted){
										raf.write(b,0,n);
										addSize += n;
										task.setTotal_uploadSize(completeSize);
										if(addSize >= blockSize){
											FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
											addSize = 0;
										}
									}
									/**
									 * 如果leftSize == completeSize说明最后一部分处理完成
									 */
									if(leftSize == completeSize){
										raf.close();
										FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
										addSize = 0;
										//完成一个文件的上传,将该文件的完成百分比改为100%
										task.setTaskComplete();
										//完成一个文件的上传,将该文件的isStopped改为stopped
										FileUploadUtil.setConfigFileStoped(request, uploadRecord);
										fileMD5_server = MD5Util.getFileMD5String(file);
										if(fileMD5_server != null && fileMD5_client != null){
											if(fileMD5_server.equals(fileMD5_client)){
												fileUploadResults.put(file.getName(), "OK");
												/**
												 * 文件上传完成并且校验成功,则将其配置文件转移到已上传成功的文件夹
												 */
												FileUploadUtil.MoveFile(config_file, session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT));
											}
										}else{
											/**
											 * 文件上传完成,但是文件已损坏
											 */
											fileUploadResults.put(file.getName(), "DAMAGED");
										}
										readLine(in);
										line = readLine(in);
										if("------file begin------".equals(line)){
											saveFile(in, request,task,fileUploadResults);
										}
										break;
									}
								}
							}

而不是:



								in.read(b, 0, n);
								if(! isStarted){
									raf.write(b,0,n);
									total += n;
									completeSize += n;
									addSize += n;
									raf.close();
									task.setTotal_uploadSize(completeSize);
									FileUploadUtil.updateUploadSize(request, uploadRecord, addSize);
									addSize = 0;
									//完成一个文件的上传,将该文件的完成百分比改为100%
									task.setTaskComplete();
									//完成一个文件的上传,将该文件的isStopped改为stopped
									FileUploadUtil.setConfigFileStoped(request, uploadRecord);
									fileMD5_server = MD5Util.getFileMD5String(file);
									if(fileMD5_server != null && fileMD5_client != null){
										if(fileMD5_server.equals(fileMD5_client)){
											fileUploadResults.put(file.getName(), "OK");
											/**
											 * 文件上传完成并且校验成功,则将其配置文件转移到已上传成功的文件夹
											 */
											FileUploadUtil.MoveFile(config_file, session.getServletContext().getRealPath(UploadConfigConstants.BREAK_POINT));
										}
									}else{
										/**
										 * 文件上传完成,但是文件已损坏
										 */
										fileUploadResults.put(file.getName(), "DAMAGED");
									}
									file = null;
								}
								readLine(in);
								line = readLine(in);
								if("------file begin------".equals(line)){
									saveFile(in, request,task,fileUploadResults);
								}
								break;
							}
						}
						System.out.println(completeSize);
					}else{//判断文件从断点开始,如果剩余文件太小(小于 1 k )就要特殊处理
						in.read(b, 0, (int)leftSize);
						if(!isStarted){
							raf.write(b, 0, (int)leftSize);
						}
						raf.close();
						task.setTotal_uploadSize(leftSize);
						FileUploadUtil.updateUploadSize(request, uploadRecord, leftSize);
						//完成一个文件的上传,将该文件的完成百分比改为100%
						task.setTaskComplete();
						//完成一个文件的上传,将该文件的isStopped改为stopped
						FileUploadUtil.setConfigFileStoped(request, uploadRecord);
						readLine(in);
						line = readLine(in);
						if("--file begin--".equals(line)){
							saveFile(in, request,task,fileUploadResults);
							fileMD5_server = MD5Util.getFileMD5String(file);
							if(fileMD5_server != null && fileMD5_client != null){
								if(fileMD5_server.equals(fileMD5_client)){
									fileUploadResults.put(file.getName(), "OK");
								}
							}else{
								fileUploadResults.put(file.getName(), "DAMAGED");
							}
						}else if("------complete------".equals(line)){
							in.close();
							in = null;
							System.out.println("------complete------");
						}
					}
					/**
					 * 文件上传完成,将状态修改为 “stoped”
					 */
					if(uploadRecord != null){
						FileUploadUtil.setConfigFileStoped(request, uploadRecord);
					}
				}
//			}
		} catch (IOException e) {
			/**
			 * 上传过程中出现异常,将状态修改为 “stoped”
			 */
			e.printStackTrace();
		}finally{
			/**
			 * 上传过程中出现异常,将状态修改为 “stoped”
			 */
			if(uploadRecord != null){
				FileUploadUtil.setConfigFileStoped(request, uploadRecord);
			}
			if(raf != null){
				try {
					raf.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				raf = null;
			}
		}
	}
	
	/**
	 * 读取一行内容
	 */
	private String readLine(InputStream in) {
		return FileUploadUtil.readLine(in);
	}
}

 

 

这个问题的解决让我进一步明白,出现问题的时候,尽量从自己身上找问题,这才是正道眨眼

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值