我们接到一个需求,开发一个互联网废旧垃圾回收平台。需求方只是一个想法,技术团队如何落地,不仅仅涉及到系统设计,更重要的是先梳理清业务链路。
业务分析
废旧品处理厂期望直面普通用户进行废物回收,这是一个串行的长链路业务,所以我们以业务参与人员的角度切入分析:
用户Customer
- 用户可以通过微信公众号,手机App,官网进行回收呼叫,另外考虑到部分老年用户不太会用手机,需要同时支持400热线电话
客服CustomerSupportor
- 客服人员一方面对于用户发起的呼叫进行审查,因为回收业务是强依赖地址的,需要确保相应订单尽可能准确,否则直接派送回收人员会造成一些问题。
- 另外一方面,对于直接通过热线电话发起的回收呼叫,我们需要客服人员进行会员注册,订单创建等操作。
- 此外,对于用户投诉,车辆故障等突发问题,需要客服协调解决。
调度员Dispatcher
- 对于回收订单分配,考虑到项目上线时间和初期规模,不会有特别智能的订单分配策略方案(路线规划,订单分派,车辆负载检查等),所以通过调度人员进行订单分派补偿,才能保证业务平稳运行。
- 另外考虑回收后,需要给用户付钱,回收人员需要统一携带现金,并每日进行核对,也需要调度员这样一个和回收员紧密联系的角色进行资金汇总分发等操作。
调度管理员GeneralDispatcher
- 主要是能看到基于地图的全局数据大盘和综合业务数据汇总(例如总车辆,总回收单数,总服务用户等)。
回收员Recycler
- 支持接单,接收审核后调度分派的订单,然后找到用户,审核废旧品,并贴上全局唯一的条码,实现全链路追溯,录入系统相应订单和回收物品,完成2C的付款。
- 支持现场帮用户下单,考虑有些用户看到工作人员后可能会现场回收。
- 支持对订单做一些操作,例如拒绝接收改单,完成该订单,停止接单,汇报车辆负载等。
仓储:需求是打通上下游,那么废旧品集中回收后,需要运送到专门的仓库进行分拣归类,然后统一配送给末端处理企业,这就要求有相应仓储工作人员接入。
仓储_原料入库下货员
- 回收车辆进入仓库后,由下货员进行废旧品卸货,扫描回收品上的全局唯一条码,核对物品类型是否与回收员现场录入的物品类型一致,核对整车物品数目是否和系统一致。之后将物品放入循环传送带上,交由下一个环节处理。
仓储_原料分拣员
- 分拣员按照废旧品类型分组,例如电视机线,冰箱线,电脑线,手机线,纺织品线等。具体操作可以从主传送带上拿取自己负责的物品类型,进行扫码装框(每个框子是国家标准制作的大铁笼),这样我们就知道一个在客户A回收的类型为电视机20寸的物品1001,最终被分拣线工人识别为类型电视机20寸,并装入了31框。当然回收员因为自己的专业知识可能会对物品规格识别错误,这里有可能出现仓库分拣的物品类型与回收员录入的物品类型不一致的情况,我们需要识别分析进行报告。
- 分拣完成后,当一个框子满框,分拣员触发入库操作,会自动打印入库单,并通知叉车员将整框同类型物品送往称重台称重,称重之后放置于指定存储区域,叉车员通过入库单进行搬送业务,并不直接在系统中参与到业务链路中,可以通过人工喊话进行沟通。
仓储_成品入库称重员
- 通过电子秤,直接获取框以及满框物品的总重量,减去框标准重量,就得到了物品总重,称重员扫描框码,确认重量,打印标准入库单,表明框号,批次号,类型,数量,重量并录入系统中,之后由叉车员放入成品存放区。
仓储_成品出库员
- 当运营人员发现某类型废旧品分拣成品达到一定数量,联系相应终端处理企业出库,此时会有一辆大货车,在出库员处录入企业信息,车辆信息,司机信息,生成预出库单,凭借预出库单,前往大电子秤处称空车重量,然后返回仓库出库口,进行出库品装载,每个框出库时进行扫码,这样系统就知道哪些物品,多重的物品,被哪个企业接收走了。
仓储_成品出库称重员
- 对出库车辆进行空载称重
- 对出库车辆进行满载称重
仓储_管理员
- 看到所有物品的仓储情况
- 看到所有仓储人员的处理数据
- 特殊权限针对物品做强制出入库,报废,订正等操作
财务
财务人员
- 全量订单数据
- 全量支付数据
- 全量仓储数据,包括库存,入库,出库,各种进销存
- 定制化报税单
- 全量人员数据
至此整个业务链路基本处理完毕,对于终端处理企业,由于数目众多类型众多,我们无法再进行更深一步的打通。
核心业务模型抽取
账户Account
- 用户,客服,调度,仓储人员需要统一的核身逻辑,所以我们要打造规范的账户体系。账户的核心抽象实际上就是登陆凭证,我们通过Account模型,内含登录名,登录密码,账户类型等关键字段,就可以完成统一核身和权限管控。另外对于不同的业务参与者Customer,CustomerSupportor等,其描述字段很不相同,例如用户有用户昵称,但是对于Supportor没有,但是Supportor却有坐席编号(方便呼叫系统进行热线转接),所以参与者具体模型Customer等与Account以一一对应的关系设立。这样微信公众号中的OAuth验证,我们就会创建WechatUser模型,将其与Customer进行一一关联绑定。这样实现了全站统一的账户体系,Account成为核身领域模型。
回收单RecycleOrder
- 对于用户来说会进行呼叫回收操作,其包含呼叫人,呼叫时间,回收内容等相关成员。
客服工单SupportorWorkOrder
- 为了对客服工作进行审计,以及客服通话记录的管理,我们需要客服工单模型,为什么这部分没有并入订单原因也很简单,一个订单可能有多次咨询,会产生多个客服工单;另外一些咨询投诉,可能并不强关联任何一笔回收单,所以客服工单从产生角度,关联关系,成员数据来看,都应该是独立模型。
回收工单RecyclerWorkOrder
- 用户产生回收单后,实际是需要回收人员上门处理的,订单分配由调度员产生,所以在回收单基础上我们设立了回收工单,其主要属性包括:回收单承担人,回收单创建时间,派送时间,考虑到以后可能会赋予回收员更多的业务处理能力,例如搬家,商品配送,我们希望工单模型相对独立,是对回收单,以后可能产生的搬家服务单,商品配送单的接口模型。
业务端回收物品Item
- 全局唯一可追溯的回收物品模型,包含物品类型,规格,关联订单,处理人等信息
仓储段回收物品Item
- 考虑到面向用户的物品分类和面向仓储末端处理企业的物品分类不尽相同,我们在仓储端需要另一种回收物品模型。
仓储入库单WarehouseInOrder
仓储出库单WarehouseOutOrder
除了上述核心模型外,其它模型例如用户地址Address,车辆Car,车辆位置CarLocation,回收品类Category,SubCategory,规格Spec,城市City,全局唯一框Backet,分拣员等模型其实本质都是对于上述核心模型的描述和补充。
系统划分
我们在进行系统划分时遵循的几个基本原则:
- 方便高效开发。最少的代码,最少的重复工作,最快的产生结果。
- 功能职责单一,高扩展,避免耦合。
- 系统单元与业务单元尽可能保持一致。
- 由复杂到简单,由深到浅。不能只按最简单的来,最简单就是一个web应用 + 一个mysql全部搞定,我们应该按照能想到的最细粒度的方式入手,然后根据实际情况和需求往上层走。
- 梳理整个系统功能边界,哪些功能要自己实现,哪些功能是要调用第三方,这些服务是面向局部的还是面向全局的。
- 迭代。花必要的时间进行多种方案的头脑风暴和不断推演。
- 梳理技术难点。难点有两个角度,一是公认的技术难点,例如分布式环境中保证强一致性;一是团队不熟悉的技术,例如java开发团队面临前端开发任务或者去开发一个windows桌面应用。
- 时刻牢记项目投资规模,初期性能要求以及上线时间,避免过度设计和开发。
- 保持强的领域意识。就是说当我们划分出一个系统后,我们要站在这个系统的内部,作为系统owner思考该系统的边界,那些事情作为系统owner愿意做,哪些事情作为系统owner不愿意做。
- 主导者应预先思考一个设计方案,否则在团队头脑风暴时会容易发生讨论发散,主线不清,结果不明。
- 提取领域模型。通过参与人角度非常有利于分析业务链路和梳理前端入口,但是对于后端系统划分不够深入,所以需要提取领域模型,基于领域模型做后端系统划分更加合适
划分实践
我们的划分过程经历多轮头脑风暴和迭代,具体过程就不详细展开了,直接看看最终的方案
互联网回收系统
- 系统模块
- 呼叫回收模块
- 客服模块
- 调度模块
- 前端:客户app
- 前端:客户微信公众号
- 前端:客户官网
- 前端:回收员app
- 前端:客服Web页
- 前端:调度Web页
- 划分原因
- 基于用户Customer,客服CustomerSupptor,调度人员Dispatcher的行为进行领域模型抽象,回收单RecycleOrder,客服工单CustomerSupportorOrder,回收工单RecycleWorkOrder相互持有,且有很多共同属性例如都是基于Account模型。所以将这三个模块并入一个系统开发,有利于实现代码复用,减少工作量。
- 如果将三者划分成三个独立系统,代价是必须要实现单点登录,即增加一个独立的核身系统AccountSystem,另外三个模块紧密耦合,划分会带来大量的跨系统调用,进而需要引入分布式框架保证一致性和代码解耦,结合项目上线时间,投资规模,和实际使用量来看,不适合划分过细。
- 技术选型
- 因为没有足够的技术资源和专业的运维人员,选择Spring Boot这种轻量级的开发部署框架
- 选择Spring MVC,一方面是有相关开发经验,另一方面阿里系基于Spring框架做到这样一个体量,相关文档和解决方案很多,系统扩展潜力会比较大。
- 此外使用了Spring Security做安全架构LogBack做日志框架,Spring Test做接口测试。
- 因为业务模型梳理较为细致,且考虑了扩展性,例如客服工单独立,回收员工单模型独立,足以支持以后上线的搬家服务,商城服务。我们有足够的信心不会大范围发生领域模型变化,所以采用Hibernate以及Spring JPA来实现持久化层的代码。
- 考虑一期的预期调用量,MySQL足以应对,底层数据存储使用阿里云MySQL双机版。
- 考虑静态资源访问可能消耗珍贵接口带宽,静态资源统一采用CDN存储
- 考虑业务稳定性,灾备,以及线上无感知发布更新,系统采取3机部署,通过前挂LBS实现负载均衡,LBS可以采取nginx或者阿里云现成服务。
- 问题
- 多模块耦合导致系统代码复杂,影响水平扩展,长久来看还是要做更细一步的拆分的
支付系统
- 系统结构
- 对外调用支付宝接口,微信支付接口进行企业=》用户的付款,付款查询等操作
- 对回收系统提供调用接口,回收系统不需要识别支付细节,只需要告知支付系统支付对象,支付金额,支付原因,支付渠道即可
- 划分原因
- 支付系统不同于常见的电商支付系统是用户向企业付款,有问题可以走退款链路。而是由企业账户打款到用户账户,因为回收业务是要给用户付钱。基本所有支付机构都是不可逆操作。而且该接口都是直连企业第三方支付的企业账号,对账号内资金具有全部操作权限,所以该系统的安全要求和门面系统不可同日而语。单独划分系统后,可以通过防火墙,ip白名单等等策略,限制入口调用和出口调用,关闭无关端口,走单独的运维策略,在初期项目技术资源有限的条件下,保证其安全性。
- 回收系统不应当耦合不同支付渠道的实现细节,不应当识别不同支付机构的接口代码,因为其本身承担了三个模块的代码,再耦合支付系统复杂性和维护性就不在一个可以接受的水平上了。
- 技术选型
- Apache Tomcat
- Spring MVC(对业务系统提供基于Http的REST调用接口,并有相应的回调逻辑)
- MyBatis(主要了是为了扩宽技术栈,所以没有用Hibernate)
- MySQL
- 问题
- 基于传统的同步调用异步回调实现,代码不够简洁,还耦合回收系统业务代码(订单查询保证幂等),应该采用消息机制实现更合理的系统调用。
- 数据库独立,在后期数据汇总时,例如一个数据报表需要从回收系统数据库和支付系统数据库同时出,带来了一些复杂性。
仓储系统
- 系统结构
- 仓储操作端:用C#写的运行在windows上的GUI应用,通过DLL对接打印机,通过串口对接电子秤,通过USB对接扫码枪
- 仓储服务端:提供基于Http RESTFul的接口,提供物品入库,分拣,称重,出库等等请求。
- 划分原因
- 从业务逻辑上和领域模型上看,仓储系统相对很独立,唯一与主系统耦合的点在,物品入库时请求主系统获取该物品类型规格回收人员等。
- 该系统涉及到对接其它设备较多,这些技术点与回收系统,支付系统,不相关。
- 仓储系统有特殊的稳定性和私有化部署要求,即使回收系统故障,也不应该影响仓储系统运作,例如回收系统故障,但是仓储系统应该能够正常出入库。
- 技术选型
- Apache Tomcat
- Spring MVC,Shiro,Log4j
- MyBatis
- MySQL
- C#
- 问题
- 客户端采用C#导致扩展升级不变,仓库里投放了多台壁挂式一体机,成本较大,如果采用手机App作为操作员端,会更加合理(类比快递员的手持设备)。我们之所以采用C#一方面是考虑开发成本,C#有拖拽式界面IDE,另外传统的磅秤,打印机对windows有良好的调用支持,如果采用App的话,需要自研中间服务,实现操作端对打印机,电子秤的交互,考虑我们时间资源有限,没有采用,准备在第二个分拣仓库建设时进行重构。
- 因为回收系统Account没有开放出来,导致仓储系统无法实现单点登录,只能自己也存储了仓储人员信息,也重复了一些身份验证代码,但是后期实践时发现仓储的身份核验和回收系统不太一样,管理要求也不一致,所以整体来看,不是特别大的问题。
数据库独立,在后期数据汇总时,例如一个数据报表需要从回收系统数据库和仓储系统数据库同时出,带来了一些复杂性。 - 仓储系统业务模型相对稳定,并发要求不高,应该采用Hibernate进行持久层而不应该采用MyBatis
400呼叫系统
- 划分原因
- 400呼叫系统主要职责是将用户打进的电话分配到7个坐席人员的电脑上,坐席人员通过耳麦和网络与用户沟通完成服务。职责功能非常单一,就是实现电话呼入功能。
- 技术选型
- 目前的坐席呼叫系统本质都是把用户通过手机也好,固话也好,电信会将用户呼入通过专线投递给呼叫设备,呼叫设备下挂语音IP卡,将语音数据转化为可以基于网络传输的数字语音数据,然后定向发给与呼叫设备相连的电脑端,这样达成坐席通过电脑耳麦和网络与用户进行对话。
- 另外一种方案是云坐席方案,顾客本地不需要购买呼叫设备,所有客服人员打开网页,通过flash建立和云中心的链接,云中心部署了呼叫设备完成与电信的信号对接。但是这种云坐席要价比较高,另外稳定性难以保证,与自有系统不好对接,所以我们采用了第一种。
- 挑战
- 呼叫系统接收一个来电,分配好坐席后,我们需要相应的坐席人员能够根据来电号码查找是否注册用户,查找其关联订单,关联客服工单等信息。最终解决方案是,呼叫系统定制化开发,坐席接通后post相关来电信息(来电号码,坐席编号)到回收系统客服模块,回收系统后端与客服前端通过WebSocket保持链接,一单后端接收到来电就发送信息给前端页面,前端页面通过js进行用户信息查询,订单查询等操作并刷新页面。
- 呼叫系统初期只是通过简单的路由设备和坐席人员联通,稳定性很差,因为局域网不同部门公用,经常波动。后来接入另一个地点的外包客服,根本不在内网,带来了更大挑战。这之中,学习了组网知识,购买了企业级路由器,建立了呼叫设备与本地坐席和远程坐席的VPN链路,稳定性安全性得以保证。这中间经历了不知道多少坑,和华为技术支持,电信技术支持,外包坐席网络工程师接连对接了N次,付出很多也学到很多。
- 问题
- 呼叫系统是一个.NetFramework + SQL Server的本地服务器模式,定制化开发比较难,和业务系统对接不容易,例如我们想实时获取一段通话录音,很难从SQL Server的库里导出,制作音频文件,这一步目前往往都是人工实现。另外呼叫系统自带评分机制,这个数据也是需要人工同步。
数据报表系统
- 划分原因
- 我们在分析财务人员和仓储管理员需求时发现,他们的核心诉求是各种维度,各种组合的数据汇总,这些数据汇总没有固定的业务模型对应,另外他们的诉求往往需要我们进行跨数据库的查询,而跨库数据交换在前面几个业务系统中往往通过接口调用实现。
- 基于实际的需求,数据报表在代码逻辑上非常薄,数据层实现多数据源抓取,上面封装一些数据整合,缓存,分页等功能,以标准的json或者map设置就可以直接返回给前端了,前端基于ECharts或者D3或者JQWidget就可以做渲染了,这和其它系统处理链路是很不相同的。
- 另外还有一个原因,我们团队有位同学是前支付宝平台数据部的数仓工程师,SQL能力很强,所以大部分的数据报表,我们能够很快得写出高效的查询语句。
- 技术选型
- Apache Tomcat
- Spring MVC,Shiro,Log4j
- MyBatis
- 多个MySQL数据库
- 问题
- 系统涉及多数据源,涉及到跨库join,如果我们的数据库考虑其稳定性放置在不同的MySQL实例中,是无法直接通过SQL实现一些合并操作的。一期我们通过代码,讲两个库查询到的数据分别缓存起来,然后按照呈现逻辑进行整合,代价是增加了系统复杂性,消耗了报表系统的性能
- 二期,我们做了妥协,将多个系统的独立数据库合并到了一个MySQL实例中,大部分数据集合的操作通过SQL实现。结果是,上层业务系统独立,数据库独立,但是数据服务器不独立。从运维管控和扩展性来讲,都不是很合理。
- 三期,未完成,最终梳理的解决方案是,将数据报表需求分为两部分,一部分是实时数据展示,一部分是非实时数据展示。
- 实时数据展示涉及到跨库join的场景很少,复杂性低,可以通过接口调用缓存本地,然后用代码处理数据合并,和一期保持一致;
- 非实时数据,可以通过数据仓库,每天定时将分布在多个数据库的数据进行抓取清洗,然后存放起来,需要查询时可以直接访问数仓,或者由数仓回写到指定数据库,访问相应的数据库。这样的好处是,数仓借用阿里云彩云间也即ODPS,是不用耦合业务代码的,其所有操作通过类SQL操作即可实现,从而将数据join,清洗的工作与代码解耦。大家稍微看下数仓的理念或者了解下阿里采云间就会明白。
B2B电子订单系统
- 这是一个后期补充需求,业务模型相对独立,主要是为了方便终端处理厂可以电子化的和仓储运营人员进行货物买卖。举个例子,末终端处理厂,需要2000个触屏手机,或者1吨废旧衬衫,都可以在该系统中下采购单;同时,他也能通过该系统,获得仓储中物品规格数量的大致信息,根据现有数量和物品下采购订单。这样就将废旧品出库采销整体打通并实现电子化
- 技术选型
- 前段采用基于MVVM的angularjs,是缺少专业前端人员团队进行前端开发的利器
- Spring Boot
- Spring MVC, Spring Security, LogBack
- Hibernate
- MySQL
至此,回收平台系统已经设计完毕,技术选型也初步确定。项目成功上线,除了因为某些小bug造成的单点故障,在设计规模下,没有发生因系统设计不合理引发的问题,系统扩展性也比较强,对于新增业务基本能够很好得支持。
其它小结
代码仓库:我们的全部代码初期存放于gitoschina上,后来一段时间经常服务异常,无法commit,所以转移到了aliyuncode,非常稳定,速度很快。
持续集成:我们基于jenkins做了全部系统的持续集成,支线代码提交就会部署开发环境,主干代码更新就会部署测试环境,生产环境部署也是一键达成。其中很多小点还是值得分享的,具体我会在其它博文里介绍。
代码工程:我们打造了非常规范,并经过实战优化的Spring Boot多Module工程目录,晚些也会在专题博文里分享介绍。
依赖管理:一般项目用的Maven,一般项目用的Gradle,对比来说,更喜欢Gradle,配置灵活功能强大。
埋点实现:
- 业务埋点通过打印日志,然后使用阿里云日志分析服务实现
- 访问埋点通过友盟,功能强大稳定,好像已经被阿里收购了
- App埋点通过腾讯bugly,非常好用,追踪安装量,打开量,异常很方便,还有在线模拟器可以用,强烈安利!
静态资源通过阿里云CDN服务支持访问,资费很低,节约了系统服务器的宝贵带宽
车辆定位使用了G7货运人,还是比较稳定的,中间出过一次问题,及时找到对方技术接口人并解决,此外定制开发也能找到接口人,效率也挺高。
WebSocket使用了leancloud,但是稳定性有段时间不好,后来基于开源自建了一个Socket服务器。
短信服务初期使用leancloud,送达率略低,后期切入阿里大于,自费和送达率都符合业务要求。
微信开发,因为有微信公众号,所以对接了微信的各种接口,也填了微信浏览器的各种坑,具体细节会在其它文章分享
支付开发,对接微信支付,支付宝支付,相关分享会在其它专题文章里讲述。
前端框架,使用了angularjs,不得不说是后端MVC人员转型前端开发利器啊。
前端框架,报表使用了JQWidgets,非常好用
MVC异常处理,我们把所有前端请求分为两类,page类和api类,page类主要是返回html,api类返回json数据,所有的返回即使在异常情况下(大部分异常)也是标准的{"result": "","data": "","success": "","message": ""},前端通过angularjs的拦截器功能,实现了全站的异常封装和交互,节约了很多代码
因为我们的遵循了从繁入简的设计思想,所以虽然因为实际情况妥协将回收模块,客服模块,调度模块进行了耦合,但大家都清楚系统未来演进,业务继续生长是一定会进行拆分的,所以我们在代码迭代中会刻意保持不同模块的独立性,领域模型不要过度耦合,跨模块调用统一采用基于bean的服务实现,关键编号保留分库分表位。这样后期如果必须进行拆分时,也会很高效。
...还有很多,一些细节想起来再加吧,这个项目已经交付好久了。
至此基本罗列完了我们对该项目的需求整理,分析以及系统设计的大致过程。
大家如果对其中的部分细节感兴趣,或者有异议,非常欢迎留言谈论批评指正,我会及时勘正补缺。