基本能力编写完成

This commit is contained in:
2025-12-31 14:23:53 +08:00
parent ac5aa1eb70
commit 2b51050ca8
33 changed files with 5464 additions and 7 deletions

View File

@@ -0,0 +1,177 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/service"
)
// RepoHandler 仓库API处理器
type RepoHandler struct {
repoService *service.RepoService
}
// NewRepoHandler 创建仓库处理器
func NewRepoHandler(repoService *service.RepoService) *RepoHandler {
return &RepoHandler{
repoService: repoService,
}
}
// AddBatch 批量添加仓库
func (h *RepoHandler) AddBatch(w http.ResponseWriter, r *http.Request) {
var req service.AddReposRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid request body")
return
}
if len(req.URLs) == 0 {
respondError(w, http.StatusBadRequest, 40001, "urls cannot be empty")
return
}
resp, err := h.repoService.AddRepos(r.Context(), &req)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to add repositories")
respondError(w, http.StatusInternalServerError, 50000, "failed to add repositories")
return
}
respondJSON(w, http.StatusOK, 0, "success", resp)
}
// List 获取仓库列表
func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
repos, total, err := h.repoService.ListRepos(r.Context(), status, page, pageSize)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to list repositories")
respondError(w, http.StatusInternalServerError, 50000, "failed to list repositories")
return
}
data := map[string]interface{}{
"total": total,
"page": page,
"page_size": pageSize,
"repositories": repos,
}
respondJSON(w, http.StatusOK, 0, "success", data)
}
// Get 获取仓库详情
func (h *RepoHandler) Get(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
return
}
repo, err := h.repoService.GetRepo(r.Context(), id)
if err != nil {
respondError(w, http.StatusNotFound, 40400, "repository not found")
return
}
respondJSON(w, http.StatusOK, 0, "success", repo)
}
// SwitchBranch 切换分支
func (h *RepoHandler) SwitchBranch(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
return
}
var req struct {
Branch string `json:"branch"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid request body")
return
}
if req.Branch == "" {
respondError(w, http.StatusBadRequest, 40001, "branch cannot be empty")
return
}
task, err := h.repoService.SwitchBranch(r.Context(), id, req.Branch)
if err != nil {
logger.Logger.Error().Err(err).Int64("repo_id", id).Msg("failed to switch branch")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "branch switch task submitted", task)
}
// Update 更新仓库
func (h *RepoHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
return
}
task, err := h.repoService.UpdateRepo(r.Context(), id)
if err != nil {
logger.Logger.Error().Err(err).Int64("repo_id", id).Msg("failed to update repository")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "update task submitted", task)
}
// Reset 重置仓库
func (h *RepoHandler) Reset(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
return
}
task, err := h.repoService.ResetRepo(r.Context(), id)
if err != nil {
logger.Logger.Error().Err(err).Int64("repo_id", id).Msg("failed to reset repository")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "reset task submitted", task)
}
// Delete 删除仓库
func (h *RepoHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
return
}
if err := h.repoService.DeleteRepo(r.Context(), id); err != nil {
logger.Logger.Error().Err(err).Int64("repo_id", id).Msg("failed to delete repository")
respondError(w, http.StatusInternalServerError, 50000, "failed to delete repository")
return
}
respondJSON(w, http.StatusOK, 0, "repository deleted successfully", nil)
}

View File

@@ -0,0 +1,32 @@
package handlers
import (
"encoding/json"
"net/http"
)
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// respondJSON 返回JSON响应
func respondJSON(w http.ResponseWriter, statusCode, code int, message string, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
resp := Response{
Code: code,
Message: message,
Data: data,
}
json.NewEncoder(w).Encode(resp)
}
// respondError 返回错误响应
func respondError(w http.ResponseWriter, statusCode, code int, message string) {
respondJSON(w, statusCode, code, message, nil)
}

View File

@@ -0,0 +1,130 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/service"
)
// StatsHandler 统计API处理器
type StatsHandler struct {
statsService *service.StatsService
}
// NewStatsHandler 创建统计处理器
func NewStatsHandler(statsService *service.StatsService) *StatsHandler {
return &StatsHandler{
statsService: statsService,
}
}
// Calculate 触发统计计算
func (h *StatsHandler) Calculate(w http.ResponseWriter, r *http.Request) {
var req service.CalculateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, 40001, "invalid request body")
return
}
if req.RepoID == 0 {
respondError(w, http.StatusBadRequest, 40001, "repo_id is required")
return
}
if req.Branch == "" {
respondError(w, http.StatusBadRequest, 40001, "branch is required")
return
}
// 校验约束参数
if err := service.ValidateStatsConstraint(req.Constraint); err != nil {
respondError(w, http.StatusBadRequest, 40001, err.Error())
return
}
task, err := h.statsService.Calculate(r.Context(), &req)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to submit stats task")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "statistics task submitted", task)
}
// QueryResult 查询统计结果
func (h *StatsHandler) QueryResult(w http.ResponseWriter, r *http.Request) {
repoID, _ := strconv.ParseInt(r.URL.Query().Get("repo_id"), 10, 64)
branch := r.URL.Query().Get("branch")
constraintType := r.URL.Query().Get("constraint_type")
from := r.URL.Query().Get("from")
to := r.URL.Query().Get("to")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if repoID == 0 {
respondError(w, http.StatusBadRequest, 40001, "repo_id is required")
return
}
if branch == "" {
respondError(w, http.StatusBadRequest, 40001, "branch is required")
return
}
req := &service.QueryResultRequest{
RepoID: repoID,
Branch: branch,
ConstraintType: constraintType,
From: from,
To: to,
Limit: limit,
}
result, err := h.statsService.QueryResult(r.Context(), req)
if err != nil {
if err.Error() == "statistics not found, please submit calculation task first" {
respondError(w, http.StatusNotFound, 40400, err.Error())
return
}
logger.Logger.Error().Err(err).Msg("failed to query stats result")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "success", result)
}
// CountCommits 统计提交次数
func (h *StatsHandler) CountCommits(w http.ResponseWriter, r *http.Request) {
repoID, _ := strconv.ParseInt(r.URL.Query().Get("repo_id"), 10, 64)
branch := r.URL.Query().Get("branch")
from := r.URL.Query().Get("from")
if repoID == 0 {
respondError(w, http.StatusBadRequest, 40001, "repo_id is required")
return
}
if branch == "" {
respondError(w, http.StatusBadRequest, 40001, "branch is required")
return
}
req := &service.CountCommitsRequest{
RepoID: repoID,
Branch: branch,
From: from,
}
result, err := h.statsService.CountCommits(r.Context(), req)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to count commits")
respondError(w, http.StatusInternalServerError, 50000, err.Error())
return
}
respondJSON(w, http.StatusOK, 0, "success", result)
}

65
internal/api/router.go Normal file
View File

@@ -0,0 +1,65 @@
package api
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/gitcodestatic/gitcodestatic/internal/api/handlers"
"github.com/gitcodestatic/gitcodestatic/internal/service"
)
// Router 路由配置
type Router struct {
repoHandler *handlers.RepoHandler
statsHandler *handlers.StatsHandler
}
// NewRouter 创建路由
func NewRouter(repoService *service.RepoService, statsService *service.StatsService) *Router {
return &Router{
repoHandler: handlers.NewRepoHandler(repoService),
statsHandler: handlers.NewStatsHandler(statsService),
}
}
// Setup 设置路由
func (rt *Router) Setup() http.Handler {
r := chi.NewRouter()
// 中间件
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy"}`))
})
// API routes
r.Route("/api/v1", func(r chi.Router) {
// 仓库管理
r.Route("/repos", func(r chi.Router) {
r.Post("/batch", rt.repoHandler.AddBatch)
r.Get("/", rt.repoHandler.List)
r.Get("/{id}", rt.repoHandler.Get)
r.Post("/{id}/switch-branch", rt.repoHandler.SwitchBranch)
r.Post("/{id}/update", rt.repoHandler.Update)
r.Post("/{id}/reset", rt.repoHandler.Reset)
r.Delete("/{id}", rt.repoHandler.Delete)
})
// 统计
r.Route("/stats", func(r chi.Router) {
r.Post("/calculate", rt.statsHandler.Calculate)
r.Get("/result", rt.statsHandler.QueryResult)
r.Get("/commit-count", rt.statsHandler.CountCommits)
})
})
return r
}

178
internal/cache/file_cache.go vendored Normal file
View File

@@ -0,0 +1,178 @@
package cache
import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// FileCache 基于文件+DB的缓存实现
type FileCache struct {
store storage.Store
statsDir string
}
// NewFileCache 创建文件缓存
func NewFileCache(store storage.Store, statsDir string) *FileCache {
return &FileCache{
store: store,
statsDir: statsDir,
}
}
// Get 获取缓存
func (c *FileCache) Get(ctx context.Context, cacheKey string) (*models.StatsResult, error) {
// 从DB查询缓存元数据
cache, err := c.store.StatsCache().GetByCacheKey(ctx, cacheKey)
if err != nil {
return nil, err
}
if cache == nil {
return nil, nil // 缓存不存在
}
// 读取结果文件
stats, err := c.loadStatsFromFile(cache.ResultPath)
if err != nil {
logger.Logger.Error().Err(err).Str("cache_key", cacheKey).Msg("failed to load stats from file")
return nil, err
}
// 更新命中次数
if err := c.store.StatsCache().UpdateHitCount(ctx, cache.ID); err != nil {
logger.Logger.Warn().Err(err).Int64("cache_id", cache.ID).Msg("failed to update hit count")
}
result := &models.StatsResult{
CacheHit: true,
CachedAt: &cache.CreatedAt,
CommitHash: cache.CommitHash,
Statistics: stats,
}
logger.Logger.Info().
Str("cache_key", cacheKey).
Int64("cache_id", cache.ID).
Msg("cache hit")
return result, nil
}
// Set 设置缓存
func (c *FileCache) Set(ctx context.Context, repoID int64, branch string, constraint *models.StatsConstraint,
commitHash string, stats *models.Statistics) error {
// 生成缓存键
cacheKey := GenerateCacheKey(repoID, branch, constraint, commitHash)
// 保存统计结果到文件
resultPath := filepath.Join(c.statsDir, cacheKey+".json.gz")
if err := c.saveStatsToFile(stats, resultPath); err != nil {
return fmt.Errorf("failed to save stats to file: %w", err)
}
// 获取文件大小
fileInfo, err := os.Stat(resultPath)
if err != nil {
return fmt.Errorf("failed to stat result file: %w", err)
}
// 创建缓存记录
cache := &models.StatsCache{
RepoID: repoID,
Branch: branch,
ConstraintType: constraint.Type,
ConstraintValue: SerializeConstraint(constraint),
CommitHash: commitHash,
ResultPath: resultPath,
ResultSize: fileInfo.Size(),
CacheKey: cacheKey,
}
if err := c.store.StatsCache().Create(ctx, cache); err != nil {
// 如果创建失败,删除已保存的文件
os.Remove(resultPath)
return fmt.Errorf("failed to create cache record: %w", err)
}
logger.Logger.Info().
Str("cache_key", cacheKey).
Int64("cache_id", cache.ID).
Int64("file_size", fileInfo.Size()).
Msg("cache saved")
return nil
}
// InvalidateByRepoID 使指定仓库的所有缓存失效
func (c *FileCache) InvalidateByRepoID(ctx context.Context, repoID int64) error {
// 查询该仓库的所有缓存
// 注意:这里简化实现,实际应该先查询再删除文件
if err := c.store.StatsCache().DeleteByRepoID(ctx, repoID); err != nil {
return fmt.Errorf("failed to delete cache records: %w", err)
}
logger.Logger.Info().Int64("repo_id", repoID).Msg("cache invalidated")
return nil
}
// saveStatsToFile 保存统计结果到文件gzip压缩
func (c *FileCache) saveStatsToFile(stats *models.Statistics, filePath string) error {
// 确保目录存在
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// 创建文件
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
// 创建gzip writer
gzipWriter := gzip.NewWriter(file)
defer gzipWriter.Close()
// 编码JSON
encoder := json.NewEncoder(gzipWriter)
if err := encoder.Encode(stats); err != nil {
return fmt.Errorf("failed to encode stats: %w", err)
}
return nil
}
// loadStatsFromFile 从文件加载统计结果
func (c *FileCache) loadStatsFromFile(filePath string) (*models.Statistics, error) {
// 打开文件
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// 创建gzip reader
gzipReader, err := gzip.NewReader(file)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzipReader.Close()
// 解码JSON
var stats models.Statistics
decoder := json.NewDecoder(gzipReader)
if err := decoder.Decode(&stats); err != nil {
return nil, fmt.Errorf("failed to decode stats: %w", err)
}
return &stats, nil
}

44
internal/cache/key.go vendored Normal file
View File

@@ -0,0 +1,44 @@
package cache
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/gitcodestatic/gitcodestatic/internal/models"
)
// GenerateCacheKey 生成缓存键
func GenerateCacheKey(repoID int64, branch string, constraint *models.StatsConstraint, commitHash string) string {
var constraintStr string
if constraint != nil {
if constraint.Type == models.ConstraintTypeDateRange {
constraintStr = fmt.Sprintf("dr_%s_%s", constraint.From, constraint.To)
} else if constraint.Type == models.ConstraintTypeCommitLimit {
constraintStr = fmt.Sprintf("cl_%d", constraint.Limit)
}
}
data := fmt.Sprintf("repo:%d|branch:%s|constraint:%s|commit:%s",
repoID, branch, constraintStr, commitHash)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
// SerializeConstraint 序列化约束为JSON字符串
func SerializeConstraint(constraint *models.StatsConstraint) string {
if constraint == nil {
return "{}"
}
if constraint.Type == models.ConstraintTypeDateRange {
return fmt.Sprintf(`{"type":"date_range","from":"%s","to":"%s"}`,
constraint.From, constraint.To)
} else if constraint.Type == models.ConstraintTypeCommitLimit {
return fmt.Sprintf(`{"type":"commit_limit","limit":%d}`, constraint.Limit)
}
return "{}"
}

214
internal/config/config.go Normal file
View File

@@ -0,0 +1,214 @@
package config
import (
"fmt"
"os"
"time"
"gopkg.in/yaml.v3"
)
// Config 应用配置
type Config struct {
Server ServerConfig `yaml:"server"`
Workspace WorkspaceConfig `yaml:"workspace"`
Storage StorageConfig `yaml:"storage"`
Worker WorkerConfig `yaml:"worker"`
Cache CacheConfig `yaml:"cache"`
Security SecurityConfig `yaml:"security"`
Git GitConfig `yaml:"git"`
Log LogConfig `yaml:"log"`
Metrics MetricsConfig `yaml:"metrics"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
}
// WorkspaceConfig 工作空间配置
type WorkspaceConfig struct {
BaseDir string `yaml:"base_dir"`
CacheDir string `yaml:"cache_dir"`
StatsDir string `yaml:"stats_dir"`
}
// StorageConfig 存储配置
type StorageConfig struct {
Type string `yaml:"type"` // sqlite/postgres
SQLite SQLiteConfig `yaml:"sqlite"`
Postgres PostgresConfig `yaml:"postgres"`
}
// SQLiteConfig SQLite配置
type SQLiteConfig struct {
Path string `yaml:"path"`
}
// PostgresConfig PostgreSQL配置
type PostgresConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Database string `yaml:"database"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"sslmode"`
}
// WorkerConfig Worker配置
type WorkerConfig struct {
CloneWorkers int `yaml:"clone_workers"`
PullWorkers int `yaml:"pull_workers"`
StatsWorkers int `yaml:"stats_workers"`
GeneralWorkers int `yaml:"general_workers"`
QueueBuffer int `yaml:"queue_buffer"`
}
// CacheConfig 缓存配置
type CacheConfig struct {
MaxTotalSize int64 `yaml:"max_total_size"`
MaxSingleResult int64 `yaml:"max_single_result"`
RetentionDays int `yaml:"retention_days"`
CleanupInterval int `yaml:"cleanup_interval"` // seconds
}
// SecurityConfig 安全配置
type SecurityConfig struct {
EncryptionKey string `yaml:"encryption_key"`
}
// GitConfig Git配置
type GitConfig struct {
CommandPath string `yaml:"command_path"`
FallbackToGoGit bool `yaml:"fallback_to_gogit"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `yaml:"level"` // debug/info/warn/error
Format string `yaml:"format"` // json/text
Output string `yaml:"output"` // stdout/file path
}
// MetricsConfig 指标配置
type MetricsConfig struct {
Enabled bool `yaml:"enabled"`
Path string `yaml:"path"`
}
// LoadConfig 从文件加载配置
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
// 从环境变量覆盖敏感配置
if key := os.Getenv("ENCRYPTION_KEY"); key != "" {
cfg.Security.EncryptionKey = key
}
if dbPath := os.Getenv("DB_PATH"); dbPath != "" {
cfg.Storage.SQLite.Path = dbPath
}
// 设置默认值
setDefaults(&cfg)
return &cfg, nil
}
// setDefaults 设置默认值
func setDefaults(cfg *Config) {
if cfg.Server.Host == "" {
cfg.Server.Host = "0.0.0.0"
}
if cfg.Server.Port == 0 {
cfg.Server.Port = 8080
}
if cfg.Server.ReadTimeout == 0 {
cfg.Server.ReadTimeout = 30 * time.Second
}
if cfg.Server.WriteTimeout == 0 {
cfg.Server.WriteTimeout = 30 * time.Second
}
if cfg.Workspace.BaseDir == "" {
cfg.Workspace.BaseDir = "./workspace"
}
if cfg.Workspace.CacheDir == "" {
cfg.Workspace.CacheDir = "./workspace/cache"
}
if cfg.Workspace.StatsDir == "" {
cfg.Workspace.StatsDir = "./workspace/stats"
}
if cfg.Storage.Type == "" {
cfg.Storage.Type = "sqlite"
}
if cfg.Storage.SQLite.Path == "" {
cfg.Storage.SQLite.Path = "./workspace/data.db"
}
if cfg.Worker.CloneWorkers == 0 {
cfg.Worker.CloneWorkers = 2
}
if cfg.Worker.PullWorkers == 0 {
cfg.Worker.PullWorkers = 2
}
if cfg.Worker.StatsWorkers == 0 {
cfg.Worker.StatsWorkers = 2
}
if cfg.Worker.GeneralWorkers == 0 {
cfg.Worker.GeneralWorkers = 4
}
if cfg.Worker.QueueBuffer == 0 {
cfg.Worker.QueueBuffer = 100
}
if cfg.Cache.MaxTotalSize == 0 {
cfg.Cache.MaxTotalSize = 10 * 1024 * 1024 * 1024 // 10GB
}
if cfg.Cache.MaxSingleResult == 0 {
cfg.Cache.MaxSingleResult = 100 * 1024 * 1024 // 100MB
}
if cfg.Cache.RetentionDays == 0 {
cfg.Cache.RetentionDays = 30
}
if cfg.Cache.CleanupInterval == 0 {
cfg.Cache.CleanupInterval = 3600 // 1 hour
}
if cfg.Git.FallbackToGoGit {
// Default: allow fallback
}
if cfg.Log.Level == "" {
cfg.Log.Level = "info"
}
if cfg.Log.Format == "" {
cfg.Log.Format = "json"
}
if cfg.Log.Output == "" {
cfg.Log.Output = "stdout"
}
if cfg.Metrics.Path == "" {
cfg.Metrics.Path = "/metrics"
}
}
// DefaultConfig 返回默认配置
func DefaultConfig() *Config {
cfg := &Config{}
setDefaults(cfg)
return cfg
}

185
internal/git/cmd_git.go Normal file
View File

@@ -0,0 +1,185 @@
package git
import (
"context"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
)
// CmdGitManager 基于git命令的实现
type CmdGitManager struct {
gitPath string
}
// NewCmdGitManager 创建命令行Git管理器
func NewCmdGitManager(gitPath string) *CmdGitManager {
if gitPath == "" {
gitPath = "git"
}
return &CmdGitManager{gitPath: gitPath}
}
// IsAvailable 检查git命令是否可用
func (m *CmdGitManager) IsAvailable() bool {
cmd := exec.Command(m.gitPath, "--version")
err := cmd.Run()
return err == nil
}
// Clone 克隆仓库
func (m *CmdGitManager) Clone(ctx context.Context, url, localPath string, cred *models.Credential) error {
// 注入凭据到URL如果有
cloneURL := url
if cred != nil {
cloneURL = m.injectCredentials(url, cred)
}
cmd := exec.CommandContext(ctx, m.gitPath, "clone", cloneURL, localPath)
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // 禁止交互式提示
output, err := cmd.CombinedOutput()
if err != nil {
// 脱敏日志
sanitizedURL := sanitizeURL(url)
logger.Logger.Error().
Err(err).
Str("url", sanitizedURL).
Str("output", string(output)).
Msg("failed to clone repository")
return fmt.Errorf("failed to clone repository: %w", err)
}
logger.Logger.Info().
Str("url", sanitizeURL(url)).
Str("local_path", localPath).
Msg("repository cloned successfully")
return nil
}
// Pull 拉取更新
func (m *CmdGitManager) Pull(ctx context.Context, localPath string, cred *models.Credential) error {
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "pull")
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0")
output, err := cmd.CombinedOutput()
if err != nil {
logger.Logger.Error().
Err(err).
Str("local_path", localPath).
Str("output", string(output)).
Msg("failed to pull repository")
return fmt.Errorf("failed to pull repository: %w", err)
}
logger.Logger.Info().
Str("local_path", localPath).
Msg("repository pulled successfully")
return nil
}
// Checkout 切换分支
func (m *CmdGitManager) Checkout(ctx context.Context, localPath, branch string) error {
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "checkout", branch)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Logger.Error().
Err(err).
Str("local_path", localPath).
Str("branch", branch).
Str("output", string(output)).
Msg("failed to checkout branch")
return fmt.Errorf("failed to checkout branch: %w", err)
}
logger.Logger.Info().
Str("local_path", localPath).
Str("branch", branch).
Msg("branch checked out successfully")
return nil
}
// GetCurrentBranch 获取当前分支
func (m *CmdGitManager) GetCurrentBranch(ctx context.Context, localPath string) (string, error) {
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "--abbrev-ref", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get current branch: %w", err)
}
branch := strings.TrimSpace(string(output))
return branch, nil
}
// GetHeadCommitHash 获取HEAD commit hash
func (m *CmdGitManager) GetHeadCommitHash(ctx context.Context, localPath string) (string, error) {
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get HEAD commit hash: %w", err)
}
hash := strings.TrimSpace(string(output))
return hash, nil
}
// CountCommits 统计提交次数
func (m *CmdGitManager) CountCommits(ctx context.Context, localPath, branch, fromDate string) (int, error) {
args := []string{"-C", localPath, "rev-list", "--count"}
if fromDate != "" {
args = append(args, "--since="+fromDate)
}
args = append(args, branch)
cmd := exec.CommandContext(ctx, m.gitPath, args...)
output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("failed to count commits: %w", err)
}
countStr := strings.TrimSpace(string(output))
count, err := strconv.Atoi(countStr)
if err != nil {
return 0, fmt.Errorf("failed to parse commit count: %w", err)
}
return count, nil
}
// injectCredentials 注入凭据到URL
func (m *CmdGitManager) injectCredentials(url string, cred *models.Credential) string {
if cred == nil || cred.Username == "" {
return url
}
// 简单的URL凭据注入仅支持https
if strings.HasPrefix(url, "https://") {
credentials := cred.Username
if cred.Password != "" {
credentials += ":" + cred.Password
}
return strings.Replace(url, "https://", "https://"+credentials+"@", 1)
}
return url
}
// sanitizeURL 脱敏URL移除用户名密码
func sanitizeURL(url string) string {
re := regexp.MustCompile(`(https?://)[^@]+@`)
return re.ReplaceAllString(url, "${1}***@")
}

31
internal/git/manager.go Normal file
View File

@@ -0,0 +1,31 @@
package git
import (
"context"
"github.com/gitcodestatic/gitcodestatic/internal/models"
)
// Manager Git管理器接口
type Manager interface {
// Clone 克隆仓库
Clone(ctx context.Context, url, localPath string, cred *models.Credential) error
// Pull 拉取更新
Pull(ctx context.Context, localPath string, cred *models.Credential) error
// Checkout 切换分支
Checkout(ctx context.Context, localPath, branch string) error
// GetCurrentBranch 获取当前分支
GetCurrentBranch(ctx context.Context, localPath string) (string, error)
// GetHeadCommitHash 获取HEAD commit hash
GetHeadCommitHash(ctx context.Context, localPath string) (string, error)
// CountCommits 统计提交次数
CountCommits(ctx context.Context, localPath, branch, fromDate string) (int, error)
// IsAvailable 检查Git是否可用
IsAvailable() bool
}

72
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,72 @@
package logger
import (
"io"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var Logger zerolog.Logger
// InitLogger 初始化日志
func InitLogger(level, format, output string) error {
// 设置日志级别
var logLevel zerolog.Level
switch level {
case "debug":
logLevel = zerolog.DebugLevel
case "info":
logLevel = zerolog.InfoLevel
case "warn":
logLevel = zerolog.WarnLevel
case "error":
logLevel = zerolog.ErrorLevel
default:
logLevel = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(logLevel)
// 设置输出
var writer io.Writer
if output == "stdout" || output == "" {
writer = os.Stdout
} else {
file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
writer = file
}
// 设置格式
if format == "text" {
writer = zerolog.ConsoleWriter{Out: writer}
}
Logger = zerolog.New(writer).With().Timestamp().Logger()
log.Logger = Logger
return nil
}
// WithFields 创建带字段的日志
func WithFields(fields map[string]interface{}) *zerolog.Event {
event := Logger.Info()
for k, v := range fields {
switch val := v.(type) {
case string:
event = event.Str(k, val)
case int:
event = event.Int(k, val)
case int64:
event = event.Int64(k, val)
case bool:
event = event.Bool(k, val)
default:
event = event.Interface(k, val)
}
}
return event
}

28
internal/models/repo.go Normal file
View File

@@ -0,0 +1,28 @@
package models
import "time"
// Repository 仓库模型
type Repository struct {
ID int64 `json:"id" db:"id"`
URL string `json:"url" db:"url"`
Name string `json:"name" db:"name"`
CurrentBranch string `json:"current_branch" db:"current_branch"`
LocalPath string `json:"local_path" db:"local_path"`
Status string `json:"status" db:"status"` // pending/cloning/ready/failed
ErrorMessage *string `json:"error_message,omitempty" db:"error_message"`
LastPullAt *time.Time `json:"last_pull_at,omitempty" db:"last_pull_at"`
LastCommitHash *string `json:"last_commit_hash,omitempty" db:"last_commit_hash"`
CredentialID *string `json:"-" db:"credential_id"` // 不返回给前端
HasCredentials bool `json:"has_credentials" db:"-"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Repository Status constants
const (
RepoStatusPending = "pending"
RepoStatusCloning = "cloning"
RepoStatusReady = "ready"
RepoStatusFailed = "failed"
)

90
internal/models/stats.go Normal file
View File

@@ -0,0 +1,90 @@
package models
import "time"
// StatsCache 统计缓存模型
type StatsCache struct {
ID int64 `json:"id" db:"id"`
RepoID int64 `json:"repo_id" db:"repo_id"`
Branch string `json:"branch" db:"branch"`
ConstraintType string `json:"constraint_type" db:"constraint_type"` // date_range/commit_limit
ConstraintValue string `json:"constraint_value" db:"constraint_value"` // JSON string
CommitHash string `json:"commit_hash" db:"commit_hash"`
ResultPath string `json:"result_path" db:"result_path"`
ResultSize int64 `json:"result_size" db:"result_size"`
CacheKey string `json:"cache_key" db:"cache_key"`
HitCount int `json:"hit_count" db:"hit_count"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
LastHitAt *time.Time `json:"last_hit_at,omitempty" db:"last_hit_at"`
}
// StatsConstraint 统计约束
type StatsConstraint struct {
Type string `json:"type"` // date_range 或 commit_limit
From string `json:"from,omitempty"` // type=date_range时使用
To string `json:"to,omitempty"` // type=date_range时使用
Limit int `json:"limit,omitempty"` // type=commit_limit时使用
}
// Constraint Type constants
const (
ConstraintTypeDateRange = "date_range"
ConstraintTypeCommitLimit = "commit_limit"
)
// StatsResult 统计结果
type StatsResult struct {
CacheHit bool `json:"cache_hit"`
CachedAt *time.Time `json:"cached_at,omitempty"`
CommitHash string `json:"commit_hash"`
Statistics *Statistics `json:"statistics"`
}
// Statistics 统计数据
type Statistics struct {
Summary StatsSummary `json:"summary"`
ByContributor []ContributorStats `json:"by_contributor"`
}
// StatsSummary 统计摘要
type StatsSummary struct {
TotalCommits int `json:"total_commits"`
TotalContributors int `json:"total_contributors"`
DateRange *DateRange `json:"date_range,omitempty"`
CommitLimit *int `json:"commit_limit,omitempty"`
}
// DateRange 日期范围
type DateRange struct {
From string `json:"from"`
To string `json:"to"`
}
// ContributorStats 贡献者统计
type ContributorStats struct {
Author string `json:"author"`
Email string `json:"email"`
Commits int `json:"commits"`
Additions int `json:"additions"` // 新增行数
Deletions int `json:"deletions"` // 删除行数
Modifications int `json:"modifications"` // 修改行数 = min(additions, deletions)
NetAdditions int `json:"net_additions"` // 净增加 = additions - deletions
}
// Credential 凭据模型
type Credential struct {
ID string `json:"id" db:"id"`
Username string `json:"username,omitempty" db:"-"` // 不直接存储存在EncryptedData中
Password string `json:"password,omitempty" db:"-"` // 不直接存储
AuthType string `json:"auth_type" db:"auth_type"`
EncryptedData []byte `json:"-" db:"encrypted_data"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Auth Type constants
const (
AuthTypeBasic = "basic"
AuthTypeToken = "token"
AuthTypeSSH = "ssh"
)

54
internal/models/task.go Normal file
View File

@@ -0,0 +1,54 @@
package models
import "time"
// Task 任务模型
type Task struct {
ID int64 `json:"id" db:"id"`
TaskType string `json:"task_type" db:"task_type"`
RepoID int64 `json:"repo_id" db:"repo_id"`
Status string `json:"status" db:"status"`
Priority int `json:"priority" db:"priority"`
Parameters string `json:"parameters,omitempty" db:"parameters"` // JSON string
Result *string `json:"result,omitempty" db:"result"` // JSON string
ErrorMessage *string `json:"error_message,omitempty" db:"error_message"`
RetryCount int `json:"retry_count" db:"retry_count"`
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
DurationMs *int64 `json:"duration_ms,omitempty" db:"-"` // 计算字段
}
// Task Type constants
const (
TaskTypeClone = "clone"
TaskTypePull = "pull"
TaskTypeSwitch = "switch"
TaskTypeReset = "reset"
TaskTypeStats = "stats"
TaskTypeCountCommits = "count_commits"
)
// Task Status constants
const (
TaskStatusPending = "pending"
TaskStatusRunning = "running"
TaskStatusCompleted = "completed"
TaskStatusFailed = "failed"
TaskStatusCancelled = "cancelled"
)
// TaskParameters 任务参数结构
type TaskParameters struct {
Branch string `json:"branch,omitempty"`
Constraint *StatsConstraint `json:"constraint,omitempty"`
}
// TaskResult 任务结果结构
type TaskResult struct {
CacheKey string `json:"cache_key,omitempty"`
StatsCacheID int64 `json:"stats_cache_id,omitempty"`
CommitCount int `json:"commit_count,omitempty"`
Message string `json:"message,omitempty"`
}

View File

@@ -0,0 +1,279 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"regexp"
"strings"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
"github.com/gitcodestatic/gitcodestatic/internal/worker"
)
// RepoService 仓库服务
type RepoService struct {
store storage.Store
queue *worker.Queue
cacheDir string
}
// NewRepoService 创建仓库服务
func NewRepoService(store storage.Store, queue *worker.Queue, cacheDir string) *RepoService {
return &RepoService{
store: store,
queue: queue,
cacheDir: cacheDir,
}
}
// AddReposRequest 批量添加仓库请求
type AddReposRequest struct {
URLs []string `json:"urls"`
}
// AddReposResponse 批量添加仓库响应
type AddReposResponse struct {
Total int `json:"total"`
Succeeded []AddRepoResult `json:"succeeded"`
Failed []AddRepoFailure `json:"failed"`
}
// AddRepoResult 添加仓库成功结果
type AddRepoResult struct {
RepoID int64 `json:"repo_id"`
URL string `json:"url"`
TaskID int64 `json:"task_id"`
}
// AddRepoFailure 添加仓库失败结果
type AddRepoFailure struct {
URL string `json:"url"`
Error string `json:"error"`
}
// AddRepos 批量添加仓库
func (s *RepoService) AddRepos(ctx context.Context, req *AddReposRequest) (*AddReposResponse, error) {
resp := &AddReposResponse{
Total: len(req.URLs),
Succeeded: make([]AddRepoResult, 0),
Failed: make([]AddRepoFailure, 0),
}
for _, url := range req.URLs {
// 校验URL
if !isValidGitURL(url) {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: "invalid git URL",
})
continue
}
// 检查是否已存在
existing, err := s.store.Repos().GetByURL(ctx, url)
if err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to check existing repo: %v", err),
})
continue
}
if existing != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: "repository already exists",
})
continue
}
// 创建仓库记录
repoName := extractRepoName(url)
localPath := filepath.Join(s.cacheDir, repoName)
repo := &models.Repository{
URL: url,
Name: repoName,
LocalPath: localPath,
Status: models.RepoStatusPending,
}
if err := s.store.Repos().Create(ctx, repo); err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to create repository: %v", err),
})
continue
}
// 提交clone任务
task := &models.Task{
TaskType: models.TaskTypeClone,
RepoID: repo.ID,
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to enqueue clone task: %v", err),
})
continue
}
resp.Succeeded = append(resp.Succeeded, AddRepoResult{
RepoID: repo.ID,
URL: url,
TaskID: task.ID,
})
logger.Logger.Info().
Int64("repo_id", repo.ID).
Str("url", url).
Int64("task_id", task.ID).
Msg("repository added")
}
return resp, nil
}
// GetRepo 获取仓库详情
func (s *RepoService) GetRepo(ctx context.Context, id int64) (*models.Repository, error) {
return s.store.Repos().GetByID(ctx, id)
}
// ListRepos 获取仓库列表
func (s *RepoService) ListRepos(ctx context.Context, status string, page, pageSize int) ([]*models.Repository, int, error) {
return s.store.Repos().List(ctx, status, page, pageSize)
}
// SwitchBranch 切换分支
func (s *RepoService) SwitchBranch(ctx context.Context, repoID int64, branch string) (*models.Task, error) {
// 检查仓库是否存在
repo, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建切换分支任务
params := models.TaskParameters{
Branch: branch,
}
paramsJSON, _ := json.Marshal(params)
task := &models.Task{
TaskType: models.TaskTypeSwitch,
RepoID: repoID,
Parameters: string(paramsJSON),
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Str("branch", branch).
Int64("task_id", task.ID).
Msg("switch branch task submitted")
return task, nil
}
// UpdateRepo 更新仓库pull
func (s *RepoService) UpdateRepo(ctx context.Context, repoID int64) (*models.Task, error) {
// 检查仓库是否存在
repo, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建pull任务
task := &models.Task{
TaskType: models.TaskTypePull,
RepoID: repoID,
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Int64("task_id", task.ID).
Msg("update task submitted")
return task, nil
}
// ResetRepo 重置仓库
func (s *RepoService) ResetRepo(ctx context.Context, repoID int64) (*models.Task, error) {
// 检查仓库是否存在
_, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
// 创建reset任务
task := &models.Task{
TaskType: models.TaskTypeReset,
RepoID: repoID,
Priority: 1, // 高优先级
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Int64("task_id", task.ID).
Msg("reset task submitted")
return task, nil
}
// DeleteRepo 删除仓库
func (s *RepoService) DeleteRepo(ctx context.Context, id int64) error {
return s.store.Repos().Delete(ctx, id)
}
// isValidGitURL 校验Git URL
func isValidGitURL(url string) bool {
// 简单校验https:// 或 git@ 开头
return strings.HasPrefix(url, "https://") ||
strings.HasPrefix(url, "http://") ||
strings.HasPrefix(url, "git@")
}
// extractRepoName 从URL提取仓库名称
func extractRepoName(url string) string {
// 移除.git后缀
url = strings.TrimSuffix(url, ".git")
// 提取最后一个路径部分
parts := strings.Split(url, "/")
if len(parts) > 0 {
name := parts[len(parts)-1]
// 移除特殊字符
name = regexp.MustCompile(`[^a-zA-Z0-9_-]`).ReplaceAllString(name, "_")
return name
}
return "repo"
}

View File

@@ -0,0 +1,221 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gitcodestatic/gitcodestatic/internal/cache"
"github.com/gitcodestatic/gitcodestatic/internal/git"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
"github.com/gitcodestatic/gitcodestatic/internal/worker"
)
// StatsService 统计服务
type StatsService struct {
store storage.Store
queue *worker.Queue
cache *cache.FileCache
gitManager git.Manager
}
// NewStatsService 创建统计服务
func NewStatsService(store storage.Store, queue *worker.Queue, fileCache *cache.FileCache, gitManager git.Manager) *StatsService {
return &StatsService{
store: store,
queue: queue,
cache: fileCache,
gitManager: gitManager,
}
}
// CalculateRequest 统计请求
type CalculateRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
Constraint *models.StatsConstraint `json:"constraint"`
}
// Calculate 触发统计计算
func (s *StatsService) Calculate(ctx context.Context, req *CalculateRequest) (*models.Task, error) {
// 校验参数
if err := ValidateStatsConstraint(req.Constraint); err != nil {
return nil, err
}
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建统计任务
params := models.TaskParameters{
Branch: req.Branch,
Constraint: req.Constraint,
}
paramsJSON, _ := json.Marshal(params)
task := &models.Task{
TaskType: models.TaskTypeStats,
RepoID: req.RepoID,
Parameters: string(paramsJSON),
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", req.RepoID).
Str("branch", req.Branch).
Int64("task_id", task.ID).
Msg("stats task submitted")
return task, nil
}
// QueryResultRequest 查询统计结果请求
type QueryResultRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
ConstraintType string `json:"constraint_type"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Limit int `json:"limit,omitempty"`
}
// QueryResult 查询统计结果
func (s *StatsService) QueryResult(ctx context.Context, req *QueryResultRequest) (*models.StatsResult, error) {
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 构建约束
constraint := &models.StatsConstraint{
Type: req.ConstraintType,
}
if req.ConstraintType == models.ConstraintTypeDateRange {
constraint.From = req.From
constraint.To = req.To
} else {
constraint.Limit = req.Limit
}
// 获取当前HEAD commit hash
commitHash, err := s.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
if err != nil {
return nil, fmt.Errorf("failed to get HEAD commit hash: %w", err)
}
// 生成缓存键
cacheKey := cache.GenerateCacheKey(req.RepoID, req.Branch, constraint, commitHash)
// 查询缓存
result, err := s.cache.Get(ctx, cacheKey)
if err != nil {
logger.Logger.Warn().Err(err).Str("cache_key", cacheKey).Msg("failed to get cache")
}
if result != nil {
return result, nil
}
// 缓存未命中
return nil, errors.New("statistics not found, please submit calculation task first")
}
// CountCommitsRequest 统计提交次数请求
type CountCommitsRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
From string `json:"from"`
}
// CountCommitsResponse 统计提交次数响应
type CountCommitsResponse struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
From string `json:"from"`
To string `json:"to"`
CommitCount int `json:"commit_count"`
}
// CountCommits 统计提交次数(辅助查询)
func (s *StatsService) CountCommits(ctx context.Context, req *CountCommitsRequest) (*CountCommitsResponse, error) {
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 统计提交次数
count, err := s.gitManager.CountCommits(ctx, repo.LocalPath, req.Branch, req.From)
if err != nil {
return nil, fmt.Errorf("failed to count commits: %w", err)
}
resp := &CountCommitsResponse{
RepoID: req.RepoID,
Branch: req.Branch,
From: req.From,
To: "HEAD",
CommitCount: count,
}
logger.Logger.Info().
Int64("repo_id", req.RepoID).
Str("branch", req.Branch).
Str("from", req.From).
Int("count", count).
Msg("commits counted")
return resp, nil
}
// ValidateStatsConstraint 校验统计约束
func ValidateStatsConstraint(constraint *models.StatsConstraint) error {
if constraint == nil {
return errors.New("constraint is required")
}
if constraint.Type != models.ConstraintTypeDateRange && constraint.Type != models.ConstraintTypeCommitLimit {
return fmt.Errorf("constraint type must be %s or %s", models.ConstraintTypeDateRange, models.ConstraintTypeCommitLimit)
}
if constraint.Type == models.ConstraintTypeDateRange {
if constraint.From == "" || constraint.To == "" {
return fmt.Errorf("%s requires both from and to", models.ConstraintTypeDateRange)
}
if constraint.Limit != 0 {
return fmt.Errorf("%s cannot be used with limit", models.ConstraintTypeDateRange)
}
} else if constraint.Type == models.ConstraintTypeCommitLimit {
if constraint.Limit <= 0 {
return fmt.Errorf("%s requires positive limit value", models.ConstraintTypeCommitLimit)
}
if constraint.From != "" || constraint.To != "" {
return fmt.Errorf("%s cannot be used with date range", models.ConstraintTypeCommitLimit)
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package service
import (
"context"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// TaskService 任务服务
type TaskService struct {
store storage.Store
}
// NewTaskService 创建任务服务
func NewTaskService(store storage.Store) *TaskService {
return &TaskService{
store: store,
}
}
// GetTask 获取任务详情
func (s *TaskService) GetTask(ctx context.Context, id int64) (*models.Task, error) {
return s.store.Tasks().GetByID(ctx, id)
}
// ListTasks 获取任务列表
func (s *TaskService) ListTasks(ctx context.Context, repoID int64, status string, page, pageSize int) ([]*models.Task, int, error) {
return s.store.Tasks().List(ctx, repoID, status, page, pageSize)
}
// CancelTask 取消任务
func (s *TaskService) CancelTask(ctx context.Context, id int64) error {
return s.store.Tasks().Cancel(ctx, id)
}

View File

@@ -0,0 +1,175 @@
package stats
import (
"bufio"
"context"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
)
// Calculator 统计计算器
type Calculator struct {
gitPath string
}
// NewCalculator 创建统计计算器
func NewCalculator(gitPath string) *Calculator {
if gitPath == "" {
gitPath = "git"
}
return &Calculator{gitPath: gitPath}
}
// Calculate 计算统计数据
func (c *Calculator) Calculate(ctx context.Context, localPath, branch string, constraint *models.StatsConstraint) (*models.Statistics, error) {
// 构建git log命令
args := []string{
"-C", localPath,
"log",
"--no-merges",
"--numstat",
"--pretty=format:COMMIT:%H|AUTHOR:%an|EMAIL:%ae|DATE:%ai",
}
// 添加约束条件
if constraint != nil {
if constraint.Type == models.ConstraintTypeDateRange {
if constraint.From != "" {
args = append(args, "--since="+constraint.From)
}
if constraint.To != "" {
args = append(args, "--until="+constraint.To)
}
} else if constraint.Type == models.ConstraintTypeCommitLimit {
args = append(args, "-n", strconv.Itoa(constraint.Limit))
}
}
args = append(args, branch)
logger.Logger.Debug().
Str("local_path", localPath).
Str("branch", branch).
Interface("constraint", constraint).
Msg("running git log")
cmd := exec.CommandContext(ctx, c.gitPath, args...)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to run git log: %w", err)
}
// 解析输出
stats, err := c.parseGitLog(string(output))
if err != nil {
return nil, fmt.Errorf("failed to parse git log: %w", err)
}
// 填充摘要信息
stats.Summary.TotalContributors = len(stats.ByContributor)
if constraint != nil {
if constraint.Type == models.ConstraintTypeDateRange {
stats.Summary.DateRange = &models.DateRange{
From: constraint.From,
To: constraint.To,
}
} else if constraint.Type == models.ConstraintTypeCommitLimit {
stats.Summary.CommitLimit = &constraint.Limit
}
}
return stats, nil
}
// parseGitLog 解析git log输出
func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
stats := &models.Statistics{
Summary: models.StatsSummary{},
ByContributor: make([]models.ContributorStats, 0),
}
contributors := make(map[string]*models.ContributorStats)
var currentAuthor, currentEmail string
commitCount := 0
scanner := bufio.NewScanner(strings.NewReader(output))
commitPattern := regexp.MustCompile(`^COMMIT:(.+?)\|AUTHOR:(.+?)\|EMAIL:(.+?)\|DATE:(.+)$`)
numstatPattern := regexp.MustCompile(`^(\d+|-)\s+(\d+|-)\s+(.+)$`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// 匹配提交行
if matches := commitPattern.FindStringSubmatch(line); matches != nil {
currentAuthor = matches[2]
currentEmail = matches[3]
commitCount++
// 初始化贡献者统计
if _, ok := contributors[currentEmail]; !ok {
contributors[currentEmail] = &models.ContributorStats{
Author: currentAuthor,
Email: currentEmail,
}
}
contributors[currentEmail].Commits++
continue
}
// 匹配文件变更行
if matches := numstatPattern.FindStringSubmatch(line); matches != nil && currentEmail != "" {
additionsStr := matches[1]
deletionsStr := matches[2]
// 处理二进制文件(显示为 -
additions := 0
deletions := 0
if additionsStr != "-" {
additions, _ = strconv.Atoi(additionsStr)
}
if deletionsStr != "-" {
deletions, _ = strconv.Atoi(deletionsStr)
}
contrib := contributors[currentEmail]
contrib.Additions += additions
contrib.Deletions += deletions
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading git log output: %w", err)
}
// 计算修改行数和净增加
for _, contrib := range contributors {
// 修改的定义:被替换的行数 = min(additions, deletions)
contrib.Modifications = min(contrib.Additions, contrib.Deletions)
contrib.NetAdditions = contrib.Additions - contrib.Deletions
stats.ByContributor = append(stats.ByContributor, *contrib)
}
stats.Summary.TotalCommits = commitCount
return stats, nil
}
// min 返回两个整数的最小值
func min(a, b int) int {
if a < b {
return a
}
return b
}

346
internal/worker/handlers.go Normal file
View File

@@ -0,0 +1,346 @@
package worker
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/gitcodestatic/gitcodestatic/internal/cache"
"github.com/gitcodestatic/gitcodestatic/internal/git"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/stats"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// CloneHandler 克隆任务处理器
type CloneHandler struct {
store storage.Store
gitManager git.Manager
}
func NewCloneHandler(store storage.Store, gitManager git.Manager) *CloneHandler {
return &CloneHandler{
store: store,
gitManager: gitManager,
}
}
func (h *CloneHandler) Type() string {
return models.TaskTypeClone
}
func (h *CloneHandler) Timeout() time.Duration {
return 10 * time.Minute
}
func (h *CloneHandler) Handle(ctx context.Context, task *models.Task) error {
// 获取仓库信息
repo, err := h.store.Repos().GetByID(ctx, task.RepoID)
if err != nil {
return fmt.Errorf("failed to get repository: %w", err)
}
// 更新仓库状态为cloning
repo.Status = models.RepoStatusCloning
h.store.Repos().Update(ctx, repo)
// 获取凭据(如果有)
var cred *models.Credential
if repo.CredentialID != nil {
cred, _ = h.store.Credentials().GetByID(ctx, *repo.CredentialID)
}
// 克隆仓库
if err := h.gitManager.Clone(ctx, repo.URL, repo.LocalPath, cred); err != nil {
errMsg := err.Error()
repo.Status = models.RepoStatusFailed
repo.ErrorMessage = &errMsg
h.store.Repos().Update(ctx, repo)
return err
}
// 获取当前分支和commit hash
branch, err := h.gitManager.GetCurrentBranch(ctx, repo.LocalPath)
if err != nil {
logger.Logger.Warn().Err(err).Msg("failed to get current branch")
branch = "main"
}
commitHash, err := h.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
if err != nil {
logger.Logger.Warn().Err(err).Msg("failed to get HEAD commit hash")
}
// 更新仓库状态为ready
now := time.Now()
repo.Status = models.RepoStatusReady
repo.CurrentBranch = branch
repo.LastCommitHash = &commitHash
repo.LastPullAt = &now
repo.ErrorMessage = nil
h.store.Repos().Update(ctx, repo)
return nil
}
// PullHandler 拉取任务处理器
type PullHandler struct {
store storage.Store
gitManager git.Manager
}
func NewPullHandler(store storage.Store, gitManager git.Manager) *PullHandler {
return &PullHandler{
store: store,
gitManager: gitManager,
}
}
func (h *PullHandler) Type() string {
return models.TaskTypePull
}
func (h *PullHandler) Timeout() time.Duration {
return 5 * time.Minute
}
func (h *PullHandler) Handle(ctx context.Context, task *models.Task) error {
repo, err := h.store.Repos().GetByID(ctx, task.RepoID)
if err != nil {
return err
}
var cred *models.Credential
if repo.CredentialID != nil {
cred, _ = h.store.Credentials().GetByID(ctx, *repo.CredentialID)
}
if err := h.gitManager.Pull(ctx, repo.LocalPath, cred); err != nil {
return err
}
// 更新commit hash
commitHash, _ := h.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
now := time.Now()
repo.LastCommitHash = &commitHash
repo.LastPullAt = &now
h.store.Repos().Update(ctx, repo)
return nil
}
// SwitchHandler 切换分支处理器
type SwitchHandler struct {
store storage.Store
gitManager git.Manager
}
func NewSwitchHandler(store storage.Store, gitManager git.Manager) *SwitchHandler {
return &SwitchHandler{
store: store,
gitManager: gitManager,
}
}
func (h *SwitchHandler) Type() string {
return models.TaskTypeSwitch
}
func (h *SwitchHandler) Timeout() time.Duration {
return 1 * time.Minute
}
func (h *SwitchHandler) Handle(ctx context.Context, task *models.Task) error {
repo, err := h.store.Repos().GetByID(ctx, task.RepoID)
if err != nil {
return err
}
var params models.TaskParameters
if err := json.Unmarshal([]byte(task.Parameters), &params); err != nil {
return fmt.Errorf("failed to parse parameters: %w", err)
}
if err := h.gitManager.Checkout(ctx, repo.LocalPath, params.Branch); err != nil {
return err
}
// 更新仓库当前分支
repo.CurrentBranch = params.Branch
commitHash, _ := h.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
repo.LastCommitHash = &commitHash
h.store.Repos().Update(ctx, repo)
return nil
}
// ResetHandler 重置仓库处理器
type ResetHandler struct {
store storage.Store
gitManager git.Manager
fileCache *cache.FileCache
}
func NewResetHandler(store storage.Store, gitManager git.Manager, fileCache *cache.FileCache) *ResetHandler {
return &ResetHandler{
store: store,
gitManager: gitManager,
fileCache: fileCache,
}
}
func (h *ResetHandler) Type() string {
return models.TaskTypeReset
}
func (h *ResetHandler) Timeout() time.Duration {
return 10 * time.Minute
}
func (h *ResetHandler) Handle(ctx context.Context, task *models.Task) error {
repo, err := h.store.Repos().GetByID(ctx, task.RepoID)
if err != nil {
return err
}
// 1. 删除统计缓存
h.fileCache.InvalidateByRepoID(ctx, repo.ID)
// 2. 删除本地目录
if err := os.RemoveAll(repo.LocalPath); err != nil {
logger.Logger.Warn().Err(err).Str("path", repo.LocalPath).Msg("failed to remove local path")
}
// 3. 更新仓库状态为pending
repo.Status = models.RepoStatusPending
repo.CurrentBranch = ""
repo.LastCommitHash = nil
repo.LastPullAt = nil
repo.ErrorMessage = nil
h.store.Repos().Update(ctx, repo)
// 4. 重新克隆
var cred *models.Credential
if repo.CredentialID != nil {
cred, _ = h.store.Credentials().GetByID(ctx, *repo.CredentialID)
}
repo.Status = models.RepoStatusCloning
h.store.Repos().Update(ctx, repo)
if err := h.gitManager.Clone(ctx, repo.URL, repo.LocalPath, cred); err != nil {
errMsg := err.Error()
repo.Status = models.RepoStatusFailed
repo.ErrorMessage = &errMsg
h.store.Repos().Update(ctx, repo)
return err
}
// 更新为ready
branch, _ := h.gitManager.GetCurrentBranch(ctx, repo.LocalPath)
commitHash, _ := h.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
now := time.Now()
repo.Status = models.RepoStatusReady
repo.CurrentBranch = branch
repo.LastCommitHash = &commitHash
repo.LastPullAt = &now
repo.ErrorMessage = nil
h.store.Repos().Update(ctx, repo)
return nil
}
// StatsHandler 统计任务处理器
type StatsHandler struct {
store storage.Store
calculator *stats.Calculator
fileCache *cache.FileCache
gitManager git.Manager
}
func NewStatsHandler(store storage.Store, calculator *stats.Calculator, fileCache *cache.FileCache, gitManager git.Manager) *StatsHandler {
return &StatsHandler{
store: store,
calculator: calculator,
fileCache: fileCache,
gitManager: gitManager,
}
}
func (h *StatsHandler) Type() string {
return models.TaskTypeStats
}
func (h *StatsHandler) Timeout() time.Duration {
return 30 * time.Minute
}
func (h *StatsHandler) Handle(ctx context.Context, task *models.Task) error {
repo, err := h.store.Repos().GetByID(ctx, task.RepoID)
if err != nil {
return err
}
var params models.TaskParameters
if err := json.Unmarshal([]byte(task.Parameters), &params); err != nil {
return fmt.Errorf("failed to parse parameters: %w", err)
}
// 获取当前HEAD commit hash
commitHash, err := h.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
if err != nil {
return fmt.Errorf("failed to get HEAD commit hash: %w", err)
}
// 检查缓存
cacheKey := cache.GenerateCacheKey(repo.ID, params.Branch, params.Constraint, commitHash)
cached, _ := h.fileCache.Get(ctx, cacheKey)
if cached != nil {
// 缓存命中,直接返回
logger.Logger.Info().Str("cache_key", cacheKey).Msg("cache hit during stats calculation")
result := models.TaskResult{
CacheKey: cacheKey,
Message: "cache hit",
}
resultJSON, _ := json.Marshal(result)
resultStr := string(resultJSON)
task.Result = &resultStr
h.store.Tasks().Update(ctx, task)
return nil
}
// 执行统计
statistics, err := h.calculator.Calculate(ctx, repo.LocalPath, params.Branch, params.Constraint)
if err != nil {
return fmt.Errorf("failed to calculate statistics: %w", err)
}
// 保存到缓存
if err := h.fileCache.Set(ctx, repo.ID, params.Branch, params.Constraint, commitHash, statistics); err != nil {
logger.Logger.Warn().Err(err).Msg("failed to save statistics to cache")
}
// 更新任务结果
result := models.TaskResult{
CacheKey: cacheKey,
Message: "statistics calculated successfully",
}
resultJSON, _ := json.Marshal(result)
resultStr := string(resultJSON)
task.Result = &resultStr
h.store.Tasks().Update(ctx, task)
logger.Logger.Info().
Int64("repo_id", repo.ID).
Str("branch", params.Branch).
Int("total_commits", statistics.Summary.TotalCommits).
Int("contributors", statistics.Summary.TotalContributors).
Msg("statistics calculated")
return nil
}

78
internal/worker/pool.go Normal file
View File

@@ -0,0 +1,78 @@
package worker
import (
"context"
"sync"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// Pool Worker池
type Pool struct {
queue *Queue
workers []*Worker
handlers map[string]TaskHandler
store storage.Store
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// NewPool 创建Worker池
func NewPool(workerCount int, queueSize int, store storage.Store, handlers map[string]TaskHandler) *Pool {
ctx, cancel := context.WithCancel(context.Background())
queue := NewQueue(queueSize, store)
pool := &Pool{
queue: queue,
workers: make([]*Worker, 0, workerCount),
handlers: handlers,
store: store,
ctx: ctx,
cancel: cancel,
}
// 创建workers
for i := 0; i < workerCount; i++ {
worker := NewWorker(i+1, queue, store, handlers)
pool.workers = append(pool.workers, worker)
}
return pool
}
// Start 启动Worker池
func (p *Pool) Start() {
logger.Logger.Info().Int("worker_count", len(p.workers)).Msg("starting worker pool")
for _, worker := range p.workers {
worker.Start(p.ctx)
}
}
// Stop 停止Worker池
func (p *Pool) Stop() {
logger.Logger.Info().Msg("stopping worker pool")
p.cancel()
for _, worker := range p.workers {
worker.Stop()
}
p.queue.Close()
logger.Logger.Info().Msg("worker pool stopped")
}
// GetQueue 获取队列
func (p *Pool) GetQueue() *Queue {
return p.queue
}
// QueueSize 获取队列长度
func (p *Pool) QueueSize() int {
return p.queue.Size()
}

88
internal/worker/queue.go Normal file
View File

@@ -0,0 +1,88 @@
package worker
import (
"context"
"fmt"
"sync"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// Queue 任务队列
type Queue struct {
taskChan chan *models.Task
store storage.Store
mu sync.RWMutex
}
// NewQueue 创建任务队列
func NewQueue(bufferSize int, store storage.Store) *Queue {
return &Queue{
taskChan: make(chan *models.Task, bufferSize),
store: store,
}
}
// Enqueue 加入任务到队列
func (q *Queue) Enqueue(ctx context.Context, task *models.Task) error {
// 检查是否存在相同的待处理任务(去重)
existing, err := q.store.Tasks().FindExisting(ctx, task.RepoID, task.TaskType, task.Parameters)
if err != nil {
return fmt.Errorf("failed to check existing task: %w", err)
}
if existing != nil {
// 已存在相同任务,返回已有任务
logger.Logger.Info().
Int64("task_id", existing.ID).
Int64("repo_id", task.RepoID).
Str("task_type", task.TaskType).
Msg("task already exists, returning existing task")
task.ID = existing.ID
task.Status = existing.Status
task.CreatedAt = existing.CreatedAt
return nil
}
// 创建新任务
task.Status = models.TaskStatusPending
if err := q.store.Tasks().Create(ctx, task); err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
// 加入队列
select {
case q.taskChan <- task:
logger.Logger.Info().
Int64("task_id", task.ID).
Int64("repo_id", task.RepoID).
Str("task_type", task.TaskType).
Msg("task enqueued")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// Dequeue 从队列取出任务
func (q *Queue) Dequeue(ctx context.Context) (*models.Task, error) {
select {
case task := <-q.taskChan:
return task, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// Size 返回队列长度
func (q *Queue) Size() int {
return len(q.taskChan)
}
// Close 关闭队列
func (q *Queue) Close() {
close(q.taskChan)
}

150
internal/worker/worker.go Normal file
View File

@@ -0,0 +1,150 @@
package worker
import (
"context"
"fmt"
"sync"
"time"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// TaskHandler 任务处理器接口
type TaskHandler interface {
Handle(ctx context.Context, task *models.Task) error
Type() string
Timeout() time.Duration
}
// Worker 工作器
type Worker struct {
id int
queue *Queue
handlers map[string]TaskHandler
store storage.Store
stopCh chan struct{}
wg *sync.WaitGroup
}
// NewWorker 创建工作器
func NewWorker(id int, queue *Queue, store storage.Store, handlers map[string]TaskHandler) *Worker {
return &Worker{
id: id,
queue: queue,
handlers: handlers,
store: store,
stopCh: make(chan struct{}),
wg: &sync.WaitGroup{},
}
}
// Start 启动工作器
func (w *Worker) Start(ctx context.Context) {
w.wg.Add(1)
go w.run(ctx)
}
// Stop 停止工作器
func (w *Worker) Stop() {
close(w.stopCh)
w.wg.Wait()
}
// run 运行工作器
func (w *Worker) run(ctx context.Context) {
defer w.wg.Done()
logger.Logger.Info().Int("worker_id", w.id).Msg("worker started")
for {
select {
case <-w.stopCh:
logger.Logger.Info().Int("worker_id", w.id).Msg("worker stopped")
return
case <-ctx.Done():
logger.Logger.Info().Int("worker_id", w.id).Msg("worker context cancelled")
return
default:
// 从队列取任务
task, err := w.queue.Dequeue(ctx)
if err != nil {
if err == context.Canceled {
return
}
logger.Logger.Error().Err(err).Int("worker_id", w.id).Msg("failed to dequeue task")
time.Sleep(time.Second)
continue
}
if task == nil {
continue
}
// 处理任务
w.handleTask(ctx, task)
}
}
}
// handleTask 处理任务
func (w *Worker) handleTask(ctx context.Context, task *models.Task) {
startTime := time.Now()
logger.Logger.Info().
Int("worker_id", w.id).
Int64("task_id", task.ID).
Str("task_type", task.TaskType).
Int64("repo_id", task.RepoID).
Msg("task started")
// 更新任务状态为运行中
if err := w.store.Tasks().UpdateStatus(ctx, task.ID, models.TaskStatusRunning, nil); err != nil {
logger.Logger.Error().Err(err).Int64("task_id", task.ID).Msg("failed to update task status to running")
return
}
// 查找处理器
handler, ok := w.handlers[task.TaskType]
if !ok {
errMsg := fmt.Sprintf("no handler found for task type: %s", task.TaskType)
logger.Logger.Error().Int64("task_id", task.ID).Str("task_type", task.TaskType).Msg(errMsg)
w.store.Tasks().UpdateStatus(ctx, task.ID, models.TaskStatusFailed, &errMsg)
return
}
// 创建带超时的上下文
timeout := handler.Timeout()
taskCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// 执行任务
err := handler.Handle(taskCtx, task)
duration := time.Since(startTime)
if err != nil {
errMsg := err.Error()
logger.Logger.Error().
Err(err).
Int("worker_id", w.id).
Int64("task_id", task.ID).
Str("task_type", task.TaskType).
Int64("duration_ms", duration.Milliseconds()).
Msg("task failed")
w.store.Tasks().UpdateStatus(ctx, task.ID, models.TaskStatusFailed, &errMsg)
return
}
// 任务成功
logger.Logger.Info().
Int("worker_id", w.id).
Int64("task_id", task.ID).
Str("task_type", task.TaskType).
Int64("duration_ms", duration.Milliseconds()).
Msg("task completed")
w.store.Tasks().UpdateStatus(ctx, task.ID, models.TaskStatusCompleted, nil)
}