Files
PR-Helper/docs/09-development-guide.md
wonder 275e5cc886 docs: 添加 10 份技术文档,README 改为中文
- 01-architecture.md: 架构概览
- 02-backend-services.md: 后端服务层
- 03-frontend-interaction.md: 前端交互设计
- 04-database-design.md: 数据库设计
- 05-api-reference.md: API 接口文档
- 06-sse-streaming.md: SSE 流式传输
- 07-llm-integration.md: LLM 集成
- 08-deployment.md: 部署运维
- 09-development-guide.md: 开发指南
- 10-troubleshooting.md: 故障排查
2026-06-23 22:38:43 +08:00

14 KiB

开发指南

概述

本文档为 PR-Helper 开发者提供开发环境搭建、代码规范和贡献指南。

开发环境

系统要求

工具 版本 说明
Go 1.21+ 后端语言
MySQL 5.7+ / 8.0+ 数据库
Git 2.0+ 版本控制
Node.js 18+ (可选) 前端工具链

IDE 推荐

  • GoLand: JetBrains Go IDE
  • VS Code: + Go 扩展
  • Vim/Neovim: + vim-go

环境配置

# 安装 Go
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# 安装 MySQL
sudo apt install mysql-server
sudo mysql_secure_installation

# 安装 Git
sudo apt install git

项目搭建

克隆项目

git clone https://github.com/your-org/pr-helper.git
cd pr-helper

安装依赖

go mod download

配置环境

cp .env.example .env
# 编辑 .env 文件

创建数据库

CREATE DATABASE pr_helper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

运行项目

go run .

项目结构

pr-helper/
├── main.go              # 应用入口
├── go.mod               # Go 模块定义
├── go.sum               # 依赖校验
├── .env.example         # 环境变量示例
├── Dockerfile           # Docker 构建
├── docker-compose.yml   # Docker Compose
├── config/              # 配置加载
│   └── config.go
├── database/            # 数据库操作
│   └── db.go
├── handlers/            # HTTP 处理器
│   ├── auth.go
│   ├── generate.go
│   ├── pages.go
│   ├── repos.go
│   ├── review.go
│   ├── settings.go
│   └── middleware.go
├── models/              # 数据模型
│   ├── repository.go
│   ├── analysis.go
│   ├── settings.go
│   └── user.go
├── services/            # 业务逻辑
│   ├── git.go
│   ├── llm.go
│   ├── generate.go
│   ├── review.go
│   ├── cache.go
│   └── notes.go
├── templates/           # HTML 模板
│   ├── layouts/
│   ├── pages/
│   └── partials/
├── static/              # 静态资源
│   ├── css/
│   ├── js/
│   └── lib/
└── docs/                # 文档

代码规范

Go 代码规范

命名规范

// 包名: 小写单词
package services

// 结构体: 大驼峰
type ReviewResult struct {
    FileReviews []FileReview
    Summary     ReviewSummary
}

// 函数: 大驼峰(导出)/ 小驼峰(未导出)
func GenerateReview(...) (*ReviewResult, error) {
    // ...
}

func countDiffLines(patch string) int {
    // ...
}

// 常量: 大驼峰或全大写
const MaxDiffSize = 60000
const MAX_FILES = 300

// 变量: 小驼峰
var defaultModel = "deepseek-v4-pro"

注释规范

// GenerateReview performs AI code review on diff files with Top-N strategy.
// It streams events (file_start, suggestion, file_end, summary, done) via callback
// and returns the complete ReviewResult for persistence.
func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency int, userID int64, callback StreamCallback) (*ReviewResult, error) {
    // ...
}

错误处理

// 显式错误检查
result, err := doSomething()
if err != nil {
    return fmt.Errorf("do something: %w", err)
}

// 忽略不需要的错误
db.conn.Exec(s) // 忽略错误(列已存在)

表驱动测试

func TestCountDiffLines(t *testing.T) {
    tests := []struct {
        name     string
        patch    string
        expected int
    }{
        {"empty", "", 0},
        {"single add", "+line", 1},
        {"single del", "-line", 1},
        {"mixed", "+line1\n-line2", 2},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := countDiffLines(tt.patch)
            if result != tt.expected {
                t.Errorf("expected %d, got %d", tt.expected, result)
            }
        })
    }
}

JavaScript 代码规范

命名规范

// 常量: 全大写
const MAX_FILES = 300;
const BRANCH_COLORS = ['#3b82f6', '#f97316', ...];

// 变量/函数: 小驼峰
let selectedBase = null;
function countDiffLines(patch) { ... }

// 类/对象: 大驼峰
const DiffViewer = { ... };
const GitGraph = { ... };

// 私有方法: 下划线前缀
_ensureFileIndexed(filename) { ... }

注释规范

/**
 * POST to a streaming endpoint and handle SSE events.
 * @param {string} url - The endpoint URL
 * @param {object} body - JSON request body
 * @param {object} handlers - Map of event name → callback(data)
 * @returns {object} controller with abort() method
 */
async post(url, body, handlers = {}) {
    // ...
}

HTML 模板规范

<!-- 使用语义化标签 -->
<header>...</header>
<main>...</main>
<footer>...</footer>

<!-- 使用 Tailwind CSS -->
<div class="bg-white shadow rounded-lg p-6">
    <h2 class="text-xl font-semibold mb-4">标题</h2>
</div>

<!-- HTMX 属性 -->
<button hx-post="/api/repos/1/pull"
        hx-trigger="click"
        hx-indicator="#spinner">
    拉取更新
</button>

添加新功能

1. 添加新 API 端点

定义路由 (main.go)

r.POST("/api/repos/:id/new-feature", authMw, handler.NewFeature)

实现处理器 (handlers/new_feature.go)

type NewFeatureHandler struct {
    db *sql.DB
}

func NewNewFeatureHandler(db *sql.DB) *NewFeatureHandler {
    return &NewFeatureHandler{db: db}
}

func (h *NewFeatureHandler) NewFeature(c *gin.Context) {
    user := GetCurrentUser(c)
    if user == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
        return
    }

    // 解析请求
    var req struct {
        Param1 string `json:"param1" binding:"required"`
        Param2 int    `json:"param2"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 调用服务层
    result, err := services.DoSomething(h.db, req.Param1, req.Param2)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // 返回响应
    c.JSON(http.StatusOK, result)
}

实现服务层 (services/new_feature.go)

func DoSomething(db *sql.DB, param1 string, param2 int) (*Result, error) {
    // 业务逻辑
    // ...
    return result, nil
}

2. 添加 SSE 流式端点

处理器

func (h *Handler) StreamEndpoint(c *gin.Context) {
    // 设置 SSE 头
    c.Header("Content-Type", "text/event-stream")
    c.Header("Cache-Control", "no-cache")
    c.Header("Connection", "keep-alive")
    c.Header("X-Accel-Buffering", "no")
    c.Status(http.StatusOK)

    flusher, ok := c.Writer.(http.Flusher)
    if !ok {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "不支持流式传输"})
        return
    }

    sendEvent := func(event string, data interface{}) {
        jsonData, _ := json.Marshal(data)
        fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, jsonData)
        flusher.Flush()
    }

    // 调用服务层
    err := services.DoStream(h.db, sendEvent)
    if err != nil {
        sendEvent("error", map[string]interface{}{"message": err.Error()})
    }
}

前端调用

SSE.post('/api/repos/1/stream', {}, {
    progress: (data) => updateProgress(data),
    done: () => hideSpinner(),
    error: (data) => showError(data.message)
});

3. 添加新数据模型

定义模型 (models/new_model.go)

type NewModel struct {
    ID        int64     `json:"id"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
}

数据库迁移 (database/db.go)

// 在 migrate() 函数中添加
"CREATE TABLE IF NOT EXISTS new_models (" +
    "id         BIGINT AUTO_INCREMENT PRIMARY KEY," +
    "name       VARCHAR(255) NOT NULL," +
    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

4. 添加新前端组件

创建 JS 文件 (static/js/new_component.js)

const NewComponent = {
    init(containerId) {
        this.container = document.getElementById(containerId);
        if (!this.container) {
            console.error('Container not found:', containerId);
            return;
        }
        this.loadData();
    },

    async loadData() {
        try {
            const resp = await fetch('/api/data');
            const data = await resp.json();
            this.render(data);
        } catch (err) {
            this.container.innerHTML = `<div class="text-red-500">加载失败</div>`;
        }
    },

    render(data) {
        this.container.innerHTML = `
            <div class="new-component">
                ${data.map(item => `<div>${item.name}</div>`).join('')}
            </div>
        `;
    }
};

window.NewComponent = NewComponent;

在模板中使用

<script src="/static/js/new_component.js"></script>
<div id="new-component-container"></div>
<script>
    NewComponent.init('new-component-container');
</script>

测试

单元测试

// services/git_test.go
package services

import (
    "testing"
)

func TestCountDiffLines(t *testing.T) {
    tests := []struct {
        name     string
        patch    string
        expected int
    }{
        {"empty", "", 0},
        {"single add", "+line", 1},
        {"single del", "-line", 1},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := countDiffLines(tt.patch)
            if result != tt.expected {
                t.Errorf("expected %d, got %d", tt.expected, result)
            }
        })
    }
}

集成测试

// handlers/repos_test.go
package handlers

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
)

func TestListRepos(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    r.GET("/api/repos", handler.ListRepos)

    req, _ := http.NewRequest("GET", "/api/repos", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("expected status 200, got %d", w.Code)
    }
}

运行测试

# 运行所有测试
go test ./...

# 运行特定包的测试
go test ./services/...

# 运行特定测试
go test -run TestCountDiffLines ./services/

# 生成覆盖率报告
go test -cover ./...

调试

Go 调试

# 使用 delve
dlv debug .

# 设置断点
(dlv) break main.main
(dlv) continue
(dlv) print cfg

日志调试

import "log"

log.Printf("DEBUG: variable = %v", variable)

浏览器调试

  • 打开开发者工具 (F12)
  • Network 面板查看请求
  • Console 面板查看日志
  • Elements 面板查看 DOM

Git 工作流

分支策略

main        ← 生产分支
├── develop ← 开发分支
│   ├── feature/xxx ← 功能分支
│   └── fix/xxx     ← 修复分支
└── release/x.x.x  ← 发布分支

提交规范

<type>(<scope>): <subject>

<body>

<footer>

类型:

  • feat: 新功能
  • fix: 修复
  • docs: 文档
  • style: 格式
  • refactor: 重构
  • test: 测试
  • chore: 构建/工具

示例:

feat(review): add Top-N strategy for large diffs

- Sort files by change size
- Analyze only top N files
- Generate summary from all results

Closes #123

Pull Request

  1. 从 develop 创建功能分支
  2. 完成开发并测试
  3. 提交 PR 到 develop
  4. 代码审查
  5. 合并并删除分支

发布流程

版本号

遵循语义化版本 (SemVer):

MAJOR.MINOR.PATCH

MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能添加
PATCH: 向后兼容的修复

发布步骤

# 1. 更新版本号
# 编辑 main.go 或使用 git tag

# 2. 更新 CHANGELOG.md

# 3. 提交
git add .
git commit -m "release: v1.2.0"

# 4. 打标签
git tag -a v1.2.0 -m "Release v1.2.0"

# 5. 推送
git push origin main --tags

# 6. 构建 Docker 镜像
docker build -t pr-helper:v1.2.0 .
docker tag pr-helper:v1.2.0 pr-helper:latest

# 7. 推送镜像
docker push pr-helper:v1.2.0
docker push pr-helper:latest

文档

代码文档

  • 使用 GoDoc 格式
  • 包级别文档在 doc.go 中
  • 导出函数必须有文档注释

用户文档

  • README.md: 项目简介
  • docs/: 详细文档
  • CHANGELOG.md: 变更日志

API 文档

  • 使用 OpenAPI/Swagger (可选)
  • 或在 docs/ 中手动维护

贡献指南

贡献流程

  1. Fork 项目
  2. 创建功能分支
  3. 提交代码
  4. 创建 Pull Request
  5. 代码审查
  6. 合并

代码审查清单

  • 代码符合规范
  • 测试通过
  • 文档更新
  • 无安全漏洞
  • 性能可接受

报告问题

使用 GitHub Issues 报告问题,包含:

  • 问题描述
  • 复现步骤
  • 期望行为
  • 实际行为
  • 环境信息

常见问题

Q: 如何添加新的 LLM 提供商?

A: 实现 services/llm.go 中的 ChatStream 函数,支持不同的 API 格式。

Q: 如何修改数据库表结构?

A: 在 database/db.go 的 migrate() 函数中添加 ALTER TABLE 语句。

Q: 如何添加新的前端库?

A: 将库文件放入 static/lib/ 目录,然后在模板中引入。

Q: 如何调试 SSE 流?

A: 使用浏览器开发者工具的 Network 面板,查看 EventStream 请求。

资源

官方文档

社区