大数据案例--网站流量项目(上)

目录

一、网站流量统计项目概述

1、项目背景

2、统计指标说明

二、数据的埋点和采集

1、概述

三、项目整体架构

1、架构图

四、项目环境搭建

1、准备

2、搭建

3、字段说明

五、日志服务器-Flume连通

1、新建weblog.conf

2、离线批处理搭建 ​


一、网站流量统计项目概述

1、项目背景

网站流量统计是改进网站服务的重要手段之一,通过获取用户在网站的行为,可以分析出哪些内容受到欢迎,哪些页面存在问题,从而使网站改进活动更具有针对性。

2、统计指标说明

常用的网站流量统计指标一般包括以下情况分析:

①、按在线情况分析

        在线情况分析分别记录在线用户的活动信息,包括:来访时间、访客地域、来路页面、当前停留页面等,这些功能对企业实时掌握自身网站流量有很大的帮助。

②、按时间段分析

        时段分析提供网站任意时间内的流量变化情况.或者某一段时间到某一段时间的流量变化,比如小时段分布,日访问量分布,对于企业了解用户浏览网页的的时间段有一个很好的分析。

③、按来源分析

        来源分析提供来路域名带来的来访次数、IP、独立访客、新访客、新访客浏览次数、站内总浏览次数等数据。这个数据可以直接让企业了解推广成效的来路,从而分析出那些网站投放的广告效果更明显。

 ④、按访客地区分析

本项目的8个指标:

1、PV-PageView

页面访问量,也是常说的流量。用户点击一次页面,就算作一个PV,包括刷新操作也算

2、UV-Unique Visitor

独立访客数量。是按不同的用户来统计的。

实现思路:

①、当一个新用户初次访问网站时,网站后台会为此用户生成一个唯一的标识id(uvid)

②、将uvid存到用户浏览器的cookie中

③、后续用户再次访问网站时,会携带cookie中的信息(uvid)

④、后台可以根据uvid来统计uv

比如,某网站当前的流量数据:

则当天总的pv=5 总的uv=3 

3、VV-Vistor View

独立会话数量。统计有多少个不同的Session会话数量

产生Session会话的场景:

①、关闭浏览器,再次打开会产生一个新会话

②、超过了指定的会话操作超时时间(比如30分钟),再次操作会产生新会话

实现思路:

①、每当产生一个新Session会话时,网站后台会为此会话生成一个唯一标识(ssid)

②、将ssid存到用户浏览器的cookie中

③、后续用户再次点击访问时,会携带cookie中的ssid

④、根据ssid来统计vv数量

比如,某网站当天的流量数据:

则当天总的pv=5 总的uv=3 总的vv=4

4、BR-Bounced Rate

页面跳出率,BR=总的跳出会话数量/总的会话数量(vv)

跳出会话指的是:对目标网站只有一次访问记录的会话,就是一个跳出会话

比如,某网站当天的流量数据:

 则当天总的pv=5 总的uv=3 总的vv=4 总的跳出会话=3 br=3/4=0.75

BR一般用于衡量网站质量的优良性。如果BR高,则说明网站对用户的吸引力低

5、NewIp

新增ip数量,统计当前有那些ip在以往的数据中从未出现过的ip数量(这样的ip是新增ip)

比如,某网站当天的流量数据:

历史数据:

当天数据:

6、NewCust

新增用户数。统计当天有哪些用户(uvid)在历史数据中从未出现过的数量。处理思路同newip,判断指标更换为以uvid来统计判断

7、AvgDeep

平均会话访问深度

AvgDeep=总的会话访问深度/总的会话数量(vv)

会话访问深度指的是:一个会话浏览多少个不同的访问地址

比如:

AvgDeep=(3+2)/2=2.5 

8、AvgTime

平均的会话访问时长

AvgTime=总的会话时长/总的会话数量(vv)

比如:

统计会话在页面的停留时长,处理方式:记录点击打开页面的时间戳。

所以要统计一个会话的总时长:此会话最大时间戳-此会话最小时间戳

补充:AvgTime得到的结果一般会小于真实结果。因为最后一个页面的停留时长获取不到 

二、数据的埋点和采集

1、概述

所谓埋点就是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,即通过数据埋点来采集数据,比如采集:访问(Vistis),访客(Vistor),停留时间(Time On Site),页面查看(Page Views,又称为页面浏览)和跳出率(Bounce Rate,又可称为蹦失率)等等

一个典型的数据平台,对于数据的处理,是由如下的5个步骤组成的:

在这个流程里,我们认为第一个步骤,也就是数据埋点和采集数据是最基础的问题,数据采集是否丰富,采集的数据是否准确,采集是否及时,都直接影响整个数据平台的应用的效果。

埋点的两种方式:

第一种:自己公司研发在产品中注入埋点代码进行采集。将埋点代码比如写道一个js里,然后放到某个应用网站上。

第二种:使用第三方统计工具,如友盟,百度移动,魔方,App Annie,talking data等。

本项目的埋点实现:

我们通过js代码来实现埋点。编写特定的js脚本,然后嵌入到需要做日志分析的web页面上(实际是通过<script>标签来嵌入js文件)

三、项目整体架构

1、架构图

四、项目环境搭建

1、准备

IDEA、Maven、Tomcat

2、搭建

①、创建maven工程,名字自己定义,我的为WebProject,选择webapp。

 ②、选择maven的仓库,我直接选择我的本地仓库

③、 然后再新建两个FluxAppServer和FluxLogServer模块,还是maven选择webapp

 

④、添加本地tomcat环境,指定发布到Tomcat的web工程,并指定访问的路径:

⑤、添加FluxAppServer工程相关的js文件与网页文件

tongji.js:

/**函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。*/
function ar_encode(str)
{
	//进行URL编码
	return encodeURI(str);
}


/**屏幕分辨率*/
function ar_get_screen()
{
	var c = "";

	if (self.screen) {
		c = screen.width+"x"+screen.height;
	}

	return c;
}

/**颜色质量*/
function ar_get_color()
{
	var c = ""; 

	if (self.screen) {
		c = screen.colorDepth+"-bit";
	}

	return c;
}

/**返回当前的浏览器语言*/
function ar_get_language()
{
	var l = "";
	var n = navigator;

	if (n.language) {
		l = n.language.toLowerCase();
	}
	else
	if (n.browserLanguage) {
		l = n.browserLanguage.toLowerCase();
	}

	return l;
}

/**返回浏览器类型IE,Firefox*/
function ar_get_agent()
{
	var a = "";
	var n = navigator;

	if (n.userAgent) {
		a = n.userAgent;
	}

	return a;
}

/**方法可返回一个布尔值,该值指示浏览器是否支持并启用了Java*/
function ar_get_jvm_enabled()
{
	var j = "";
	var n = navigator;

	j = n.javaEnabled() ? 1 : 0;

	return j;
}

/**返回浏览器是否支持(启用)cookie */
function ar_get_cookie_enabled()
{
	var c = "";
	var n = navigator;
	c = n.cookieEnabled ? 1 : 0;

	return c;
}

/**检测浏览器是否支持Flash或有Flash插件*/
function ar_get_flash_ver()
{
	var f="",n=navigator;

	if (n.plugins && n.plugins.length) {
		for (var ii=0;ii<n.plugins.length;ii++) {
			if (n.plugins[ii].name.indexOf('Shockwave Flash')!=-1) {
				f=n.plugins[ii].description.split('Shockwave Flash ')[1];
				break;
			}
		}
	}
	else
	if (window.ActiveXObject) {
		for (var ii=10;ii>=2;ii--) {
			try {
				var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");
				if (fl) {
					f=ii + '.0';
					break;
				}
			}
			 catch(e) {}
		}
	}
	return f;
} 

 
/**匹配顶级域名*/
function ar_c_ctry_top_domain(str)
{
	var pattern = "/^aero$|^cat$|^coop$|^int$|^museum$|^pro$|^travel$|^xxx$|^com$|^net$|^gov$|^org$|^mil$|^edu$|^biz$|^info$|^name$|^ac$|^mil$|^co$|^ed$|^gv$|^nt$|^bj$|^hz$|^sh$|^tj$|^cq$|^he$|^nm$|^ln$|^jl$|^hl$|^js$|^zj$|^ah$|^hb$|^hn$|^gd$|^gx$|^hi$|^sc$|^gz$|^yn$|^xz$|^sn$|^gs$|^qh$|^nx$|^xj$|^tw$|^hk$|^mo$|^fj$|^ha$|^jx$|^sd$|^sx$/i";

	if(str.match(pattern)){ return 1; }

	return 0;
}

/**处理域名地址*/
function ar_get_domain(host)
{
	//如果存在则截去域名开头的 "www."
	var d=host.replace(/^www\./, "");

	//剩余部分按照"."进行split操作,获取长度
	var ss=d.split(".");
	var l=ss.length;

	//如果长度为3,则为xxx.yyy.zz格式
	if(l == 3){
		//如果yyy为顶级域名,zz为次级域名,保留所有
		if(ar_c_ctry_top_domain(ss[1]) && ar_c_ctry_domain(ss[2])){
		}
		//否则只保留后两节
		else{
			d = ss[1]+"."+ss[2];
		}
	}
	//如果长度大于3
	else if(l >= 3){

		//如果host本身是个ip地址,则直接返回该ip地址为完整域名
		var ip_pat = "^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$";
		if(host.match(ip_pat)){
			return d;
		}
		//如果host后两节为顶级域名及次级域名,则保留后三节
		if(ar_c_ctry_top_domain(ss[l-2]) && ar_c_ctry_domain(ss[l-1])) {
			d = ss[l-3]+"."+ss[l-2]+"."+ss[l-1];
		}
		//否则保留后两节
		else{
			d = ss[l-2]+"."+ss[l-1];
		}
	}
		
	return d;
}


/**返回cookie信息*/
function ar_get_cookie(name)
{
	//获取所有cookie信息
	var co=document.cookie;
	
	//如果名字是个空 返回所有cookie信息
	if (name == "") {
		return co;
	}
	
	//名字不为空 则在所有的cookie中查找这个名字的cookie
	var mn=name+"=";
	var b,e;
	b=co.indexOf(mn);

	//没有找到这个名字的cookie 则返回空
	if (b < 0) {
		return "";
	}

	//找到了这个名字的cookie 获取cookie的值返回
	e=co.indexOf(";", b+name.length);
	if (e < 0) {
		return co.substring(b+name.length + 1);
	}
	else {
		return co.substring(b+name.length + 1, e);
	}
}

/**
 	设置cookie信息
	操作符:
		0 表示不设置超时时间 cookie是一个会话级别的cookie  cookie信息保存在浏览器内存当中 浏览器关闭时cookie消失
		1 表示设置超时时间为10年以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止
		2 表示设置超时时间为1个小时以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止
 * */
function ar_set_cookie(name, val, cotp) 
{ 
	var date=new Date; 
	var year=date.getFullYear(); 
	var hour=date.getHours(); 

	var cookie="";

	if (cotp == 0) { 
		cookie=name+"="+val+";"; 
	} 
	else if (cotp == 1) { 
		year=year+10; 
		date.setYear(year); 
		cookie=name+"="+val+";expires="+date.toGMTString()+";"; 
	} 
	else if (cotp == 2) { 
		hour=hour+1; 
		date.setHours(hour); 
		cookie=name+"="+val+";expires="+date.toGMTString()+";"; 
	} 

	var d=ar_get_domain(document.domain);
	if(d != ""){
		cookie +="domain="+d+";";
	}
	cookie +="path="+"/;";

	document.cookie=cookie;
}



/**返回客户端时间*/
function ar_get_stm() 
{ 
	return new Date().getTime();
} 


/**返回指定个数的随机数字串*/
function ar_get_random(n) {
	var str = "";
	for (var i = 0; i < n; i ++) {
		str += String(parseInt(Math.random() * 10));
	}
	return str;
}

/* main function */
function ar_main() {
	
	//收集完日志 提交到的路径
	var dest_path   = "http://localhost:8090/FluxLogServer/servlet/LogServlet?";
	var expire_time = 30 * 60 * 1000;//会话超时时长

	//处理uv
	//--获取cookie ar_stat_uv的值
	var uv_str = ar_get_cookie("ar_stat_uv");
	var uv_id = "";
	//--如果cookie ar_stat_uv的值为空
	if (uv_str == ""){
		//--为这个新uv配置id,为一个长度20的随机数字
		uv_id = ar_get_random(20);
		//--设置cookie ar_stat_uv 保存时间为10年
		ar_set_cookie("ar_stat_uv", uv_id, 1);
	}
	//--如果cookie ar_stat_uv的值不为空
	else{
		//--获取uv_id
		uv_id  = uv_str;
	}

	//处理ss
	//--获取cookie ar_stat_ss
	var ss_str = ar_get_cookie("ar_stat_ss"); 
	var ss_id = "";  //sessin id
	var ss_no = 0;   //session有效期内访问页面的次数

	//--如果cookie中不存在ar_stat_ss 说明是一次新的会话
	if (ss_str == ""){
		//--随机生成长度为10的session id
		ss_id = ar_get_random(10);
		//--session有效期内页面访问次数为0
		ss_no = 0;
		//--拼接cookie ar_stat_ss 值 格式为 会话编号_会话期内访问次数_客户端时间_网站id
		value = ss_id+"_"+ss_no+"_"+ar_get_stm();
		//--设置cookie ar_stat_ss
		ar_set_cookie("ar_stat_ss", value, 0); 
	} 
	//--如果cookie中存在ar_stat_ss
	else { 
		//获取ss相关信息
		var items = ss_str.split("_");
		//--ss_id
		var cookie_ss_id  = items[0];
		//--ss_no
		var cookie_ss_no  = parseInt(items[1]);
		//--ss_stm
		var cookie_ss_stm = items[2];

		//如果当前时间-当前会话上一次访问页面的时间>30分钟,虽然cookie还存在,但是其实已经超时了!仍然需要重新生成cookie
		if (ar_get_stm() - cookie_ss_stm > expire_time) { 
			//--重新生成会话id
			ss_id = ar_get_random(10);
			//--设置会话中的页面访问次数为0
			ss_no = 0;
		} 
		//--如果会话没有超时
		else{
			//--会话id不变
			ss_id = cookie_ss_id;
			//--设置会话中的页面方位次数+1
			ss_no = cookie_ss_no + 1;
		}

		//--重新拼接cookie ar_stat_ss的值 
		value = ss_id+"_"+ss_no+"_"+ar_get_stm();
		ar_set_cookie("ar_stat_ss", value, 0); 
	}

	//当前地址
	var url = document.URL; 
	url = ar_encode(String(url)); 
	
	//当前资源名
	var urlname = document.URL.substring(document.URL.lastIndexOf("/")+1);
	urlname = ar_encode(String(urlname)); 
	
    //返回导航到当前网页的超链接所在网页的URL
	var ref = document.referrer; 
	ref = ar_encode(String(ref)); 


	//网页标题
	var title = document.title;
	title = ar_encode(String(title)); 

	//网页字符集
	var charset = document.charset;
	charset = ar_encode(String(charset)); 

	//屏幕信息
	var screen = ar_get_screen(); 
	screen = ar_encode(String(screen)); 

	//颜色信息
	var color =ar_get_color(); 
	color =ar_encode(String(color)); 

	//语言信息
	var language = ar_get_language(); 
	language = ar_encode(String(language));

 	//浏览器类型
	var agent =ar_get_agent(); 
	agent =ar_encode(String(agent));

	//浏览器是否支持并启用了java
	var jvm_enabled =ar_get_jvm_enabled(); 
	jvm_enabled =ar_encode(String(jvm_enabled)); 

	//浏览器是否支持并启用了cookie
	var cookie_enabled =ar_get_cookie_enabled(); 
	cookie_enabled =ar_encode(String(cookie_enabled)); 

	//浏览器flash版本
	var flash_ver = ar_get_flash_ver();
	flash_ver = ar_encode(String(flash_ver)); 

	
	//当前ss状态 格式为"会话id_会话次数_当前时间"
	var stat_ss = ss_id+"_"+ss_no+"_"+ar_get_stm();
	//拼接访问地址 增加如上信息
	dest=dest_path+"url="+url+"&urlname="+urlname+"&title="+title+"&chset="+charset+"&scr="+screen+"&col="+color+"&lg="+language+"&je="+jvm_enabled+"&ce="+cookie_enabled+"&fv="+flash_ver+"&cnv="+String(Math.random())+"&ref="+ref+"&uagent="+agent+"&stat_uv="+uv_id+"&stat_ss="+stat_ss;


    //通过插入图片访问该地址
    document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
    
}

window.onload = function(){
	//触发main方法
	ar_main();
}

pom.xml:


<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
</dependency>

<dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-servlet-api</artifactId>
      <version>7.0.63</version>
      <scope>provided</scope>
</dependency>

a.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<script type="text/javascript" src="js/tongji.js"></script>
	<title>页面A</title>
</head>

<body>
 <span>页面A</span>
  AAAAAAAAAAAAA
  <a href="http://localhost:8090/FluxAppServer/b.jsp">BBBB</a>
</body>

</html>

b.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<script type="text/javascript" src="js/tongji.js"></script>
	<title>页面B</title>
</head>
<body>
	BBBBBBBBBBBBB
</body>
</html>

index.jsp:

<html>
<body>
<h2>Hello World!</h2>
</body>
</html>

 ⑥、添加FluxLogServer工程相关文件

pom.xml

<dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-servlet-api</artifactId>
      <version>7.0.63</version>
      <scope>provided</scope>
 </dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<dependency>
    <groupId>org.apache.flume</groupId>
    <artifactId>flume-ng-core</artifactId>
    <version>1.9.0</version>
</dependency>

<dependency>
    <groupId>org.apache.flume.flume-ng-clients</groupId>
    <artifactId>flume-ng-log4jappender</artifactId>
    <version>1.9.0</version>
</dependency>

log4j配置:日志一条线发给控制台,另一条线发给我们的flume,注意地址是我们的hadoop01的地址

log4j.rootLogger = info,stdout,flume

log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %m%n

log4j.appender.flume = org.apache.flume.clients.log4jappender.Log4jAppender
log4j.appender.flume.Hostname =192.168.186.128
log4j.appender.flume.Port = 44444
log4j.appender.flume.UnsafeMode = true

LogServlet:

package cn.yang;


import org.apache.log4j.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;

public class LogServlet extends HttpServlet {

    private Logger logger = Logger.getLogger(LogServlet.class);

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String info =URLDecoder.decode(req.getQueryString(),"utf-8") ;
        String[] kvs = info.split("\\&");
        //key 不要,最后将所有value拼接在一起,统一由|拼接
        StringBuffer buffer = new StringBuffer();
        for (String kv : kvs) {
            //ref来路页面可能没有value,为了防止报错,需要进行处理
            String value = kv.split("=").length==2?kv.split("=")[1]:"";
            buffer.append(value+"|");
        }
        //最后再拼接用户的ip地址
        buffer.append(req.getRemoteAddr());
        //日志记录
        //记录级别从低到高:debug->info->warn->error->fatal
        //此外还需要引入log4j配置文件。在配置文件指定最低的日志记录级别和输出的目的地
        logger.info(buffer);
        System.out.println(buffer);

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

⑦、项目初始结构为:

⑧、 最后启动tomcat

3、字段说明

字段

字段名

字段说明

http://localhost:8090/Demo/a.jsp

url

a.jsp

urlname

页面A

title

页面标题

UTF-8

chset

字符集

scr

1024*768

屏幕

24-bit

col

颜色

lg

zh-cn

语言

je

0

浏览器是否支持java

ec

1

是否支持并启用cookie

fv

27.0

浏览器的flash版本

ref

来源页面

uagent

Mozilla/5.0……

浏览器

stat_uv

78024453153757966560

uv_id

stat_ss

4633702647_8_1511204198645

ssid_sscount_sstime

cip

0:0:0:0:0:0:0:1

公网ip

五、日志服务器-Flume连通

1、新建weblog.conf

内容如下:

a1.sources=r1
a1.sinks=s1
a1.channels=c1

a1.sources.r1.type=avro
a1.sources.r1.bind=0.0.0.0
a1.sources.r1.port=44444

a1.sinks.s1.type=logger

a1.channels.c1.type=memory
a1.channels.c1.capacity=1000
a1.channels.c1.transactionCapacity=100

a1.sources.r1.channels=c1
a1.sinks.s1.channel=c1

 

启动flume,在flume的data目录下执行:

../bin/flume-ng agent -n a1 -c ./ -f ./weblog.conf

-Dflume.root.logger=INFO,console 

启动成功 

然后我们启动tomcat,访问a.jsp。就会发现我们的flume捕捉到了数据

2、离线批处理搭建 

 ①、配置flume,编辑weblog.conf

a1.sources=r1
a1.channels=c1
a1.sinks=k1

a1.sources.r1.type=avro
a1.sources.r1.bind=0.0.0.0
a1.sources.r1.port=44444
a1.sources.r1.interceptors=i1
a1.sources.r1.interceptors.i1.type=timestamp

a1.sinks.k1.type=hdfs
a1.sinks.k1.hdfs.path=hdfs://192.168.186.128:9000/weblog/reportTime=%Y-%m-%d
a1.sinks.k1.hdfs.fileType=DataStream
a1.sinks.k1.hdfs.rollInterval=30
a1.sinks.k1.hdfs.rollSize=0
a1.sinks.k1.hdfs.rollCount=1000

a1.channels.c1.type=memory
a1.channels.c1.capacity=1000
a1.channels.c1.transactionCapacity=100

a1.sources.r1.channels=c1
a1.sinks.k1.channel=c1

②、启动hadoop

start-all.sh

③、启动flume

 ../bin/flume-ng agent -n a1 -c ./ -f ./weblog.conf -Dflume.root.logger=INFO,console

④、启动tomcat,访问埋点服务器进行测试,看HDFS上是否能够产生数据

 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小先生

知识是无价的,白嫖也可以的。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值