package handlers import ( "errors" "io" "log" "net" "net/http" "strconv" "strings" "syscall" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/puddle/v2" "booklib/internal/auth" "booklib/internal/config" "booklib/internal/ports" ) // H 只依赖 ports 里的小口径接口(Task 25):具体实现由 main.go 装配, // 测试可注入手写 fake(portsfake),无需 PG/Redis。 type H struct { cfg *config.Config users ports.UserStore libs ports.LibraryStore books ports.BookStore progress ports.ProgressStore bookmarks ports.BookmarkStore rl ports.RateLimiter sc ports.Scanner med ports.Media up ports.UploadSessions } func New(cfg *config.Config, users ports.UserStore, libs ports.LibraryStore, books ports.BookStore, progress ports.ProgressStore, bookmarks ports.BookmarkStore, rl ports.RateLimiter, sc ports.Scanner, med ports.Media, up ports.UploadSessions) *H { return &H{cfg: cfg, users: users, libs: libs, books: books, progress: progress, bookmarks: bookmarks, rl: rl, sc: sc, med: med, up: up} } func err(c *gin.Context, status int, code, msg string) { c.AbortWithStatusJSON(status, gin.H{"error": gin.H{"code": code, "message": msg}}) } // idParam 解析 :id 路径参数,失败已回复 400。 func idParam(c *gin.Context) (int64, bool) { id, e := strconv.ParseInt(c.Param("id"), 10, 64) if e != nil { err(c, http.StatusBadRequest, "bad_request", "bad id") return 0, false } return id, true } // dbErr 统一处理 store 层失败:记日志;连接类错误 503(Service Unavailable),其余 500 // 注:brief 里的 pgxpool.ErrClosedPool 在 pgx v5 不存在,实际由 puddle 原样透出,用它替代; // PG 停机时池内连接先收到 SQLSTATE 57P01(administrator shutdown),故把 08xx/57Pxx 也归为 503 func dbErr(c *gin.Context, e error) { log.Printf("db: %v", e) status, code := http.StatusInternalServerError, "internal" var pgErr *pgconn.PgError connClass := errors.As(e, &pgErr) && (strings.HasPrefix(pgErr.Code, "08") || strings.HasPrefix(pgErr.Code, "57P")) if connClass || errors.Is(e, syscall.ECONNREFUSED) || errors.Is(e, io.ErrUnexpectedEOF) || errors.Is(e, net.ErrClosed) || errors.Is(e, puddle.ErrClosedPool) { status, code = http.StatusServiceUnavailable, "unavailable" } err(c, status, code, "db error") } func (h *H) AuthMw() gin.HandlerFunc { return func(c *gin.Context) { hdr := c.GetHeader("Authorization") tok, ok := strings.CutPrefix(hdr, "Bearer ") if !ok { err(c, http.StatusUnauthorized, "unauthorized", "missing bearer token") return } cl, perr := auth.Parse(h.cfg.JWTSecret, tok) if perr != nil { err(c, http.StatusUnauthorized, "unauthorized", "invalid token") return } c.Set("uid", cl.UID) c.Set("role", cl.Role) c.Next() } } func (h *H) AdminOnly() gin.HandlerFunc { return func(c *gin.Context) { if c.GetString("role") != "admin" { err(c, http.StatusForbidden, "forbidden", "admin only") return } c.Next() } } func uid(c *gin.Context) int64 { return c.GetInt64("uid") } func isAdmin(c *gin.Context) bool { return c.GetString("role") == "admin" }