【Lecture】项目总结与归纳

在这里插入图片描述

1.图片验证码

导入依赖

<dependency>
			<groupId>com.github.axet</groupId>
			<artifactId>kaptcha</artifactId>
			<version>${kaptcha.version}</version>
		</dependency>

首先前端先传一个UUID

// 获取验证码
    getCaptcha() {
      this.dataForm.uuid = getUUID();
      this.captchaPath = this.$http.adornUrl(
        `/captcha.jpg?uuid=${this.dataForm.uuid}`
      );
    },

后端:利用kaptcha生成字符串,然后生成对应图片返回。敲黑板:此刻的UUID被保存了,用于后续验证

@Override
    public BufferedImage getCaptcha(String uuid) {
        if(StringUtils.isBlank(uuid)){
            throw new RRException("uuid不能为空");
        }
        //生成文字验证码
        String code = producer.createText();

        SysCaptchaEntity captchaEntity = new SysCaptchaEntity();
        captchaEntity.setUuid(uuid);
        captchaEntity.setCode(code);
        //5分钟后过期
        captchaEntity.setExpireTime(DateUtils.addDateMinutes(new Date(), 5));
        this.save(captchaEntity);

        return producer.createImage(code);
    }

正式登录时,验证该UUID:是否存在及有没有超出过期时间

@Override
    public boolean validate(String uuid, String code) {
        SysCaptchaEntity captchaEntity = this.getOne(new QueryWrapper<SysCaptchaEntity>().eq("uuid", uuid));
        if(captchaEntity == null){
            return false;
        }

        //删除验证码
        this.removeById(uuid);

        if(captchaEntity.getCode().equalsIgnoreCase(code) && captchaEntity.getExpireTime().getTime() >= System.currentTimeMillis()){
            return true;
        }

        return false;
    }

2.动态菜单路由

src/router/index.js
前置请求

router.beforeEach((to, from, next) => {
  // 添加动态(菜单)路由
  // 1. 已经添加 or 全局路由, 直接访问
  // 2. 获取菜单列表, 添加并保存本地存储
  if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, globalRoutes) === 'global') {
    next()
  } else {
    http({
      url: http.adornUrl('/sys/menu/nav'),
      method: 'get',
      params: http.adornParams()
    }).then(({data}) => {
      if (data && data.code === 0) {
        fnAddDynamicMenuRoutes(data.menuList)
        router.options.isAddDynamicMenuRoutes = true
        sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]'))
        sessionStorage.setItem('permissions', JSON.stringify(data.permissions || '[]'))
        next({ ...to, replace: true })
      } else {
        sessionStorage.setItem('menuList', '[]')
        sessionStorage.setItem('permissions', '[]')
        next()
      }
    }).catch((e) => {
      console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue')
      router.push({ name: 'login' })
    })
  }
})

后端返回:
在这里插入图片描述
前端组装:

/**
 * 添加动态(菜单)路由
 * @param {*} menuList 菜单列表
 * @param {*} routes 递归创建的动态(菜单)路由
 */
function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
  var temp = []
  for (var i = 0; i < menuList.length; i++) {
    if (menuList[i].list && menuList[i].list.length >= 1) {
      temp = temp.concat(menuList[i].list)
    } else if (menuList[i].url && /\S/.test(menuList[i].url)) {
      menuList[i].url = menuList[i].url.replace(/^\//, '')
      var route = {
        path: menuList[i].url.replace('/', '-'),
        component: null,
        name: menuList[i].url.replace('/', '-'),
        meta: {
          menuId: menuList[i].menuId,
          title: menuList[i].name,
          isDynamic: true,
          isTab: true,
          iframeUrl: ''
        }
      }
      // url以http[s]://开头, 通过iframe展示
      if (isURL(menuList[i].url)) {
        route['path'] = `i-${menuList[i].menuId}`
        route['name'] = `i-${menuList[i].menuId}`
        route['meta']['iframeUrl'] = menuList[i].url
      } else {
        try {
          route['component'] = _import(`modules/${menuList[i].url}`) || null
        } catch (e) {}
      }
      routes.push(route)
    }
  }
  if (temp.length >= 1) {
    fnAddDynamicMenuRoutes(temp, routes)
  } else {
    mainRoutes.name = 'main-dynamic'
    mainRoutes.children = routes
    router.addRoutes([
      mainRoutes,
      { path: '*', redirect: { name: '404' } }
    ])
    sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '[]'))
    console.log('\n')
    console.log('%c!<-------------------- 动态(菜单)路由 s -------------------->', 'color:blue')
    console.log(mainRoutes.children)
    console.log('%c!<-------------------- 动态(菜单)路由 e -------------------->', 'color:blue')
  }
}

3.前端权限控制

v-if="isAuth('product:lecinfo:save')"

PS:在动态菜单路由时,将men在动态菜单路由时将这些都存在浏览器的会话存储中

/**
 * 是否有权限
 * @param {*} key
 */
export function isAuth (key) {
  return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
}

存:sessionStorage.setItem(‘permissions’, JSON.stringify(data.permissions || ‘[]’))

(base) xu@xu:~/Desktop/sharing$ netstat -an | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 6
FIN_WAIT1 22
CLOSE_WAIT 35
LISTEN 54
ESTABLISHED 294

4.限制XSS

link1 link2

注入的恶意代码有两种类型:

在img或者iframe等标签中,后面接着代码,可能以javascript:开头,。
以<script>标签包裹着代码

拓展:注意上述两种写法不区分大小写,“javascript:”也可以写成“JaVaScRiPt:”,因为XSS的恶意代码一般都是插入在html标签里,而HTML不区分大小写。

在vue中处理:
1.通常如果要解释成html代码则要用v-html。而此指令相当于innerHTML。虽然像innerHTML一样不会直接输出script标签,但也可以输出img,iframe等标签。

2.对于img,iframe等标签,后端用xss filter进行特殊标签进行转义成&lt,&gt等,html可识别的转义符,会反转义显示(不渲染,即不会执行该内部JS脚本)
在这里插入图片描述
上面是数据库中保存的MD内容,在前端vue中会通过SimpleMDE将它转成对应的html

this.contentMarkDown = SimpleMDE.prototype.markdown(
            this.lecResp.content
          );

PS:富文本中出现JS脚本的话,为录入的代码段

3.开启CSP(Content Security Policy)
在入口文件的head添加meta标签

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';style-src 'self' 'unsafe-inline'">

CSP设置表示,script脚本资源和style样式资源只能加载当前域名下的资源。这样子可以避免外部恶意的脚本的加载和执行。

扩展:设置 HTTPS,可以防止提交时的用户名或者密码被拦截或读取

5.定时任务

参考link
Quartz是一个开源的作业调度框架,它完全由 Java 写成。实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
在这里插入图片描述
在这里插入图片描述

调度器Scheduler

代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;
@Configuration
public class ScheduleConfig {

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
		factory.setDataSource(dataSource);
		...
		return factory;
	}

触发器Trigger

Trigger是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。
当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;
而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;
@Service("scheduleJobService")
public class ScheduleJobServiceImpl extends ServiceImpl<ScheduleJobDao, ScheduleJobEntity> implements ScheduleJobService {
	@Autowired
	@Qualifier("schedulerFactoryBean")
    private Scheduler scheduler;
	
	/**
	 * 项目启动时,初始化定时器
	 */
	@PostConstruct
	public void init(){
		List<ScheduleJobEntity> scheduleJobList = this.list();
		for(ScheduleJobEntity scheduleJob : scheduleJobList){
			CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
            //如果不存在,则创建
            if(cronTrigger == null) {
                ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
            }else {
                ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
            }
		}
	}

在这里插入图片描述

任务Job

Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,
JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

public class ScheduleJob extends QuartzJobBean { //QuartzJobBean实现了Job
在这里插入图片描述利用反射去执行task中的run方法

JobDetail

JobDetail实例是通过JobBuilder类创建的

Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。
因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。

    JobDetail job = newJob(DumbJob.class)
      .withIdentity("myJob", "group1") // name "myJob", group "group1"
      .usingJobData("jobSays", "Hello World!")
      .usingJobData("myFloatValue", 3.141f)
      .build();
      
	// Tell quartz to schedule the job using our trigger
    sched.scheduleJob(job, trigger);

将job加入到scheduler之前,在构建JobDetail时,可以将数据放入JobDataMap;在job的执行过程中,可以从JobDataMap中取出数据

Calendar、ThreadPool

Calendar:

org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar
看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。
一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,
但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。

ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

redis缓存记录

在这里插入图片描述

shiro存储
在这里插入图片描述

短信防刷存储

在这里插入图片描述
redis的key为 sms:code:手机号

发送短信:前端倒计时+redis中存储判断

 String code = UUID.randomUUID().toString().substring(0, 6);
String redis_code = code + "_" + System.currentTimeMillis();
// 缓存验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, redis_code , 10, TimeUnit.MINUTES);

上架流程

预约活动存储

在这里插入图片描述
seckill:sessions:活动开始的时间戳 + “_” + 活动结束的时间戳 过期时间:预约活动结束就过期

list中元素是 预约活动Id-讲座活动Id

    private String getThisWeekDay(Integer days) {
        LocalDate now = LocalDate.now();
        LocalDate beginDayOfWeek = now.with(DayOfWeek.MONDAY);
        LocalDate endDay = beginDayOfWeek.plusDays(days);
        return LocalDateTime.of(endDay, LocalTime.MIN).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
1. 筛选预约活动开启在执行时间范围内的:当前~周days结束  PS:days是前端指定
2. 剔除没有关联讲座的预约活动
3. 借助HashSet保存现存的预约活动:更新缓存,剔除多余的,添加没有的。对于缓存List中的sessionId-actId,也可以这么干

3.过期时间还没到,但预约活动状态被更改了。

//获取redis中所有以seckill:sessions:开头的key
Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX+"*");
keys.removeAll(session_begin_end);
//清除现在不存在的
stringRedisTemplate.delete(keys);

其中HashSet session_begin_end是维护的是现存的预约活动构成的keys

3.当然啦预约活动没变,但关联的讲座活动变了(即List中的个数变化了)

redisTemplate.opsForList().range("list",0,-1); //查出制定key=“list”中的所有元素
remove(K key, long count, Object value) 当count=0删除等于value的所有元素
示例:redisTemplate.opsForList().remove("list",0,"w");

秒杀信息存储

在这里插入图片描述hash中的key是sessionId-actId即上一步缓存List中保存的值

        //准备hash操作
        BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); //这样绑定的话SKUKILL_CACHE_PREFIX是hash类型
        Set<Object> keys = ops.keys();
        //获取缓存中已过时的sessionId-actId  毕竟hash类型的子key不会自动过期
        keys.removeAll(set);
        if (!keys.isEmpty()) {
            ops.delete(keys.toArray());
        }
4. 同样的操作用事先保存好的现存活的sessionId_actId_set,剔除缓存中过时的秒杀信息(hash类型的子key不会自动过期)
5. 产生商品随机码String randomCode = UUID.randomUUID().toString().replace("-", "");
6. 幂等性处理:如果秒杀信息中有该sessionId-actId就不重复缓存了。强制foreFlag=true除外,变化的库存量给加上方可

库存存储

在这里插入图片描述
redis的key为 seckill:stock:商品随机码 过期时间:预约活动结束就过期

// 5.使用库存作为分布式信号量  限流
RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
stringRedisTemplate.expire(SKUSTOCK_SEMAPHONE + randomCode, session.getEndTime().getTime()-new Date().getTime(),TimeUnit.MILLISECONDS);

强制覆盖缓存的话,需要修正初始库存量

RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);
int srcValue = semaphore.availablePermits();

semaphore.addPermits(seckillSkuVo.getSeckillCount().intValue()-srcValue);
stringRedisTemplate.expire(SKUSTOCK_SEMAPHONE + randomCode, session.getEndTime().getTime()-new Date().getTime(),TimeUnit.MILLISECONDS);
7. 同样的套路,利用现存的seckill_stock_set去除多余的stock缓存
//去除redis中多余的seckill:stock
Set<String> redisKeys = stringRedisTemplate.keys(SKUSTOCK_SEMAPHONE + "*");
//获取多余的key
redisKeys.removeAll(seckill_stock_set);
stringRedisTemplate.delete(redisKeys);

预约

在这里插入图片描述

    @Cacheable(value = {"appointment"},key = "#root.methodName")
    @Override
    public List<AppointmentVo> getAppointmentInfoList() {

用的是SpringCache失效模式 link

防重复预约

http://lecture.com/api/seckill/appointment/kill?killId=3-4&code=2812ef8f62bf484a8649b16247c4e898&killNum=1

killId是秒杀信息hash中的键,值为具体的秒杀信息
code是商品随机码
在这里插入图片描述
redis的key为:userId-sessionId-actId
校验:

1.取出缓存中具体的秒杀信息,看商品随机码对不(看当前时间是否在起止时间更好)
2.killNum不超过限制
3.该用户没有预约过该场次的该讲座
4.剩余库存够不

生成预约信息单号,通过消息队列发送消息。Product服务监听该消息创建订单
删除订单时得恢复下信号量,去除抢票占位
PS:创建成功以及删除 记得更新余票,上架时信号量时根据余票初始化的

String randomCode = seckillVo.getRandomCode();
//更新信号量
RSemaphore semaphore = redissonClient.getSemaphore(Constant.REDIS_KEY_PREFIX.SKUSTOCK_SEMAPHONE+randomCode);
//            int srcRemainPermits = semaphore.availablePermits();
semaphore.addPermits(item.getKillNum()); //恢复

//去除抢票占位,以便取消预约后还可以抢
String kill_seat = item.getUserId()+"-"+item.getSessionId()+"-"+item.getActId();
stringRedisTemplate.delete(kill_seat);

梳理几个点

1.接口安全

(1)接口权限:shiro权限
(2)接口防刷、幂等性

要防止的情况
1.用户多次点击按钮
2.用户页面回退再次提交
3.微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制

商品随机码randomCode 防止提前暴露(其实直接在后端限制当前是否在起止时间更好)
订单防重令牌
图片验证码
发送短信

(3)重定向防止表单重复提交

foward转发请求方式不变
redirect重定向 但获取不到model中放的数据(请求域中)
RedirectAttributes redirectAttributes 重定向携带数据重定向也能共享,但似乎是以FlashMap实体方式存储在session中的
 redirectAttributes.addFlashAttribute("errors", errors); 只能取一次

2.最终一致性

(1)支付的最大努力通知
在这里插入图片描述这里的notify_url一定得是外网可访问的,要不支付宝的通知发不进来。
在这里插入图片描述
在这里插入图片描述
通过这些参数生成支付客户端->发起请求->将返回支付宝的支付页面显示->

等待支付成功->通过notify_url进行最大努力通知(直到return “success”)

在nginx监听这个server_name yfxu.ngrok2.xiaomiqiu.cn; 然后我就反手转发给了order服务进行处理

 location /payed/ {
        proxy_set_header Host order.gulimall.com; #指定域名
        proxy_pass http://gulimall; #在nginx.conf中设置的上游服务器组
    }

order服务中有这么个controller,@PostMapping("/payed/notify") 方法中return "success"则支付宝就不用再通知了。

(2)消息队列补偿
库存
在这里插入图片描述延迟队列TTL到期就会成为死信,通过指定的死信交换机发送到对应的死信队列。延迟队列声明时会指定消息过期时的用于转发的死信交换机和路由键
在这里插入图片描述
A创建订单时,会远程调用服务B进行锁库存:
若B异常回滚,那A也可以跟着回滚;若B正常,之后A异常,则B没法跟着A回滚,得反向补偿

在order.release.order.queue 的监听器进行消息处理时,会判断当前订单的状态,若没还未支付,则关闭订单->同时给stock.realse.stock.queue发送消息
在该监听器进行消息处理时:库存之前没锁成功不作处理,锁成功的话:若没有这个订单或有这个订单但订单状态为已取消 得解库存

3.Nginx

动静分离
nginx还存储静态资源(比如css、js),动态资源放在微服务里面。减轻后端服务器压力,提高静态资源访问速度

proxy_set_header Host $host; #不让host丢失,要不网关没法根据host进行lb

限流

在这里插入图片描述
(1)网关限流
在这里插入图片描述也可以考虑Sentinel
(2)Nginx限流
nginx限制IP:请求数或并发数

limit_req_zone $binary_remote_addr zone=lecture:10m rate=10r/s; #按照ip限制每秒的请求数

limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 "leaky bucket"。
limit_req_conn 用来限制同一时间连接数,即并发限制。

在这里插入图片描述

花式限流看这篇:link

简历涉及

Java内存模型 JMM

1.主内存、工作内存
在这里插入图片描述
2.锁、volatile、final的内存语义

volatile的写-读与锁的释放-获取具有相同的内存语义
锁的释放,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
锁的获取,JMM会把该线程对应的本地内存置为无效。从而使该线程从主内存中读取共享变量

写final域的重排序规则:
JMM禁止编译器把final域的写重排序到构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

3.重排序
为了提高性能,编译器和处理器常常会对指令做重排序

在这里插入图片描述

上述的1属于编译器重排序,2和3属于处理器重排序。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令

5.happens-before

组件间通信

原文

1.props方式
2.@绑定监听
3.发布订阅
4.slot
5.vuex

6.v-model的方式

<SingleUpload v-model="dataForm.logo"></SingleUpload>
...
import SingleUpload from "@/components/upload/singleUpload.vue";
...
import SingleUpload from "@/components/upload/singleUpload.vue";

在子组件中可以下面的方式this.$emit(‘input’, val),去改变父组件中v-model和子组件中的value值为val

/home/xu/PersonProjects/FrontEndProjects/renren-fast-vue/src/components/upload/singleUpload.vue

props: {
      value: String  // 声明接收标签属性
    },
    ...
methods: {
   this.$emit('input', val)
  },
}

Canvas画布

HTML5 的Canvas使用JavaScript程序绘图(动态生成),基于像素

先获得一个二维的上下文对象,然后就可以利用这个context进行各种绘制操作

var canvas = document.getElementById("canvas");
    //检测浏览器是否支持canvas 该方法是否存在 取得上下文对象
    if (canvas.getContext) {
        var context = canvas.getContext('2d'); //2d用单引用括起来
    }

通过context设置图像属性:画笔, 粗细,大小,颜色

Axios

是对XHR的一种封装

readyState: 标识请求状态的只读属性
    0: 初始
    1: open()之后
    2: send()之后
    3: 请求中
    4: 请求完成
onreadystatechange: 绑定readyState改变的监听

在这里插入图片描述
原文link
请求时会产生一个Promise对象:可以解决回调地狱问题。

什么是回调地狱? 回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件
回调地狱的缺点?  不便于阅读 / 不便于异常处理

在这里插入图片描述

promise.then().then().catch()

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

星空•物语

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值