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

9.5 KiB
Raw Blame History

后端服务层

概述

后端服务层是 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)

核心功能

克隆仓库

func Clone(opts CloneOptions, progressFn func(event string, data interface{})) (*CloneResult, error)
  • 支持基本认证(用户名/密码)
  • 可选深度克隆
  • 进度回调(SSE 事件)
  • 返回仓库路径、分支、标签、提交数、大小

获取引用

func GetRefs(repo *git.Repository) ([]RefInfo, error)
  • 返回所有分支和标签
  • 标记 HEAD 分支
  • 解析注解标签到提交

获取图形数据

func GetGraph(repo *git.Repository, maxCommits int) (*GraphData, error)
  • D3.js 兼容格式
  • 包含提交、引用、边
  • BFS 遍历,限制最大提交数

获取差异

func GetDiff(repo *git.Repository, baseRef, headRef string) (string, error)
func GetDiffFiles(repo *git.Repository, baseRef, headRef string) ([]FileDiff, error)
  • 统一差异格式
  • 按文件分割
  • 解析变更行数

获取提交日志

func GetCommitLog(repo *git.Repository, refName string, maxCommits int) ([]CommitInfo, error)
  • BFS 遍历父提交
  • 限制最大提交数
  • 包含作者、时间、父提交

数据结构

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)

配置管理

type LLMConfig struct {
    Endpoint string
    APIKey   string
    Model    string
}

func GetLLMConfig(db *sql.DB, userID int64) (LLMConfig, error)
  • 从 user_settings 表读取
  • 每用户独立配置
  • 默认模型: deepseek-v4-pro

流式调用

type StreamCallback func(event string, data interface{})

func ChatStream(config LLMConfig, messages []goopenai.ChatCompletionMessage, callback StreamCallback) (string, error)
  • 使用 go-openai 客户端
  • SSE 流式响应
  • 回调函数处理每个 chunk
  • 返回完整响应文本

JSON 提取

func extractJSON(s string) string
  • 从 Markdown 代码块提取
  • 从混合文本中提取 JSON 对象/数组
  • 处理嵌套括号

PR 生成服务 (generate.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 描述

提示模板

你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。

## Commit 记录
{commits}

## 代码变更 (Diff)
{diff}

请直接输出 Markdown 格式的 PR 描述,包含以下部分:

# 标题
**类型**: feat|fix|refactor|docs|chore|style|test|perf

## 概述
一段话概述变更内容

## 详细说明
按模块分组的详细变更说明

## 影响范围
影响范围说明

大型差异处理

  • 截断到 60k 字符
  • 保留完整提交记录
  • 标记截断位置

代码审查服务 (review.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 策略

// 按变更行数排序
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 表示分析所有文件

并发控制

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
  • 信号量控制最大并发
  • 线程安全的回调函数

单文件审查提示

你是一个资深代码审查专家。请审查以下代码变更,给出专业的 Review 意见。

## 文件: {filename}
## 变更行数: +{additions} / -{deletions}

## Diff
{patch}

请按以下 JSON 格式输出审查意见(直接输出 JSON 数组,不要包含 markdown 代码块标记):
[
  {
    "severity": "critical 或 warning 或 info",
    "description": "问题描述",
    "suggestion": "建议的修改方案",
    "code_example": "建议的代码(如有)"
  }
]

严重程度说明:
- critical: 严重问题(安全漏洞、数据丢失风险、崩溃风险)
- warning: 建议改进(性能问题、代码规范、可维护性)
- info: 提示信息(最佳实践、可选优化)

如果代码没有问题,输出空数组 []。
请用中文回复。

汇总生成

以下是多个文件的代码审查结果,请给出整体评估:

{reviews}

请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
{
  "score": 7,
  "overall": "总体评价(2-3 句话)",
  "findings": "按严重程度排序的主要发现汇总",
  "recommendations": "改进建议,用换行分隔多条建议"
}

数据结构

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)

笔记操作

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 超限

增量渲染

  • 按文件分批
  • 异步处理
  • 进度反馈