Skip to content

Blog 实战总览

一个完整的博客项目后端。使用仓颉 + Spire + SQLSharp,采用 CQRS 风格分层架构。本指南带你从零理解这个项目是怎么搭起来的,以及你自己开新项目时可以参考什么。

源码仓库:gitcode.com/soulsoft/blog


架构全景


这个项目能帮你解决什么

如果你正在用 Spire 框架做项目,大概率会遇到这些问题:

  • 目录不知道怎么分,controller、service、domain 到底怎么放
  • 查询和命令混在一起,代码越写越难改
  • 数据访问直接写在 controller 里,换个数据库就要改几十个文件
  • 不知道 JWT、OpenAPI、静态文件托管怎么拼到一起

这个博客项目就是针对这些问题的参考答案。它不是单个功能的演示,而是一个完整的、可运行的系统。

技术栈

技术用途
仓颉(Cangjie)编程语言
Spire 框架Web 全家桶:MVC、DI、认证、序列化、Mediator
SQLSharpORM:DbContext + DbSet + LINQ 风格查询
MariaDB关系型数据库
JWT Bearer身份认证

能力矩阵

项目实现了博客系统的全部核心能力,每一项都对应一个真实可调的 API。

模块能力读写路径关键实现
文章列表(分页+筛选+排序)、详情、创建、更新、删除读走 Queries + 视图,写走 Mediator + 事务VBlogPost 视图 join 了作者、分类、统计
评论列表、创建、删除,支持嵌套回复读走 Queries + 视图,写走 Mediator两级结构:parent_id 实现回复
收藏切换状态、检查状态写走 Mediator(Toggle 模式)调一次收藏,再调一次取消
关注切换状态、检查状态写走 Mediator(Toggle 模式)与收藏完全相同的模式
分类/标签获取全部纯读取,直接走 Queries文章列表筛选的数据源
用户注册、资料、文章列表、编辑、注销读写混合注册不登录,编辑需登录
认证登录获取 JWT、获取当前用户走 ConnectControllerJWT sub claim 存 userId
系统健康检查、OpenAPI 文档、静态文件托管中间件短路一行注册,框架接管

请求如何流转

text
读路径(查询):
  客户端 → Controller → Queries → DbContext 视图 → MariaDB

写路径(命令):
  客户端 → Controller → Mediator → TransactionHandler → CommandHandler → DbContext → MariaDB

读写分离的核心收益:读不需要事务,直接走 Queries 最快;写通过 Mediator 统一管理事务和横切逻辑,业务代码不写 begin/commit

建议的项目结构

下面是你自己开新项目时推荐采用的目录结构。这个结构已经在博客项目中验证过:

text
your-project/
├── web-api/                        # 后端:仓颉 + Spire
│   ├── src/
│   │   ├── main.cj                 # 入口:WebHost 配置、中间件管道
│   │   ├── controllers/            # 控制器:路由入口,参数绑定
│   │   ├── application/
│   │   │   ├── commands/           # 命令(CQRS 写):Command + Handler
│   │   │   ├── queries/            # 查询(CQRS 读):Query 对象
│   │   │   ├── services/           # 业务服务:跨模块复用的逻辑
│   │   │   ├── models/             # 应用层模型:SearchModel 等
│   │   │   └── behaviors/          # 管道行为:事务、日志等
│   │   ├── domain/
│   │   │   ├── entities/           # 实体:数据库表映射(代码生成)
│   │   │   ├── models/             # DTO:序列化模型(代码生成)
│   │   │   └── views/              # 视图实体(代码生成)
│   │   ├── infrastructure/
│   │   │   ├── AdminDbContext.cj          # 数据库上下文
│   │   │   ├── repositories/             # 仓储层
│   │   │   │   ├── BlogRepository.cj
│   │   │   │   ├── UserRepository.cj
│   │   │   │   └── CommentRepository.cj
│   │   │   ├── MapperUtilities.cj        # 对象映射
│   │   │   ├── UserAccessor.cj           # 当前用户访问器
│   │   │   └── middlewares/              # 中间件
│   │   └── abstractions/          # 抽象基类:Entity、QueryModel、PageResult
│   ├── template/                  # 代码生成模板
│   ├── library/                   # 本地依赖
│   └── wwwroot/                   # 静态文件托管目录
└── blog.sql                       # 数据库初始化脚本

目录结构的设计思路

从外到内看:

第一层:Controller(控制器) — 它只做三件事:接收请求参数、调用下层、返回结果。Controller 里不应该写任何 SQL 语句,也不应该写业务判断逻辑。

第二层:Application(应用层) — 业务逻辑的家。它分成三条独立路径:

路径负责什么时候用
Queries(查询)读取数据列表、详情、搜索,不修改数据
Commands(命令)写入数据创建、更新、删除,需要事务保护
Services(服务)跨模块复用一段逻辑被多个 Command 或 Controller 用到

第三层:Domain(领域层) — 纯粹的数据结构定义。Entity 对应数据库表,Model 是传给客户端的 DTO,View 对应数据库视图。这层不含任何业务逻辑

第四层:Infrastructure(基础设施层) — 技术细节。数据库连接、仓储实现、当前用户获取、异常处理中间件都在这层。

为什么要加仓储层

注意上面结构中 infrastructure/repositories/ 这一层。你可能会问:DbContext 直接就能增删改查,为什么还要包一层 Repository?

一个简单的场景就能说明问题。假设你的 BlogService 里这样写:

cangjie
// ❌ 直接在 Service 里写数据访问
public func getPublishedPosts(authorId: Int64): ArrayList<BlogPost> {
    let param = DbParameters()
    param.add("authorId", authorId)
    return context.blogPosts
        .params(param)
        .whereIf("author_id = @authorId AND is_public = 1")
        .toList()
}

这段代码有两个问题:

  1. SQL 散落各处"author_id = @authorId AND is_public = 1" 这个条件在 BlogServiceBlogQueriesUserService 里各写一遍。字段名一变,你要改三个地方。
  2. 测试困难:要测 BlogService,你必须连真实数据库,因为 AdminDbContext 没法轻易替换。

用仓储层之后:

cangjie
// ✅ Service 只关心业务,不关心 SQL
public func getPublishedPosts(authorId: Int64): ArrayList<BlogPost> {
    return blogRepo.findPublishedByAuthor(authorId)
}

具体怎么从"无仓储"演进到"有仓储",后面的 仓储层实战 会一步步讲解。

相关专题

如果你对某个基础概念不熟悉,建议先补基础再看本指南: