5

系统之间的数据对接和传输,产品经理视角的万字总结

 3 years ago
source link: http://www.woshipm.com/data-analysis/4345387.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

编辑导语:一个孤立的系统即使录入了再多的数据,其本身的作用也是有限的,只有和其他系统产生关联,互相之间进行数据对接和传输,才能发挥其真正的功能和作用。本篇文章中,作者为我们分析总结了数据传输的场景和意义、数据传输的方式、数据传输的处理机制以及数据传输的注意事项。

imii63F.jpg!mobile

一个系统装再多数据,不与其他系统交互,那也是孤岛系统。一个系统若很外向,不断撩拨周围的系统,也乐意被撩拨,成为了众系统中的“交际花”,那么这货基本就是中台的性质。

而更多的系统是介于上述两种极端之间的,像人一样,自己搞生产,也要参与社交——就是系统之间的数据对接。对接的本质是为了实现数据信息的传输,在后端产品的世界里,各子系统之间,或与外部系统之间的对接非常常见。

作为产品经理,不仅要知道数据从哪来,还要理清楚获取数据之后的握手方式、运算逻辑、异常规则、容错机制、数据日志等等。

本文尝试聊聊如下话题:

  • 数据传输的场景和意义
  • 数据传输的方式
  • 数据传输的处理机制
  • 数据传输的注意事项

一、 数据传输的场景和意义

1. 数据传输的应用场景

  • 前端和后端本身无时不刻的数据互动;
  • 公司的各个系统之间的信息共享:比如,式系统部署之后,就需要各个系统模块之间进行数据的配合,比如订单系统的库存扣减数据要同步给备货系统进行采购;
  • 与第三方平台的对接:比如入驻第三方销售平台亚马逊之后,店家可能自己需要管理自己的订单,这时候就要从亚马逊平台获取订单数据,也就是抓取;
  • 调用现成的公共插件:避免重复造轮子,市场上很多开放性的功能插件可以调用或接入,比如接入百度地图的API,接入微信小程序的二次开发。

2. 数据传输的意义

  • 不重复生产数据库,避免资源和功能的浪费;
  • 统一数据的维护或生产源头,避免数据不同步:比如同一个公司的两个系统都要用人员信息架构数据,如果各自都能维护,势必出现不一致,也浪费资源;
  • 别人家的数据,自己没办法生产;
  • 复用现成的轮子,API或SDK共享(可能自己也发明不出来)。

二、数据传输的方式

数据传输的方式,作为产品经理我将其分为:接口传输、中间件传输、message方式传输等。散开了说,比如:MQ(队列)、HTTP接口、otter、文件共享传输等,每一种又有细分的方式和适合的场景。

1. 接口

这是一种传统的问答式的传输方式,是典型才c/s 交互模式。相当于一台客户机,一台服务器(注:这里的客户机或服务器根据数据的提供方和接收方相对而言的,并不一定是实际的)。

目前我们常用的http调用、java远程调用、webserivces 都属于这种方式,只不过,不同的就是传输协议以及报文格式的区别。

2I7niyA.png!mobile

1)接口的作用

通过接口,可以调用成熟的第三方功能插件为我所用(一般就是API接口),也可以根据实际需求由开发写具体的接口代码解决具体场合的信息传输问题(一般所说的http接口)。

对后端产品经理来说,http接口的使用场景最多。比如:公司先上线了OA系统,后上线了订单系统,订单系统需要同步OA系统的人员组织结构信息。那么一个可行做法就是OA系统创建一个接口,订单系统请求,获取最新的人员结构信息。

这个笼统的方案描述中,包含了这么些信息:创建接口、请求接口、获取最新信息等,那么分别是什么以及有什么原则呢?下面分别讨论。

2)哪一方负责创建接口?

在讨论需求的时候,开发会问哪方创建接口呢?有时候产品经理只知道需要建接口,不知道哪个系统来建。可以这样理解,如果把数据源比成一缸水,那么接口就像是凿的一个口,口只能是在缸上面的。所以接口必须是在被请求的数据源这边,由被请求的一方定义接口。

注意,这里的数据源是相对的数据源,就是被请求的一方就是数据源方。实际上可能目标数据在请求方。比如例子中也可以是OA系统请求订单系统,但是如果这样的话,接口就是订单系统创建了,因此确切说是被请求的一方创建接口。

通俗的讲就像是求婚:男方去求婚带一百万,女方接到后就把姑娘嫁过去,这是一来一回。女方也可以去求婚,只是是直接带着姑娘去敲开男方的门,而后男方才把一百万送到女方,这也是一来一回。

3)什么是定义接口

定义接口,其实就是定义缸上的出水口。口的大小、滤网、放水的频率等…就是个规则。这个规则约定了哪些数据是需要流过去,以及流过去的条件(像门禁密码一样)。

定义接口就是设定口令、数据范围、推送前的筛选、转化运算规则等,这是接口的核心内容。

4)数据在哪一方做转义?

某些时候,数据从源头到应用端不是原封不动的,而是转化了。比如80分、90分都是及格,可能使用者只需要两个值:及格or不及格。

那么这就涉及到是在接收之前就转化为是否及格,还是接收之后自己转化的问题。考虑的依据主要是:该数据获取之后是否还有其他用处,只要有可能被二次使用,最好是取原数据。

提前转化的好处是,流转的数据会变得简单直接。但是需要注意的是转化后数据量不一定会少,比如:数据源是订单维度的,而目标是转化为订单+商品维度的数据,这就可能一条变多条了。

5)是主动获取还是对方推送

有时候开发还会问是对方推,还是我们主动去取,这就是接口的post/ get方式问题。

get是从服务器方请求数据,post是向服务器方传送数据。前面也提到了,接口交互数据可以是主动推送,也可以是请求获取。

主动推送一般是数据生产方一旦更新,则触发推送,将所需字段对应值传递过去;请求获取就是数据需求方传递请求参数(请求参数一般是若干条件,比如:账号+密码)。数据生产方则按照协议响应,给出满足条件的数据到请求方(也就是返回参数)。

所以可以看出来,如果对时效要求高的,则建议生产方主动推。比如产生一个新用户,那么就可以理解把用户的信息主动推送给运营方使用。如果是时效不高或者数据量大,则可以按一定频率主动请求,有利于系统负荷压力稳定。

在具体使用的时候,如果你对接的系统比较多,那么建议做一个公共接口,以后谁想用他们自己来对接就好了,不然就要来一个对接一次,麻烦还有风险。

另外,选择post/ get,最终由双方开发权衡决定,但是一般而言:get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。get安全性非常低,post安全性较高。

6)接口定义是开发的事情,但产品经理需要给出轮廓

在输出方案的时候,接口定义的规则是什么?传参和返回参数是什么?重复传参时是跳过还是再次获取(一般都再获取)?必传参数是什么?是否回传接收结果给数据生产方?这些都是要有大致明确并传达给开发测试的。

比如:每小时/次取对方表中第一页最新的50条数据。超过的数据下个小时继续取,可以这样设计:

7niQzyA.png!mobile

因为一些关键参数牵扯到业务的唯一性维度,这些都在产品经理调研的时候获知的,而这些可能开发根本不知道,因此产品经理要给出轮廓和大概方向。

7)数据流转的时效

接口创建之后,如果是接收的对方数据库中的信息,那么上线之后,要考虑先进行数据的初始化(保持基础数据一致),然后确保后续双方是同步的。

同步的机制和要求是在定义方案的时候就确定的,那么怎么确保同步呢?方法是两种:触发式和定时任务。

  • 触发式:就是一旦一个参数值满足条件,则触发同步;
  • 定时任务式:一般用在不知道数据源什么时候更新,需求方就要设置一个定时任务的脚本,隔一段时间查询一次,请求的频率需要与更新的频率相协调。

8)总结接口的特点

  • 优点:时效性强,可以触发式实时问答。容易控制权限,通过传输层协议https,加密传输的数据,使得安全性提高。通用性比较强,无论客户端是.net架构,java,python 都是可以的。
  • 缺点:服务器和客户端必须同时工作,当服务器端不可用的时候,整个数据交互是不可进行。当传输数据量比较大的时候,严重占用网络带宽,可能导致连接超时。使得在数据量交互的时候,服务变的很不可靠。

9)相关概念扩展

API:即“应用程序编程接口”,是一些预先定义的函数,无需访问源码或理解内部工作机制的细节,即可调用的对象。比如和Windows系统沟通,需要调用Windows提供得API。和新浪微博进行沟通,需要调用新浪微博提供得Api,其实它就是一个软件系统对其他软件系统提供得服务。

open api:是指对外开发的接口,比如百度地图API、facebook的API等。

SDK(“软体开发工具包”):可以理解为api的集合,也就是封装后的API为,功能更完善。

http接口:是基于接口的传输方式(HTTP协议)来命名的,当然也有基于其他协议传输的接口。

比如:

和Windows系统沟通,需要调用Windows的API(CreateWindowEx, bitblt,等等),是C语言函数形式的接口。

和.Net框架进行沟通,需要调用.Net提供得Api,是以C#,VB函数/类形式的接口。和新浪微博进行沟通,需要调用新浪微博提供得Api,是以Http请求形式的接口。

API接口的叫法相对http接口叫法更笼统和概念化一些。因此在写方案的时候,http接口和API接口都可以,在具体的场景开发都可以理解的。

2. 数据库对库同步

接口完成的是信息的传输,相对来说比较保守,易于保护敏感信息。而数据库同步实际就是表对表的共享,相对接口就大方多了,因此多发生在企业内部两小无猜的系统之间。

数据库同步有这么几种办法:

1)使用中间表

例如:B系统要用A系统的数据,可以新建一个数据库DB,A系统将数据写入DB,B系统再到数据库中读取数据。

也就是将数据放进一个中间表中,A、B两个系统都对这个表有访问权限,这样的好处就是选择性地将一大批数据共享出去。

2)直接调取对方数据表

这个方式就是在B系统在开发时,在代码中加载A系统的数据表,直接从数据表中取数据。这就是实时拉取对方的数据,B系统自己本地不做表保存。比较省,事但是耦合性较大,数据量大的时候不建议。

3)同步对方的数据表

直接将对方的数据表copy一份过来,并保持实时同步,otter技术就是常用的一个方法。

otte可以将mysql的数据同步至另外mysql或者oracle,也支持双向同步(即A库同步给B库,B库也同步给A库)、文件同步等,主要应用应用是多数据中心、BI系统抽取数据、灾备。

eueEzmQ.png!mobile

该方式需要DB协助配置。也就是做了一个mysql的同步平台(带WEB管理界面),在界面上,你可以定义相应的映射规则,otter进程就会根据你定义的规则读取binlog,并更新到目标库中去。

该方式主要用于内部系统之间(一般一个公司的才这样)库对库的传输,可以实现数据的相互同步更新。建议应用在数据量大的时候,或者基础数据对周边兄弟系统提供基础支持的时候。优点是占用资源少,交互更加简单可靠。

当连接B的系统越来越多的时候,由于数据库的连接池是有限的,导致每个系统分配到的连接不会很多,当系统越来越多的时候,可能导致无可用的数据库连接。

这时候otter比较适合。而两个不同公司的系统,不太会开放自己的数据库给对方连接,因为这样会有安全性影响。

3. 文件包共享方式

一些第三方公司为了保密,不愿意提供接口,那么会把文件存在类似网盘或网页上,供需求方下载。

双方系统约定文件服务器地址、密码、文件命名规则、文件内容格式等,通过上传文件到文件服务器,进行数据交互,对大数据量的也很适合。这就是一种异步的上传下载机制,双方的操作割裂开,并且一旦上传可以被多个需求方使用。

euu2aaf.jpg!mobile

比如:第三方支付公司与需求方约定好SFTP服务器(一种文件服务器,可以理解为网盘)的账号密码,然后支付公司将账单数据上传到SFTP服务器上,那么需求方就可以登陆SFTP客户端,进行下载、解析,然后保存使用。这也实现了数据在服务器之间的传输。

实际上这些数据本身也是加密的,所以只有协议的公司才能拿到解码钥匙。长期合作的公司就会持续更新,授权的公司就可以持续下载和解析。这里就有一个下载频率的问题,一般使用定时任务按一定频率去下载。并且要考虑丢包的机制。

案例:

到SFTP服务器抓取并解析WP支付平台的账单明细,方案如下:到SFTP服务器找到文件路径,筛选出需要的类型文件,打开文件,按规则解析所需的字段,对应写入本地数据表。

脚本执行逻辑:每次抓取路径下‘修改时间’为前一天的文件。(这里有个隐患:如果出了故障,可能某天的数据就漏了)。

问题:怎么防止丢抓呢?

分析:因为是外部数据,所以这里无法对源数据做“是否被抓取过”的标注,因此建议防丢方案是增加断抓补抓机制。

断抓补抓机制:比如4号抓了修改时间为3号的数据。5号断抓,则6号继续抓取4、5号的数据。断抓补抓的机制就是说一旦某一天的数据中断了,发现不连续,那么系统就自动在下次重新抓一次,看看是否能补上。直到三次都未取到,则不再补救。

该方案可以降低我方抓取故障导致的抓空情况。有时候开发懒省事不愿意考虑这么多,这时候产品经理要提醒他。

4. 消息队列MQ(Message Queue)

1)MQ概念

消息队列技术是分布式应用间交换信息的一种技术。目前市场上有很多开源的jms消息中间件,比如ActiveMQ, OpenJMS。

简单说就是一方不断把信息推到队列中,像排队进隧道一样,另一方依次消费这些信息。消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读走。

该方式更适用于公司内部,数据量大,规律性强,批量往来的数据。主要解决应用解耦,异步消息,流量削锋等问题。

2)以异步处理举例说明

用户注册后,需要发注册邮件和注册短信。传统的做法有两种:

  • 串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。

YZZnMb.png!mobile

  • 并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。

aEzYZb2.png!mobile

假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。

因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈,如何解决这个问题呢?

引入消息队列,异步处理,改造后的架构如下:

3QrYvi3.jpg!mobile

按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。

注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。

在设计方案的时候要注意异常情况的处理机制,比如:首次消费失败?

如果第一次数据消费的时候,无法识别没有匹配上,但是又想下次再消费一次看是否匹配的上怎么办?可以设定机制:无法识别的则重新插到队列后面继续推送。

如果一直循环仍消费不掉信息积压怎么办?

设定处理机制:超过一定积压数据量或者循环时间过长则进行报警。

3)MQ、文件包共享、接口的对比

MQ推送过去之后,是否推送成功无需对方再用MQ返回,因为推到中间站就意味着我方能做的事情已经做完。

而接口是一来一往才知道是否成功的,也就是要返回一个信息。这点与mq是不一样的。但是如果非要对方再返回是否接受成功的话,那么就要做反向MQ,这相当于另一个独立的MQ。

文件包共享也不需要反馈机制,因此传到了文件服务器之后,数据方的事情就做完了。队列的一个信息只能被消费一次,不同系统不能共同消费一个队列。因此如果对接多个系统则要多次创建MQ。而接口可以创建一个,让其他很多系统调取。

在订单系统对接各个销售网站和平台的时候就可以采用这样的机制,避免多次对接。文件包共享也是可以上传一次,供多个需求方下载。这点和接口有相似之处,是MQ所不具备的。

5. 其他手段

数据传输包含了数据信息的获取和写入,其实除了线上的自动机制,还有很多土办法,在后端产品系统中也是常使用的。

1)导入导出

场景:没有办法做系统之间的对接,但是线下能获得数据。数据量不太大,且有规则数据,则可以通过导入的方式。

文档一般用csv格式,该格式文件较小,兼容性好,然后需要定义好excel表格对应字段的关系,比如A列对应字段‘name’,B列对应字段’age’。

上传时需要对文件检验,比如格式不对、必填项为空等,建议一旦一处错误,就全部不予导入。并返回错误提示,修改后继续导入。

若数据太大(与服务器的性能也有关系,比如超过一万条),可以采用异步上传机制,就是上传之后不立即执行写入,而是后台自动分批写入。

2)爬取

作为数据需求方,获取数据可以通过协商接口的方式、SFTP解析的方式、或者直接爬取的方式。

比如需要获取第三方网站商标库中最新商标名称、注册地、logo、授权期限等信息,如果该网站不给于开放的接口授权,可能就需要我们开发写爬虫代码爬取(当然有的商业数据也是带有反爬机制的,这就看谁道高一尺魔高一丈了)。

三、数据传输的处理机制

1. 数据同步的触发机制

前面提到了数据获取的方式,那么数据获取频次或者触发机制是怎么样的呢?这要根据应用场景来设定方案,但是一般都是要求持续获取的。

一种方式是操作事件触发,比如页面的按钮点击则触发传递最新状态。这种的时效性比较高,但是也会由于并发而增加系统负荷。

如果对时效要求不高就可以采用异步机制。比如使用脚本监控。设定脚本的运行频率,当读取到更新时间为频宽内的数据,则将其捕获并传输。定时脚本也叫定时任务等,定时脚本在后端是很常用的。

比如说每次获取A系统6小时内更新的数据,那么每2小时取一次的话是没问题的。但是若每7小时获取一次就会漏掉1小时的数据。因此一定是每次获取的数据时间区间,要高于数据获取的时间间隔。

当然用时间是一种维度,更安全的是用标示性字段。比如每次获取is_got为0的数据。前台是is_got做表索引(索引前面讲到过),这样遍历(遍历约等于全表查询)数据库的时候就不会太慢。

2. 是否异步执行数据处理

如果获取后还要在本地进行规则运算,则最好先落地到中间表,再由中间表写入最终表,也就是异步写入。

比如:按照订单+包裹号维度,从物流系统获取运费到财务系统,然后财务系统再将其分摊到包裹的商品上面,算出每个商品分摊的运费金额。

这时候就很容易出错,因为分摊规则是个算法,算法就带有规则的可变动性。一旦分摊规则的参数不准确,或者算法结构变化,都会导致最终分摊的运费金额错误,那么这时候追查错误原因并修复数据就很麻烦。

所以在进行分摊之前,先落地到财务系统的临时表(中间表)中,然后获取数据完成,再进行写入分摊运费的操作。

除了上述方便查错误原因外(有种数据清洗的意思),这种异步操作同时也确保了较少的偶联,不至于一个环节出错,则联动出错。同时它作为一个基础数据,也可以被其他功能调用。若数据量万级以上的,必须这样做。

3. 判重机制

数据通道搭建好之后,数据流往往是持续获的。而数据源在别人那里,可能会被增删改,因此常常有相似或相关的数据进来。在写入本地表的时候,不管是覆盖、更新还是插入,都是以确定若干字段做为判重的标示为前题的。

比如职工信息表:(姓名+手机号+性别+家乡+身份证号)。(姓名+手机号+性别+家乡)这几个字段对一个职工不一定唯一,但是身份证号就是唯一的。因此如果我们更新这里的数据,就以身份证号为唯一标示。

比如获取到同一个身份证号的手机号与我们的数据库的不同,则更新。遇到我们的数据库不存在的身份证号码则插入。

某些时候无法确定那几个是唯一字段,则可以添加一个备用字段,人为定义其取值规则,然后作为去重字段,比如这个字段叫unique_code,取数据源表的主键+日期,(或者直接就取源表的id,也就是外键)。

有了判重字段(也就是数据唯一的字段),就可以进行更新、插入或者跳过规则设定了。

注意:若一段时间之后,改变了表的去重规则,则需要考虑到历史数据对新数据的影响,因为二者的判重维度不一样,可能会进来和以前的历史数据冲突的交叉数据。

4. 获取到数据之后,如果使用?

一种是直接在页面展示,不保存在本地数据库中。相当于每刷新一次页面则通过接口调取一次对方的数据展示。但这种从性能和场景上都是比较少的,一般都是先保存到本地数据库上,自己本地各种调用。

对于先保存到本地的情况,有两个问题要考虑:是否异步保存,和如何确保同源同步。

5. 处理日志

数据日志:目的是记录数据的来龙去脉,追溯以分析问题,日志要记录三个主要事项:数据源系统是否提供数据、目标系统是否接收到数据、目标系统是否写入了数据。

产品经理告诉开发加数据捕获日志的时候,需要告知是否存到表里,因为系统一般都有一个类似缓存一样的日记,只是会定期清理的,只有保存下来才能一直记录和追溯。

开发后台本身是有数据log日志的,因为log4j开源代码定义了5个主要级别的log:FATAL、ERROR、WARN、INFO、DEBUG,一般情况下,开发都会自觉配置INFO或DEBUG级别的日志,以方便查数据。

但是代码中的kog保存时间不会太长,比如一个月就会清除了。因此如果需要保留的时间长,则可以将其保存到本地数据库。

根据实习需要,存了数据库就可以做成页面,展示给用户看,比如可以从以下维度展示:

NrqiYn2.jpg!mobile

四、数据传输的注意事项

1. 目标数据表最好和中间表的维度一致

假设从A系统获取的数据存入B系统,先落地到中间表b,然后经过一些列运算后将数据从b写入到b’表。注意b和b’表的去重字段要对应起来,并传递下去。因为维度相同,做到一对一,方便实现异常数据溯源。

2. 不同入口写入同一类型数据时,如何与自身入口的数据去重,且与其他入口的数据互相去重?

案例:有新旧两个不同的写入程序,写数据到利润表,写入的都是‘退件入库’利润类型,是殊途同归。不巧的是两个写入入口各自有本身的去重规则,彼此去重的规则不能通用:假设入口1对应的去重字段是A+B,入口2写入的去重字段是B+C。

这就意味着同一个数据如果分多次写入,有可能从两个入口都会写入。如何实现避免重复写入是核心问题。我们首先考虑的是,如果一条源信息从一个入口已经写入了利润表,那么就不能从另一个入口再写。

其次,如果从入口1写入一次,那么后面源数据更新再次触发写入的时候(判重,确定是插入还是更新),就还要从入口1写。也就是一旦从一个入口写入,后面该数据的变更触发的再次写入也只能从这个入口继续变更。

只有这样才能保证这个数据不重复。好比先找到是哪家的孩子,再确定是第几个孩子,且只能是基于这家内部去确认。

方案一:比如入口1遇到一个待写入的数据,则先按自己的去重字段A+B校验。如果发现不存在该数据,则再按照入口2的去重字段B+C(这个事先是知道的)判断是否存在,若也不存在,则回到入口1写入。若存在,则入口1不在写入,且结束进程(因为入口2会触发写入该数据的)。

方案二:比如入口1遇到一个待写入的数据,则先按入口2的去重字段B+C校验。

查看对方入口下是否有重复数据,有,则本入口不写(继续按对方的路径写);无,则自己的路径写。显然方案二的判断路径更短,相对好一点。

3. 同步基础数据的时候是否提前过滤

这个在上面内容中也提到过。比如:A系统维护了员工的基础信息,其中有个状态为【是否有效】,只有有效状态的才能在整个系统中看得到才是生效的。B系统要取用员工信息的数据,但不做数据维护。那么是否只取启用状态的数据到B,还是不区分状态都取呢?

答案是:在数据量差异不大的情况下,取全量。

原因之一就是,若启用状态的用户忽然被A系统禁用,那么可能该用户在B系统的生产数据报错,这时候到中间表看状态就可以看出来问题,而不需跨系统或跨部门沟通查证。

#专栏作家#

唧唧歪歪PM,公众号:唧唧歪歪PM(ID:jjyypm),人人都是产品经理专栏作家,2019年年度作者。《后端产品经理宝典》作者,药学硕士转行互联网产品多年;熟悉跨境电商业务,医药领域;擅长大型后台体系,社交APP。

本文原创发布于人人都是产品经理,未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK