用户画像学习笔记1

1. 相关背景

用户画像建模其实就是对用户“打标签”,标签一般分为三类:1.统计类标签,如性别、年龄、近7日活跃时长等可以从此用户注册数据、用户访问、消费数据中统计得出;2.规则类标签,该类标签基于用户行为及确定的规则产生,如“消费活跃”用户定义为“近30天交易次数≥2”;3.机器学习挖掘类标签,对用户的某些属性或行为进行预测判断。
用户画像数仓架构
用户画像方案整体:
用户画像主要覆盖模块
用户画像建设项目流程
注意:
第三阶段——需求场景讨论与明确:输出产品用户画像需求文档,在该文档中明确画像应用场景、最终开发出的标签内容与应用方式,并就该文档与需求方反复沟通并确认无误。
第四阶段——应用场景与数据口径确认:数据运营方需要输出产品用户画像开发文档,该文档需要明确应用场景、标签开发的模型、涉及的数据库与表以及应用实施流程。该文档不需要与运营方讨论,只需面向数据团队内部就开发实施流程达成一致。

2. 案例

基于spark计算引擎,涉及HiveQL、Python、Scala、Shell。

2.1 相关元数据

  1. 业务类数据:平台上下单、购买、收藏、配送等;
  2. 用户行为数据:搜索、访问、点击、提交表单等通过操作行为产生(在解析日志的埋点表中)的数据。

涉及表:

  • 用户信息表(字段、字段类型、字段定义、备注)
  • 商品订单表
  • 埋点日志表
  • 访问日志表
  • 商品评论表
  • 搜索日志表
  • 用户收藏表
  • 购物车信息表

2.2 画像表结构设计

两种设计思路:一是每日全量数据的表结构;二是每日增量数据的表结构。
日增量数据可视为ODS层的用户行为画像,在应用时还需基于增量数据做进一步的建模加工。

  1. 日全量数据
    在每天对应的日期分区中插入截止到当天为止的全量数据。
CREATE TABLE 'dw.userprofile_attritube_all' (
	'userid' STRING COMMENT 'userid',
	'labelweight' STRING COMMENT '标签权重',)
COMMENT 'userid 用户画像数据'
PARTITIONED BY ('data_date' STRING COMMENT '数据日期', 'theme' STRING COMMENT '二级主题', 'labelid' STRING COMMENT '标签id')
//设置三个分区字段便于开发和查询数据
//标签权重仅考虑统计类型标签的权重
//表名加'_all'的规范化命名形式说明为日全量表
//对于主题类型为“会员”的标签,插入“20190101”日的全量数据
insert overwrite table dw.userprofile_userlabel_all partition(
	data_date = '20190101',
	theme = 'member',
	labelid = 'ATTRITUBE_U_05_001')
//查询截止到“20190101”日的被打上会员标签的用户量
select count(distinct userid) from dw.userprofile_userlabel_all where data_date='20190101'
  1. 日增量数据
    在每天的日期分区中插入当天业务运行产生的数据,用户进行查询时通过限制查询的日期范围,就可以找出特定时间范围内被打上特定标签的用户。
CREATE TABLE dw.userprofile_act_feature_append(//日增量表
	labelid STRING COMMENT '标签ID',
	cookieid STRING COMMENT '用户ID',
	act_cnt int COMMENT '行为次数',
	tag_type_id int COMMENT '标签类型编码',//母婴、数码
	act_type_id int COMMENT '行为类型编码')//浏览、搜索、收藏、下单
COMMENT '用户画像-用户行为标签表'
PARTITIONED BY (data_date STRING COMMENT '数据日期')
//对于某用户ID为001,查询其在“20180701”到“20180707”日被打上的标签
select * from dw.userprofile_act_feature_append where userid='001' and data_date>='20180701' and data_date<='20180707'
关于宽表的设计

用户属性宽表设计,主要记录用户基本属性信息。
用户日活跃宽表设计,主要记录用户每天访问的信息。

2.3 定性类画像

多见于用户研究等运营类岗位,通过电话调研、网络调研问卷、当面深入访谈、网上第三方权威数据等方式收集用户信息。

2.4 数据指标体系

结合企业的业务情况设定相关的指标。互联网相关企业在建立用户画像时一般除了基于用户维度(userid)建立一套用户标签体系外,还会基于用户使用设备维度(cookieid)建立的相应的标签体系。

2.4.1 用户属性维度

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对于相同的一级标签类型,需要判断多个标签之间的关系为互斥关系还是非互斥关系。

2.4.2 用户行为维度

在这里插入图片描述

2.4.3 用户消费维度

对于用户消费维度指标体系的建设,可从用户浏览、加购、下单、收藏、搜索商品对应的品类入手,品类越细越精确,给用户推荐或营销商品的准确性越高。
在这里插入图片描述
在这里插入图片描述

2.4.4 风险控制维度

为有效监控平台的不良用户,如薅羊毛、恶意刷单、借贷欺诈等行为的用户,建立风险控制维度。可从账号风险、设备风险、借贷风险等维度入手。
在这里插入图片描述

2.4.5 社交属性维度

在这里插入图片描述

2.4.6 总结

标签命名:
标签主题_刻画维度_标签类型_一级归类
ATTRITUBE_U_ol_001

标签主题用户维度标签类型一级归类
ATTRITUBE:人口属性;ACTION:行为属性;CONSUME:用户消费;RISKMANAGE:风险控制C:cookieid;U:userid统计型;规则型;算法型;自然性别;购物性别;年龄;地域

在这里插入图片描述

2.5 标签数据存储

2.5.1 Hive数据仓库

数据仓库建模:
  • 事实表
    • 事务事实表
      • 单事务事实表
      • 多事务事实表:当不同业务过程有相似性时考虑将多业务过程放到一张表中,新增字段判断属于哪个业务过程。
    • 周期快照事实表:在一个确定的时间间隔内对业务状态进行度量。例如查看一个用户近一年付款金额。
    • 累计快照事实表:用于查看不同事件之间的时间间隔。例如分析用户从购买到支付的时长,一般适用于有明确时间周期的业务过程。
  • 维度表
    • 缓慢变化维
      • 重写维度值
      • 保留多条记录
      • 开发日期分区表,每日分区数据记录当日维度的属性
      • 开发拉链表按时间变化进行全量存储
分区存储:

解决ETL花费时间较长:

  • 将数据分区存储,分别执行作业;
  • 标签脚本性能调优;
  • 基于一些标签共同的数据来源开发中间表。
标签汇聚:

由于用户的全部标签存储在不同的分区下面,需要将用户身上的标签做聚合处理。

CREATE TABLE 'dw.userprofile_userlabel_map_all'(
	'userid' STRING COMMENT 'userid',
	'userlabels' map<STRING,STRING> COMMENT 'tagsmap',)
COMMENT 'userid用户标签汇聚'
PARTITIONED BY ('data_date' STRING COMMENT '数据日期')
//
insert overwrite table dw.userprofile_userlabel_map_all partion(data_date="data_date")
	select userid,cast_to_json(concat_ws(",",collect_set(concat(labelid,":",labelweight)))) as userlabels from "用户各维度的标签表"
	where data_date="data_date"
group by userid
ID-MAP:

ID-Mapping,即把用户不同来源的身份标识通过数据手段识别为同一个主体,消除数据孤岛,打通userid和cookieid的对应关系,可以在用户登录、未登录时都能捕获其行为轨迹。(用户和设备间的多对多关系)
拉链表是针对缓慢变化维表的一种设计方式,记录一个事物从开始到当前状态的全部状态变化信息。

  • 首先,从埋点表和访问日志表里获取到cookieid和userid同时出现的访问记录;
INSERT OVERWRITE TABLE ods.cookie_user_signin PARTITION (data_date='${data_date}')
	SELECT t.* FROM (
		SELECT userid,cookieid,from_unixtime(eventtime,'yyyyMMdd') as signdate
		FROM ods.page_event_log //埋点表
		WHERE data_date = '${data_date}'
	UNION ALL
		SELECT userid,cookieid,from_unixtime(viewtime,'yyyyMMdd') as signdate
		FROM ods.page_view_log //访问日志表
		WHERE data_date = '${data_date}'
		) t
  • 创建ID-Map的拉链表,将每天新增到ods.cookie_user_signin表中的数据与拉链表历史数据做比较,如果有变化或新增数据则进行更新。
CREATE TABLE 'dw.cookie_user_zippertable' (
	'userid' STRING COMMENT '账号id',
	'cookieid' STRING COMMENT '设备id',
	'start_date' STRING COMMENT 'start_date',
	'end_date' STRING COMMENT 'end_date')
COMMENT 'id-map拉链表'
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
  • 每天ETL调度将数据更新到ID-Mapping拉链表中
INSERT OVERWRITE TABLE ods.cookie_user_zippertable
SELECT t.* FROM (
	SELECT t1.user_num, t1.mobile, t1.reg_date, t1.start_date,
		CASE WHEN t1.end_date='99991231' AND t2.userid IS NOT NULL THEN '${data_date}' 
			ELSE t1.end_date
		END AS end_date
	FROM dw.cookie_user_zippertable t1
LEFT JOIN(
	SELECT * FROM ods.cookie_user_signin
	WHERE data_date='${data_date}'
	) t2
	ON t1.userid = t2.userid
UNION
	SELECT userid,cookieid,'${data_date}' AS start_date,'99991231' AS end_date
	FROM ods.cookie_user_signin
	WHERE data_date='${data_date}'
) t		
  • 实践中,还需将用户在不同平台间(如web端和APP端)行为打通的应用场景。

2.5.2 MySQL存储

元数据管理

Hive适合于大数据量的批处理作业,对于量级较小的数据,MySQL具有更快的读写速度。
在这里插入图片描述
在这里插入图片描述

监控预警数据

需要监控的环节:每天标签的产出量、服务层数据同步情况的监控。
调度流每跑完相应的模块,就将该模块的监控数据插入MySQL中,当校验任务判断达到触发告警阈值时,发送告警邮件,同时中断后续的调度任务。

结果集存储

存储多维透视分析用的标签、圈人服务用的用户标签、当日记录各标签数量,用于校验标签数据是否出现异常。
sqoop是一个用来将Hadoop和关系型数据库中(MySQL、Oracle、postgreSQL)的数据相互迁移的工具。

  • 首先,在Hive中建立一张记录用户身份相关信息的表,设置日期分区以满足按日期选取当前人群的需要。
CREATE TABLE 'dw.userprofile_userservice_all' (
	'user_id' STRING COMMENT 'userid',
	'user_sex' STRING COMMENT 'user_sex',
	'city' STRING COMMENT 'city',
	'payid_money' STRING COMMENT 'payid_money',
	'payid_num' STRING COMMENT 'payid_num',
	'latest_product' STRING COMMENT 'latest_product',
	'date' STRING COMMENT 'date',
	'data_status' STRING COMMENT 'data_status',)
COMMENT 'userid用户客服数据'
PARTITIONED BY ('data_date' STRING COMMENT '数据日期')
  • 在MySQL中建立一张用于接收同步数据的表。
CREATE TABLE 'userservice_data' (
'user_id' varchar(128) COMMENT '用户id',
	'user_sex' varchar(128) COMMENT '用户性别',
	'city' varchar(128) COMMENT '城市',
	'payid_money' varchar(128) COMMENT '消费金额',
	'payid_num' varchar(128) COMMENT '消费次数',
	'latest_product' varchar(128) COMMENT '最近购买产品',
	'date' varchar(64) COMMENT '传输日期',
	'data_status' varchar(64) COMMENT '0:未传输,1:传输中,2:成功,3:失败',
	PRIMARY KEY ('user_id'),
) ENGINE=InnoDB AUTO_INCREMENT=2261628 DEFAULT CHARSET=utf8 COMMENT '用户客服数据表'
  • 通过Python脚本调用Shell命令,将Hive中数据同步到MySQL中。执行如下脚本:
# -*- coding: utf-8 -*-
import os
import MySQLdb
import sys
def export_data(hive_tab, data_date):
	sqoop_command = "sqoop export --connect jdbc:mysql://10.xxx.xxx.xxx:3306/mysql_database --username username --password password --table mysql_table --export-dir hdfs://nameservicel/user/hive/warehouse/dw.db/" + hive_tab + "/data_date=" + data_date + " --input-fields-terminated-by '\001'"//,分隔符
	os.system(sqoop_command)
	print(sqoop_command)

if __name__ == '__main__':
	export_data("dw.userprofile_userservice_all", '20181201')

2.5.3 HBase存储

HBase是一个高性能、列存储、可伸缩、实时读写的分布式存储系统,与Hive不同,HBase能够在数据库上实时运行,而不是跑MapReduce任务,适合进行大数据的实时查询。画像系统每天在Hive里跑出的结果集数据可同步到HBase数据库,用于线上实时应用的场景。
访问HBase中的行只有三种方式:

  • 通过单个row key访问;
  • 通过row key的正则访问;
  • 全表扫描。

rowkey设计的三大原则:

  • 唯一性原则:rowkey需要保证唯一性,画像中一般使用用户id作为rowkey;
  • 长度原则:rowkey的长度一般为10-100bytes;
  • 散列原则:rowkey的散列分布有利于数据均衡分布在每个RegionServer,可实现负载均衡。

ColumnFamily指列簇,HBase中的每个列都归属于某个列簇,列簇是表的schema的一部分,在使用表前定义。划分列簇的原则包括:是否具有相似的数据格式;是否具有相似的访问类型。

//创建表,指定表名和列簇名
create '<table name>','<column family>'
//扫描表中数据,显示10条
scan '<table name>',{LIMIT=>10}
//get命令读取数据
get '<table name>','row1'
//插入数据
put '<table name>','row1','<colfamily:colname>','<value>'
//更新数据
put '<table name>','row','<colfamily:colname>','new value'
//删除表之前先禁用
disable '<table name>'
drop '<table name>'

案例:
HBase的服务器体系结构遵循主从服务器架构,同一时刻只有一个HMaster处于活跃状态,当活跃的Master挂掉后,Backup HMaster自动接管整个HBase集群。在同步数据前,先判断当前活跃节点是哪台机器。

global activenode
for node in ("10.xxx.xx.xxx","10.xxx.xx.xxx"):
	command = "curl http://" + str(node) + ":9870/jmx?qry=Hadoop:service=NameNode,name=NameNodeStatus"
	status = os.popen(command).read()
	print("HBase Master Status:".format(status))
	if ("active" in status)://不活跃standby
		activenode = node

为避免数据都写入一个region,造成数据倾问题,在活跃节点上创建预分区表:

create ‘userprofile_labels’,{NAME=>"f",BLOCKCACHE=>"true",BLOOMFILTER=>"ROWCOL",COMPRESSION=>"snappy",IN_MEMORY=>'true'},{NUMREGIONS=>10,SPLITALGO=>'HexStringSplit'}

将待同步的数据写入HFile,以key-value键值对方式存储,然后将HFile数据使用BulkLoad批量写入HBase集群中。

//Scala
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.hadoop.HBase.client.ConnectionFactory
import org.apache.hadoop.HBase.{HBaseConfiguration, KeyValue, TableName}
import org.apache.hadoop.HBase.io.ImmutableBytesWritable
import org.apache.hadoop.HBase.mapreduce.{HFileOutputFormat2, LoadIncrementalHFiles}
import org.apache.hadoop.HBase.util.Bytes
import org.apache.hadoop.mapreduce.Job
import org.apache.spark.sql.SparkSession

object Hive2HBase {
  def main(args: Array[String]): Unit = {
      // 传入日期参数 和 当前活跃的master节点
    val data_date = args(0)
    val node = args(1)   //当前活跃的节点ip

    val spark = SparkSession
      .builder()
      .appName("Hive2HBase")
      .config("spark.serializer","org.apache.spark.serializer.KryoSerializer")
      .config("spark.storage.memoryFraction", "0.1")
      .config("spark.shuffle.memoryFraction", "0.7")
      .config("spark.memory.useLegacyMode", "true")
      .enableHiveSupport()
      .getOrCreate()
        
       //创建HBase的配置
    val conf = HBaseConfiguration.create()
        conf.set("HBase.zookeeper.quorum", "10.xxx.xxx.xxx,10.xxx.xxx.xxx")
        conf.set("HBase.zookeeper.property.clientPort", "8020")

    //为了预防hfile文件数过多无法进行导入,设置参数值
      conf.setInt("HBase.hregion.max.filesize", 10737418240)
    conf.setInt("HBase.mapreduce.bulkload.max.hfiles.perRegion.perFamily", 3200)
      
    val Data = spark.sql(s"select userid,userlabels from dw.userprofile_usergroup_labels_all where data_date='${data_date}'")
    val dataRdd = Data.rdd.flatMap(row =&gt; {    
      val rowkey = row.getAs[String]("userid".toLowerCase)
      val tagsmap = row.getAs[Map[String, Object]]("userlabels".toLowerCase)
      val sbkey = new StringBuffer()  // 对MAP结构转化 a-&gt;b  'a':'b'
      val sbvalue = new StringBuffer()
      for ((key, value) &lt;- tagsmap){
        sbkey.append(key + ":")
        val labelght = if (value == ""){
          "-999999"
        } else {
          value
        }
        sbvalue.append(labelght + ":")
      }
      val item = sbkey.substring(0,sbkey.length -1)
      val score = sbvalue.substring(0,sbvalue.length -1)
      Array(
        (rowkey,("f","i",item)),
        (rowkey,("f","s",score))
      )
    })

    // 将rdd转换成HFile需要的格式
    val rdds = dataRdd.filter(x=&gt;x._1 != null).sortBy(x=&gt;(x._1,x._2._1, x._2._2)).map(x =&gt; {
      //KeyValue的实例为value
      val rowKey = Bytes.toBytes(x._1)
      val family = Bytes.toBytes(x._2._1)
      val colum = Bytes.toBytes(x._2._2)
      val value = Bytes.toBytes(x._2._3.toString)
      (new ImmutableBytesWritable(rowKey), new KeyValue(rowKey, family, colum, value))
    })

    //文件保存在hdfs的位置
    val locatedir = "hdfs://" + node.toString + ":8020/user/bulkload/hfile/usergroup_HBase_" + data_date

    //在locatedir生成的Hfile文件
    rdds.saveAsNewAPIHadoopFile(locatedir,
      classOf[ImmutableBytesWritable],
      classOf[KeyValue],
      classOf[HFileOutputFormat2],
      conf)
    //HFile导入到HBase
    val load = new LoadIncrementalHFiles(conf)

    //HBase的表名
    val tableName = "userprofile_labels"
    //创建HBase的链接,利用默认的配置文件,读取HBase的master地址
    val conn = ConnectionFactory.createConnection(conf)
    //根据表名获取表
    val table = conn.getTable(TableName.valueOf(tableName))

    try {
      //获取HBase表的region分布
      val regionLocation = conn.getregionLocation(TableName.valueOf(tableName))
      //创建一个hadoop的mapreduce的job
      val job = Job.getInstance(conf)
      //设置job名称,任意命名
      job.setJobName("Hive2HBase")
      //输出文件的内容KeyValue
      job.setMapOutputValueClass(classOf[KeyValue])
      //设置文件输出key, outkey要用ImmutableBytesWritable
      job.setMapOutputKeyClass(classOf[ImmutableBytesWritable])
      //配置HFileOutputFormat2的信息
      HFileOutputFormat2.configureIncrementalLoad(job, table, regionLocation)
      //开始导入
      load.doBulkLoad(new Path(locatedir), conn.getAdmin, table, regionLocation)
    } finally {
      table.close()
      conn.close()
    }
    spark.close()
  }
}

用Elasticsearch存储HBase索引数据。

在这里插入图片描述

# 查询Hive中数据
def check_Hive_data(data_date):
      r = os.popen("Hive -S -e\"select count(1) from dw.userprofile_usergroup_labels_all where data_date='"+data_date+"'\"")
      Hive_userid_count = r.read()
      r.close()
      Hive_count = str(int(Hive_userid_count) 
      print "Hive_result: " + str(Hive_count)
      print "Hive select finished!"

# 查询HBase中数据 
def check_HBase_data(data_date):
      r = os.popen("HBase  org.apache.hadoop.HBase.mapreduce.RowCounter 'userprofile_labels'\" 2&gt;&amp;1 |grep ROWS")
      HBase_count = r.read().strip()[5:]
      r.close()
      print "HBase result: " + str(HBase_count)
      print "HBase select finished!"

# 连接 DB,将查询结果插入表
db = MySQLdb.connect(host="xx.xx.xx.xx",port=3306,user="username", passwd="password", db="xxx", charset="utf8")
cursor = db.cursor()
cursor.execute("INSERT INTO service_monitor(date, service_type, Hive_count, HBase_count) VALUES('"+datestr_+"', 'advertisement', "+str(Hive_userid_count)+","+str(HBase_count)+")")
db.commit()

2.5.4 Elasticsearch存储

Elasticsearch是面向文档型数据库,一条数据就是一个文档,用json作为文档格式。
在这里插入图片描述
将dw,userprofile_userlabel_map_all数据写入Elasticsearch中,Scala代码如下:

object HiveDataToEs {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .AppName("EsData")
      .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .config("spark.dynamicAllocation.enabled", "false")
      .config("es.index.auto.create", "true")
      .config("es.nodes", "10.xx.xx.xx")
      .config("es.batch.write.retry.count", "3")    // 默认重试3次
      .config("es.batch.write.retry.wait", "5")   // 每次重试等待时间为5秒
      .config("thread_pool.write.queue_size", "1000")
      .config("thread_pool.write.size", "50")
      .config("thread_pool.write.type", "fixed")   
      .config("es.batch.size.bytes", "20mb")  
      .config("es.batch.size.entries", "2000")  
      .config("es.http.timeout","100m")
      .enableHiveSupport()
      .getOrCreate()

    val data_date  = args(0).toString
    import spark.sql
    val hiveDF = sql(
      s"""
         | SELECT userid, tagsmap FROM dw.userprofile_userlabel_map_all where data_date = '${data_date}'
      """.stripMargin)    // dw.userprofile_userlabel_map_all在3.1.3节中讲过,是聚合用户标签的表

    val rdd = hiveDF.rdd.map {
      row =&gt; {
        val userid = row.getAs[String]("userid")
        val userlabels = row.getAs[Map[String, Object]]("userlabels")
        Map("userid" -&gt; userid, "userlabels" -&gt; userlabels)
      }
    }
    EsSpark.saveToEs(rdd , "userprofile/tags", Map[String,String]("es.mApping.id"-&gt;"userid")   
    spark.stop()
  }
}
//工程依赖
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch-hadoop</artifactId>
    <version>6.4.2</version>
</dependency>
//将该工程打包之后提交任务,传入日期分区参数“20190101”执行
spark-submit--class com.example.HiveDataToEs--master yarn--deploy-mode client--executor-mory 2g--num-executors 50--driver-memory 3g--executor-cores 2 spark-hive-to-es.jar 20190101
//python脚本查看数据校验逻辑,校验Elasticsearch和hive中的数据量是否一致
# 查询Hive中的数据
def monitor_hive_data(data_date):
    hive_user = " select count(1) from dw.userprofile_userlabel_map_all where data_date='{}' ".format(data_date)
    user_count = os.popen("hive -S -e \"" + hive_user + "\"").read().strip()
    return user_count

# 查询es中的数据
def monitor_es_data(data_date):
    userid_search = "curl http://10.xxx.xxx.xxx:9200/_cat/count/" + data_date + "_userid/"
    userid_num = str(os.popen(userid_search).read()).split(' ')[-1].strip()
    return userid_num

# 比较Hive和es中的数据,如通过校验,更新MySQL状态位
def update_es_data(data_date):
    '''
    data_date: 查询数据日期
    '''
    esdata = monitor_es_data(data_date)    # 查询es中的数据
    hivedata = monitor_hive_data(data_date)  # 查询Hive中的数据
    print("esdata ======&gt;{}".format(esdata))
    print("hivedata ======&gt;{}".format(hivedata))

    # 更新MySQL状态位 
    if (esdata[0] == hivedata[0] ):
        db = MySQLdb.connect(host="10.xx.xx.xx", port=3306, user="username", passwd="password",
                             db="userprofile", charset="utf8")
        cursor = db.cursor()
        try:
            select_command = "INSERT INTO `elasticsearch_state` VALUES ('"+ str(data_date) +"', 'elasticsearch', '0', '2');"
            cursor.execute(select_command)
            db.commit()
        except Exception as e:
            db.rollback()
           exit(1)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值