cangjie课堂演示
Blog
全栈博客系统 — 后端使用仓颉(Cangjie)+ Spire 框架,前端使用 React + TypeScript + Vite。
开发方式: 本项目使用 DeepSeek V4 + Claude Code 进行 AI 辅助开发。
项目预览

项目结构
blog/
├── web-api/ # 后端:仓颉 Web API
│ ├── src/
│ │ ├── main.cj # 入口:WebHost 配置、认证、路由
│ │ ├── controllers/ # 控制器:路由入口,参数转发
│ │ ├── application/
│ │ │ ├── commands/ # 命令(CQRS 写操作):增删改 + Handler
│ │ │ ├── queries/ # 查询(CQRS 读操作):分页列表、详情
│ │ │ ├── services/ # 业务服务:可复用逻辑
│ │ │ ├── models/ # 应用层模型:SearchModel 等
│ │ │ └── behaviors/ # 管道行为:事务等
│ │ ├── domain/
│ │ │ ├── entities/ # 实体:数据库表映射(entity.bat 生成)
│ │ │ ├── models/ # DTO:序列化模型(entity.bat 生成)
│ │ │ └── views/ # 视图实体(entity.bat 生成)
│ │ └── infrastructure/
│ │ ├── AdminDbContext.cj # 数据库上下文
│ │ ├── MapperUtility.cj # 对象映射工具
│ │ └── middlewares/ # 中间件(异常处理等)
│ ├── template/ # entity.bat 代码生成模板
│ ├── library/ # 本地依赖(MariaDB 驱动)
│ └── wwwroot/ # 前端构建产物(部署目录)
├── web-ui/ # 前端:React SPA
│ └── src/
│ ├── api/ # API 调用层
│ ├── components/ # 通用组件
│ ├── pages/ # 页面组件
│ ├── stores/ # Zustand 状态管理
│ ├── router/ # 路由配置
│ └── types/ # TypeScript 类型
└── blog.sql # 数据库初始化脚本
技术栈
后端
| 技术 | 说明 |
|---|---|
| 仓颉(Cangjie) | 编程语言,高性能系统级语言 |
| Spire 框架 | SoulSoft Web 全家桶:MVC、DI、认证、序列化、Mediator |
| SQLSharp | ORM 框架,DbContext + DbSet + LINQ 风格查询 |
| MariaDB | 关系型数据库 |
| JWT Bearer | 身份认证方案 |
Spire 框架组成:
| 包 | 用途 |
|---|---|
soulsoft_web_mvc |
Web 主机、路由、中间件管道 |
soulsoft_mediator |
CQRS 中介者:IRequest → IRequestHandler |
soulsoft_serialization |
JSON 序列化/反序列化,@Serialization 注解 |
soulsoft_web_authentication_jwtbearer |
JWT Bearer 认证 |
soulsoft_identity_tokens |
JWT Token 颁发与验证 |
soulsoft_extensions_* |
DI 容器、配置、日志等基础设施 |
前端
| 技术 | 说明 |
|---|---|
| React 18 | UI 框架 |
| TypeScript | 类型安全 |
| Vite | 构建工具 & 开发服务器 |
| Tailwind CSS | 原子化 CSS(GitHub Dark 主题) |
| Zustand | 轻量状态管理 |
| React Router 6 | 客户端路由 |
快速开始
环境要求
- 仓颉 SDK(Cangjie 1.1.0+)
- Node.js 18+
- MariaDB / MySQL 8.0+
- sqlsharp_utils — 代码生成工具,需克隆并编译(见步骤 2)
1. 初始化数据库
执行 blog.sql 建库建表:
mysql -u root -p < blog.sql
数据库配置在 web-api/appsettings.Development.json:
{
"ConnectionStrings": {
"mysql": "mariadb://localhost:3306?username=root&password=1024&database=blog&pool.max.size=100&pool.max.idle.size=100",
"opengauss": "opengauss://gaussdb:OpenGauss@123@115.120.192.208:5432/cangjie_framework?sslmode=disable"
}
}
2. 代码生成(DB-First)
下载代码生成工具
首先下载并编译 sqlsharp_utils:
git clone https://gitcode.com/soulsoft/sqlsharp_utils.git
cd sqlsharp_utils
# 编译 dbgen 工具
cjpm update
cjpm build
编译完成后将 dbgen 可执行文件放到 PATH 路径下,或复制到 web-api/ 目录。
运行代码生成
数据库表结构变更后,运行 entity.bat 自动生成 Entity / Model / View:
cd web-api
./entity.bat
生成的文件:
src/domain/entities/— 表映射实体src/domain/views/— 视图实体src/domain/models/— DTO 模型
3. 启动后端
cd web-api
# 安装依赖
cjpm update
# 编译
cjpm build
# 运行(默认端口 5000,默认 Production 环境)
cjpm run
切换运行环境:
# 方式一:环境变量
export CANGJIE_ENVIRONMENT=Development
cjpm run
# 方式二:命令行参数
cjpm run -- --environment Development
💡 VS Code 用户: 在
.vscode/settings.json中配置"terminal.integrated.env.windows": { "CANGJIE_ENVIRONMENT": "Development" },终端打开即自动设置环境变量。
启动后访问 http://localhost:5000。
Swagger 文档地址:http://localhost:5000/openapi。
4. 启动前端(开发模式)
cd web-ui
# 安装依赖
npm install
# 启动开发服务器(端口 5173)
npm run dev
开发服务器自动代理 /api 到后端 http://localhost:5000。
5. 构建前端(生产模式)
cd web-ui
npm run build
构建产物输出到 web-api/wwwroot/,后端直接托管静态文件,单服务部署。
后端架构
分层设计
请求 → Controller → Mediator/Queries → Service → DbContext → Database
本项目遵循 CQRS 风格 读写分离:
| 层 | 读(Query) | 写(Command) |
|---|---|---|
| 入口 | Controller Action + Queries 直接调用 | Controller Action + Mediator 派发 |
| 处理 | Queries → DbContext → SQL | CommandHandler → Service → DbContext → SQL |
| 返回 | PageResult<T> / 实体 |
void / Bool |
| 管道 | 无 | 走 Mediator 管道(可插事务等 Behavior) |
要点:
- 查询直接调用 Queries,避免 Mediator 开销
- 命令必须通过 Mediator 派发,支持管道扩展(事务、日志等)
- Service 只在多 Command/Controller 复用时抽取
参数绑定规则
Controller Action 参数必须正确标注绑定来源:
| 注解 | 适用场景 | 示例 |
|---|---|---|
| 无注解(默认 FromBody) | 复杂类型(class,有 @Serialization) |
create(command: CreateCommand) |
@FromQuery |
基本类型(Int64、String)从 URL 取值 |
getDetail(@FromQuery id: Int64) |
@FromForm |
form-urlencoded 表单(如登录) | token(@FromForm username: String) |
@FromRoute |
从路由路径取值 | getBySlug(@FromRoute slug: String) |
⚠️ 基本类型(
Int64、String、Bool)必须显式标注@FromQuery、@FromForm或@FromRoute,否则参数值为默认值(0、空字符串)。
whereIf 筛选语法
// 模糊搜索
.whereIf("POSITION(@keyword IN title) > 0", model.keyword.isSome())
// 精确匹配
.whereIf("category_code = @categoryCode", model.categoryCode.isSome())
// 子查询
.whereIf("id IN (SELECT pt.post_id FROM post_tag pt JOIN tag t ON t.id = pt.tag_id WHERE t.name = @tagName)", model.tagName.isSome())
// 固定条件(始终生效)
.whereIf("is_public = 1")
控制器示例
@Route["api/[controller]/[action]"]
public class BlogController <: Controller {
public BlogController(let mediator: IMediator, let queries: BlogQueries) {}
@HttpPost
public func getList(model: BlogPostSearchModel) {
return queries.getBlogPostList(model)
}
@HttpPost
public func getDetail(@FromQuery id: Int64) {
return queries.getBlogPostDetail(id)
}
@HttpPost
public func create(command: CreateBlogPostCommand) {
mediator.send(command)
}
}
API 概览
| 模块 | 接口 | 方法 | 说明 |
|---|---|---|---|
| 认证 | /api/Connect/token |
POST | 登录获取 JWT |
| 认证 | /api/Connect/userinfo |
GET | 获取当前用户信息 |
| 用户 | /api/User/register |
POST | 注册 |
| 用户 | /api/User/getUserProfile |
POST | 获取用户资料 |
| 用户 | /api/User/updateProfile |
POST | 编辑资料 |
| 用户 | /api/User/getUserPosts |
POST | 用户的文章列表 |
| 博客 | /api/Blog/getBlogPostList |
POST | 文章列表(分页+筛选+排序) |
| 博客 | /api/Blog/getBlogPostDetail |
POST | 文章详情 |
| 博客 | /api/Blog/createBlogPost |
POST | 创建文章 |
| 博客 | /api/Blog/updateBlogPost |
POST | 更新文章 |
| 博客 | /api/Blog/deleteBlogPost |
POST | 删除文章 |
| 博客 | /api/Blog/getComments |
POST | 评论列表 |
| 博客 | /api/Blog/createComment |
POST | 创建评论 |
| 博客 | /api/Blog/toggleStar |
POST | 收藏/取消收藏 |
| 博客 | /api/Blog/toggleFollow |
POST | 关注/取消关注 |
| 博客 | /api/Blog/getCategories |
POST | 全部分类 |
| 博客 | /api/Blog/getTags |
POST | 全部标签 |
前端页面
| 路由 | 页面 | 说明 |
|---|---|---|
/ |
Home | 首页:文章列表 + 侧边栏筛选 |
/login |
Login | 登录 |
/register |
Register | 注册 |
/write |
Write | 创建文章 |
/write/:id |
Write | 编辑文章 |
/blog/:id |
BlogDetail | 文章详情 + 评论 |
/profile/:username |
Profile | 用户主页 |
/settings |
Settings | 个人设置 |
注意事项
- 数据库表名用单数(
user而非users) - 仓颉关键字(如
key)不能作为字段名,需改列名或使用反引号转义 - 布尔字段在数据库中为
TINYINT(1),仓颉中映射为Int8 - 前端构建到
wwwroot/后,后端作为静态文件服务,无需单独部署