202 lines
7.1 KiB
Go
202 lines
7.1 KiB
Go
package service
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"view/model"
|
|
"view/platform"
|
|
)
|
|
|
|
const (
|
|
insightMaxFileSize = 1024 * 1024
|
|
insightMaxTotalRead = 20 * 1024 * 1024
|
|
)
|
|
|
|
// InsightService 基于既有统计快照做维护性检查。
|
|
// 它不会重新遍历整个仓库,只读取已记录的小型源码文件,用于发现 TODO、长文件等轻量风险。
|
|
type InsightService struct{}
|
|
|
|
func (InsightService) Analyze(ctx context.Context, project model.Project, structure model.StructureStats, git model.GitStats) (model.ProjectInsights, error) {
|
|
desc, e := platform.ResolvePath(project.Path)
|
|
if e != nil {
|
|
return model.ProjectInsights{}, e
|
|
}
|
|
out := model.ProjectInsights{ProjectID: project.ID, GeneratedAt: time.Now().Format(time.RFC3339), Issues: []model.InsightIssue{}}
|
|
add := func(issue model.InsightIssue) {
|
|
out.Issues = append(out.Issues, issue)
|
|
switch issue.Severity {
|
|
case "high":
|
|
out.Summary.High++
|
|
case "medium":
|
|
out.Summary.Medium++
|
|
default:
|
|
out.Summary.Low++
|
|
}
|
|
}
|
|
|
|
checkLanguageHealth(project.Languages, add)
|
|
out.Summary.LargeFiles = len(structure.LargeFiles)
|
|
checkStructureHealth(structure, add)
|
|
checkGitHealth(git, add)
|
|
if e = scanTextMarkers(ctx, desc.WindowsPath, structure.Files, add, &out.Summary); e != nil {
|
|
add(model.InsightIssue{Severity: "medium", Type: "scan_error", Title: "源码检查未完全完成", Detail: e.Error(), Suggestion: "检查项目目录权限,或重新执行项目统计。"})
|
|
}
|
|
|
|
sort.SliceStable(out.Issues, func(i, j int) bool {
|
|
return severityRank(out.Issues[i].Severity) > severityRank(out.Issues[j].Severity)
|
|
})
|
|
out.HealthScore = healthScore(out.Summary)
|
|
return out, nil
|
|
}
|
|
|
|
func checkLanguageHealth(langs []model.LanguageStat, add func(model.InsightIssue)) {
|
|
for _, x := range langs {
|
|
total := x.Code + x.Comments
|
|
if x.Code < 500 || x.Files < 3 || total == 0 {
|
|
continue
|
|
}
|
|
ratio := float64(x.Comments) / float64(total)
|
|
if ratio < 0.03 {
|
|
add(model.InsightIssue{Severity: "medium", Type: "low_comment_ratio", Title: "注释率偏低", Detail: x.Name + " 的注释占比低于 3%。", Suggestion: "为核心业务、复杂条件和公共接口补充解释性注释。"})
|
|
} else if ratio < 0.06 {
|
|
add(model.InsightIssue{Severity: "low", Type: "low_comment_ratio", Title: "注释率略低", Detail: x.Name + " 的注释占比低于 6%。", Suggestion: "优先补充复杂模块和团队协作频繁的文件。"})
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkStructureHealth(s model.StructureStats, add func(model.InsightIssue)) {
|
|
for _, f := range s.LargeFiles {
|
|
severity := "medium"
|
|
if f.Size >= 5*1024*1024 {
|
|
severity = "high"
|
|
}
|
|
add(model.InsightIssue{Severity: severity, Type: "large_file", Title: "发现大文件", Detail: "文件体积可能影响仓库加载和代码审查。", Path: f.Path, Suggestion: "确认是否可以拆分、压缩或移动到对象存储。"})
|
|
}
|
|
if len(s.Folders) > 0 && s.TotalSize > 0 {
|
|
top := s.Folders[0]
|
|
if float64(top.Size)/float64(s.TotalSize) > 0.65 && top.Files > 20 {
|
|
add(model.InsightIssue{Severity: "medium", Type: "folder_concentration", Title: "目录体量过于集中", Detail: top.Name + " 占据了项目大部分体积。", Path: top.Name, Suggestion: "检查该目录是否混入构建产物、缓存或可拆分模块。"})
|
|
}
|
|
}
|
|
unknown := 0
|
|
for _, x := range s.Extensions {
|
|
if x.Extension == "" {
|
|
unknown += int(x.Files)
|
|
}
|
|
}
|
|
if unknown > 20 {
|
|
add(model.InsightIssue{Severity: "low", Type: "unknown_files", Title: "无扩展名文件较多", Detail: "项目中存在较多无扩展名文件。", Suggestion: "确认这些文件是否都是必要脚本、配置或运行产物。"})
|
|
}
|
|
}
|
|
|
|
func checkGitHealth(g model.GitStats, add func(model.InsightIssue)) {
|
|
if g.CommitCount == 0 || len(g.Contributors) == 0 {
|
|
return
|
|
}
|
|
top := g.Contributors[0]
|
|
if float64(top.Commits)/float64(g.CommitCount) > 0.75 && g.CommitCount > 20 {
|
|
add(model.InsightIssue{Severity: "medium", Type: "contributor_concentration", Title: "提交集中度较高", Detail: top.Name + " 贡献了超过 75% 的提交。", Suggestion: "关注知识分散、代码评审和关键模块交接风险。"})
|
|
}
|
|
for _, c := range g.Commits[:min(30, len(g.Commits))] {
|
|
if c.Added+c.Deleted > 3000 {
|
|
add(model.InsightIssue{Severity: "low", Type: "large_commit", Title: "近期存在大提交", Detail: c.Message, Path: c.Hash, Suggestion: "大提交建议拆分审查,降低回滚和定位问题的成本。"})
|
|
}
|
|
}
|
|
}
|
|
|
|
func scanTextMarkers(ctx context.Context, root string, files []model.FileEntry, add func(model.InsightIssue), summary *model.InsightSummary) error {
|
|
totalRead := int64(0)
|
|
for _, f := range files {
|
|
if f.IsDir || f.Size <= 0 || f.Size > insightMaxFileSize || !isInsightTextFile(f.Extension) {
|
|
continue
|
|
}
|
|
if totalRead+f.Size > insightMaxTotalRead {
|
|
return nil
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
totalRead += f.Size
|
|
path := filepath.Join(root, filepath.FromSlash(f.Path))
|
|
lines, markers := inspectTextFile(path)
|
|
if lines >= 1500 {
|
|
summary.LongFiles++
|
|
add(model.InsightIssue{Severity: "high", Type: "long_file", Title: "文件过长", Detail: "文件超过 1500 行,理解和评审成本较高。", Path: f.Path, Suggestion: "按职责拆分模块,提取复用函数或服务。"})
|
|
} else if lines >= 800 {
|
|
summary.LongFiles++
|
|
add(model.InsightIssue{Severity: "medium", Type: "long_file", Title: "文件偏长", Detail: "文件超过 800 行。", Path: f.Path, Suggestion: "关注是否存在多个职责混在同一文件中。"})
|
|
}
|
|
for _, issue := range markers {
|
|
summary.TodoCount++
|
|
issue.Path = f.Path
|
|
add(issue)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func inspectTextFile(path string) (int, []model.InsightIssue) {
|
|
file, e := os.Open(path)
|
|
if e != nil {
|
|
return 0, nil
|
|
}
|
|
defer file.Close()
|
|
scanner := bufio.NewScanner(file)
|
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
|
line := 0
|
|
issues := []model.InsightIssue{}
|
|
for scanner.Scan() {
|
|
line++
|
|
text := scanner.Text()
|
|
upper := strings.ToUpper(text)
|
|
kind, severity := "", "low"
|
|
switch {
|
|
case strings.Contains(upper, "FIXME"):
|
|
kind, severity = "FIXME", "medium"
|
|
case strings.Contains(upper, "HACK"):
|
|
kind, severity = "HACK", "medium"
|
|
case strings.Contains(upper, "TODO"):
|
|
kind = "TODO"
|
|
}
|
|
if kind != "" {
|
|
issues = append(issues, model.InsightIssue{Severity: severity, Type: "todo_marker", Title: "发现 " + kind + " 标记", Detail: "源码中存在待处理标记。", Line: line, Suggestion: "确认该标记是否仍有效,并补充负责人或处理计划。", Evidence: strings.TrimSpace(text)})
|
|
}
|
|
}
|
|
return line, issues
|
|
}
|
|
|
|
func isInsightTextFile(ext string) bool {
|
|
switch strings.ToLower(ext) {
|
|
case ".go", ".php", ".js", ".ts", ".vue", ".jsx", ".tsx", ".css", ".scss", ".sass", ".less", ".html", ".md", ".json", ".yaml", ".yml", ".xml", ".sql", ".py", ".java", ".cs", ".rb", ".rs", ".c", ".cpp", ".h":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func healthScore(s model.InsightSummary) int {
|
|
score := 100 - s.High*12 - s.Medium*6 - s.Low*2
|
|
if score < 0 {
|
|
return 0
|
|
}
|
|
return score
|
|
}
|
|
|
|
func severityRank(s string) int {
|
|
switch s {
|
|
case "high":
|
|
return 3
|
|
case "medium":
|
|
return 2
|
|
default:
|
|
return 1
|
|
}
|
|
}
|