Files
PR-Helper/docs/02-backend-services.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

420 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端服务层
## 概述
后端服务层是 PR-Helper 的核心业务逻辑层,位于 handlers 和数据层之间。主要职责包括 Git 操作、LLM 调用、PR 生成和代码审查。
## 服务架构
```
handlers/
↓ 调用
services/
├── git.go # Git 操作
├── llm.go # LLM API 调用
├── generate.go # PR 描述生成
├── review.go # AI 代码审查
├── cache.go # 缓存管理
└── notes.go # 审查笔记
↓ 使用
database/ + filesystem/
```
## Git 服务 (git.go)
### 核心功能
#### 克隆仓库
```go
func Clone(opts CloneOptions, progressFn func(event string, data interface{})) (*CloneResult, error)
```
- 支持基本认证(用户名/密码)
- 可选深度克隆
- 进度回调(SSE 事件)
- 返回仓库路径、分支、标签、提交数、大小
#### 获取引用
```go
func GetRefs(repo *git.Repository) ([]RefInfo, error)
```
- 返回所有分支和标签
- 标记 HEAD 分支
- 解析注解标签到提交
#### 获取图形数据
```go
func GetGraph(repo *git.Repository, maxCommits int) (*GraphData, error)
```
- D3.js 兼容格式
- 包含提交、引用、边
- BFS 遍历,限制最大提交数
#### 获取差异
```go
func GetDiff(repo *git.Repository, baseRef, headRef string) (string, error)
func GetDiffFiles(repo *git.Repository, baseRef, headRef string) ([]FileDiff, error)
```
- 统一差异格式
- 按文件分割
- 解析变更行数
#### 获取提交日志
```go
func GetCommitLog(repo *git.Repository, refName string, maxCommits int) ([]CommitInfo, error)
```
- BFS 遍历父提交
- 限制最大提交数
- 包含作者、时间、父提交
### 数据结构
```go
type RefInfo struct {
Name string `json:"name"`
Hash string `json:"hash"`
IsHead bool `json:"is_head"`
IsTag bool `json:"is_tag"`
}
type CommitInfo struct {
Hash string `json:"hash"`
ShortHash string `json:"short_hash"`
Message string `json:"message"`
Author string `json:"author"`
Email string `json:"email"`
Timestamp string `json:"timestamp"`
ParentIDs []string `json:"parent_ids"`
}
type GraphData struct {
Commits []CommitInfo `json:"commits"`
Refs []RefInfo `json:"refs"`
Edges []Edge `json:"edges"`
}
type FileDiff struct {
Filename string `json:"filename"`
Patch string `json:"patch"`
}
```
## LLM 服务 (llm.go)
### 配置管理
```go
type LLMConfig struct {
Endpoint string
APIKey string
Model string
}
func GetLLMConfig(db *sql.DB, userID int64) (LLMConfig, error)
```
- 从 user_settings 表读取
- 每用户独立配置
- 默认模型: deepseek-v4-pro
### 流式调用
```go
type StreamCallback func(event string, data interface{})
func ChatStream(config LLMConfig, messages []goopenai.ChatCompletionMessage, callback StreamCallback) (string, error)
```
- 使用 go-openai 客户端
- SSE 流式响应
- 回调函数处理每个 chunk
- 返回完整响应文本
### JSON 提取
```go
func extractJSON(s string) string
```
- 从 Markdown 代码块提取
- 从混合文本中提取 JSON 对象/数组
- 处理嵌套括号
## PR 生成服务 (generate.go)
### 生成流程
```go
func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback StreamCallback) (string, error)
```
1. **读取 LLM 配置**: 从用户设置获取
2. **打开仓库**: 使用 go-git
3. **获取提交**: 过滤 base 和 head 之间的提交
4. **获取差异**: 生成统一差异
5. **构建提示**: 包含提交记录和差异
6. **调用 LLM**: 流式生成
7. **返回结果**: Markdown 格式的 PR 描述
### 提示模板
```markdown
你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。
## Commit 记录
{commits}
## 代码变更 (Diff)
{diff}
请直接输出 Markdown 格式的 PR 描述,包含以下部分:
# 标题
**类型**: feat|fix|refactor|docs|chore|style|test|perf
## 概述
一段话概述变更内容
## 详细说明
按模块分组的详细变更说明
## 影响范围
影响范围说明
```
### 大型差异处理
- 截断到 60k 字符
- 保留完整提交记录
- 标记截断位置
## 代码审查服务 (review.go)
### 审查流程
```go
func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency int, userID int64, callback StreamCallback) (*ReviewResult, error)
```
1. **获取差异文件**: 按文件分割
2. **排序**: 按变更行数降序
3. **Top-N 过滤**: 只分析前 N 个文件
4. **并发审查**: 使用信号量控制并发
5. **生成汇总**: 聚合所有文件结果
6. **返回结果**: 结构化 JSON
### Top-N 策略
```go
// 按变更行数排序
sort.Slice(files, func(i, j int) bool {
return countDiffLines(files[i].Patch) > countDiffLines(files[j].Patch)
})
// 应用 Top-N
if topN > 0 && topN < len(files) {
files = files[:topN]
}
```
- 默认 Top-N: 20
- 可通过设置或请求参数调整
- 0 表示分析所有文件
### 并发控制
```go
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for i, file := range files {
wg.Add(1)
go func(idx int, f FileDiff) {
defer wg.Done()
sem <- struct{}{} // 获取槽位
defer func() { <-sem }() // 释放槽位
// ... 审查逻辑
}(i, file)
}
```
- 默认并发数: 5
- 信号量控制最大并发
- 线程安全的回调函数
### 单文件审查提示
```markdown
你是一个资深代码审查专家。请审查以下代码变更,给出专业的 Review 意见。
## 文件: {filename}
## 变更行数: +{additions} / -{deletions}
## Diff
{patch}
请按以下 JSON 格式输出审查意见(直接输出 JSON 数组,不要包含 markdown 代码块标记):
[
{
"severity": "critical 或 warning 或 info",
"description": "问题描述",
"suggestion": "建议的修改方案",
"code_example": "建议的代码(如有)"
}
]
严重程度说明:
- critical: 严重问题(安全漏洞、数据丢失风险、崩溃风险)
- warning: 建议改进(性能问题、代码规范、可维护性)
- info: 提示信息(最佳实践、可选优化)
如果代码没有问题,输出空数组 []。
请用中文回复。
```
### 汇总生成
```markdown
以下是多个文件的代码审查结果,请给出整体评估:
{reviews}
请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
{
"score": 7,
"overall": "总体评价(2-3 句话)",
"findings": "按严重程度排序的主要发现汇总",
"recommendations": "改进建议,用换行分隔多条建议"
}
```
### 数据结构
```go
type ReviewSuggestion struct {
Severity string `json:"severity"`
Description string `json:"description"`
Suggestion string `json:"suggestion"`
CodeExample string `json:"code_example,omitempty"`
}
type FileReview struct {
FileName string `json:"file_name"`
ChangeLines int `json:"change_lines"`
Suggestions []ReviewSuggestion `json:"suggestions"`
RawReview string `json:"raw_review"`
}
type ReviewSummary struct {
Score int `json:"score"`
Overall string `json:"overall"`
Findings string `json:"findings"`
Recommendations string `json:"recommendations"`
}
type ReviewResult struct {
FileReviews []FileReview `json:"file_reviews"`
Summary ReviewSummary `json:"summary"`
TopN int `json:"top_n"`
}
```
## SSE 事件格式
### PR 生成事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `content` | `{content: string}` | LLM 输出的 Markdown 片段 |
| `done` | `{content: ""}` | 生成完成 |
### 代码审查事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `start` | `{total_files, reviewed_files, top_n}` | 审查开始 |
| `file_start` | `{file, index, total}` | 开始审查文件 |
| `content` | `{content: string}` | LLM 输出片段 |
| `suggestion` | `{file, severity, content}` | 审查建议 |
| `file_end` | `{file}` | 文件审查完成 |
| `summary` | `{score, overall, findings, recommendations}` | 汇总结果 |
| `progress` | `{step: string}` | 进度更新 |
| `error` | `{message: string}` | 错误信息 |
| `done` | `{content: ""}` | 审查完成 |
## 缓存服务 (cache.go)
### 仓库缓存
- 存储路径: `data/repos/`
- 命名规则: `{url}_{timestamp}`
- 过期清理: 可配置天数(默认 7 大)
- 大小限制: 可配置最大大小(默认 5GB)
### 缓存生命周期
1. **克隆**: 首次访问时克隆
2. **使用**: 更新 last_used 时间
3. **过期**: 定期清理过期仓库
4. **删除**: 手动或自动删除
## 笔记服务 (notes.go)
### 笔记操作
```go
func SaveNote(db *sql.DB, analysisID int64, scope, scopeKey, content string) (*models.ReviewNote, error)
func GetNotes(db *sql.DB, analysisID int64, scope string) ([]models.ReviewNote, error)
```
### 作用域类型
- `overall`: 整体审查笔记
- `file`: 文件级别笔记(scope_key = 文件名)
- `suggestion`: 建议级别笔记(scope_key = 建议 ID)
## 错误处理
### 错误传播
- 服务层返回 error
- 处理器层转换为 HTTP 状态码
- SSE 端点通过 error 事件发送
### 常见错误
- 仓库不存在
- LLM API 密钥未配置
- LLM 调用失败
- 差异过大
- 数据库操作失败
## 性能优化
### 并发审查
- 信号量控制并发数
- 线程安全的回调
- 避免数据竞争
### 差异截断
- 单文件: 30k 字符
- PR 生成: 60k 字符
- 避免 token 超限
### 增量渲染
- 按文件分批
- 异步处理
- 进度反馈