给讲讲大家淘天淘工厂财务开发的相关内容。
财务开发好陌生,是什么?好了,现在假如你五行属商家,并且就在淘宝上卖东西。当消费者买了你的东西,淘宝是不是需要给你结算这笔交易订单的钱,另外淘宝是不是还要收你一笔服务费?那财务开发的一部分职责,就是主要和作为商家的你打钱扣钱打交道的。 另外这里我们不讨论直接扣消费者钱的那一部分,所以我们其实高并发还好,但依然有着不小挑战。
而这里笔者之所以“大言不惭“在叨叨,其一是为了说明财务管理不是简单的报表和 SQL,在业务和技术上都有非常多挑战,其二希望能给各位同学带来一点兴趣,从而可以加入我们一起迎接挑战:)
作者:王艺辉(晓灰)
一、前言
个人吃穿用度、迎来送往,都得花钱。节日红包、工资年终,也记账上。那么一家企业更是如此,新开了什么项目,办了什么活动,哪里该收钱,哪里该花钱,更是一分不能漏,一厘不能错。比如
-
消费者在淘宝上买的东西到了,还是包邮的,他没有付钱,但是你作为商家会被收取物流费、仓内操作费、包裹的材料费;
-
消费者在淘宝上要买冰箱,下单的时候平台顺便送冰格或保鲜膜,他也不会付钱,消费者未必会想到为什么他们可以在一起买一送一件赠品。不过这自然是平台在背后有这个营销活动,而撮合了这笔交易的同时你作为商家要出一笔服务费;
-
再比如消费者买东西需要评价,有时候页面上会提示消费者评价了返红包给消费者,同样地我们需要扣你钱然后再把这笔钱当作红包给消费者;
-
又比如消费者买东西有运费险不需要消费者单独付钱,但是如果是你提供运费险,是肯定要给保险公司打钱的;
-
另外如果是收钱,需要给消费者开发票;如果是打钱,需要给淘天开发票。
诸如此类,而淘天本身也可以理解为一个大的商家,他和你与钱打交道的这个过程可以简单描述流程如下:
报价可以简单理解为我们会在你作为商家入驻平台或者后续报名活动时与商家签订相关协议,明确各项费用的收取标准。那么我们作为财务开发,如何从技术系统上管好公司这些打钱收钱的事?
二、定义问题
2.1 业务问题
✪ 2.1.1 你问:为什么扣我这么多钱?
如前所述,双方会明确报价,但是你怎么知道淘天有没有算错,从而多扣钱呢?所以你有必要对淘天给你的账单,看和你预期的支出是否一致。而且更重要的是,你还得算好账,你要知道对于每个提供给消费者的商品链接目前投入了多少钱、已经赚了多少钱、还有多少钱没到账,不然你如何知道还要不要投入钱到这个商品链接上,或者要怎么继续投入?这个专业一点说,就是你作为商家也有自己的财务团队,他们会通过对公司各项业务数据的统计和分析,计算公司的投资回报率(Return on investment,ROI),定期为公司提供详细的财务报告,以便公司领导层了解公司的经营状况,为公司的发展制定合适的策略。所以我们作为财务开发不仅这个钱不能搞错了或者搞迟了,还得有凭有据--也就是要有清晰明确的账单,以及对应的经营分析数据。
✪ 2.1.2 淘天问:我 ROI 怎么样?
那我们在的独立核算的 BU 业务自然更关注成本支出的细节,计算我们自己的 ROI,出财报以及决策后续如何投资。所以我们作为财务开发需要保证对外对内的数据都一致,并且对内的数据会实际用于分析决策以及稽核。
应收应付 vs 实收实付
另外具体在处理公司财务时,我们还需要区分应收应付和实收实付。
应收应付是指公司在一段时间内应该收到或支付的款项,而实收实付是指公司实际收到或支付的款项。你打给别人钱,他的账户被银行冻结了收不回来,这就是应付而未付;同理,你收别人钱,结果他账户没有这么多钱,这就是应收而未收;而那些没有成功结算的钱我们称之为不可结算。就像商家会问“为什么扣我这么多钱?”一样,我们的财务需要应收应付来看当前公司的财务情况,也要关注不可结算及时追回该结算的钱。
当然,如果财报里只有实收实付本身也是有合规性风险的。所以我们作为财务开发要通过合理的设计,清楚的记录应收应付,并且确保应收应付与实收实付最后能准确对应,避免出现资金的损失。
业财一体
业财一体的概念诞生于国内复杂的商业活动,本身是一个很大的概念,相比字面的简洁,其阐释是复杂的。这里做个人不准确的一些阐述。
-
业:指的是企业所有的经营活动和企业运营活动;
-
财:财务会计(帮企业数钱)和管理会计(帮企业赚钱)。
业财一体的核心目标是建立财务和业务运营两个领域数据的映射关系,从而允许从财务角度及时和准确地解读业务运营中存在的问题。
业财一体目前具体在淘工厂技术领域的正向实现可以简单表示为以下流程
-
收单: 收集基本业财数据;
-
业财链路:补全业财数据;
-
业财集成:生成应收应付单;
-
销账: 根据应收应付单结算。
所以我们作为财务开发要实现业财一体,这样才能解决账单、财报等数据从哪里来的问题。
✪ 2.1.2 淘天问:代码不到位,资损两行泪
而说到资损,就不得不提到一句中国的俗话,“常在河边走,哪能不湿鞋”;或者说外国的墨菲定律,“该发生的一定会发生”。也就是说,代码总有出 bug 的时候、业务逻辑总有少考虑的时候、三方系统调用总会不符合预期的时候等等等等,但是一定不能出资损。
所以作为财务开发的你要如何做到无资损?
2.2 技术难点
说完了上面业务上给你提的问题,实际开发中你就要问这些问题了:
-
业务和财务的数据分散在各处,怎么关联?
-
上百个结算场景,怎么快速接入?
-
上百个账单类目,怎么快速接入?
-
几百亿条账单,hold 得住吗?
-
上百个账单类目,财务时不时要换角度看怎么办?
-
流程这么长,怎么样让所有数据都对得上?
三、核心技术
别一个头两个大了,我们都有解决办法。
3.1 接的快: 淘宝 TMF 2.0
TMF,全称为 Trade Module Framwork,诞生自阿里巴巴交易中台的业务场景。2017 年双 11,阿里的交易峰值达到了 32.5 万笔/秒,同时系统需要支撑全集团几十个事业部的所有交易类需求:要考虑如何能更快响应需求、加快发布周期;如何能为新小业务提供快速支撑、降低准入门槛;是否足够开放使得业务方能做到自助式扩展;新需求是否已经在其他事业部有可复用资产等问题。TMF 核心架构如下:
由此诞生的 TMF 成功为快速接入多个业务流程到平台提供了一套解决方案。我们模拟一下它在财务结算场景下的技术实现流程:
-
物流费的业务方找到你,你按照业财一体的流程实现了一笔交易物流费的收单、业财链路、业财集成与销账。
-
买冰箱送冰格这个营销活动的业务方希望你也找到你,你发现收单的某些信息可以复用,其他流程也是,而销账这个流程几乎整个都可以复用,你想起来设计模式里面的策略模式和责任链模式,于是封装了一系列策略接口,让不同的业务实现不同的策略接口,并通过判断业务身份决定走哪些业务逻辑。
-
业务越来越多,你会发现流程中多出了一个又一个的策略点需要对应的业务方开发实现,这时候业务方需要理解具体的结算流程才能实现这一个个点,接入和 debug 门槛过高。于是你引入了 TMF2.0,将你的策略接口封装入一个个模版类,并开发扩展点,接入者只需要实现扩展点即可,而对于某些必要要指定的逻辑通过模型的校验逻辑、抽象方法等会“强迫”接入方实现。
-
模版类也越来越多,业务方开发仍然需要梳理清楚自己需要实现哪些扩展点,这时你进一步将扩展点的类生成逻辑封装到可交互的 GUI 中(我们是 IDEA 插件),业务方只需要填写一些信息,就能生成对应的类和部分通用代码,这样对于新手他就知道自己要实现哪些扩展点;对于老手也避免了复制黏贴。
-
业务逻辑更加复杂,以至于模版类越写越长,于是可以再用 TMF 将策略接口和模版类都封装成解决某个通用业务问题的产品,让有业务开发者直接以 JAR 包的形式实现产品的特定逻辑,进而启动应用时加载对应 JAR 包事实上我们还没有走到第 5 步。
如此我们就解决了结算场景如何快速接入的问题。
3.2 对的清: 钱账票一体化
就像开篇介绍的报价-计费-结算-账单-发票流程一样,这其中钱账票在业务链路上是天然的顺序关系,所以是否能将这整个过程自动化,即智能感知市场报价,根据交易计费、结算后,自动化地生成账单和发票是一个业务上、效率上和数据质量上都值得思考的问题。特别是数据质量上,只有大家的数据尽可能少的从外部传入,都是在业务流程上自上而下消费,数据一致性的问题才能自然而然解决。钱账票一体化在业务流程上的表示:
✪ 3.2.1 底层数据模型的统一
举个账单中常见的数据模型例子,将其表示为二维表格的形式:
可以看到,除了抽象出的 owner_id、 bill_type、bill_id,对于不同的账单,它们各自的属性并不一致,想要统一这些账单就需要使用阿里云的 TabelStore 的 Schma-Free 特性,即对于 TableStore 的宽行模型来说,每一行都可以有不同的属性列,数据列的个数也没有限制。这个特性很好地满足了账单存储时不同属性列难以抽象统一的问题,同时业务变更时,也可以进行任意的属性列变更。统一后的架构如下图所示,其中选用 hologres 作为聚合数据的数据库是利用了其列存适合 OLAP 的特性。
✪ 3.2.2 明细数据查询、下载的统一
TableStore 支持 JDBC 的语法,我们是可以直接利用已有的 SQL 生成工具如 Mybatis Plus。但其实还有更简便的方法,即因为实际上后续业务查询的都是一张表,所以我们可以实际上写好一个组装查询参数的生成工具类,可以接收 JSON 组装包括不同账单类型在内的查询参数进行查询和导出。以物流服务费的查询为例,JSON 配置如下:
{
"billType":"XXX_FEE",
"billName":"XXX账单",
"fields":[
{
"field":"a_field",
"translate":"a字段"
},
{
"field":"b_field",
"translate":"b字段"
}
],
"search":[
{
"queryType":"TERM_QUERY",
"targetColumnName":"id",
"name":"主键"
}
],
"order":[
{
"orderType":"DESC",
"targetColumnName":"a_field"
}
]
}
这串 JSON 起到的作用生成一个面向 OTS 的查询参数,告诉 OTS 我们要查哪些字段,where 条件是什么,按照什么排序。
✪ 3.2.3 聚合数据模块渲染的统一
对数据看板设计图:
做如下抽象:
抽象出模型后,配合对应的模块加载器,实现输入一段结构体的 JSON,输出前端所需要的所有结构和数据的功能。如此前端的页面结构就可以让后端决定,前端只需要负责对应模块的渲染逻辑即可。通过这样的方式可以大大加速后续新增经典图形模块的接入速度,同时对于页面结构的变动也不再需要麻烦前端反复变更,后端一方就可以控制。此外,除了页面结构本身,页面中的金额名称、图标等也都由后端在 Diamond 中控制,如果有变更,更改起来十分的高效。
如此我们解决了如何快速接入百亿级别账单,以及让所有数据都对得上的问题,特别地,对于财务时不时要换角度看我们还对前端图形做了抽象。
3.3 行的稳: 事前、事中、事后
✪ 3.3.1 事前约定与评估
正式开发之前,每一个业务的接入或变更,都需要经过业务方、产品经理和领域资深同学的评估,并且将结论落实到文档中以便后续查阅和对焦。另一方面,实际开发中,标准化命名和类型约定将节省大量繁琐的工作。
✪ 3.3.2 事中测试、Code Review
测试
在实际业务开发中,账单涉及数据时间跨度长,计费规则也常变,如何既覆盖各种异常 case,同时长期保证 transformation 的数据质量值得思考。而测试是帮助我们将发现问题的时间尽可能提前的好手段,同时也是保障长期不会出意外变更问题的必要手段。不过实际生产中,除了普通的单元测试,我们可能还需要根据业务需要自己开发工具。比如在简单的参数化测试基础上:
@ParameterizedTest
@CsvSource(value = {
"1,2",
"3,4"
})
void should_show_how_to_parse_multi_args_with_csv(Integer in,Integer out){
assertEquals(out,in + 1);
}
相比 @CsvSource 再开发 @JsonFileSource 允许传入 JSON 格式的对象,并将每个历史上变动的计费逻辑都覆盖到,于是我们对数据处理正确的信心大大增加了。
Code Review
CR 的重要性毋庸置疑,不仅是代码上线的最后一关,也是让 CR 双方快速学习的重要手段。在淘天,代码写得好的前辈很多,新人来淘天才能看到他们写的优秀代码,也才能让自己被他们 CR。
✪ 3.3.3 事后自动化测试、资损监控
自动化测试
之前写的测试在实际生产过程中,每次的变更都会触发其运行,从而起到保证原有逻辑不受意外变更、保驾护航的作用。
另外当我们把数据导入数据仓库时,ETL 中的每个步骤中都可能会遇到数据质量错误,比如:
-
与源系统的连接错误,抽取数据可能会失败;
-
由于数据类型冲突,数据转换可能会失败;
-
由于数据生产者新增或变更了存储逻辑,导致处理后的数据异常。
数据任务监控
对于 ETL 过程中不是单点的错误,我们监控一些数据任务的指标,比如数量、特征值、非空值等。而对于长期维护数据消费逻辑的过程中,每一条记录的数据质量保证,最好还得基于约定和测试。
数据核对
核对是对代码执行逻辑是否符合预期的事后保障。比如你认为当前 1 笔交易应该产生 2 笔资金流,你就可以通过对既成事实的交易数和资金流数目进行核对,从而知道事实上运行的系统是否保证了 1 笔交易应该产生 2 笔资金流的逻辑。
如此,我们尽力地保证了没有资损。也就是保证了所有资金流都是准确的。
四、总结
公司的财务管理远不止打钱收钱,远不止业财一体和钱账票一体化,而仅仅业财一体也是非常大的话题,非几本书不能讲清楚。而笔者能力也有限,不能完全保证表达的概念都准确,希望大家批判地阅读。不过笔者简要介绍的我们做的业财一体和钱账票一体化,不知道有没有引起优秀的你们一点点兴趣,快来加入我们吧!
如果你对财务技术感兴趣,欢迎来信邮箱:xiaohui.wyh@taobao.com 多多交流,希望能和你一起共事!