07. API 设计与实现

18 阅读7分钟

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/verifyGitea 认证
数据分析/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 状态码表示请求的处理结果:

状态码含义使用场景
200OK成功获取/更新资源
201Created成功创建资源
400Bad Request请求参数错误
401Unauthorized未认证(Token 无效)
403Forbidden无权限访问
404Not Found资源不存在
500Internal 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 相关路由。

路由初始化流程

无标题-2025-12-24-1545.png

完整路由定义

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(&params); 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),
    )
}

业务逻辑分层

项目采用清晰的分层架构:

无标题-2025-12-24-1545.png

  • 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 类似但更简单:

无标题-2025-12-24-1545.png

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}%`);
};