前段时间一直在做合同管理相关的工作,从需求分析到最终落地,整个过程中踩了不少坑,也积累了一些经验。正好探索过程也记录下来了,一方面是给自己做个总结,另一方面也希望能给正在做类似系统的朋友一些参考。

技术选型的那些事

在确定要做独立服务之后,首先面临的就是技术选型。当时主要关注几个关键点:

持久层框架的选择

最开始想的是用 JPA,毕竟 Spring 生态下这个方案最成熟,而且我也想试试JPA。但考虑到业务场景:

  • 查询场景比较复杂,经常需要多表关联和动态条件组合
  • 需要精细控制 SQL 的执行,特别是在性能敏感的场景下
  • 本人对 SQL 更熟悉,调试和优化起来更顺手

最终还是选了 MyBatis Plus。它保留了 MyBatis 的灵活性,同时又提供了很多开箱即用的功能,比如分页、条件构造器等,能减少不少重复代码。

领域驱动设计的实践

这个项目是我第一次比较完整地实践 DDD 思想。之前也在一些小项目里用过,但都比较零散。这次我决定彻底一点:

  • domain 层完全隔离,不依赖任何基础设施实现
  • 通过 Repository 接口来抽象数据访问,让业务逻辑和持久化解耦
  • 使用 值对象来封装那些有业务含义的字段组合

这样做的初期成本确实不低,写代码的时候要多考虑一些层次边界。但当后面需求变更的时候,优势就体现出来了——业务逻辑集中在 domain 层,改起来心里踏实。

模块架构一览

整个模块采用的是分层架构,从内到外分为四层:

┌─────────────────────────────────────────────────────┐
│                  Interface Layer                     │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ REST API     │  │ Feign Client │  │ MQ Handler │ │
│  └──────────────┘  └──────────────┘  └────────────┘ │
├─────────────────────────────────────────────────────┤
│                 Application Layer                    │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ DTOs         │  │ Commands     │  │ Queries    │ │
│  │ Convertors   │  │ App Services │  │ Validators │ │
│  └──────────────┘  └──────────────┘  └────────────┘ │
├─────────────────────────────────────────────────────┤
│                   Domain Layer                       │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ Aggregates   │  │ Value Objects│  │ Domain Svc │ │
│  │ Repositories │  │ Domain Events│  │ Exceptions │ │
│  └──────────────┘  └──────────────┘  └────────────┘ │
├─────────────────────────────────────────────────────┤
│               Infrastructure Layer                   │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ Entities     │  │ Mappers      │  │ Handlers   │ │
│  │ Converters   │  │ Repository   │  │ Messaging  │ │
│  └──────────────┘  └──────────────┘  └────────────┘ │
└─────────────────────────────────────────────────────┘

Interface Layer:对外的门面

这一层主要处理和前端的交互。REST Controller 负责处理 HTTP 请求,Feign Client 用于服务间调用,MQ Handler 则处理异步消息。

在设计 API 的时候,遵循了一些约定:

  • API 版本化:通过 URL 前缀来区分版本,为后续升级留空间
  • 统一响应格式:所有接口都返回统一的响应结构,便于前端处理
  • 详细的错误信息:业务异常会返回明确的错误码和描述,减少排查成本

Application Layer:用例的编排

应用层是很多 DDD 实践者容易忽略的一层。它的职责是把 domain 层的能力组合起来,完成一个个具体的用例。

比如"创建合同"这个用例,应用层要做的事情包括:

  1. 接收并验证请求参数
  2. 调用 domain 层的验证逻辑确保业务规则
  3. 组装领域模型并持久化
  4. 发布领域事件通知其他服务
  5. 返回响应给调用方

通过这样的编排,domain 层可以保持纯净,只关注核心业务逻辑。

Domain Layer:核心的价值

这是整个模块的心脏,也是投入最多精力的地方。

聚合根设计:合同(Contract)是核心聚合根,它管理着合同的完整生命周期。所有的状态变更都要通过聚合根来执行,这样能保证业务规则不被绕过。

值对象:像合同金额、参与方信息这些有明确业务含义的概念,我做成了值对象。它们是不可变的,通过构造函数就能保证数据的有效性。

领域服务:有些逻辑不适合放在实体或值对象里,比如跨聚合的操作,我会放在领域服务中。这样既能保持模型的纯净,又能让代码有个明确的归属。

Infrastructure Layer:技术的支撑

基础设施层主要处理技术相关的实现细节:

  • Entity 和 Mapper:把领域模型映射到数据库表结构
  • Repository 实现:实现 domain 层定义的 Repository 接口
  • Type Handler:处理 PostgreSQL 的 JSONB 类型,让我能在数据库里存复杂对象
  • 消息队列配置:封装 RabbitMQ 的操作逻辑

数据库设计的思考

为什么选择 PostgreSQL

在选型的时候也对比了几个主流数据库。最终选 PostgreSQL 主要是因为:

  • JSONB 支持:合同相关的很多配置信息结构灵活,用关系型字段存会很麻烦,JSONB 刚好能解决这个问题
  • 事务能力:合同状态变更对一致性要求很高,需要强事务保证
  • 性能表现:在复杂查询场景下,PostgreSQL 的优化器表现不错

迁移脚本管理

数据库迁移用的是 Flyway。每个变更都会创建一个版本化的 SQL 脚本,文件名遵循 V{version}__Description.sql 的格式。

了解后使用起来确实很方便,但是有一个十分严重的问题,开发的时候经常需要修改数据库,导致每一次都需要重新创建一遍,导致很多时间浪费在重新执行脚本和重新准备数据上。

JSONB 类型的应用场景

有几个地方用 JSONB 特别合适:

  • 合同元数据:不同类型合同需要的属性差异很大,用 JSON 字段灵活很多
  • 审核配置:审核规则的配置比较复杂,而且经常变化,用 JSONB 存储可以避免频繁改表结构
  • 操作详情:审计日志里需要记录具体的操作内容,用 JSONB 可以存结构化的详细信息

不过 JSONB 也有坑,比如查询性能不如普通字段,索引建立也麻烦一些。所以在用的时候要权衡,不是什么场景都往里装的。

与其他模块的协作

作为一个微服务,合同管理模块不是孤立的。它需要和其他服务配合才能完成完整的业务流程。

与审核引擎的集成

合同创建后需要自动启动审核流程。是通过消息队列来解耦的:

  1. 合同管理服务发布一个"合同已创建"的事件
  2. 审核引擎监听这个事件,触发对应的审核流程
  3. 审核完成后,审核引擎回调本模块的接口更新合同状态

这样双方的服务可以独立演进,互不影响。

与文件服务的交互

合同附件的存储是个独立的服务。本模块只存文件的引用 ID,具体的文件操作都通过文件服务的客户端来完成。

这里遇到过一次性能问题:批量下载附件的时候,每次都要调用文件服务,网络开销不小。后来做了个简单的优化,在查询合同详情的时候并行去获取文件信息,减少等待时间。

与 AI 服务的配合

有些场景需要用到 AI 的能力,比如合同要素提取、风险识别等。这些能力是通过 AI 服务的 Feign 客户端来调用的。

因为是远程调用,必须考虑超时和重试的问题。用的是 Resilience4j 做熔断和降级,避免因为 AI 服务不稳定影响核心业务。

开发中遇到的那些坑

缓存策略

使用了 Redis 缓存,配合合适的过期时间,这个问题算是解决了。但又遇到了新问题:缓存更新的时候如何保证数据一致性?

最终采用的方案是"延迟双删"——更新数据库后先删一次缓存,隔一小段时间再删一次。虽然不是完美的方案,但在我的业务场景下够用了。

事务边界的控制

在一个用例中既要更新合同状态,又要发布消息给审核引擎,还要记录操作日志。最开始这些逻辑都放在一个事务里,结果消息队列的故障导致整个事务回滚,用户体验不好。

后来把事务边界收窄了,只把数据库操作放在事务里,消息发送放在事务外。虽然增加了最终一致性的复杂度,但整体系统的可用性提升了不少。

复杂查询的优化

有个需求是要按照多种条件组合查询合同,还要支持分页和排序。用 MyBatis Plus 的条件构造器写了一堆 if 判断,代码很难维护。

后来引入了一个查询助手类,把常用的查询条件封装起来。对于特别复杂的场景,直接写 XML 映射文件,虽然繁琐一点,但可读性和可维护性都更好。

回顾和展望

这个模块从设计到落地用了大概一个月的时间。过程中确实走了不少弯路,但回头来看,这些经历都挺宝贵的。

做得好的地方

  • 清晰的分层架构让代码有明确的归属,改动的时候不容易牵一发动全身
  • DDD 的实践让领域模型比较贴近业务语言,新人接手的时候理解成本不算太高
  • 完善的测试虽然增加了开发时间,但几次重构都顺利过来了,没有出大的问题

需要改进的地方

  • 模块边界模糊,engine和management模块对于一些业务实体的定义没有完全分离,导致编写业务代码的时候发生了必须互相使用feign的耦合,应该先不分离模块,等到一定业务量再分离。
  • 文档还是有点欠缺,特别是领域模型的决策过程,应该早点记录下来
  • 性能优化的空间还很大,有些查询在数据量大的时候会比较慢
  • 错误处理可以更细致一些,现在很多异常直接抛到全局处理器了

下一步的计划

  • 引入事件溯源:对于合同状态变更这种关键操作,想尝试用事件溯源的方式来记录,方便审计和回溯
  • 优化查询性能:考虑引入一些缓存策略,或者针对高频查询场景做专门的优化
  • 完善监控体系:建立更细粒度的监控指标,提前发现潜在的问题

结语

写到这里,从最初的构思到现在逐步稳定,每一步都不容易。技术选型的纠结、架构设计的思考、编码过程中的调试、上线后的故障处理,这些经历都在不知不觉中塑造着我的技术认知。

如果这篇分享能给正在做类似系统的你一些启发,那就更好了。毕竟,在技术这条路上,我们都是在彼此的经验中成长。


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐