feat: Phase 1 — 项目骨架搭建,集成 Gin、SQLite 和设置页面

This commit is contained in:
2026-06-18 22:48:16 +08:00
parent 46f1bbf0f1
commit 1e9b1a2129
26 changed files with 2518 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
)
type GenerateHandler struct {
db *sql.DB
}
func NewGenerateHandler(db *sql.DB) *GenerateHandler {
return &GenerateHandler{db: db}
}
// Generate handles POST /api/repos/:id/generate — stub for Phase 4
func (h *GenerateHandler) Generate(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "generate not implemented yet — coming in Phase 4"})
}
+83
View File
@@ -0,0 +1,83 @@
package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"github.com/HoHD/PR-Helper/models"
)
type PageHandler struct {
db *sql.DB
}
func NewPageHandler(db *sql.DB) *PageHandler {
return &PageHandler{db: db}
}
func (h *PageHandler) Index(c *gin.Context) {
repos, _ := h.listRepos()
c.HTML(http.StatusOK, "pages/index.html", gin.H{
"Repos": repos,
})
}
func (h *PageHandler) Repo(c *gin.Context) {
c.HTML(http.StatusOK, "pages/repo.html", gin.H{
"ID": c.Param("id"),
})
}
func (h *PageHandler) Generate(c *gin.Context) {
c.HTML(http.StatusOK, "pages/generate.html", gin.H{
"ID": c.Param("id"),
})
}
func (h *PageHandler) Review(c *gin.Context) {
topN := "20"
h.db.QueryRow(`SELECT value FROM settings WHERE key = 'review.top_n'`).Scan(&topN)
c.HTML(http.StatusOK, "pages/review.html", gin.H{
"ID": c.Param("id"),
"TopN": topN,
})
}
func (h *PageHandler) Settings(c *gin.Context) {
settings := make(map[string]string)
for key := range models.DefaultSettings {
settings[key] = ""
}
rows, err := h.db.Query(`SELECT key, value FROM settings`)
if err == nil {
defer rows.Close()
for rows.Next() {
var k, v string
if rows.Scan(&k, &v) == nil {
settings[k] = v
}
}
}
c.HTML(http.StatusOK, "pages/settings.html", gin.H{
"Settings": settings,
})
}
func (h *PageHandler) listRepos() ([]models.Repository, error) {
rows, err := h.db.Query(`SELECT id, url, local_path, size_bytes, cloned_at, last_used FROM repositories ORDER BY last_used DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var repos []models.Repository
for rows.Next() {
var r models.Repository
if err := rows.Scan(&r.ID, &r.URL, &r.LocalPath, &r.SizeBytes, &r.ClonedAt, &r.LastUsed); err != nil {
continue
}
repos = append(repos, r)
}
return repos, nil
}
+121
View File
@@ -0,0 +1,121 @@
package handlers
import (
"database/sql"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
type ReposHandler struct {
db *sql.DB
reposDir string
}
func NewReposHandler(db *sql.DB, reposDir string) *ReposHandler {
return &ReposHandler{db: db, reposDir: reposDir}
}
func (h *ReposHandler) ListRepos(c *gin.Context) {
rows, err := h.db.Query(`SELECT id, url, local_path, size_bytes, cloned_at, last_used FROM repositories ORDER BY last_used DESC`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var repos []gin.H
for rows.Next() {
var id int64
var url, localPath string
var sizeBytes int64
var clonedAt, lastUsed string
if rows.Scan(&id, &url, &localPath, &sizeBytes, &clonedAt, &lastUsed) == nil {
repos = append(repos, gin.H{
"id": id, "url": url, "local_path": localPath,
"size_bytes": sizeBytes, "cloned_at": clonedAt, "last_used": lastUsed,
})
}
}
if repos == nil {
repos = []gin.H{}
}
c.JSON(http.StatusOK, repos)
}
func (h *ReposHandler) DeleteRepo(c *gin.Context) {
id := c.Param("id")
var localPath string
err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ?`, id).Scan(&localPath)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "repository not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Remove from filesystem
os.RemoveAll(localPath)
// Remove from database
h.db.Exec(`DELETE FROM analyses WHERE repo_id = ?`, id)
h.db.Exec(`DELETE FROM repositories WHERE id = ?`, id)
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *ReposHandler) CleanupRepos(c *gin.Context) {
// Get max age from settings
var maxAgeDays string
h.db.QueryRow(`SELECT value FROM settings WHERE key = 'cache.max_age_days'`).Scan(&maxAgeDays)
if maxAgeDays == "" {
maxAgeDays = "7"
}
// Find expired repos
rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE last_used < datetime('now', '-' || ? || ' days')`, maxAgeDays)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var cleaned []int64
for rows.Next() {
var id int64
var localPath string
if rows.Scan(&id, &localPath) == nil {
os.RemoveAll(localPath)
h.db.Exec(`DELETE FROM analyses WHERE repo_id = ?`, id)
h.db.Exec(`DELETE FROM repositories WHERE id = ?`, id)
cleaned = append(cleaned, id)
}
}
c.JSON(http.StatusOK, gin.H{"cleaned": cleaned})
}
// CloneRepo handles POST /api/repos — stub for Phase 2
func (h *ReposHandler) CloneRepo(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "clone not implemented yet — coming in Phase 2"})
}
// GetGraph handles GET /api/repos/:id/graph — stub for Phase 2
func (h *ReposHandler) GetGraph(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "graph not implemented yet — coming in Phase 2"})
}
// GetDiff handles GET /api/repos/:id/diff — stub for Phase 2
func (h *ReposHandler) GetDiff(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "diff not implemented yet — coming in Phase 2"})
}
// dirSize returns the total size of a directory in bytes
func dirSize(path string) int64 {
var size int64
filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
size += info.Size()
return nil
})
return size
}
+36
View File
@@ -0,0 +1,36 @@
package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
)
type ReviewHandler struct {
db *sql.DB
}
func NewReviewHandler(db *sql.DB) *ReviewHandler {
return &ReviewHandler{db: db}
}
// Review handles POST /api/repos/:id/review — stub for Phase 4
func (h *ReviewHandler) Review(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "review not implemented yet — coming in Phase 4"})
}
// SaveNotes handles POST /api/repos/:id/review/notes — stub for Phase 5
func (h *ReviewHandler) SaveNotes(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "notes not implemented yet — coming in Phase 5"})
}
// GetNotes handles GET /api/repos/:id/review/notes — stub for Phase 5
func (h *ReviewHandler) GetNotes(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "notes not implemented yet — coming in Phase 5"})
}
// GeneratePDF handles POST /api/repos/:id/review/pdf — stub for Phase 5
func (h *ReviewHandler) GeneratePDF(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "pdf not implemented yet — coming in Phase 5"})
}
+59
View File
@@ -0,0 +1,59 @@
package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
)
type SettingsHandler struct {
db *sql.DB
}
func NewSettingsHandler(db *sql.DB) *SettingsHandler {
return &SettingsHandler{db: db}
}
func (h *SettingsHandler) GetSettings(c *gin.Context) {
settings := make(map[string]string)
rows, err := h.db.Query(`SELECT key, value FROM settings`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
for rows.Next() {
var k, v string
if rows.Scan(&k, &v) == nil {
settings[k] = v
}
}
c.JSON(http.StatusOK, settings)
}
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
var body map[string]string
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
tx, err := h.db.Begin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for key, val := range body {
_, err := tx.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`, key, val)
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}