DDD领域驱动设计和实施

  |   0 评论   |   0 浏览

一、DDD缘起

1.1、服务端开发模式

服务端开发模式至今共有三种开发模式

三种开发模式

面向过程的事务脚本开发:将业务逻辑组织为面向过程的事务脚本的集合,高度面向过程的开发模式。初始简单,业务复杂后,维护难度指数上升。-->基本不为主流使用

面向对象的数据库表开发:先进行数据库表设计,再通过数据流向去串联对应的业务流程的开发模式。初始难度中,业务复杂后,维护难度延迟后再指数上升。--->目前市面上主流

面向对象的业务领域模型开发:应付复杂业务逻辑,以聚合模型替代数据表模型,以并发的事件驱动替代串联的消息驱动。典型架构代表是DDD+SOA微服务的事件驱动的CQRS读写分离架构。真正实现以业务实体为核心的灵活拓展。初始难度高,业务复杂后,维护难度线性上升。--->已很不错。

可见,面向对象的数据库表开发是基于数据驱动,一般是贫血模型。面向对象的业务领域模型开发是基于领域驱动,基于实体实现充血模型。

1.2、数据驱动和领域驱动

数据驱动和领域驱动对比如下图,数据驱动的设计方式关注具体的业务数据承载,比较适合一些简单的小的系统中。而领域驱动通过领域模型驱动整个系统的软件设计,让领域模型与数据模型解耦,明确业务边界,从而能够更好的指导我们完成复杂系统的架构设计。由此可见,领域驱动更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系,这是二者最显著的区别。

两种设计对比

1.3、DDD的兴起

所谓DDD即Domain Driven Design,字面意思就是领域驱动设计。早在2004年著名建模专家Eric Evans在他的颇具影响力的书中《Domain-Driven Design –Tackling Complexity in the Heart of Software》(领域驱动设计:软件核心复杂性应对之道)中就提出来DDD相应的概念。但是直到Martin Fowler大神的《Microservices》才引起大家注意, 也就是微服务盛行之后, DDD再次回到人们视野中间。

软件架构的演进

微服务分布式架构解决了集中式架构的单体应用很多问题, 比如水平扩展性、弹性伸缩能力,但是新的问题应运而生 , 微服务的粒度应该多大 ?微服务如何设计呢?微服务如何拆分 ?微服务边界在哪里 ?微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。而DDD就是用边界上下文解决了业务边界的问题,DDD并不是一种技术架构,而是一种划分业务领域范围的方法论。DDD的兴起是由于很多熟悉领域驱动建模(DDD)的工程师在进行微服务设计时, 发现用DDD的思路进行业务梳理可以很好规划服务边界, 可以很好实现微服务内部和外部的"高内聚、低耦合"。

二、DDD本质

2.1、DDD的定义

Eric在他的《领域驱动设计:软件核心复杂性应对之道》书中给出的定义:DDD是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

DDD是一种高度复杂的领域设计思想,将我们的问题拆分成一个个的域, 试图分离技术实现的复杂性,主要解决的是软件难以理解难以演进的问题,目的就是将复杂问题领域简单化,帮助我们设计出清晰的领域和边界,可以很好的实现技术架构的演进。

我们在设计软件架构的时候需要考虑到如何更好的实现需求、更好的应对不断变化的需求以及更好的和他人协作来实现。因此传统的数据驱动设计的方式无法满足我们的实际需要了,而DDD正好能指导我们进行复杂业务系统的设计。

2.2、DDD的价值

DDD的价值大致总结为以下四点

DDD的四大价值

1)统一业务语言:通过使用统一的领域语言,消除了团队间的分歧,提升团队间的沟通效率。统一语言主要体现在两个方面:统一的领域术语和统一的领域行为。

2)领域模型建模:通过领域模型沉淀业务领域知识,提升业务建模能力,清晰表达业务核心语义。领域模型就是针对某个特定业务领域的软件模型,如电商业务领域中的订单、积分、库存、配送等都是电商业务领域的子域。领域模型通过对象模型描述真实的业务场景,精确表达业务语义。

3)清晰业务边界:统一各个子域的认识,通过领域模型界定需求实现边界,表面无休止的团队之间扯皮,拉通团队认知。限界上下文是一个显式的概念性边界,领域模型是使用同一语言描述的软件模型,每个领域模型都有对应的属性和动作,这些属性和动作需要在一个特定的上下文环境中才有特定的意义。

4)关注点分离:通过领域模型与数据模型分离,业务复杂度与技术复杂度分离,保持结构清晰和可演化,以应对不可预测性挑战,提升应对变化的能力。这样将核心业务的不变与需求的变进行有效隔离,核心业务逻辑和技术实现的隔离,提升架构应变化的能力。

2.3、DDD的核心概念

DDD的核心概念总览

1)实体

实体是一种具有唯一身份标识的对象,具有持续的生命周期。实体通过它的唯一标识被区分。实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。实体具有唯一性、延续性、可变性。

2)值对象

值对象是用来描述领域的特定方面或属性,并且是一个没有标识符的对象。值对象本质上是一个集合,这个集合中包含若干用于描述目的、具有整体概念和不可修改的属性。值对象具有不变性、相等性和可替换性。值对象一般依附于实体而存在,是实体属性的一部分。

3)聚合

聚合是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的事务持久化。聚合被视为一个整体,在每个行为完成时,必须满足聚合内部所应用的固定规则的要求,即保证数据变化的一致性。聚合的特点是高内聚、低耦合。

4)领域服务

领域中的一些概念不太适合建模为对象,它们本质上是一些操作、一些动作,代表领域中一个重要的行为。这些操作或动作往往涉及多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作,这就是领域服务。它没有属性,只有行为,因此说它是无状态的。

5)工厂

工厂是将创建复杂对象的实例和聚合的职责转移到一个单独的对象,这个对象本身在领域模型中可能没有职责,但它也是领域设计中的一部分。工厂一般作为一种创建复杂对象和聚合的实现方式。

6)资源库

资源库作为对象的提供方,能够实现对象的持久化,解耦领域内业务逻辑与底层持久化。资源库和数据访问对象DAO的作用类似,资源库以“领域”为中心,所描述的是“领域语言”,不涉及数据库实现的细节;而DAO是比资源库更低的一层,其包含如何从数据库中提取数据的代码。

6)领域事件

领域事件表示领域中所发生的重要事件,在事件发生后通常会导致进一步的业务操作,或者在系统其他地方引起反应。DDD 的一个重要原则便是一次事务只能更新一个聚合实例,但存在一个业务流程涉及修改多个聚合的事务,怎么实现整个业务流程的数据一致性呢?在 DDD 中,领域事件便可以用于处理上述问题,此时最终一致性取代了事务一致性,通过领域事件的方式达到各个组件之间的数据一致性。

三、DDD领域设计

领域驱动设计是一套面对复杂业务进行建模和设计的方法论和实践,建立了以领域为核心驱动力的设计体系。领域驱动设计分为 两个主要过程:战略设计和战术设计。具体过程如下图

DDD两步设计

3.1、战略设计

战略设计

1)提炼问题域

DDD 的一个核心原则是所有人员包括领域专家和技术专家进行任何沟通都使用一种基于模型的通用语言,然后基于统一语言进行需求分析和知识提炼,获得指定业务背景下清晰的问题域,最后再基于问题域再拆解分为各种问题子域。

2)划分领域和子域

领域是用于确定范围和边界的,DDD将业务上的问题限定归属在特定的边界内,而这些边界就可以叫作领域。为了降低业务理解和系统实现的复杂度,DDD会将领域进一步划分为更细粒度,也就是子域。子域根据自身的重要程度和功能属性又可以划分为三类子域。

核心域:决定应用和系统核心竞争力,它是决定业务是否成功的主要因素,是领域建模的重心。比如电商系统中关注的会员、商品、订单、交易、库存、营销等。

通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域,可以通过现有解决方案集成的服务。比如统一的认证和权限管理系统。

支撑域:既不包含核心竞争力的功能,又不包含通用功能的子域,但该功能子域又是必需的,体现的是支撑服务,也就是支撑域。比如某个特定领域的数据字典。

3)识别限界上下文

在一个领域 / 子域中,DDD创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义,这样边界便称为限界上下文,其本质上是限界+上下文。上下文(Context)其实是动态的业务流程被边界(Bounded)静态切分的产物。从语义相关性和功能相关性两个方面识别业务边界,进而提炼出初步的限界上下文,然后再从业务逻辑、团队组织、技术应用三个视角确定一个个自治的单元。自治要满足以下四个特点:

最小完备是实现自治的基本条件,指的是自治单元履行的职责是根据业务价值的完整性和最小功能集进行设计的,这让自治单元无需求助其他自治单元获得信息,避免了不必要的依赖关系,同时也避免了不必要和不合适的职责添加到该自治单元上。

自我履行意味着由自治单元自身决定要做什么。是否应该履行某职责,由限界上下文拥有的信息来决定。自我履行其实意味着对知识的掌握,为避免风险,你要履行的职责一定是你掌握的知识范畴之内。

稳定空间指的是减少外界变化对限界上下文内部的影响。稳定空间符合开放封闭原则(OCP),即对修改是封闭的,对扩展是开放的,该原则其实体现了一个单元的封闭空间与开放空间。封闭空间体现为对细节的封装与隐藏,开放空间体现为对共性特征的抽象与统一,二者共同确保了整个空间的稳定。

独立进化指的是减少限界上下文的变化对外界的影响。要做到独立进化,就必须保证对外公开接口的稳定性。一个独立进化的限界上下文,需要一个稳定、设计良好的接口设计,并在版本上考虑了兼容与演化。

最小完备是基础,只有赋予了限界上下文足够的信息,才能保证它的自我履行。稳定空间与独立进化则一个对内一个对外,是对变化的有效应对,而它们又是通过最小完备和自我履行来保障限界上下文受到变化的影响最小。

4)上下文映射图

限界上下文仅是一种对领域问题域的静态划分,还缺少一个重要的关注点,即:限界上下文之间是如何协作的?当我们发现彼此协作存在问题时,说明限界上下文的划分出现了问题,也是识别限界上下文的一种验证方法。在限界上下文中,通过上下文映射图(Context Map)确立上下文之间的关系,通过上下游来表达依赖,最后形成限界上下文如何在应用程序中相互配合的全局视图。在实际工作中,目前使用比较广的是防腐层和统一协议。

已发布的语言(Published Language):两个上下文使用共同的语言,比如SOA服务总线定义了一堆XML模型,或者基于共享的文件或数据库,上下文可以基于此进行直接交互。

开放主机服务(Open Host):又叫统一协议,为上下文之间的服务定义一套包含标准化数据结构在内的协议,比如基于HTTP风格的RESTful接口协议。

共享内核(Shared Kernel):两个上下文使用一个共同的代码内核作为通用语言,比如两个工程使用同一个Bean基础模型库。

客户/供应商(Customer/Supplier):一个上下文使用另一个上下文的服务,有显著的上下游依赖关系,比如基于RPC的服务交互方式。

顺从者(Conformist):一个上下文使用另一个上下文的服务,但彼此之间的关系并不紧密,比如基于消息传递机制的交互模式。

防腐层(Anti-Corruption Layer,ACL):使用一层适配层来协助上下文的交互,隔离业务逻辑,比如在商品子域和采购子域之间提供防腐层,将商品的变更进行收口,隔离子域内后端业务。

为了实现上下文映射,常借助的方式是诸如 RPC、HTTP、消息队列等,依照上下文间映射类型,挑选一件趁手的组件实现即可。

3.2、战术设计

战略设计为我们提供一种高层视角来审视我们的软件系统,而战术设计则将战略设计的成果具体化和细节化,它关注的是单个限界上下文内部技术层面的实施。DDD 提供了一整套技术工具集,包括实体、值对象、领域服务和资源库等。而我们可以用以下常用的领域建模方法进行战术设计

战术设计

1)用例分析法

用例分析是比较通用的领域建模方法,可以在比较传统的需求调研过程中结合领域模型的设计思路进行,核心是通过业务需求、场景流程等梳理用例,进而规划领域模型。用例分析的前提是业务架构的需求输入,其中核心是业务能力与业务流程。根据语义来整理用例,然后整理领域模型,大概步骤如下

用例分析法

收集用例:从业务能力、业务流程、业务需求描述中进行提取,收集相应的名词、动词、形容词,以及对应的业务场景。

提取实体:从名词中定位出主要概念实体,如商品、SKU、品类等。

提取属性:从形容词和名词中提取实体属性,如颜色、价格等。

添加关联:从动词或形容词中添加实体和实体之间的关联和领域服务,如商品“包含”SKU,卖家“开设”“多家”店铺等。

完善模型:识别出初聚合模型,验证并迭代模型,同时补充用例验证模型、业务流程验证模型。

假设有这样的需求描述:“会员使用代金券兑换了很多促销的商品。”我们先从名词“会员”“代金券”“商品”中提取实体,并从形容词“促销的”提取商品的属性,进而将动词“使用”“兑换”识别成关联,同时结合行业知识得知,代金券属于优惠券的一种,最终得出领域模型。

2)四色建模法

四色建模法关注的是某个人(Party)的角色(PartyRole)在某个地点(Place)的角色(PlaceRole)用某个东西(Thing)的角色(ThingRole)做了某件事情(Moment-Interval),其包括以下四个原型。

时标原型(Moment-Interval):具有可追溯性的记录运营或管理数据的时刻或时段对象(如用粉红色表示)。

人事物原型(Party-Place-Thing Archetype,PPT):代表参与到流程中的参与方(人)、地点(场)、物(货)(如用绿色表示)。

角色原型(Role):在时间记录与PPT对象(通常是参与方)之间参与的角色(如用黄色表示)。

描述原型(Description):对PPT对象的一种补充描述(如用蓝色表示)。

电商域四色模型示例

3)事件风暴法

事件风暴又称事件建模,可以快速分析复杂的业务领域,完成领域建模的目标。事件风暴是事件驱动设计的典型代表,是一种快速、轻量且未得到充分认可的群体建模技术,它对于加速开发团队非常适用。事件风暴法关注以下元素。

事件:发生了什么事情,产生了什么结果(如用橘黄色表示)。

属性:事件的输入、输出,是对时间的细化描述。

命令:某个动作的发起者,可能是人、外部事件、定时器等(如用蓝色表示)。

领域:领域的聚合、内聚、低耦合,聚合内部保证数据的一致性(如用黄色表示)。

简单理解就是谁在何时基于什么(输入)做了什么(命令),产生了什么(输出),影响了什么(事件),最后聚合成了什么(领域)。

通过一个电商的例子说明事件风暴的主要步骤:

首先,我们基于业务流程和业务流程的输入,对事件进行头脑风暴,主要识别应用层面的主要状态结果。比如识别出“商品已创建”,“库存已扣减”,“订单已支付”等。

其次,我们来识别命令,即什么人做什么事,可以识别出运营人员可以添加商品和编辑库存,用户可以创建订单,并伴随着对应的事件

再次,我们来进行聚合,即将相关的实体聚合在一起,可以看到商品、库存、订单三个领域初步识别,并与相关的命令和事件结合在一起

最后,我们对这些领域进行边界划分,识别出对应的限界上下文。

电商事件风暴实例示例

四、DDD实施

4.1、架构设计

架构的本质简单来说就是要素结构。所谓的要素(Components)是指架构中的主要元素,结构是指要素之间的相互关系(Relationship)。

架构的定义

DDD不是架构,而是一种设计思想和方法论。DDD可以帮助我们很好梳理出业务架构,进而从技术角度实现成应用架构和技术架构。此次我们讨论的架构设计主要是应用架构设计,领域驱动设计给出了 DDD 分层架构、CQRS架构、六边形架构、洋葱架构、整洁架构、清晰架构、整洁面向对象的分层架构(COLA)等架构,它们遵循“关注点分离”原则,旨在分离和隔离业务复杂度和技术复杂度,凸显了领域模型,保证了领域模型的稳定性和一致性。

1、DDD分层架构

DDD 分层架构将属于业务逻辑的关注点放到领域层(Domain Layer)中,将支撑业务逻辑的技术实现放到基础设施层(Infrastructure Layer)中,DDD 创新性地引入了应用层(Application Layer),应用层扮演了两重角色。一作为业务逻辑的门面(Facade),暴露了能够体现业务用例的应用服务接口,又是业务逻辑与技术实现的粘合剂,实现二者之间的协作.

分层架构

2、CQRS架构

CQRS(命令与查询职责分离) 使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的,这样读和写逻辑就隔离开来了。使用 CQRS 分离了读写职责之后,可以对数据进行读写分离操作来改进性能,可扩展性和安全。DDD 和 CQRS 结合,分别对读和写建模,从而优化读写操作。

CQRS架构

3、六边形架构

六边形架构又称为端口-适配器,也是一种分层架构,不是从上下或左右分,而是从内部和外部来分,内部代表应用的业务逻辑,外部代表应用的驱动逻辑、基础设施等。适配器分为两类:左侧代表的称为主动适配器,它们发起对应用的操作。右边表示和后端工具链接的适配器称为被动适配器,它们只会对主动适配器的操作做出响应,比如数据资源库适配器等

六边形架构

4、洋葱架构

2008年Jeffrey Palermo已经提出了具有分层思想的洋葱架构(Onion Architecture)。如下图,同心圆代表软件的不同部分,从里向外依次是领域模型,领域服务,应用服务和外层的基础设施和用户终端。洋葱架构根据依赖原则,定义了各层的依赖关系,越往里依赖程度越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的情况,这种架构也是典型的分层架构,和DDD分层架构一样,都体现了高内聚,低耦合的设计特性。

洋葱架构

5、整洁架构

2012年Robert C. Martin 提出了整洁架构(Clean Architecture),整洁架构将EBI、六边形架构Hexagonal、洋葱架构Onoin等整合成一个可以实际落地的架构。与洋葱架构相比,整洁架构调整两点: Application Services调整为Use Cases和Domain Services, Domain Model调整为Entities。

整洁架构

6、清晰架构

2017年Herberto Graca在其软件架构编年史系列文章中提出清晰架构(Explicit Architecture),即将DDD, Hexagonal, Onion, Clean, CQRS等进行融合后的架构。依赖方向由外到内,且内层不知道外层,如下图:

  • 最中心的红色多边形Application Core即表示业务逻辑实现,即应用核心。
  • 红色多边形的边界即表示端口Port,即应用核心的入口/出口定义。
  • Application Layer - 应用层,包括:Application Services,业务用例的编排服务即及其interface定义,CQRS命令/查询处理器,Event Listener事件监听器,Port端口定义,如ORM 接口、Repostitory、搜索引擎接口、消息接口等等
  • Domain Layer - 领域层,这一层含了数据和操作数据的逻辑,它们只和领域本身有关,独立于调用这些逻辑的业务过程。它们完全独立,对应用层完全无感知。其包含Domain Services - 领域服务,封装涉及多实体(相同或不同实体类型)的领域逻辑,且领域服务间可以相互调用。Domain Models - 领域模型,在架构的正中心,完全不依赖外部任何层次的领域模型。它包含了那些表示领域中某个概念的业务对象,如实体、值对象、枚举以及其它领域模型种用到的任何对象(如领域事件Domain Events,简单理解为MQ消息)。
  • 红色多边形的外侧左半圆部分即为主/主动适配器(用户界面User Interface实现)。如Spring MVC中的Controller实现和Command Query Bus 命令查询总线
  • 红色多边形的外侧右半圆部分即次/被动适配器(基础设置Infrastructure实现)。如数据持久化实现Mysql、短信通知实现、MQ通知、搜索引擎ES实现等,Event Bus 事件总线

清晰架构

7、整洁面向对象的分层架构

整洁面向对象的分层架构(COLA)是阿里2019开源的DDD架构,COLA 的主要目的是为应用架构提供一套简单的可以复制、可以理解、可以落地、可以控制复杂性的”指导和约束"。 目前,COLA发展到4.0,在架构设计上,COLA主张像六边形架构那样,使用端口-适配器去解耦技术细节;主张像洋葱架构那样,以领域为核心,并通过依赖倒置反转领域层的依赖方向。从COLA应用处理响应一个请求的过程来看,COLA使用了CQRS来分离命令和查询的职责,使用扩展点和元数据来提供更高应用的可扩展性。COLA 是一种整合了洋葱圈架构、适配器架构、DDD、整洁架构、TMF 等架构思想的一种应用架构。

COLA架构

4.2、框架实现

好的应用架构,都遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度等。COLA既是一种DDD架构,也是一种基于DDD设计的应用框架。COLA会被分成COLA架构和COLA组件两个部分:COLA架构关注应用架构的定义和构建,提升应用质量。COLA组件提供应用开发所需要的可复用组件,提升研发效率。具体详见 COLA开源

COLA架构

1)包结构规范

按照COLA的分包结构如下

分包结构

a) 适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;

b) 应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;

c)Client模块(Client Module):包含的代码应该是常见的服务接口Facade和DTO数据传输对象,如API、DTO、领域事件、Command和Query对象等等。

d)领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;

e)基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。

6)启动模块(Start Module):Spring Boot的启动类,应用入口。没有任何逻辑,只需要配置 application.properties 配置文件。

2)对象分层规范

VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。

DTO(Data Transfer Object): 数据传输对象,分布式应用提供粗粒度的数据实体,也是一种数据传输协议,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,这里泛指用于展示层与服务层之间的数据传输对象。RPC 对外暴露的服务涉及对象 API 就是 DTO,如GRPC、Dubbo。

Entity(Domain entity): 领域模型中的实体或值对象,就是从现实世界中抽象出来的有形或无形的业务实体,具有领域业务逻辑。

DO(Domain Object):领域持久化对象,一般就是是简单的 POJO对象。

DDD分层规范

五、DDD总结

DDD 不是一套框架,而是一种面向复杂问题的建模方法论和实践,所以在代码层面缺乏了足够的约束,导致 DDD 在实际应用中上手门槛很高,技术人员必须转变思维和架构习惯,软件系统最终交付的是业务价值,不是功能和技术方案,一切要以问题和业务为核心去建模和架构。DDD带来了很多好处,本质上是设计模式的改变,让领域与数据解耦,从业务需求出发,从领域出发,分析领域内模型及它们之间的关系,并进行领域建模,设计核心业务逻辑,进而实现技术细节。

应用架构的本质,就是要从繁杂的业务系统中提炼出共性,找到解决业务问题的最佳共同模式,为开发人员提供统一的认知,治理混乱。帮助应用系统“从混乱到有序”,COLA架构就是为此而生,其核心职责就是定义良好的应用结构,提供最佳实践。

原文https://zhuanlan.zhihu.com/p/603826546


标题:DDD领域驱动设计和实施
作者:michael
地址:https://blog.junxworks.cn/articles/2023/11/25/1700895879529.html