数字化平台之微信平台策略

当下,互联网技术正在深刻地重构我们的社会,各大企事业单位——大到万人集团公司,小到图文复印店——都在争先恐后地从所谓的“传统行业”中脱胎换骨一番以完成数字化转型。

在这个过程中,“企业即IT”、“科技即商业”等口号被屡屡提及,企业开始重新审视已有的CRM系统、供应链体系等诸多IT资产,发现割裂的信息化并不能给企业带来多少价值,于是CIO们逐渐意识到他们所需要的其实是一个IT生态系统。简单的讲,这里的IT生态系统即可以理解为数字化平台,这个平台不单单是个技术平台,同时也是一个业务平台。

微信平台策略

在中国,我们似乎有天然的优势,因为我们有微信呀!是的,就是那个连咱们的二大爷三大姑都能够用之在朋友圈分享点养生常识,以及那些成天被城管追着跑的路边烧烤小哥都能用之完成无现金收款的微信。

微信本身便是一个数字化平台。这个平台为个人或组织提供了一种新的展示空间——市场营销、自媒体、移动支付、业务办理等等等等。事实上,“平台”一词有两种含义:一种是上文提到的微信平台本身,另一种是对于某个组织来说,利用微信(包含微信公众号和企业微信)做为其自身的业务平台,本文讨论的是后者。

通常来说,企业在构建微信生态的过程中,可以采用三种微信平台策略。前两种是比较基本的策略,这里只做简要介绍。本文重点将讲解第三种:结合了微信公众号和企业微信的“行业解决方案平台”。

微信公众号

第一种,也是最简单的一种,即大家在平时生活中都能够接触到的微信公众号。微信公众号已经在悄然地取代官网成为企业新的门户,诸如寄快递、订机票这些先前只能在Web网站中完成的业务,现在只需关注某个公司的微信公众号便可完成。

请注意,微信公众号分为订阅号和服务号,订阅号主要用于宣传营销,并不能有效用于那些需要在微信中开展业务的场景;而服务号则主要用于开展业务,因此本文在说到公众号时,主要指的是微信的服务号。

事实上,这种方式还算不上“平台”可言,但是它的确是一种行之有效并得到广泛采纳的触达终端用户的手段。通常来说,微信公众号并不完全独立运作,它依然需要与企业其他业务系统集成起来以形成完整的业务闭环,这里的“其他系统”有可能为了配合公众号而进行全新开发,也有可能是企业已有的IT系统,而微信公众号充当的只是另一种业务接口而已。

在技术上,公众号开发可以有多种形式,包括微信自身提供的“消息应答机制”、HTML5网页应用以及小程序等。

微信公众号平台

公众号+企业微信联合平台

微信公众号主要面向终端用户,同时,腾讯公司还提供了使用对象为企业员工的企业微信。对于多数企业(特别是中小型企业)来说,一个微信公众号已经足够,甚至连腾讯官方的解决方案库中也主要只针对公众号而缺少对企业微信的提及。当然,当前已经有越来越多的企业开始使用企业微信,但是其中多数企业使用企业微信的目的主要集中在诸如通讯录、考勤等人员管理上,而不是本企业的核心业务。本文讨论的主要是将企业微信用于完成企业的业务环节,比如快递公司的快递员通过企业微信完成包裹的录入登记、售后人员通过企业微信完成服务工单的追踪等。

微信公众号和企业微信联合平台可以完成各大企业移动化办公的梦想。在企业微信中,我们可以开发多个业务应用,比如一个汽车生产商的企业微信中可以包含ERP、CRM、供应链以及4S店管理等应用。另外,在企业微信中开展业务流程时,我们可以直接享受到企业微信现有的支撑功能,包括通讯录、企业邮箱、企业支付以及公费电话等。可见,企业微信本身便是一个不折不扣的平台化工具,再加上面向终端用户的微信公众号,该联合平台有助于形成一个完整的客户服务以及业务运作体系。

微信公众号+企业微信联合平台

有了这个联合平台,你会发现微信不再是发个宣传图文或者购物收款这么简单,而是摇身一变成为能够满足用户自服务的核心业务工具;企业微信也不再只是管理一下通讯录或者让员工请个假打个卡,而是一款能够完成业务运作以及提升办公体验的员工伴侣。

行业解决方案平台

行业解决方案平台主要用于解决某个行业中的通用业务问题,可以是单独针对公众号的,也可以是单独针对企业微信的,还可以是同时包含了公众号和企业微信的“终极”平台形式。

腾讯官方在公众号和企业微信两端均提供了平台化支持。对于公众号,可以通过微信开放平台的第三方平台完成公众号的业务授权和代理;对于企业微信,可以通过服务商平台完成企业微信的业务授权和代理。当前,市面上已经存在大量单独的第三方平台应用或者服务商平台应用,但是将此二者结合以形成完整的业务闭环还鲜有出现。

为了便于读者理解,这里列举两个典型的应用场景:

客户投诉:

每个致力于长远发展的企业都需要一个完善并且能够实际运转的客户投诉机制。对于客户投诉的处理流程在各个行业中其实大同小异——都是基于类似于工单的处理流程。因此,某个专门做微信开发的公司做了一套关于投诉的行业微信方案。在该方案中,客户通过某企业的微信公众号完成投诉,企业员工通过企业微信完成对投诉的处理,整个过程完全移动化。企业并不需要自行研发投诉管理系统,只需将自身的公众号和企业微信授权给这个基于微信的投诉平台即可。可以看出,这里的投诉平台事实上是一个类似于具有多租户特点的云平台,而很多基于微信的行业解决方案系统的确也正是部署在云平台之上的。

汽车销售:

汽车生产商通常并不负责汽车零售,而是将其代理给多个4S店,这些4S店并不隶属于汽车生产商,他们也有自己的企业,有自己的品牌,有自己的企业微信和公众号。但是,4S店需要遵循汽车生产商的某些规范或者服务标准,从这点来讲,后者与前者又存在管理与被管理关系。汽车生产商为了规范化对客户的服务,开发了一套针对该品牌汽车销售的行业级微信平台,所有经销商均在该平台上完成汽车销售相关的业务。该平台同时包含了公众号和企业微信,终端买家可以通过经销商的公众号完成车型查看、试驾预约等业务,而经销商的员工可以在经销商自己的企业微信中完成对客户的服务业务,比如安排试驾、客户关系管理等。需要注意的是,所有销售过程都发生在经销商自己的公众号和企业微信中,而不是汽车生产商的,这有利于经销商建设其自身的品牌形象。经销商自身并不需要做任何开发,而是将公众号和企业微信授权代理给汽车生产商开发的微信行业平台即可。可以看到,在企业微信端,这是一个B2B的系统,在公众号端,这又是一个B2C系统。

以汽车销售为例,经销商的微信公众号与企业微信是两个相对独立的主体,要使他们服务于同一个业务流程,我们需要将此二者关联起来。此时,我们创建一个“经销商管理平台”系统,在经销商在管理平台上注册之后,系统将关联它的微信公众号与企业微信。由此,终端用户在公众号端发起的请求可以直达企业微信中的员工。

微信行业级解决方案平台(以汽车销售为例)

这种平台落地之后类似于:汽车制造商为了促进销售额以及规范对客户的服务,开发了一套同时兼顾B2B和B2C的微信平台,不同的经销商带着各自的微信服务号和企业微信入驻该平台。所有的经销商都通过由汽车生产商提供的同一套业务流程和标准向终端客户服务。

如果我们把“汽车销售”的例子抽象一下,便会发现各行各业均存在这种分级式的商业关系:商品的生产方并不直接将商品或服务提供给终端客户,而是通过代理的方式将这些业务交给中间的经销商或代理商,不同的经销商或代理商除了宣传生产方的商品或服务之外,也会建设属于自己的品牌。

总结

互联网时代的企业都需要一个属于自己的数字化平台,具有“中国特色”的微信能够为企业提供现成的平台基础。在整个数字化平台的生态体系中,企业可以选择微信平台以触达终端用户和完成核心业务。在该平台中,微信公众号与企业微信紧密结合,形成了一套完整的、行业级别的业务运作和客户服务体系。


更多精彩洞见,请关注微信公众号:思特沃克

Share

在微服务中使用领域事件

稍微回想一下计算机硬件的工作原理我们便不难发现,整个计算机的工作过程其实就是一个对事件的处理过程。当你点击鼠标、敲击键盘或者插上U盘时,计算机便以中断的形式处理各种外部事件。在软件开发领域,事件驱动架构(Event Driven Architecture,EDA)早已被开发者用于各种实践,典型的应用场景比如浏览器对用户输入的处理、消息机制以及SOA。最近几年重新进入开发者视野的响应式编程(Reactive Programming)更是将事件作为该编程模型中的一等公民。可见,“事件”这个概念一直在计算机科学领域中扮演着重要的角色。

认识领域事件

领域事件(Domain Events)是领域驱动设计(Domain Driven Design,DDD)中的一个概念,用于捕获我们所建模的领域中所发生过的事情。领域事件本身也作为通用语言(Ubiquitous Language)的一部分成为包括领域专家在内的所有项目成员的交流用语。比如,在用户注册过程中,我们可能会说“当用户注册成功之后,发送一封欢迎邮件给客户。”,此时的“用户已经注册”便是一个领域事件。

当然,并不是所有发生过的事情都可以成为领域事件。一个领域事件必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致进一步的业务操作。举个咖啡厅建模的例子,当客户来到前台时将产生“客户已到达”的事件,如果你关注的是客户接待,比如需要为客户预留位置等,那么此时的“客户已到达”便是一个典型的领域事件,因为它将用于触发下一步——“预留位置”操作;但是如果你建模的是咖啡结账系统,那么此时的“客户已到达”便没有多大存在的必要——你不可能在用户到达时就立即向客户要钱对吧,而”客户已下单“才是对结账系统有用的事件。

微服务(Microservices)架构实践中,人们大量地借用了DDD中的概念和技术,比如一个微服务应该对应DDD中的一个限界上下文(Bounded Context);在微服务设计中应该首先识别出DDD中的聚合根(Aggregate Root);还有在微服务之间集成时采用DDD中的防腐层(Anti-Corruption Layer, ACL);我们甚至可以说DDD和微服务有着天生的默契。更多有关DDD的内容,请参考笔者的另一篇文章或参考《领域驱动设计》《实现领域驱动设计》

在DDD中有一条原则:一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作。但是在实际应用中,我们经常发现一个用例需要修改多个聚合根的情况,并且不同的聚合根还处于不同的限界上下文中。比如,当你在电商网站上买了东西之后,你的积分会相应增加。这里的购买行为可能被建模为一个订单(Order)对象,而积分可以建模成账户(Account)对象的某个属性,订单和账户均为聚合根,并且分别属于订单系统和账户系统。显然,我们需要在订单和积分之间维护数据一致性,通常的做法是在同一个事务中同时更新两者,但是这会存在以下问题:

  • 违背DDD中”单个事务修改单个聚合根”的设计原则;
  • 需要在不同的系统之间采用重量级的分布式事务(Distributed Transactioin,也叫XA事务或者全局事务);
  • 在不同系统之间产生强耦合。

通过引入领域事件,我们可以很好地解决上述问题。 总的来说,领域事件给我们带来以下好处:

  • 解耦微服务(限界上下文);
  • 帮助我们深入理解领域模型;
  • 提供审计和报告的数据来源;
  • 迈向事件溯源(Event Sourcing)和CQRS等。

还是以上面的电商网站为例,当用户下单之后,订单系统将发出一个“用户已下单”的领域事件,并发布到消息系统中,此时下单便完成了。账户系统订阅了消息系统中的“用户已下单”事件,当事件到达时进行处理,提取事件中的订单信息,再调用自身的积分引擎(也有可能是另一个微服务)计算积分,最后更新用户积分。可以看到,此时的订单系统在发送了事件之后,整个用例操作便结束了,根本不用关心是谁收到了事件或者对事件做了什么处理。事件的消费方可以是账户系统,也可以是任何一个对事件感兴趣的第三方,比如物流系统。由此,各个微服务之间的耦合关系便解开了。值得注意的一点是,此时各个微服务之间不再是强一致性,而是基于事件的最终一致性。

事件风暴(Event Storming)

事件风暴是一项团队活动,旨在通过领域事件识别出聚合根,进而划分微服务的限界上下文。在活动中,团队先通过头脑风暴的形式罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对于每一个事件,标注出导致该事件的命令(Command),再然后为每个事件标注出命令发起方的角色,命令可以是用户发起,也可以是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。事件风暴还有一个额外的好处是可以加深参与人员对领域的认识。需要注意的是,在事件风暴活动中,领域专家是必须在场的。更多有关事件风暴的内容,请参考这里

创建领域事件

领域事件应该回答“什么人什么时候做了什么事情”这样的问题,在实际编码中,可以考虑采用层超类型(Layer Supertype)来包含事件的某些共有属性:

public abstract class Event {
    private final UUID id;
    private final DateTime createdTime;

    public Event() {
        this.id = UUID.randomUUID();
        this.createdTime = new DateTime();
    }
}

可以看到,领域事件还包含了ID,但是该ID并不是实体(Entity)层面的ID概念,而是主要用于事件追溯和日志。另外,由于领域事件描述的是过去发生的事情,我们应该将领域事件建模成不可变的(Immutable)。从DDD概念上讲,领域事件更像一种特殊的值对象(Value Object)。对于上文中提到的咖啡厅例子,创建“客户已到达”事件如下:

public final class CustomerArrivedEvent extends Event {
    private final int customerNumber;

    public CustomerArrivedEvent(int customerNumber) {
        super();
        this.customerNumber = customerNumber;
    }
}

在这个CustomerArrivedEvent事件中,除了继承自Event的属性外,还自定义了一个与该事件密切关联的业务属性——客户人数(customerNumber)——这样后续操作便可预留相应数目的座位了。另外,我们将所有属性以及CustomerArrivedEvent本身都声明成了final,并且不向外暴露任何可能修改这些属性的方法,这样便保证了事件的不变性。

发布领域事件

在使用领域事件时,我们通常采用“发布-订阅”的方式来集成不同的模块或系统。在单个微服务内部,我们可以使用领域事件来集成不同的功能组件,比如在上文中提到的“用户注册之后向用户发送欢迎邮件”的例子中,注册组件发出一个事件,邮件发送组件接收到该事件后向用户发送邮件。

在微服务内部使用领域事件时,我们不一定非得引入消息中间件(比如ActiveMQ等)。还是以上面的“注册后发送欢迎邮件”为例,注册行为和发送邮件行为虽然通过领域事件集成,但是他们依然发生在同一个线程中,并且是同步的。另外需要注意的是,在限界上下文之内使用领域事件时,我们依然需要遵循“一个事务只更新一个聚合根”的原则,违反之往往意味着我们对聚合根的拆分是错的。即便确实存在这样的情况,也应该通过异步的方式(此时需要引入消息中间件)对不同的聚合根采用不同的事务,此时可以考虑使用后台任务。

除了用于微服务的内部,领域事件更多的是被用于集成不同的微服务,如上文中的“电商订单”例子。

通常,领域事件产生于领域对象中,或者更准确的说是产生于聚合根中。在具体编码实现时,有多种方式可用于发布领域事件。

一种直接的方式是在聚合根中直接调用发布事件的Service对象。以上文中的“电商订单”为例,当创建订单时,发布“订单已创建”领域事件。此时可以考虑在订单对象的构造函数中发布事件:

public class Order {
    public Order(EventPublisher eventPublisher) {
        //create order        
        //…        
        eventPublisher.publish(new OrderPlacedEvent());    
        }
}

注:为了把焦点集中在事件发布上,我们对Order对象做了简化,Order对象本身在实际编码中不具备参考性。

可以看到,为了发布OrderPlacedEvent事件,我们需要将Service对象EventPublisher传入,这显然是一种API污染,即Order作为一个领域对象只需要关注和业务相关的数据,而不是诸如EventPublisher这样的基础设施对象。另一种方法是由NServiceBus的创始人Udi Dahan提出来的,即在领域对象中通过调用EventPublisher上的静态方法发布领域事件:

 public class Order {
    public Order() {
        //create order
        //...
        EventPublisher.publish(new OrderPlacedEvent());
    }
}

这种方法虽然避免了API污染,但是这里的publish()静态方法将产生副作用,对Order对象的测试带来了难处。此时,我们可以采用“在聚合根中临时保存领域事件”的方式予以改进:

public class Order {

    private List<Event> events;

    public Order() {
        //create order
        //...
        events.add(new OrderPlacedEvent());
    }

    public List<Event> getEvents() {
        return events;
    }

    public void clearEvents() {
        events.clear();

    }
}

在测试Order对象时,我们便你可以通过验证events集合保证Order对象在创建时的确发布了OrderPlacedEvent事件:

@Test
public void shouldPublishEventWhenCreateOrder() {
    Order order = new Order();
    List<Event> events = order.getEvents();
    assertEquals(1, events.size());
    Event event = events.get(0);
    assertTrue(event instanceof OrderPlacedEvent);
}

在这种方式中,聚合根对领域事件的保存只能是临时的,在对该聚合根操作完成之后,我们应该将领域事件发布出去并及时清空events集合。可以考虑在持久化聚合根时进行这样的操作,在DDD中即为资源库(Repository):

public class OrderRepository {
    private EventPublisher eventPublisher;

    public void save(Order order) {
        List<Event> events = order.getEvents();
        events.forEach(event -> eventPublisher.publish(event));
        order.clearEvents();

        //save the order
        //...
    }
}

除此之外,还有一种与“临时保存领域事件”相似的做法是“在聚合根方法中直接返回领域事件”,然后在Repository中进行发布。这种方式依然有很好的可测性,并且开发人员不用手动清空先前的事件集合,不过还是得记住在Repository中将事件发布出去。另外,这种方式不适合创建聚合根的场景,因为此时的创建过程既要返回聚合根本身,又要返回领域事件。

这种方式也有不好的地方,比如它要求开发人员在每次更新聚合根时都必须记得清空events集合,忘记这么做将为程序带来严重的bug。不过虽然如此,这依然是笔者比较推荐的方式。

业务操作和事件发布的原子性

虽然在不同聚合根之间我们采用了基于领域事件的最终一致性,但是在业务操作和事件发布之间我们依然需要采用强一致性,也即这两者的发生应该是原子的,要么全部成功,要么全部失败,否则最终一致性根本无从谈起。以上文中“订单积分”为例,如果客户下单成功,但是事件发送失败,下游的账户系统便拿不到事件,导致最终客户的积分并不增加。

要保证业务操作和事件发布之间的原子性,最直接的方法便是采用XA事务,比如Java中的JTA,这种方式由于其重量级并不被人们所看好。但是,对于一些对性能要求不那么高的系统,这种方式未尝不是一个选择。一些开发框架已经能够支持独立于应用服务器的XA事务管理器(如AtomikosBitronix),比如Spring Boot作为一个微服务框架便提供了对Atomikos和Bitronix的支持

如果JTA不是你的选项,那么可以考虑采用事件表的方式。这种方式首先将事件保存到聚合根所在的数据库中,由于事件表和聚合根表同属一个数据库,整个过程只需要一个本地事务就能完成。然后,在一个单独的后台任务中读取事件表中未发布的事件,再将事件发布到消息中间件中。

这种方式需要注意两个问题,第一个是由于发布了事件之后需要将表中的事件标记成“已发布”状态,即依然涉及到对数据库的操作,因此发布事件和标记“已发布”之间需要原子性。当然,此时依旧可以采用XA事务,但是这违背了采用事件表的初衷。一种解决方法是将事件的消费方创建成幂等的,即消费方可以多次消费同一个事件而不污染系统数据。这个过程大致为:整个过程中事件发送和数据库更新采用各自的事务管理,此时有可能发生的情况是事件发送成功而数据库更新失败,这样在下一次事件发布操作中,由于先前发布过的事件在数据库中依然是“未发布”状态,该事件将被重新发布到消息系统中,导致事件重复,但由于事件的消费方是幂等的,因此事件重复不会存在问题。

另外一个需要注意的问题是持久化机制的选择。其实对于DDD中的聚合根来说,NoSQL是相比于关系型数据库更合适的选择,比如用MongoDB的Document保存聚合根便是种很自然的方式。但是多数NoSQL是不支持ACID的,也就是说不能保证聚合更新和事件发布之间的原子性。还好,关系型数据库也在向NoSQL方向发展,比如新版本的PostgreSQL(版本9.4)和MySQL(版本5.7)已经能够提供具备NoSQL特征的JSON存储和基于JSON的查询。此时,我们可以考虑将聚合根序列化成JSON格式的数据进行保存,从而避免了使用重量级的ORM工具,又可以在多个数据之间保证ACID,何乐而不为?

总结

领域事件主要用于解耦微服务,此时各个微服务之间将形成最终一致性。事件风暴活动有助于我们对微服务进行拆分,并且有助于我们深入了解某个领域。领域事件作为已经发生过的历史数据,在建模时应该将其创建为不可变的特殊值对象。存在多种方式用于发布领域事件,其中“在聚合中临时保存领域事件”的方式是值得推崇的。另外,我们需要考虑到聚合更新和事件发布之间的原子性,可以考虑使用XA事务或者采用单独的事件表。为了避免事件重复带来的问题,最好的方式是将事件的消费方创建为幂等的。


更多精彩洞见,请关注微信公众号:思特沃克

Share