Files

712 lines
14 KiB
Markdown
Raw Permalink Normal View History

# 开发指南
## 概述
本文档为 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
<!-- 使用语义化标签 -->
<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)
```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 = `<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;
```
#### 在模板中使用
```html
<script src="/static/js/new_component.js"></script>
<div id="new-component-container"></div>
<script>
NewComponent.init('new-component-container');
</script>
```
## 测试
### 单元测试
```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 ← 发布分支
```
### 提交规范
```
<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: 向后兼容的修复
```
### 发布步骤
```bash
# 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 请求。
## 资源
### 官方文档
- [Go 文档](https://go.dev/doc/)
- [Gin 文档](https://gin-gonic.com/docs/)
- [go-git 文档](https://pkg.go.dev/github.com/go-git/go-git/v5)
- [HTMX 文档](https://htmx.org/docs/)
- [D3.js 文档](https://d3js.org/)
### 社区
- [Go Forum](https://forum.golangbridge.org/)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/go)
- [GitHub Discussions](https://github.com/your-org/pr-helper/discussions)