# 开发指南 ## 概述 本文档为 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 ### 环境配置 ```bash # 安装 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 ``` ## 项目搭建 ### 克隆项目 ```bash git clone https://github.com/your-org/pr-helper.git cd pr-helper ``` ### 安装依赖 ```bash go mod download ``` ### 配置环境 ```bash cp .env.example .env # 编辑 .env 文件 ``` ### 创建数据库 ```sql CREATE DATABASE pr_helper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` ### 运行项目 ```bash 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 代码规范 #### 命名规范 ```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" ``` #### 注释规范 ```go // 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) { // ... } ``` #### 错误处理 ```go // 显式错误检查 result, err := doSomething() if err != nil { return fmt.Errorf("do something: %w", err) } // 忽略不需要的错误 db.conn.Exec(s) // 忽略错误(列已存在) ``` #### 表驱动测试 ```go 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 代码规范 #### 命名规范 ```javascript // 常量: 全大写 const MAX_FILES = 300; const BRANCH_COLORS = ['#3b82f6', '#f97316', ...]; // 变量/函数: 小驼峰 let selectedBase = null; function countDiffLines(patch) { ... } // 类/对象: 大驼峰 const DiffViewer = { ... }; const GitGraph = { ... }; // 私有方法: 下划线前缀 _ensureFileIndexed(filename) { ... } ``` #### 注释规范 ```javascript /** * 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 模板规范 ```html
...
...

标题

``` ## 添加新功能 ### 1. 添加新 API 端点 #### 定义路由 (main.go) ```go r.POST("/api/repos/:id/new-feature", authMw, handler.NewFeature) ``` #### 实现处理器 (handlers/new_feature.go) ```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) ```go func DoSomething(db *sql.DB, param1 string, param2 int) (*Result, error) { // 业务逻辑 // ... return result, nil } ``` ### 2. 添加 SSE 流式端点 #### 处理器 ```go 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()}) } } ``` #### 前端调用 ```javascript SSE.post('/api/repos/1/stream', {}, { progress: (data) => updateProgress(data), done: () => hideSpinner(), error: (data) => showError(data.message) }); ``` ### 3. 添加新数据模型 #### 定义模型 (models/new_model.go) ```go type NewModel struct { ID int64 `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` } ``` #### 数据库迁移 (database/db.go) ```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) ```javascript 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 = `
加载失败
`; } }, render(data) { this.container.innerHTML = `
${data.map(item => `
${item.name}
`).join('')}
`; } }; window.NewComponent = NewComponent; ``` #### 在模板中使用 ```html
``` ## 测试 ### 单元测试 ```go // 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) } }) } } ``` ### 集成测试 ```go // 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) } } ``` ### 运行测试 ```bash # 运行所有测试 go test ./... # 运行特定包的测试 go test ./services/... # 运行特定测试 go test -run TestCountDiffLines ./services/ # 生成覆盖率报告 go test -cover ./... ``` ## 调试 ### Go 调试 ```bash # 使用 delve dlv debug . # 设置断点 (dlv) break main.main (dlv) continue (dlv) print cfg ``` ### 日志调试 ```go import "log" log.Printf("DEBUG: variable = %v", variable) ``` ### 浏览器调试 - 打开开发者工具 (F12) - Network 面板查看请求 - Console 面板查看日志 - Elements 面板查看 DOM ## Git 工作流 ### 分支策略 ``` main ← 生产分支 ├── develop ← 开发分支 │ ├── feature/xxx ← 功能分支 │ └── fix/xxx ← 修复分支 └── release/x.x.x ← 发布分支 ``` ### 提交规范 ``` ():