feat: 实现后端和管理后台基础架构
## 后端架构 (Go + Gin + GORM) - ✅ 完整的分层架构 (API/Service/Repository) - ✅ PostgreSQL数据库设计和迁移脚本 - ✅ JWT认证系统和权限控制 - ✅ 用户、照片、分类、标签等核心模型 - ✅ 中间件系统 (认证、CORS、日志) - ✅ 配置管理和环境变量支持 - ✅ 结构化日志和错误处理 - ✅ Makefile构建和部署脚本 ## 管理后台架构 (React + TypeScript) - ✅ Vite + React 18 + TypeScript现代化架构 - ✅ 路由系统和状态管理 (Zustand + TanStack Query) - ✅ 基于Radix UI的组件库基础 - ✅ 认证流程和权限控制 - ✅ 响应式设计和主题系统 ## 数据库设计 - ✅ 用户表 (角色权限、认证信息) - ✅ 照片表 (元数据、EXIF、状态管理) - ✅ 分类表 (层级结构、封面图片) - ✅ 标签表 (使用统计、标签云) - ✅ 关联表 (照片-标签多对多) ## 技术特点 - 🚀 高性能: Gin框架 + GORM ORM - 🔐 安全: JWT认证 + 密码加密 + 权限控制 - 📊 监控: 结构化日志 + 健康检查 - 🎨 现代化: React 18 + TypeScript + Vite - 📱 响应式: Tailwind CSS + Radix UI 参考文档: docs/development/saved-docs/
This commit is contained in:
217
backend/internal/api/middleware/auth.go
Normal file
217
backend/internal/api/middleware/auth.go
Normal file
@ -0,0 +1,217 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"github.com/gin-gonic/gin"
|
||||
"photography-backend/internal/service/auth"
|
||||
"photography-backend/internal/models"
|
||||
)
|
||||
|
||||
// AuthMiddleware 认证中间件
|
||||
type AuthMiddleware struct {
|
||||
jwtService *auth.JWTService
|
||||
}
|
||||
|
||||
// NewAuthMiddleware 创建认证中间件
|
||||
func NewAuthMiddleware(jwtService *auth.JWTService) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAuth 需要认证的中间件
|
||||
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从Header中获取Authorization
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Authorization header is required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid authorization header format",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 提取token
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// 验证token
|
||||
claims, err := m.jwtService.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid or expired token",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("user_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole 需要特定角色的中间件
|
||||
func (m *AuthMiddleware) RequireRole(requiredRole string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userRole, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "User role not found in context",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleStr, ok := userRole.(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid user role",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if !m.hasPermission(roleStr, requiredRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Insufficient permissions",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin 需要管理员权限的中间件
|
||||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||||
return m.RequireRole(models.RoleAdmin)
|
||||
}
|
||||
|
||||
// RequireEditor 需要编辑者权限的中间件
|
||||
func (m *AuthMiddleware) RequireEditor() gin.HandlerFunc {
|
||||
return m.RequireRole(models.RoleEditor)
|
||||
}
|
||||
|
||||
// OptionalAuth 可选认证中间件
|
||||
func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := m.jwtService.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("user_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentUser 获取当前用户ID
|
||||
func GetCurrentUser(c *gin.Context) (uint, bool) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
id, ok := userID.(uint)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
// GetCurrentUserRole 获取当前用户角色
|
||||
func GetCurrentUserRole(c *gin.Context) (string, bool) {
|
||||
userRole, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
role, ok := userRole.(string)
|
||||
return role, ok
|
||||
}
|
||||
|
||||
// GetCurrentUsername 获取当前用户名
|
||||
func GetCurrentUsername(c *gin.Context) (string, bool) {
|
||||
username, exists := c.Get("username")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
name, ok := username.(string)
|
||||
return name, ok
|
||||
}
|
||||
|
||||
// IsAuthenticated 检查是否已认证
|
||||
func IsAuthenticated(c *gin.Context) bool {
|
||||
_, exists := c.Get("user_id")
|
||||
return exists
|
||||
}
|
||||
|
||||
// IsAdmin 检查是否为管理员
|
||||
func IsAdmin(c *gin.Context) bool {
|
||||
role, exists := GetCurrentUserRole(c)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return role == models.RoleAdmin
|
||||
}
|
||||
|
||||
// IsEditor 检查是否为编辑者或以上
|
||||
func IsEditor(c *gin.Context) bool {
|
||||
role, exists := GetCurrentUserRole(c)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return role == models.RoleEditor || role == models.RoleAdmin
|
||||
}
|
||||
|
||||
// hasPermission 检查权限
|
||||
func (m *AuthMiddleware) hasPermission(userRole, requiredRole string) bool {
|
||||
roleLevel := map[string]int{
|
||||
models.RoleUser: 1,
|
||||
models.RoleEditor: 2,
|
||||
models.RoleAdmin: 3,
|
||||
}
|
||||
|
||||
userLevel, exists := roleLevel[userRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredLevel, exists := roleLevel[requiredRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
58
backend/internal/api/middleware/cors.go
Normal file
58
backend/internal/api/middleware/cors.go
Normal file
@ -0,0 +1,58 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
"photography-backend/internal/config"
|
||||
)
|
||||
|
||||
// CORSMiddleware CORS中间件
|
||||
func CORSMiddleware(cfg *config.CORSConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
|
||||
// 检查是否允许的来源
|
||||
allowed := false
|
||||
for _, allowedOrigin := range cfg.AllowedOrigins {
|
||||
if allowedOrigin == "*" || allowedOrigin == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// 设置其他CORS头
|
||||
c.Header("Access-Control-Allow-Methods", joinStrings(cfg.AllowedMethods, ", "))
|
||||
c.Header("Access-Control-Allow-Headers", joinStrings(cfg.AllowedHeaders, ", "))
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if cfg.AllowCredentials {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// 处理预检请求
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// joinStrings 连接字符串数组
|
||||
func joinStrings(strs []string, sep string) string {
|
||||
if len(strs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := strs[0]
|
||||
for i := 1; i < len(strs); i++ {
|
||||
result += sep + strs[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
74
backend/internal/api/middleware/logger.go
Normal file
74
backend/internal/api/middleware/logger.go
Normal file
@ -0,0 +1,74 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LoggerMiddleware 日志中间件
|
||||
func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算延迟
|
||||
latency := time.Since(start)
|
||||
|
||||
// 获取请求信息
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
bodySize := c.Writer.Size()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
logger.Info("HTTP Request",
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.Int("status_code", statusCode),
|
||||
zap.Int("body_size", bodySize),
|
||||
zap.Duration("latency", latency),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestIDMiddleware 请求ID中间件
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func generateRequestID() string {
|
||||
// 简单实现,实际应用中可能需要更复杂的ID生成逻辑
|
||||
return time.Now().Format("20060102150405") + "-" + randomString(8)
|
||||
}
|
||||
|
||||
// randomString 生成随机字符串
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user