275e5cc886
- 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: 故障排查
420 lines
9.5 KiB
Markdown
420 lines
9.5 KiB
Markdown
# 后端服务层
|
||
|
||
## 概述
|
||
|
||
后端服务层是 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 超限
|
||
|
||
### 增量渲染
|
||
|
||
- 按文件分批
|
||
- 异步处理
|
||
- 进度反馈
|