账务实时交易系统设计思考-【第三节】-功能设计
【思考点滴】
作者 : 杨考 微信号 : devin_cn_hd_09_16
本文是【讲解篇】和【技术分享篇】结合起来,由于****文章图片丢失,又补了一次图片。同时进行了章节拆分。
全量版 https://blog.****.net/yk200808/article/details/80755459
第一节:业务简介 https://blog.****.net/yk200808/article/details/81624677
第二节:业务分析 https://blog.****.net/yk200808/article/details/81624779
第三节:功能设计 https://blog.****.net/yk200808/article/details/81624826
第四节:热点问题 https://blog.****.net/yk200808/article/details/81624861
第五节:准确性 https://blog.****.net/yk200808/article/details/81624899
第六节:使用建议 https://blog.****.net/yk200808/article/details/81624917
第七节:思考总结 https://blog.****.net/yk200808/article/details/81624934
3. 功能设计
设计的几个原则,
1) 功能、准确、实时是基础,
2) 性能、效率是关键,
3) 健壮和可扩展是平台化所需。
3.1 接口数据设计
3.2 资金操作
资金操作,用来承接业务的配置,生成资金流转状态的记录,保证资金准确分配。
3.3 数据记录
对内使用:生成交易明细、提供交易记录实时查询
对外输出:为对账、查账、打款等提供数据支持
3.4 算法选择
3.4.1数据唯一性(四元组)
数据唯一性是数据准确性的保证
数据唯一性是数据关系、数据关联的前提
账户操作的唯一性实现,如下四元组详述
1)订单ID 如用户订单ID,商户订单ID,物流订单ID,各业务选择自己的订单ID,订单不一定局限在百度外卖,可以是任何平台的订单【如下示例的 order_id】
2)业务描述 如"百度外卖用户订单","百度外卖商户订单","百度糯米", ... 通过业务准确描述,可以完成不同公司不同业务的订单描述和区别 【如下示例的business_desc】
3)账户ID 平台为主体创建的账户 【如下示例的 account_id】
4)账务资金操作描述 结合业务场景和财务需求进行规定,如"点击消费扣费","转账入账","转账出账","充值入账" 等 【如下示例的 trade_desc】
订单ID+业务描述,保证整体账务系统中,业务订单ID的唯一性
账户ID+账务资金操作描述,保证同一账户ID在订单中多次出现时的资金操作唯一,(如上 3.3.1 中列举的账户 1111111111777777,同一个资金流中,存在多笔不同的入账、出账操作,需要通过账务资金操作描述进行唯一性区分)
保证了资金操作的最小可查询粒度和资金监控的最小粒度
order_no : 业务订单号
order_type : 业务类型,区分业务类型,防止不同业务order_no重复,且方便按业务维度查数据
account_id : 账户ID
op_code : 操作码,去重;该订单该账户的唯一标识;可配置;可控;
如下图示,就是典型的,同账户通过op_code去重的方式。
3.4.2 一棵整树和多可相对子树(绝对父子关系和相对父子关系并存)
绝对父子关系和相对父子关系并存
1) 提升操作效率(有效树在一棵绝对树上)
2) 提供按业务分段存储(查询子树即可)
3) 维护一棵整树(资金流完整统一,资金关系简单化)
4) 子树用来承接不同的业务,即资金流是一棵整树,不同的业务在资金流这棵整树上的一个子树。
3.4.3 数据唯一性支撑了图到树的转换
资金流是有向图,需要完成图到树的转换处理
3.4.4 分账模型表述
3.4.4.1 分账模型
3.4.4.2 分账配置命令格式(分账模型表述)
$postData = array(
'trade_commands' => array( // trade_commands 批量操作方法配置
array( // 第一组分账操作命令
'trade_method' => 'splitAccount', // 金额操作方法-分账
// order_id, business_desc, account_id, trade_desc 四元组,本次操作出账的唯一标识,在整个数据表的唯一标识
'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单
'business_desc' => '百度外卖用户外卖订单', // 业务描述
'account_id' => 98989777783780001, //余额出账账户的账户ID
'trade_desc' => '用户下单', //金额操作描述
'amount' => 1200, // 出账金额,单位(分) 1200=2500+1000-1500-800 会校验资金平衡关系
'remark' => '商户接单', // 备注信息
'split_formula' => array(// 入账账户信息配置
// order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识
'55555666666666' => array( // 入账账户1的账户ID,以及配置信息
'order_id' => 'shopOrder_jljljljlljl', // 商户订单ID,没有生成商户订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖商户外卖订单', // 业务描述
'account_id' => 55555666666666, //入账账户1的账户ID
'trade_desc' => '商户营业输入', //金额操作描述, 如果账户 55555666666666 在分账流中只有一次流入,则这里trade_desc可以不设置,如果有多次,则trade_desc不能重复
'amount' => 2500,
'remark' => '',
),
// order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识
'222222233333333' => array( // 入账账户2的账户ID,以及配置信息
'order_id' => 'logistics_5245787854745', // 物流订单ID,没有生成物流订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖物流冻结账户', // 业务描述
'account_id' => 222222233333333, //入账账户2的账户ID
'trade_desc' => '物流接单', //金额操作描述, 如果账户 222222233333333 在分账流中只有一次流入,则这里trade_desc可以不设置,如果有多次,则trade_desc不能重复
'amount' => 1000,
'remark' => '',
),
// order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识(本资金流中,1111111111777777两次操作的trade_desc不同)
'1111111111777777' => array( // 入账账户3的账户ID,以及配置信息
'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖新用户补贴', // 业务描述
'account_id' => 1111111111777777, //入账账户3的账户ID
'trade_desc' => '新用户补贴出账', //金额操作描述, 如果账户 1111111111777777 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同
'amount' => -1500,
'remark' => '',
),
// order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识(本资金流中,1111111111777777两次操作的trade_desc不同)
'1111111111777777' => array( // 入账账户3的账户ID,以及配置信息
'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖新用户补贴', // 业务描述
'account_id' => 1111111111777777, //入账账户3的账户ID
'trade_desc' => '优质用户嘉奖', //金额操作描述, 如果账户 1111111111777777 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同
'amount' => -800,
'remark' => '',
),
),
),
array( // 第二组分账操作命令
'trade_method' => 'splitAccount', // 金额操作方法-分账
// ....
),
),
);
3.4.5 资金关系存储算法选择:
3.4.5.1 资金关系存储模型
3.4.5.2 资金关系存储示例:
array(
// 资金交易上游账户唯一信息
'absolute_parent' => array( // 绝对上游信息
// order_id, business_desc, account_id, trade_desc 四元组的唯一性
'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单
'business_desc' => '百度外卖用户外卖订单', // 业务描述
'account_id' => 98989777783780001, //余额出账账户的账户ID
'trade_desc' => '用户下单', //金额操作描述
),
// 本业务资金的入口,在本业务属于分账根节点,相对上游为0,方便业务块操作
'relative_parent' => array(), // 相对上游信息
'amount' => 2500, // 分账金额 2500 = 2300+200
'is_split' => 1, // 0:可以分账 1:已分账
'child_details' => array( // 绝对下游信息
// order_id, business_desc, account_id, trade_desc 四元组的唯一性
'8888888888881111' => array( // 入账账户
'order_id' => 'shop_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖商户净收', // 业务描述
'account_id' => 8888888888881111, //入账账户3的账户ID
'trade_desc' => '商户净收入', //金额操作描述
'amount' => 2300,
'remark' => '',
),
// order_id, business_desc, account_id, trade_desc 四元组的唯一性
'8888888888883333' => array( // 入账账户
'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖商户抽佣', // 业务描述
'account_id' => 8888888888883333, //入账账户3的账户ID
'trade_desc' => '平台抽佣金额', //金额操作描述,
'amount' => 200,
'remark' => '',
),
)
);
基于四元组唯一性的定义,每笔账户操作在整表中都是唯一的,因此可以**交易资金流向的网状关系(有向图),实现树状结构关系存储。
1、根据四元组唯一性,入账账户可以快速查到上游出账账户,且双向关系唯一
2、根据四元组唯一性,出账账户可以快速查到资金流入的所有账户,且出账账户和每个入账账户之间的关系唯一
优化点:建立绝对上下游关系和相对上游关系
绝对上游:跨业务账务的上游账务节点是一个真实的唯一节点
相对上游:每个业务入口账务节点的上游账务节点是0,即该节点是本业务的入口节点
3.4.5.3 账单格式
array( // 账单1
'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单
'business_desc' => '百度外卖用户外卖订单', // 业务描述
'account_id' => 98989777783780001, // 余额出账账户的账户ID
'trade_desc' => '用户下单', // 金额操作描述
'flow_type' => 'out', // 资金出账
'trade_type' => '交易出账', // 交易类型
'amount' => 2500,
'remark' => '',
);
array( // 账单2
'order_id' => 'shop_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖商户净收', // 业务描述
'account_id' => 8888888888881111, //入账账户3的账户ID
'trade_desc' => '商户净收入', //金额操作描述, 如果账户 8888888888881111 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同
'flow_type' => 'in', // 资金入账
'trade_type' => '交易入账', // 交易类型
'amount' => 2300,
'remark' => '',
);
array( // 账单3
'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID
'business_desc' => '百度外卖商户抽佣', // 业务描述
'account_id' => 8888888888883333, //入账账户3的账户ID
'trade_desc' => '平台抽佣金额', //金额操作描述, 如果账户 8888888888883333 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同
'flow_type' => 'in', // 资金入账
'trade_type' => '交易入账', // 交易类型
'amount' => 200,
'remark' => '',
);
3.4.7 资金流按业务分区块
1、方便按业务块:每个业务作为一棵子树,存在于整体资金流中,方便资金按业务区块的简易、高效管理
业务需求:方便每个业务管理本业务的资金
2、方便完整资金流查询:所有业务共用一棵资金流树,整体资金流可以方便查询,且资金关系完整易维护。
财务需求:一棵完整的资金流树,方便财务跟踪整体资金流向
3.4.8 资金操作接口API
3.4.8.1 完善的API接口
入账、出账、分账、调账和撤销等,实现资金操作接口统一化,
3.4.8.2 规范、丰富的交易类型
交易类型场景化和统一化
交易类型可定制
丰富交易类型,实现按类型归纳、追踪、查询和统计等
3.4.8.3 资金分配状态机
资金操作API的背后,是规范化了的资金分配关系数据
资金分配关系数据,可以确保资金的准确分配
记录资金分配的完整过程
资金分配状态机的载体
同时通过资金分配关系,可以复盘、回放资金交易的完整过程
3.4.8.4 资金撤回
1、支持取消任意账户分账(单节点取消分账)
2、支持递归取消任意账户下的分账(按业务区块递归取消多节点分账)
3、支持递归取消整个资金流的分账(取消整棵资金分配过程)
3.5 资源管理
支持批量请求:支持单个请求处理,也支持批量请求处理,
统一事务管理:同一个请求中,无论是包含单个请求,还是多个请求,有一个请求失败,整个操作都需要回滚
统一资源管理:其中包括数据库连接、账户锁等资源,操作串行化,数据锁互斥。
统一异常处理:错误抛异常,异常格式统一化...
3.6 设计回顾总结
赘述一下交易回放的实现 : 所有的分账,都有唯一的操作记录,分账数据不复用不重用,这是交易记录可回放的前提,同时也是业务正确性的支撑(需要清除无效的历史数据)。
账户是一个概念,账户可以承载补偿、积分、优惠券等特殊的资金。
上面说了一大堆,如下图示简单总结,小清新一下。