DDD 领域模型在低代码后端服务中的应用
低代码平台的后端不仅要支撑「站点、页面、用户、系统设置」等资源的 CRUD,还要在 API 契约统一、多端对接(管理后台 BFF、对外站点)的前提下,保持领域边界清晰、业务规则可演进。本文从架构师视角,以 luban-backend 的实现为蓝本,说明 DDD 领域模型设计在低代码后端服务中的应用,包括战略层面的限界上下文划分与战术层面的实体、应用服务、领域异常及与 BFF 的协作方式。
一、为什么低代码后端需要领域模型
低代码后端的核心职责可以概括为:
- 内容模型:站点(Site)与页面(Page + PageSchema)的创建、发布、按路径访问;
- 身份与权限:用户(User)与认证(登录、/auth/me)、基于角色的访问控制(RequireUser / RequireAdmin);
- 系统配置:系统设置(SystemSettings)的读写与缓存(如 Redis);
- 对外能力:按站点 slug + 路径对外暴露已发布页面,无需鉴权(供官网等消费)。
若仅按「表结构 + Controller + Service」平铺,很容易出现:
- 业务规则散落在各处,修改一处忘记另一处;
- 错误语义不统一,前端/BFF 难以稳定处理;
- 扩展新资源或新上下文时边界模糊,影响可维护性。
引入 DDD 的领域模型思维(不必强求完整 DDD 框架),可以在保持实现轻量的前提下,获得清晰的边界、一致的业务语义和可演进的 API 契约。下面从战略与战术两方面结合 luban-backend 说明。
二、战略设计:限界上下文与能力边界
从业务能力出发,可将低代码后端划分为以下限界上下文(Bounded Context),与当前模块/包结构对应关系如下。
| 限界上下文 | 职责 | 主要实体与概念 | 对外暴露 |
|---|---|---|---|
| 站点与页面(Content) | 站点与页面的生命周期、同站点下 path 唯一、schema 存储 | Site(聚合根)、Page(实体,属 Site) | 管理端 CRUD;公开接口按 slug+path 查已发布页 |
| 用户与认证(Identity & Auth) | 用户管理、登录校验、当前用户解析 | User(聚合根)、角色/状态 | 登录、/auth/me;Header 注入 X-User-ID / X-User-Role |
| 系统设置(Settings) | 全局配置的读写与缓存 | SystemSettings(单例式聚合) | GET/PUT /settings,仅 Admin |
| 公开访问(Public) | 按站点 slug + path 返回已发布页面,只读、无鉴权 | 复用 Site、Page,只读、过滤 status=published | GET /public/sites/:slug/pages?path=… |
架构要点:
- Content 与 Public 共享同一套 Site/Page 持久化模型,但 Public 不写、只按「已发布」读,语义上属于「对外只读能力」,与管理端写操作解耦。
- Identity & Auth 通过 BFF 注入的 Header 与后端达成契约,后端不签发 JWT,只校验并消费身份,避免与 BFF 的职责重叠。
- Settings 使用 Redis 缓存(如
settings:global),读多写少,与 MySQL 的持久化形成「写库 + 缓存」的简单 CQRS 式读写路径。
这些边界在代码中体现为:SiteService / PageService、AuthService / UserService、SettingsService、PublicPageService 等应用服务的分工,以及 Controller 按资源与鉴权要求路由到对应服务。
三、战术设计:实体、聚合与不变性
3.1 实体与表结构一一对应
当前实现采用「实体与数据库表基本一一对应」的务实做法,便于与 MyBatis Mapper 配合,同时保持领域概念清晰:
- Site:id、name、slug、baseUrl、status、createdAt、updatedAt;slug 全局唯一。
- Page:id、siteId、name、path、status、schemaJson、createdAt、updatedAt;同一 site 下 path 唯一,且通过外键依赖 Site。
- User:id、username、name、role、status、password(不暴露给 API)、createdAt、updatedAt;username 唯一。
从 DDD 角度看:
- Site 可作为「站点聚合根」:页面的创建、更新、删除都应以「站点存在」为前提,path 唯一性在聚合内由应用服务 + 数据库约束共同保证。
- Page 是 Site 聚合内的实体,通过
siteId归属明确;不单独作为聚合根,避免跨聚合强一致性的复杂度。 - User 是独立聚合根,与 Site/Page 无强一致性要求,仅通过「当前用户」身份参与 Content、Settings 的访问控制。
3.2 业务规则与不变性
在应用服务层显式落实领域规则,例如:
- Page 创建/更新:先校验
siteMapper.getById(siteId) != null,再执行 path 唯一性约束;冲突时抛出PAGE_PATH_CONFLICT(409)。 - Site 创建/更新:slug 唯一,冲突时
SLUG_CONFLICT(409)。 - User 创建/更新:username 唯一,冲突时
USERNAME_CONFLICT(409);密码经 BCrypt 编码后落库。 - 登录:仅当用户存在、status=active 且密码匹配时返回 user + claims;否则
INVALID_CREDENTIALS或USER_DISABLED。
这些规则集中在各自的 Service 中,配合 Mapper 的持久化与唯一约束,形成「领域不变性」的守卫点,便于后续扩展(如更多状态、更多唯一键)时保持行为一致。
3.3 领域异常与统一 API 契约
将「业务失败」建模为领域异常(如 BusinessException),并映射到统一的 HTTP 状态码与错误体,是领域模型对外呈现的重要一环。luban-backend 采用:
- 异常类型:
BusinessException,携带httpStatus、code、message、details。 - 工厂方法:
siteNotFound()、pageNotFound()、userNotFound()、pagePathConflict()、usernameConflict()、slugConflict()、invalidCredentials()、userDisabled()、unauthenticated()、permissionDenied()、invalidArgument(message)等,对应文档中的错误码表。 - 全局处理:
@RestControllerAdvice将BusinessException转为{ "code", "message", "details" }的 JSON 响应,与 BFF/前端约定一致。
这样,领域层的「资源不存在」「冲突」「未认证」「无权限」等语义,都能以稳定、可编程的方式传递到调用方,便于前端/BFF 做统一错误处理与用户提示。
四、应用服务与分层
分层采用「Controller → Application Service → Mapper/Redis」的简洁结构,不引入额外的 Domain Service 层,以降低复杂度:
- Controller:负责 HTTP 入参解析、鉴权结果消费(如从
UserContext取 userId/role)、调用应用服务并返回 DTO。 - Application Service(*Service):编排用例、执行领域规则、调用 Mapper/Redis,必要时抛出
BusinessException。 - Mapper / Redis:持久化与缓存,不承载业务规则。
应用服务之间的依赖关系体现上下文边界,例如:
PageService依赖SiteMapper与PageMapper,在创建/列表页面前先校验站点存在。PublicPageService依赖SiteMapper、PageMapper,仅按 slug 解析站点、按 path 查询已发布页面,不写数据。AuthService仅依赖UserMapper与PasswordEncoder,与 Site/Page 无直接依赖。
这种划分有利于后续若引入「领域事件」或「跨上下文调用」时,在边界清晰的前提下扩展。
五、与 BFF 的契约与鉴权
低代码后端与 BFF 的协作遵循「契约先行」:
- Base path:
/backend,健康检查可为/ping或/backend/ping。 - 鉴权:受保护接口依赖 Header
X-User-ID(必填)、X-User-Role;缺失则 401(UNAUTHENTICATED);需 Admin 的接口再校验 role,否则 403(PERMISSION_DENIED)。 - 错误体:统一
{ "code", "message", "details" },与 API 文档 一致。 - 请求/响应:JSON 字段 camelCase,时间 ISO 8601;登录响应为
user+claims,不返回 password。
后端不关心 JWT 的签发与解析,只信任 BFF 注入的 Header,从而将「认证」留在 BFF、「授权」留在后端的领域规则(RequireUser / RequireAdmin)中,职责清晰。
六、小结与演进建议
以 luban-backend 为例,DDD 在低代码后端中的应用可以概括为:
- 战略上:按「站点与页面」「用户与认证」「系统设置」「公开访问」划分限界上下文,明确各上下文的职责与对外能力。
- 战术上:用实体(Site、Page、User)与聚合边界(Site 为页面聚合根、User 独立)承载核心概念;用应用服务落实业务规则与不变性;用领域异常 + 统一错误体表达业务失败,并与 BFF 契约对齐。
- 实现上:保持 Controller → Service → Mapper/Redis 的简洁分层,不过度抽象,便于与现有 Spring Boot + MyBatis + Redis 技术栈协同。
后续若业务扩展,可考虑:
- 在聚合内引入更丰富的值对象(如 Path、Slug、PageStatus),进一步收口校验逻辑;
- 对「页面发布」等关键操作发出领域事件,供审计、搜索或缓存失效使用;
- 在保持与 Go 版 API 兼容的前提下,将公开读路径(Public)与管理端写路径在存储或缓存策略上进一步分离,以支撑更大流量。
这样,在架构师视角下,低代码后端既能保持与 BFF、前端的稳定契约,又能在领域模型层面保持清晰边界与可演进性,为后续多运行时(如 Java/Go 双实现)与功能扩展打下基础。
