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: 故障排查
This commit is contained in:
@@ -0,0 +1,711 @@
|
||||
# 开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档为 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)
|
||||
Reference in New Issue
Block a user