07. API 设计与实现
本章目标
- 理解 RESTful API 设计原则与规范
- 掌握 Fiber 框架路由定义与中间件使用
- 学会实现 CRUD 接口和错误处理
- 理解前后端数据交互格式
RESTful API 设计原则
REST(Representational State Transfer)是目前最流行的 Web API 设计风格。Commit Dashboard 后端遵循 RESTful 规范,为前端提供清晰、统一的接口。
资源命名规范
RESTful API 的核心是「资源」。在设计 API 时,应使用名词而非动词来表示资源:
✅ GET /api/repos # 获取仓库列表(资源:仓库)
✅ GET /api/repos/:id # 获取单个仓库(资源:仓库)
✅ POST /api/sync/repos # 触发仓库同步(资源:同步操作)
❌ GET /api/getRepos # 错误:使用了动词
❌ POST /api/createRepo # 错误:使用了动词
项目中的 API 路径设计:
| 资源 | 路径 | 说明 |
|---|---|---|
| 仓库 | /api/repos | 仓库相关操作 |
| 提交 | /api/commits | 提交记录相关 |
| 贡献者 | /api/contributors | 贡献者相关 |
| 同步 | /api/sync | 数据同步操作 |
| 认证 | /api/verify | Gitea 认证 |
| 数据分析 | /api/dashboard/* | 统计与聚合 |
HTTP 方法语义
不同 HTTP 方法有不同的语义,正确使用它们可以让 API 更加直观:
| 方法 | 语义 | 示例 |
|---|---|---|
| GET | 获取资源 | GET /api/repos 获取仓库列表 |
| POST | 创建资源 | POST /api/sync/repos 触发同步 |
| PUT | 更新资源(完整) | PUT /api/repos/:id 更新仓库 |
| PATCH | 更新资源(部分) | PATCH /api/settings 更新设置 |
| DELETE | 删除资源 | DELETE /api/repos/:id 删除仓库 |
状态码规范
HTTP 状态码表示请求的处理结果:
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | 成功获取/更新资源 |
| 201 | Created | 成功创建资源 |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证(Token 无效) |
| 403 | Forbidden | 无权限访问 |
| 404 | Not Found | 资源不存在 |
| 500 | Internal Server Error | 服务器内部错误 |
项目使用统一响应格式,数据包装在 data 字段中:
// 成功响应
{
"code": 0,
"data": [...],
"message": "success"
}
// 失败响应
{
"code": -1,
"data": null,
"message": "查询失败"
}
Fiber 框架路由基础
Commit Dashboard 后端使用 Fiber 作为 Web 框架。Fiber 是 Go 语言中性能优秀的 HTTP 框架,API 设计参考了 Express.js,简单易用。
应用初始化
在 server/router/router.go 中创建 Fiber 应用:
// 创建 Fiber 应用实例
app := fiber.New(fiber.Config{
AppName: "Commit Dashboard Server", // 应用名称
ErrorHandler: customErrorHandler, // 自定义错误处理
})
// 注册全局中间件
app.Use(recover.New()) // 捕获 panic,防止崩溃
app.Use(logger.New()) // 请求日志
app.Use(cors.New()) // 跨域资源共享
// 返回应用实例
return app
fiber.Config 用于配置应用行为,其中 ErrorHandler 是自定义错误处理函数,当路由处理函数返回错误时会被调用。
中间件机制
中间件是位于请求和响应之间的函数,可以执行以下操作:
- 修改请求或响应
- 验证用户身份
- 记录日志
- 处理错误
graph LR
A[客户端请求] --> B[中间件1: Recover]
B --> C[中间件2: Logger]
C --> D[中间件3: CORS]
D --> E[路由处理函数]
E --> F[响应]
F --> C
F --> B
项目中的全局中间件配置:
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:5173", // 允许的前端地址
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Origin,Content-Type,Accept,Authorization,X-Gitea-Token,X-Gitea-Base-Url",
AllowCredentials: true, // 允许携带认证信息
}))
CORS(Cross-Origin Resource Sharing)中间件允许浏览器跨域请求。前端运行在 localhost:5173,后端在 localhost:8080,需要配置 CORS 才能正常通信。
路由定义
Fiber 使用简洁的语法定义路由:
// 基础路由
app.Get("/hello", func(c *fiber.Ctx) error {
return c.SendString("Hello World")
})
// 路由组
api := app.Group("/api")
{
api.Get("/repos", repoHandler.List)
api.Get("/repos/:id", repoHandler.Get)
api.Post("/repos", repoHandler.Create)
}
// 路径参数
app.Get("/repos/:owner/:repo", func(c *fiber.Ctx) error {
owner := c.Params("owner") // 获取路径参数
repo := c.Params("repo")
return c.SendString(owner + "/" + repo)
})
路径参数使用冒号 : 开头,通过 c.Params() 方法获取。
项目路由结构
Commit Dashboard 的路由分为两个主要部分:Gitea 相关路由和 AI 相关路由。
路由初始化流程
完整路由定义
在 server/app/gitea/router/gitea.go 中定义了所有 Gitea 相关路由:
// 认证路由
router.Post("/verify", authHandler.Verify)
// 同步路由(数据同步到本地数据库)
sync := router.Group("/sync")
{
sync.Post("/repos", syncHandler.SyncRepos) // 同步仓库列表
sync.Post("/repo-branches", syncHandler.SyncRepoBranches) // 同步分支
sync.Post("/branch-commits", syncHandler.SyncBranchCommits) // 同步提交
sync.Post("/rebuild-contributors", syncHandler.RebuildContributors) // 重建贡献者
sync.Get("/progress/:sync_id", syncHandler.SyncProgressSSE) // 同步进度
}
// 仓库路由
repos := router.Group("/repos")
{
repos.Get("/", repoHandler.List) // 仓库列表
repos.Get("/list", repoHandler.ListWithPage) // 分页查询
repos.Get("/analytics", analyticsHandler.RepoAnalytics) // 仓库分析
repos.Get("/:owner/:repo/branches", branchHandler.List) // 仓库分支
repos.Get("/:owner/:repo/contributors", contributorHandler.ListByRepo) // 仓库贡献者
repos.Get("/:owner/:repo/analytics", analyticsHandler.RepoDetailAnalytics) // 仓库详情分析
}
// 提交路由
commits := router.Group("/commits")
{
commits.Get("/", commitHandler.List) // 提交列表
commits.Get("/stats", commitHandler.Stats) // 提交统计
commits.Get("/analytics", analyticsHandler.CommitAnalytics) // 提交分析
}
// 贡献者路由
contributors := router.Group("/contributors")
{
contributors.Get("/", contributorHandler.List) // 贡献者列表
contributors.Get("/analytics", analyticsHandler.ContributorAnalytics) // 贡献者分析
contributors.Get("/:id", contributorHandler.Detail) // 贡献者详情
}
// Dashboard 统计
router.Get("/dashboard/summary", analyticsHandler.DashboardSummary)
这种分组方式让 API 结构清晰,每个路由组对应一个业务领域。
Handler 处理器实现
Handler 是处理请求的核心函数,每个路由对应一个 Handler 方法。
Handler 结构
// RepoHandler 仓库处理器
type RepoHandler struct {
repoRepo *repository.RepoRepository // 依赖 Repository
}
// NewRepoHandler 构造函数
func NewRepoHandler(repoRepo *repository.RepoRepository) *RepoHandler {
return &RepoHandler{repoRepo: repoRepo}
}
// List 处理获取仓库列表请求
func (h *RepoHandler) List(c *fiber.Ctx) error {
// 1. 调用 Repository 获取数据
repos, err := h.repoRepo.List(c.Context())
if err != nil {
return response.InternalServerCtx(c, "查询仓库列表失败")
}
// 2. 返回成功响应
return response.SuccessCtx(c, repos)
}
c.Context() 获取 Go 的 context.Context,用于超时控制和取消操作。
获取请求参数
Fiber 提供多种方式获取请求参数:
// 路径参数
app.Get("/repos/:owner/:repo", func(c *fiber.Ctx) error {
owner := c.Params("owner") // /repos/foo/bar -> "foo"
repo := c.Params("repo") // /repos/foo/bar -> "bar"
return c.SendString(owner + "/" + repo)
})
// 查询参数(URL ?后的参数)
app.Get("/search", func(c *fiber.Ctx) error {
query := c.Query("q") // /search?q=keyword
page := c.QueryInt("page", 1) // 带默认值的查询参数
return c.SendString(query)
})
// 请求体(JSON)
type CreateRepoRequest struct {
Name string `json:"name"`
Description string `json:"description"`
}
app.Post("/repos", func(c *fiber.Ctx) error {
var req CreateRepoRequest
if err := c.BodyParser(&req); err != nil {
return response.BadRequestCtx(c, "参数解析失败")
}
// 使用 req.Name, req.Description
})
分页查询实现
项目中分页查询的完整实现:
// ListWithPage 分页查询仓库列表
func (h *RepoHandler) ListWithPage(c *fiber.Ctx) error {
// 1. 解析查询参数
var params gitea_req.RepoQueryParams
if err := c.QueryParser(¶ms); err != nil {
return response.BadRequestCtx(c, "参数解析失败")
}
// 2. 验证并修正分页参数(防止负数页码)
params.Validate()
// 3. 调用 Repository 查询
repos, total, err := h.repoRepo.ListWithPage(
c.Context(),
params.Page,
params.PageSize,
params.Keywords,
)
if err != nil {
return response.InternalServerCtx(c, "查询仓库列表失败")
}
// 4. 返回分页响应
return response.SuccessCtx(c, pagination.NewPaginationResponse(
repos,
int64(total),
params.Page,
params.PageSize,
))
}
分页响应格式:
{
"code": 0,
"data": {
"data": [...],
"total": 100,
"page": 1,
"page_size": 10,
"total_page": 10
},
"message": "success"
}
统一响应格式
项目定义了统一的响应格式,便于前端处理。
响应结构体
在 server/common/response/response.go 中定义了响应结构:
// 泛型响应结构体
type Response[T any] struct {
Code int `json:"code"` // 状态码:0成功,-1失败
Data T `json:"data"` // 数据
Message string `json:"message"` // 消息
}
// 成功响应
func Success[T any](data T) Response[T] {
return Response[T]{
Code: 0,
Data: data,
Message: "success",
}
}
// 失败响应
func Fail(message string) Response[any] {
return Response[any]{
Code: -1,
Data: nil,
Message: message,
}
}
快捷响应方法
为了简化 Handler 中的响应编写,封装了快捷方法:
// 成功响应快捷方式
func SuccessCtx[T any](c *fiber.Ctx, data T) error {
return c.JSON(Success(data))
}
// 失败响应快捷方式
func FailCtx(c *fiber.Ctx, message string) error {
return c.JSON(Fail(message))
}
// 特定错误响应
func BadRequestCtx(c *fiber.Ctx, message string) error {
return c.Status(fiber.StatusBadRequest).JSON(
FailWithCode(400, message),
)
}
func NotFoundCtx(c *fiber.Ctx, message string) error {
return c.Status(fiber.StatusNotFound).JSON(
FailWithCode(404, message),
)
}
func InternalServerCtx(c *fiber.Ctx, message string) error {
return c.Status(fiber.StatusInternalServerError).JSON(
FailWithCode(500, message),
)
}
业务逻辑分层
项目采用清晰的分层架构:
- Handler 层:处理 HTTP 请求、参数校验、返回响应
- Service 层(如 SyncService):处理复杂业务逻辑
- Repository 层:封装数据库操作
错误处理
良好的错误处理对 API 的可用性至关重要。
自定义错误处理器
在 server/router/router.go 中定义全局错误处理器:
// customErrorHandler 自定义错误处理
func customErrorHandler(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "Internal Server Error"
// 提取 Fiber 框架错误
if e, ok := err.(*fiber.Error); ok {
code = e.Code
message = e.Message
}
return c.Status(code).JSON(fiber.Map{
"error": message,
})
}
业务错误返回
在 Handler 中返回业务错误:
func (h *RepoHandler) Get(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil {
return response.BadRequestCtx(c, "无效的仓库 ID")
}
repo, err := h.repoRepo.FindOne(c.Context(), id)
if err != nil {
return response.InternalServerCtx(c, "查询失败")
}
if repo == nil {
return response.NotFoundCtx(c, "仓库不存在")
}
return response.SuccessCtx(c, repo)
}
SSE 实时推送
项目使用 Server-Sent Events (SSE) 实现实时同步进度推送。
SSE 原理
SSE 是一种服务器向浏览器推送数据的技术,与 WebSocket 类似但更简单:
SSE 适用于服务器主动推送场景,如进度通知、实时监控等。
SSE 实现
// SyncProgressSSE 处理同步进度请求
func (h *SyncHandler) SyncProgressSSE(c *fiber.Ctx) error {
syncID := c.Params("sync_id")
// 设置 SSE 响应头
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
// 创建事件流
progressChan := h.syncHub.Subscribe(syncID)
defer h.syncHub.Unsubscribe(syncID)
// 持续发送进度
for {
select {
case <-c.Context().Done():
// 客户端断开连接
return nil
case progress := <-progressChan:
// 发送数据(格式:data: JSON\n\n)
if err := c.Write([]byte("data: " + progress + "\n\n")); err != nil {
return nil
}
c.Response().Flush()
}
}
}
前端通过 EventSource API 接收:
const eventSource = new EventSource(`/api/sync/progress/${syncId}`);
eventSource.onmessage = (event) => {
const progress = JSON.parse(event.data);
console.log(`进度: ${progress.progress}%`);
};