探索支付宝账单的技术实现

4,688 阅读11分钟

2017年度的支付宝账单果然不负众望,再一次刷屏了。

回顾一下这个年关,现象级的刷屏活动就有三起:

秀“18岁”;秀网易音乐歌单;秀支付宝账单。

一位网友调侃道:2018年大型“相亲”节目就此拉开帷幕……

秀“18岁”,看颜值

秀网易音乐歌单,晒品味

秀支付宝账单,炫财富

“相亲”的三个重要指标不就都齐了吗?!

玩笑归玩笑,身为一名程序员的我,“职业病”又犯了。就像微信出品的“跳一跳”小程序,各种外挂,刷分攻略络绎不绝,占领各大技术媒体头条。

于是乎,我决定探究一下支付宝账单背后的技术实现。但这并不意味着我会再造一个支付宝账单出来,毕竟这份账单核心部分是背后的海量消费数据,这并不是我等刁民能获取得到的。

由于个人经验水平有限,本文所作猜想如有不足之处,敬请指正。如果有蚂蚁金服的同学愿意分享账单背后的技术架构,想必也是非常受欢迎的。

本文是对支付宝账单技术实现的个人猜想,我将此简单粗暴的分成了两部分:前端后端。由表及里,从看得见的前端展示来推测看不见的后端逻辑。

一、寻找突破口

整个探索过程的第一步就是找入口,我认为这是一个非常关键的突破口。我将账单通过钉钉分享出来,然后进入钉钉的PC版,右键那条分享记录,即可复制整个URL。

dingding

为了方便讲解,这里把整个URL放出来给大家看看:

https://render.alipay.com/p/s/i/?scheme=alipays://platformapi/startapp?appId=68687017&showOptionMenu=NO&allowsBounceVertical=NO&transparentTitle=auto&bizScenario=Share&url=https://render.alipay.com/p/f/fd-jbg7if4k/index.html

整个scheme参数的作用就是会去尝试打开手机上的支付宝应用,这对于做移动开发的同学来说是很容易看懂的。当然这种尝试不一定都是成功的,这要看浏览器的安全策略了,以下是在Safari(iPhone)和Chrome(PC)中打开的结果:

scheme

scheme自带了6个参数,这都将会传入到APP中,执行相应的操作。本以为每个用户分享出来的scheme参数会有所不同,但是经过我一番对比后,发现都是同样的参数。经过后面对源码的一番研究后,发现「bizScenario」参数会不同,因为我是从钉钉分享找到突破口的,所以bizScenario的值都是Share,但如果是从其他渠道打开的,取值就不一定是一样了。

至于用户信息,应该是在页面中通过js与native进行交互获取的。如果我们想看最终的账单页面的话,直接把scheme当中的url参数copy出来,在浏览器中打开即可。

至此,我们就打开了一扇探索支付宝账单的大门了。

二、前端页面实现

首先需要明白的是支付宝账单是一个Single Page Application,换言之,就是一个由html+js实现的单页应用,我了解到的是静态背景图片+过场动画+数据图层。这样做不仅有效解决了跨平台问题,而且更有利于传播。

值得一提的是整个应用的css和js文件,包括资源请求url等,都做了一定的加密和混淆,要想读懂,还是有一定困难的,尤其是js代码。

静态背景图片总共有9张,我这里放三张,大家可以感受一下。

这里写图片描述
过场动画其实是一系列的mp4文件组成的,放在video标签里播放,篇幅有限,这里只放一段视频,让大家感受一下。

至于数据来源及其图层都是通过js来完成的,下面的截图来自chrome控制台,展现了页面的主体DOM元素

DOM

大致分为了三个部分:加载,账单主体内容,错误提示

加载过程中主要有两个元素,一个是加载动画,其实就是一个翻日历的gif动画;另一个就是进度百分比,这个不用细想,肯定做的是假的进度指示。通过**「AlipayJSBridge」**,委托Native APP发送账单数据请求,在这个过程中,进度指示按照一定的速率增长,大概是到了97%的时候会停下来,直到数据获取完毕了再正式进入账单页面。

账单主体内容由三部分组成:第一个是swiper,滑动屏幕切换场景,其下有9个子元素,9张静态图片,分别对应了9个场景;第二个和第三个部分则是两个video标签,分别播放下一个场景和上一个场景的mp4文件。

错误提示的部分没什么好解释的,一行提示文字,一个重试按钮,一目了然。

有读者可能会问,这些图片、视频之类的资源怎么得到的?这是我接下来要讲解的内容。

先来看一段数据结构的定义:

// 静态资源。
window.resource = [
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/epRpbpBcCZIKasROmxcL.jpg",
        "video": {
            "forward": "",
            "back": ""
        },
        "__key": 9
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/epRpbpBcCZIKasROmxcL.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/KwIPQNAHVfCMQSToOqxX.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/zRAHVrBufLRAlmOMwXgA.mp4"
        },
        "__key": 1
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/ALzmXZZYrnFDqYFFrGjY.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/QuZvhHxfIyRqgNxGQRqq.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/zRAHVrBufLRAlmOMwXgA.mp4"
        },
        "__key": 2
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/ZcmJnyzRQNuFspDzfoxX.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/CLryDglMNEQLfDxmYnUW.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/nmqWCcwURUxRdUqgdJje.mp4"
        },
        "__key": 3
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/qQLBJCGEDtXCoCiOtPzc.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/TUKbyXyonamHXwQifpDQ.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/rwcPCPShQgxvVYfobSeU.mp4"
        },
        "__key": 4
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/zQzcNkGFJJqHinrABCDa.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/OBocUmcqGaHuZJJkNQvV.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/HbyqRtKZMlKFQZcxwCJy.mp4"
        },
        "__key": 5
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/JzzNsINqFRVOCAMNJonO.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/kBYTpmZElHykGKYytIIG.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/zvFLfvTTgXdUUCFHuIlv.mp4"
        },
        "__key": 6
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/vOpPBFXzjbvlSAxZBcSB.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/dIPIDFjkwlJkxwjbtmRO.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/VIbUIjvACyUlBSVULXbB.mp4"
        },
        "__key": 7
    },
    {
        "scene": "https://gw.alipayobjects.com/zos/rmsportal/HJUaMfRgdvNwrxJgibsJ.jpg",
        "video": {
            "forward": "https://gw.alipayobjects.com/os/rmsportal/egQUtbPUbknxskiWkGgU.mp4",
            "back": "https://gw.alipayobjects.com/os/rmsportal/JlBuZvsTbtOGIiUDNTuW.mp4"
        },
        "__key": 8,
        "poster": "https://gw.alipayobjects.com/zos/rmsportal/guBrTMglMdSRsWcRnaXB.jpg"
    }
];

这段代码出自账单页面,从Chrome控制台里复制出来的。

scene 为静态图片,用作背景;forward 为前进动画,back 为后退动画;poster 似乎意义不大(从实现上考虑),可以认为是对scene的“备胎”;至于__key字段最值得斟酌,在以下给出的代码中,会有我对此字段的理解。

特别注意一下最后一组场景对象,forward 里面的视频内容在整个账单似乎都没有出现过,一个不小心就让我看到了动画制作的外包商名字了,这是程序猿哥哥加的“鸡腿”吗?

还需要注意的是 forward 和 back 的取值含义。forward 字段的取值含义还是比较好理解,就是当前场景滑向下一个场景所播放的过场动画,而 back 字段的取值含义稍微有点绕,我直接举个例子:__key=3的场景对象中,back字段记录的是回退到__key=2的场景的过场动画。

因为js源代码被混淆得实在是没法看懂了,只能根据交互的效果来猜想代码实现。以下是场景切换的代码:

//此处也可以直接赋值为0,猜想__key的作用,为了用上此字段
var index = window.resource[0].__key;
var len = window.resource.length; 
function next() { 
    if(index == window.resource[len - 1].__key) return; //at the last page
    index = (index + 1) % len; 
    var resource = window.resource[index]; 
    //1. play video in resource.forward. 
    //2. video player listener binding. 
    //3. to display data with animations after forward video completed. 
}
function previous() { 
    if(index == window.resource[0].__key) return; //at the first page 
    var resource = window.resource[index]; 
    //1. play video in resource.back. 
    //2. video player listener binding. 
    //3. to display data with animations after back video completed. 
    //4. set value for index variable.
    index = (index == window.resource[1].__key) ? window.resource[0].__key : index--; 
}

主要是通过对len取模,对场景进行前后切换,这样做可以达到循环播放的效果。对于__key字段的猜想也在代码中体现了,值得一提的是,在所有的js源代码中搜索了一番,并没有发现有任何地方用到了这个字段,不知道是不是被加密混淆了。

至此,关于前端的页面展示部分的介绍就结束了。

三、后端数据整合

这部分内容将重点介绍支付宝账单数据的形成,纯属个人对支付宝技术架构的了解进行猜想的,并不代表是真实的运作情况。

架构图

以上是我认为比较合理的架构图,架构的视角放在了Data层面。

1、Database,这一个层次表示的是云数据库集群,整个集群中的数据库极有可能是异构的,如MySQL,Oracle,PostgreSQL,MongoDB等等,此外,这里所说的集群也涵盖了淘宝,天猫,支付宝等阿里体系中的产品所使用的数据库,所以这一部分承载了较多的数据输入输出的工作,至关重要。

2、DW,Data Warehouse,即数据仓库。其中重要的数据来源是云数据库集群,也会有一些直接来自文件。在数据仓库里面能实现的功能就非常多了,其中当属ETL工作,这也是BI的必经之路,配合Reporting System,可以实现数据可视化,日志分析,运维监控等功能。

3、MaxCompute,这个其实是属于阿里云的一个大规模分布式计算平台,其中以Hadoop、Spark为代表的分布式计算框架,Hadoop擅长离线计算,Spark则可以完成快速实时计算。

4、DRDS和REST APIs。DRDS同样也是阿里云出品的数据库中间件产品,上述提到过云数据库集群是异构的,必须有一个中间件参与数据的读写工作。至于REST APIs,主要是提供一些列的API,以便客户端进行数据操作。

解释完了整张架构图后,我再进一步将整个数据请求流程梳理一遍。

1、数据的产生。主要是用户2017年度的消费记录,来自天猫,淘宝,支付宝,蚂蚁信用等平台,这些数据大部分被结构化的存储在了数据库集群中;

2、年度账单数据的生成。将用户2017年度的消费数据导入到数据仓库中,经由分布式计算平台离线计算出每位用户的账单数据,将这份结构化的账单数据再放入数据库集群中。这里使用离线计算是比较明智的,毕竟数据都是PB级别的,实时计算也只能针对个别用户,不然的话,会对用户体验造成一定的影响。这部分的工作,简单地说,就是写若干个MapReduce任务,在分布式计算平台上跑2~3天应该就差不多了;

3、数据获取。到这个步骤,说明账单数据已经准备就绪了,客户端只需要调用API即可获取,也就能做出我们现在所看到的账单页面了。

四、一点小改进

基于对前端展示的研究,我才做出了上述架构的猜想,但这并不是我第一直觉的产物。我一直认为像支付宝账单,网易歌单这类年度盘点的营销活动,可以使用「页面静态化」技术。当然,这样的架构也是有利有弊,先看一张改进后的架构图。

页面静态化技术

可以看到,前端和后端可以说已经处于一个高度解耦的状态了,后端只负责账单数据的生成,并填充好用户的账单页面,而前端访问指定的静态HTML页面即可。针对这个架构,我们来讨论如下三个问题:

1、html文件命名方式。这个想象空间还是很大的,规则也各式各样,比如简单粗暴地将用户id,生成时间等元素进行Hash。当然,对于文件目录也是有要求的,这里就不再深究了。

2、页面静态化技术选型。理论上,最佳的选择就是CDN技术,这方面的技术在市面上已经比较成熟了,可以放心使用。如果不用CDN的话,那可以考虑利用squid,做一个缓存代理缓存服务,可以认为是精简版的CDN,如果只需要内容分发而不考虑其他更高级的功能,squid不失为一个好的解决方案。

3、适用场景分析。页面静态化最吸引我的地方就是减轻了大量后端数据访问的压力,将压力转移给了CDN,但是大可不必担心,因为这是CDN的长项,实现成本低,不易触及瓶颈,此外,没有额外的网络数据访问,不仅不会暴露API,有一定的安全保障,前端页面也可以做到秒开,给用户带来了绝佳的体验。因此,既然是页面静态化,那么肯定就不适合那些页面频繁改动,或者有强交互的场景。

本来这次支付宝账单页面完全可以静态化,后来发生的「授权协议门」事件让我打消了这个念头,这个小插曲的出现就意味着需要将之前生成好的页面全部失效并整改,又会引起一大波流量,也会引起存储空间的浪费,除非是替换之前的文件。不过细想一下,支付宝账单页面嵌入了动态授权,也就不好做页面静态化了。

五、总结

本文从前端到后端两个层面,对支付宝账单的技术实现做了一次非常浅显的剖析,对于一些无法得知的技术细节,也给了一部分自己的实现思路。如果读者看了这篇文章之后,对此也非常感兴趣,也可以针对这个话题发表自己的想法。

技术汇

每日干货分享,传递互联网世界有价值的讯息,微信公众号:技术汇