在任何涉及交易的业务中,财务对账是一个不可或缺的环节。以笔者所在公司的情况为例,我们的财务系统主要承担了两部分的职责:
根据系统所有交易的信息流,以天为维度计算收入和负债,生成财务报表
将财务报表的数据用于季/年度财务审计 & 与第三方系统对账
以我们测试环境的数据为例,最终需要产出一些类似这样的报表:
那么怎样的设计才能让财务系统适应多变的业务呢?本文就简单介绍下我们在财务系统设计上的实践。
整体架构
在进行具体的设计之前,先来看看我们有什么样的数据,又需要什么样的产出。
我们有什么
这个问题很简单,显然我们有的就是不同业务产生的交易数据。交易数据是我们后续一切计算的源头,所以我们必然需要从不同业务拉取交易数据到财务系统。
我们期望得到什么
前文说过,我们需要的是以天为维度的收入和负债信息
系统架构1.0
那么我们要做的就是设计出这样一套系统,输入原始的交易数据后,经过系统一系列复杂的计算,最终产出当天的收入负债数据,简易版系统架构如下图所示:
当然这个架构还过于抽象,并不能指导我们进行具体的工作,我们还需要对它进行细化。
系统架构2.0
具体到我们的业务,主要包含会员和虚拟币两大类,它们的计算规则是不一样的。所以在计算层,我们会有两套计算规则。同时会员和虚拟币也有一些公共的数据,这部分数据我们拉取一次就可以,此时系统架构演化为:
系统架构3.0
为了更好的描述系统流程,我们抽象了 3 个概念:Job、Stage、Task
Job:一个 Job 是一个完整独立的定时任务,由一个或多个 Stage 组成
Stage:Job 的一个执行步骤称为一个 Stage,Stage 之间有依赖关系,上一个 Stage 的输出是下一个 Stage 的输入,一个 Stage 可以包含多个 Task
Task:最小的任务单元,每个 Stage 内的 Task 没有依赖关系,可以并行执行
同时我们将最复杂的计算过程拆分为了如下 3 个步骤:
聚合层:这一层会按天聚合当天所有需要的数据,同时完成数据一致性校验,解决跨天、补单问题
适配层:定义记账规则,根据记账规则把交易数据转换成可以完成复试记账的数据格式
账户层:根据复式记账规则完成记账
此时系统架构进一步演化为:
之所以这么设计,主要是出于这样的考虑:
隔离账户与交易,核心账户体系保持稳定,变化部分控制在交易
引入借贷记账法,完善账户体系,核心业务需求(报表 & 对账)都可以基于账户数据来产出
财务知识小课堂
为了完善账户体系,我们引入了借贷记账法。为此,这里有必要简单介绍下一些财务上的基本概念。
- 基本会计等式:资产 = 负债 + 所有者权益
在账户结构中,借贷只是一个抽象的记账符号,它有多方面的含义:
表示记账的方向(有借必有贷)
表示资金数量的变化(借贷必相等)
表示账户的性质(借方余额是资产性质,贷方余额是负债性质)
借贷记账法下,存在4种类型的账户:
账户类型 | 借方 | 贷方 |
---|---|---|
资产类账户 | 增加 | 减少 |
负债类账户 | 减少 | 增加 |
成本费用类账户 | 增加 | 减少 |
收入类账户 | 减少 | 增加 |
在我们后续的设计中,只会设计资产类、负债类和收入类账户,暂不涉及成本费用类。
会员
会员是互联网业务中一种常见的虚拟商品,它最基本的特征之一是具备一定的时长属性。比如我们常见的腾讯视频会员、爱奇艺会员等等,在会员有效期内,用户可以享有某些特殊的权益。
在会员生效期内,每生效一天,我们在财务上就能确认一天的收入,会员到期后,则可以确认该笔订单的全部收入。
针对会员业务,我们需要产出如下报表:
其中几个概念解释如下:
递延收入:用户已支付,但是我们还没有确认的收入,也就是我们的负债
预收收入:用户已支付的收入
确认收入:特权已消耗后完成的确认收入 - 已确认部分发起的退款
退款金额:用户发起的退款总额
账户等式:
递延期初 + 预收收入 - 确认收入 - 退款总额 = 递延期末
进一步分析会员场景,会发现它有 3 种行为都会影响我们的财务确认过程,具体的规则如下:
可以发现,其实现金账户和退款账户本质是数据资产账户的不同方向。这里我们为了表达语义上能更加明确而将二者拆开。所以现金账户只有借,而退款账户则只有贷。
另外购买会员、会员按天消耗的场景比较好理解,退款则稍微复杂一些。因为它可能涉及到从确认收入账户扣款的过程。我们按照前面介绍的 4 个 Stage 详细介绍如下:
接入层
接入层没什么业务逻辑,主要就是拉取不同业务方的交易数据落入财务系统,比如会员相关的核心字段如下:
用户ID:userID
订单ID:orderID
会员类型:membershipType
开始时间:beginTime
结束时间:endTime
聚合层
在接入层基础上,针对某一笔具体的会员订单,聚合层需要计算出这笔订单对应的如下 3 个属性值:
现金收入:cashRevenue
累计已确认收入:totalConfirmRevenue
当天确认收入:dailyConfirmRevenue
于是有:递延收入 = cashRevenue - totalConfirmRevenue
适配层
这一层主要就是定义好记账规则,根据记账规则把交易数据转换成可以完成复试记账的数据格式
比如购买会员记账规则如下,它描述了购买会员这一事件影响的账户和方向:
"addMembership": {
"RealCurrencyRules": [
{
"AccountName": "cashRevenue",
"AccountDirection": "debit"
},
{
"AccountName": "deferRevenue",
"AccountDirection": "credit"
}
]
}
而基于聚合层的数据,我们可以知道不同行为对不同账户的影响和金额,还是以购买会员为例:它会影响现金账户和递延账户,影响金额都是聚合层的 cashRevenue。会员按天消耗和退款的过程类似,在此不再赘述。
所以在这一层,我们需要记录的核心字段是事件类型及每一类型的事件对 4 类账户的影响额度:
事件类型:eventType
现金账户变化额度:cashRevenue
递延账户变化额度:deferRevenue
确认收入账户变化额度:confirmRevenue
退款账户变化额度:refundRevenue
"addMembership": {
"RealCurrencyRules": [
{
"AccountName": "cashRevenue",
"AccountDirection": "debit"
},
{
"AccountName": "deferRevenue",
"AccountDirection": "credit"
}
]
}
账户层
在适配层,我们已经知道了不同事件对不同账户的影响额度,再结合事先定义好的记账规则,借贷记账法的基本要素就齐全了,于是在账户层我们就可以按照借贷记账法的规则进行记账。这一层核心是这么几个字段:
事件类型:eventType
账户名称:accountName
账户类型:accountType
影响方向:direction
借额度:debitAmount
贷额度:creditAmount
针对每一种事件类型,我们都可以记录两笔账户明细,基于明细数据,资金的来龙去脉就很清晰了。无论是要出财报还是进行对账,这一层的数据都完全可以满足要求。
虚拟币
虚拟货币也是互联网业务中一种常见的虚拟商品。它的基本业务场景是用户可以使用人民币购买一定数量的虚拟货币,之后在整个系统内,都可以直接使用这种虚拟货币进行各种交易。比如我们熟悉的Q币或者各种游戏币都属于虚拟货币的范畴。
为了方便,我们统一将虚拟货币可以购买的商品称之为兑换物。所以在财务层面,我们不仅需要跟会员一样记录人民币相关的资金流动,还要记录虚拟货币相关的资金流动。
但与法币不同的是,在我们的场景中,使用虚拟币购买兑换物后,此时并不能确认收入,因为兑换物仍然可以理解为是一种负债,只是其表现形式从虚拟货币转换为了某一种具体的兑换物而已。只有在用户消耗兑换物后我们才可以进行收入的确认。因此虚拟币的记账规则较会员复杂一些,具体如下:
其中法币层面的记账规则与会员的基本类似,但虚拟币层面,多了一种负债类账户,不难发现,虚拟币兑换和消耗兑换物两种行为合并起来就相当于会员的按天消耗,只是将确认收入这一步做了进一步的拆分而已。
有了这个规则,再结合我们之前的架构设计,除了记账规则略有差异,虚拟币的记账过程与会员就大同小异了。
总结
本文主要介绍了我们是如何通过引入财务中常用的借贷记账法来设计一个清晰、符合财务需求的财务系统的。但在财务知识方面,我们还是不折不扣的外行,设计过程中难免还有不合理之处。随着对财务了解的逐步深入,我们也会对该系统进行迭代,以期更符合财务上的一些规范和设计。