喜马拉雅亿级用户量的离线消息推送系统架构设计实践

本文由喜马拉雅技术团队李乾坤原创,原题《推送系统实践》,感谢作者的无私分享。

1、引言

1.1 什么是离线消息推送

对于IM的开发者来说,离线消息推送是再熟悉不过的需求了,比如下图就是典型的IM离线消息通知效果。

1.2 Andriod端离线推送真心不易

移动端离线消息推送涉及的端无非就是两个——iOS端和Andriod端,iOS端没什么好说的,APNs是唯一选项。

Andriod端比较奇葩(主要指国内的手机),为了实现离线推送,各种保活黑科技层出不穷,随着保活难度的不断升级,可以使用的保活手段也是越来越少,有兴趣可以读一读我整理的下面这些文章,感受一下(文章是按时间顺序,随着Andriod系统保活难度的提升,不断进阶的)。

上面这几篇只是我整理的这方面的文章中的一部分,特别注意这最后一篇《Android保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)》。是的,当前Andriod系统对APP自已保活的容忍度几乎为0,所以那些曾今的保活手段在新版本系统里,几乎统统都失效了。

自已做保活已经没戏了,保离线消息推送总归是还得做。怎么办?按照现时的最佳实践,那就是对接种手机厂商的ROOM级推送通道。具体我就不在这里展开,有兴趣的地可以详读《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》。

自已做保活、自建推送通道的时代(这里当然指的是Andriod端啦),离线消息推送这种系统的架构设计相对简单,无非就是每台终端计算出一个deviceID,服务端通过自建通道进行消息透传,就这么点事。

而在自建通道死翘翘,只能依赖厂商推送通道的如今,小米、华为、魅族、OPPO、vivo(这只是主流的几家)等等,手机型号太多,各家的推送API、设计规范各不相同(别跟我提什么统一推送联盟,那玩意儿我等他3年了——详见《万众瞩目的“统一推送联盟”上场了》),这也直接导致先前的离线消息推送系统架构设计必须重新设计,以适应新时代的推送技术要求。

1.3 怎么设计合理呢

那么,针对不同厂商的ROOM级推送通道,我们的后台推送架构到底该怎么设计合理呢?

本文分享的离线消息推送系统设计并非专门针对IM产品,但无论业务层的差别有多少,大致的技术思路上都是相通的,希望借喜马拉雅的这篇分享能给正在设计大用户量的离线消息推送的你带来些许启发。

* 推荐阅读:喜马拉雅技术团队分享的另一篇《长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践》,有兴趣也可以一并阅读。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3621-1-1.html)

2、技术背景

首先介绍下在喜马拉雅APP中推送系统的作用,如下图就是一个新闻业务的推送/通知。

离线推送主要就是在用户不打开APP的时候有一个手段触达用户,保持APP的存在感,提高APP的日活。

我们目前主要用推送的业务包括:

  • 1)主播开播:公司有直播业务,主播在开直播的时候会给这个主播的所有粉丝发一个推送开播提醒
  • 2)专辑更新:平台上有非常多的专辑,专辑下面是一系列具体的声音,比如一本儿小说是一个专辑,小说有很多章节,那么当小说更新章节的时候给所有订阅这个专辑的用户发一个更新的提醒:
  • 3)个性化、新闻业务等。

既然想给一个用户发离线推送,系统就要跟这个用户设备之间有一个联系的通道。

做过这个的都知道:自建推送通道需要App常驻后台(就是引言里提到的应用“保活”),而手机厂商因为省电等原因普遍采取“激进”的后台进程管理策略,导致自建通道质量较差。目前通道一般是由“推送服务商”去维护,也就是说公司内的推送系统并不直接给用户发推送(就是上节内容的这篇里提到的情况:《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》)。

这种情况下的离线推送流转流程如下:

国内的几大厂商(小米、华为、魅族、OPPO、vivo等)都有自己官方的推送通道,但是每一家接口都不一样,所以一些厂商比如小米、个推提供集成接口。发送时推送系统发给集成商,然后集成商根据具体的设备,发给具体的厂商推送通道,最终发给用户。

给设备发推送的时候,必须说清楚你要发的是什么内容:即title、message/body,还要指定给哪个设备发推送。

我们以token来标识一个设备, 在不同的场景下token的含义是不一样的,公司内部一般用uid或者deviceId标识一个设备,对于集成商、不同的厂商也有自己对设备的唯一“编号”,所以公司内部的推送服务,要负责进行uid、deviceId到集成商token 的转换。

3、整体架构设计

如上图所示,推送系统整体上是一个基于队列的流式处理系统。

上图右侧:是主链路,各个业务方通过推送接口给推送系统发推送,推送接口会把数据发到一个队列,由转换和过滤服务消费。转换就是上文说的uid/deviceId到token的转换,过滤下文专门讲,转换过滤处理后发给发送模块,最终给到集成商接口。

App 启动时:会向服务端发送绑定请求,上报uid/deviceId与token的绑定关系。当卸载/重装App等导致token失效时,集成商通过http回调告知推送系统。各个组件都会通过kafka 发送流水到公司的xstream 实时流处理集群,聚合数据并落盘到mysql,最终由grafana提供各种报表展示。

4、业务过滤机制设计

各个业务方可以无脑给用户发推送,但推送系统要有节制,因此要对业务消息有选择的过滤。

过滤机制的设计包括以下几点(按支持的先后顺序):

  • 1)用户开关:App支持配置用户开关,若用户关闭了推送,则不向用户设备发推送;
  • 2)文案排重:一个用户不能收到重复的文案,用于防止上游业务方发送逻辑出错;
  • 3)频率控制:每一个业务对应一个msg_type,设定xx时间内最多发xx条推送;
  • 4)静默时间:每天xx点到xx点不给用户发推送,以免打扰用户休息。
  • 5)分级管理:从用户和消息两维度进行分级控制。

针对第5点,具体来说就是:

  • 1)每一个msg/msg_type有一个level,给重要/高level业务更多发送机会;
  • 2)当用户一天收到xx条推送时,不是重要的消息就不再发给这些用户。

5、分库分表下的多维查询问题

很多时候,设计都是基于理论和经验,但实操时,总会遇到各种具体的问题。

喜马拉雅现在已经有6亿+用户,对应的推送系统的设备表(记录uid/deviceId到token的映射)也有类似的量级,所以对设备表进行了分库分表,以 deviceId 为分表列。

但实际上:经常有根据 uid/token 的查询需求,因此还需要建立以 uid/token 到 deviceId 的映射关系。因为uid 查询的场景也很频繁,因此uid副表也拥有和主表同样的字段。

因为每天会进行一两次全局推,且针对沉默用户(即不常使用APP的用户)也有专门的推送,存储方面实际上不存在“热点”,虽然使用了缓存,但作用很有限,且占用空间巨大。

多分表以及缓存导致数据存在三四个副本,不同逻辑使用不同副本,经常出现不一致问题(追求一致则影响性能), 查询代码非常复杂且性能较低。

最终我们选择了将设备数据存储在tidb上,在性能够用的前提下,大大简化了代码。

6、特殊业务的时效性问题

6.1 基本概念

推送系统是基于队列的,“先到先推”。大部分业务不要求很高的实时性,但直播业务要求半个小时送达,新闻业务更是“欲求不满”,越快越好。

若进行新闻推送时:队列中有巨量的“专辑更新”推送等待处理,则专辑更新业务会严重干扰新闻业务的送达。

6.2 这是隔离问题?

一开始我们认为这是一个隔离问题:比如10个消费节点,3个专门负责高时效性业务、7个节点负责一般业务。当时队列用的是rabbitmq,为此改造了 spring-rabbit 支持根据msytype将消息路由到特定节点。

该方案有以下缺点:

  • 1)总有一些机器很忙的时候,另一些机器在“袖手旁观”;
  • 2)新增业务时,需要额外配置msgType到消费节点的映射关系,维护成本较高;
  • 3)rabbitmq基于内存实现,推送瞬时高峰时占用内存较大,进而引发rabbitmq 不稳定。

6.3 其实是个优先级问题

后来我们觉察到这是一个优先级问题:高优先级业务/消息可以插队,于是封装kafka支持优先级,比较好的解决了隔离性方案带来的问题。具体实现是建立多个topic,一个topic代表一个优先级,封装kafka主要是封装消费端的逻辑(即构造一个PriorityConsumer)。

备注:为描述简单,本文使用 consumer.poll(num) 来描述使用 consumer 拉取 num 个消息,与真实 kafka api 不一致,请知悉。

PriorityConsumer实现有三种方案,以下分别阐述。

1)poll到内存后重新排序:java 有现成的基于内存的优先级队列PriorityQueue 或PriorityBlockingQueue,kafka consumer 正常消费,并将poll 到的数据重新push到优先级队列。

  • 1.1)如果使用有界队列,队列打满后,后面的消息优先级再高也put 不进去,失去“插队”效果;
  • 1.2)如果使用无界队列,本来应堆在kafka上的消息都会堆到内存里,OOM的风险很大。

2)先拉取高优先级topic的数据:只要有就一直消费,直到没有数据再消费低一级topic。消费低一级topic的过程中,如果发现有高一级topic消息到来,则转向消费高优先级消息。

该方案实现较为复杂,且在晚高峰等推送密集的时间段,可能会导致低优先级业务完全失去推送机会。

3)优先级从高到低,循环拉取数据:

一次循环的逻辑为:

consumer-1.poll(topic1-num);

cosumer-i.poll(topic-i-num);

consumer-max.priority.poll(topic-max.priority-num)

如果topic1-num=topic-i-num=topic-max.priority-num,则该方案是没有优先级效果的。topic1-num 可以视为权重,我们约定:topic-高-num=2 * topic-低-num,同一时刻所有topic 都会被消费,通过一次消费数量的多少来变相实现“插队效果”。具体细节上还借鉴了“滑动窗口”策略来优化某个优先级的topic 长期没有消息时总的消费性能。

从中我们可以看到,时效问题先是被理解为一个隔离问题,后被视为优先级问题,最终转化为了一个权重问题。

7、过滤机制的存储和性能问题

在我们的架构中,影响推送发送速度的主要就是tidb查询和过滤逻辑,过滤机制又分为存储和性能两个问题。

这里我们以xx业务频控限制“一个小时最多发送一条”为例来进行分析。

第一版实现时:redis kv 结构为 <deviceId_msgtype,已发送推送数量>。

频控实现逻辑为:

  • 1)发送时,incr key,发送次数加1;
  • 2)如果超限(incr命令返回值>发送次数上限),则不推送;
  • 3)若未超限且返回值为1,说明在msgtype频控周期内第一次向该deviceId发消息,需expire key设置过期时间(等于频控周期)。

上述方案有以下缺点:

  • 1)目前公司有60+推送业务, 6亿+ deviceId,一共6亿*60个key ,占用空间巨大;
  • 2)很多时候,处理一个deviceId需要2条指令:incr+expire。

为此,我们的解决方法是:

  • 1)使用pika(基于磁盘的redis)替换redis,磁盘空间可以满足存储需求;
  • 2)委托系统架构组扩充了redis协议,支持新结构ehash。

ehash基于redis hash修改,是一个两级map <key,field,value>,除了key 可以设置有效期外,field也可以支持有效期,且支持有条件的设置有效期。

频控数据的存储结构由<deviceId_msgtype,value>变为 <deviceId,msgtype,value>,这样对于多个msgtype,deviceId只存一次,节省了占用空间。

incr 和 expire 合并为1条指令:incr(key,filed,expire),减少了一次网络通信:

  • 1)当field未设置有效期时,则为其设置有效期;
  • 2)当field还未过期时,则忽略有效期参数。

因为推送系统重度使用 incr 指令,可以视为一条写指令,大部分场景还用了pipeline 来实现批量写的效果,我们委托系统架构组小伙伴专门优化了pika 的写入性能,支持“写模式”(优化了写场景下的相关参数),qps达到10w以上。

ehash结构在流水记录时也发挥了重要作用,比如<deviceId,msgId,100001002>,其中 100001002 是我们约定的一个数据格式示例值,前中后三个部分(每个部分占3位)分别表示了某个消息(msgId)针对deviceId的发送、接收和点击详情,比如头3位“100”表示因发送时处于静默时间段所以发送失败。

附录:更多消息推送技术文章

《iOS的推送服务APNs详解:设计思路、技术原理及缺陷等》

《信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑》

《Android端消息推送总结:实现原理、心跳保活、遇到的问题等》

《扫盲贴:认识MQTT通信协议》

《一个基于MQTT通信协议的完整Android推送Demo》

《IBM技术经理访谈:MQTT协议的制定历程、发展现状等》

《求教android消息推送:GCM、XMPP、MQTT三种方案的优劣》

《移动端实时消息推送技术浅析》

《扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别》

《绝对干货:基于Netty实现海量接入的推送服务技术要点》

《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》

《为何微信、QQ这样的IM工具不使用GCM服务推送消息?》

《极光推送系统大规模高并发架构的技术实践分享》

《从HTTP到MQTT:一个基于位置服务的APP数据通信实践概述》

《魅族2500万长连接的实时消息推送架构的技术实践分享》

《专访魅族架构师:海量长连接的实时消息推送系统的心得体会》

《深入的聊聊Android消息推送这件小事》

《基于WebSocket实现Hybrid移动应用的消息推送实践(含代码示例)》

《一个基于长连接的安全可扩展的订阅/推送服务实现思路》

《实践分享:如何构建一套高可用的移动端消息推送系统?》

《Go语言构建千万级在线的高并发消息推送系统实践(来自360公司)》

《腾讯信鸽技术分享:百亿级实时消息推送的实战经验》

《百万在线的美拍直播弹幕系统的实时推送技术实践之路》

《京东京麦商家开放平台的消息推送架构演进之路》

《了解iOS消息推送一文就够:史上最全iOS Push技术详解》

《基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)》

《解密“达达-京东到家”的订单即时派发技术原理和实践》

《技术干货:从零开始,教你设计一个百万级的消息推送系统》

《长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践》

《喜马拉雅亿级用户量的离线消息推送系统架构设计实践》

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3621-1-1.html

相关推荐

鸿蒙原生即时通讯/IM系统RainbowTalk v2.8版发布,基于MobileIMSDK框架、ArkTS编写

💧 此版本解决了若干bug,以及ui和功能细节优化,同时支持最新鸿蒙7.0系统。

hellojackjiang2011的博客 165

深度剖析IM消息乱序的根本原因及优化方法

本文深度剖析即时通讯IM消息乱序的问题,梳理IM客户端时间戳不可信、多端并发、群消息扇出重试三大核心陷阱,拆解微信、钉钉、企业微信的时序设计方案,并从锚点设计、多端去交错、幂等兜底、可观测性等维度,给出IM项目消息时序性保证的落地优化策略。

hellojackjiang2011的博客 293

IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战

本文将给出一套生产级Web端即时通信方案,包括心跳检测保活 + 指数退避重连,代码直接能跑。

hellojackjiang2011的博客 317

IM全文检索技术(五):vivo敏感词匹配系统的技术实践

本文将从算法选型入手,结合两个实际场景来介绍谛听系统的敏感词匹配算法。包括底层算法选型思路和两种实际场景下提升敏感词精准度、降低漏杀率的方案。

hellojackjiang2011的博客 189

Nginx生产级WebSocket实时长连接负载均衡,一文搞定

本文从基础配置、生产配置、Docker / K8s、安全、排障、性能调优,系统性讲清 WebSocket 在 Nginx 中的正确打开方式。

hellojackjiang2011的博客 308

即时通讯IM离线消息该怎么存?全面盘点主流存储方案

离线消息是IM系统的核心能力,当用户不在线时,消息必须可靠存储,待用户上线后完整推送。本文将对比主流的离线消息存储方案,帮你找到最适合业务场景的架构。

hellojackjiang2011的博客 310

移动端实时音视频聊天框架 RainbowAV v2.2.7发布

1)

hellojackjiang2011的博客 247

都HTML5了,Web端即时通讯技术到底该用什么?一文即懂!

本文分享的是 Web 端最常用即时通讯技术(包括 WebSocket、SSE、WebRTC、轮询等)的原理、优缺点与适用场景等,以及生产环境下的高并发场景下的进阶实践。

hellojackjiang2011的博客 341

基于开源MobileIMSDK框架,即时通讯IM产品RainbowChat v12.1发布

此版本新增了短信验证码的注册和登录功能,解决了Android 16手机上发送文件失败等问题。

hellojackjiang2011的博客 209

IM千人群聊该不该用消息写扩散?详细算算成本账!

本文将为你详解 IM 群聊消息的写扩散技术原理以及写扩散过程中的服务端详细性能成本,同时对比了市面上IM大厂的技术方案,给出分规模架构选型与以及性能优化策略等。

hellojackjiang2011的博客 402

直播系统聊天技术(十):基于实时消息总线的活动系统架构设计

本文分享的是网易云音乐技术团队基于实时消息总线技术,解决了直播活动系统的模块灵活组合、消息治理与异步履约等问题,希望能给你带来启发。

hellojackjiang2011的博客 369

轻量级Web端IM即时通讯框架MobileIMSDK-Web,基于Socket.io库

MobileIMSDK-Web是一套Web端即时通讯框架,超轻量级、极少依赖,高度提炼、简单易用。

hellojackjiang2011的博客 286

socket长连接在手游场景下的技术实践

本文介绍了37手游基于B站goim框架自研长连接系统的实践。系统采用分层设计,支持多协议和发布/订阅机制,用于直播弹幕、实时推送等场景,实现了高性能与业务适配。

hellojackjiang2011的博客 403

基于开源MobileIMSDK框架,即时通讯IM产品RainbowChat v12.0发布

RainbowChat 是一套基于 MobileIMSDK 开源通信框架的产品级移动端IM系统。可能是市面上开放源码的,唯一一款同时支持TCP、UDP、WebSocket三种网络通信协议的IM产品。

hellojackjiang2011的博客 518

社交场景下的统一即时通讯im消息流交互层模块化技术实践

这些IM场景,在消息流的展示形式上是极为相似的,同时每个业务又有着自己特殊的交互需求。基于此,我们对IM消息流能力做了标准化的构建,来减少IM功能的业务接入成本;同时也是为了统一各个业务的技术方案,减少跨业务开发的理解和维护成本。

hellojackjiang2011的博客 571

现代IM系统中聊天消息的同步和存储方案探讨

本文内容主要涉及IM系统中的消息系统架构,探讨一种适用于大用户量的消息同步以及存储系统的架构实现,能够支持消息系统中的高级特性“多端同步”以及“消息漫游”。在性能和规模上,能够做到全量消息云端存储,百万TPS以及毫秒级延迟的消息同步能力。

hellojackjiang2011的博客 75

即时通讯安全篇(十六):对称加密 vs 非对称加密?一文搞懂

本文将以通俗案例讲解加密本质,介绍对称加密(含 AES、迪菲–赫尔曼密钥交换)与非对称加密(RSA)原理、特点及应用,并阐释其数学基础。

hellojackjiang2011的博客 397

网络编程入门如此简单(五):UDP跟TCP相比,到底差了什么?

本文将通俗易懂地为你类比解释UDP与TCP的核心差异,包括如何基于UDP实现TCP的可靠传输:通过模拟三次握手、添加序列号与确认机制解决顺序和丢包问题,利用滑动窗口控制流量,并引入拥塞控制算法来动态调整发送速率等。

hellojackjiang2011的博客 448

基于WebSocket的IM即时通信方案在H5游戏场景下的技术实践

本文将从H5游戏场景下的实时信息更新需求切入,探讨下在类似这种高并发、低延迟的业务需求中,如何使用 IM 方案来解决频繁的数据更新问题,也顺便介绍下 WebSocket 的基本运作流程等。

hellojackjiang2011的博客 1069

鸿蒙Next原生IM即时通讯RainbowTalk,纯ArkTS编写,基于开源MobileIMSDK框架

RainbowTalk 是一套基于 MobileIMSDK 开源通信框架的产品级纯血鸿蒙NEXT端IM系统。RainbowTalk 由纯ArkTS编写、全新开发,没有套壳、也没走捷径,原生“纯血”。

hellojackjiang2011的博客 1227

如何保障分布式IM聊天系统的消息可靠性(即消息不丢)

本文主要聚焦分布式IM聊天系统消息可靠性问题,即如何保证消息不丢失。

hellojackjiang2011的博客 732

如何保障分布式IM聊天系统的消息有序性(即消息不乱)

本篇主要总结和分享分布式IM聊天系统架构中关于消息有序性的设计和实践。

hellojackjiang2011的博客 835

B站即时通讯IM消息系统的新架构升级实践

本文要分享的是B站IM消息系统的新架构升级实践总结,内容包括原架构的问题分析,新架构的整体设计以及具体的升级实现等。

hellojackjiang2011的博客 714

AI大模型爆火的SSE技术到底是什么?万字长文,一篇读懂SSE!

本文从SSE(Server-Sent Events)技术的原理到示例代码,为你通俗易懂的讲解SSE技术的方方面面。

hellojackjiang2011的博客 786

腾讯新闻APP的消息推送Push架构技术重构实践

本文主要分享的是腾讯技术团队近年来对腾讯新闻消息推送PUSH系统做的架构优化和技术实践。

hellojackjiang2011的博客 843

移动端弱网优化专题(十五):字节跳动移动端网络HttpDNS优化实践

本文要分享的是字节跳动团队针对火山HTTPDNS Cache2.0通过自研网段库与动态划分算法,将缓存粒度从“城市-运营商”细化为“网段”,解决了传统方案的城市级调度污染问题。配合缓存分级、预取等优化,在提升调度精准度的同时保证了高命中率,最终实现了服务端调度准确性提升和客户端性能优化。

hellojackjiang2011的博客 720

转转客服IM聊天系统背后的技术挑战和实践分享

在当今互联网时代,高效的用户服务是提升用户体验的关键。转转自研的客服IM聊天系统作为用户与客服沟通的桥梁,承担着传递信息、解决问题的关键角色。然而,消息数据的流转并非一帆风顺,本文将深入探讨IM系统在消息传递过程中遇到的问题和挑战,以及相应的技术解决方案。

hellojackjiang2011的博客 1096

ChatGPT如何实现聊天一样的实时交互?快速读懂SSE实时“推”技术

本文将带你快速认识SSE实时通信协议,包括它的技术原理、常见使用场景、与同类技术的对比以及简单的示例代码等。

hellojackjiang2011的博客 124

全面适配iOS 26液态玻璃,基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v10.2发布

RainbowChat是一套基于开源IM即时通讯聊天框架 MobileIMSDK 的产品级移动端IM系统。

hellojackjiang2011的博客 1157

可靠传输的TCP协议send成功就意味着数据一定发出去了?

本篇文章我们以TCP协议的网络编程逻辑,从Socket缓冲区的角度去拆解,为什么号称可靠传输的TCP协议,在代码中调用send并成功发出数据,并不意味着这个数据就一定通过物理网络发出去了。

hellojackjiang2011的博客 125
上一篇: 阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践
下一篇: 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
jackjiang2025
jackjiang2025 网际时代技术分享账号 网际时代技术分享账号
博客等级 码龄8年 700粉丝 318原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值