feat: 完成后端服务核心业务逻辑实现
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 10m41s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped

## 主要功能
-  用户认证模块 (登录/注册/JWT)
-  照片管理模块 (上传/查询/分页/搜索)
-  分类管理模块 (创建/查询/分页)
-  用户管理模块 (用户列表/分页查询)
-  健康检查接口

## 技术实现
- 基于 go-zero v1.8.0 标准架构
- Handler → Logic → Model 三层架构
- SQLite/PostgreSQL 数据库支持
- JWT 认证机制
- bcrypt 密码加密
- 统一响应格式
- 自定义模型方法 (分页/搜索)

## API 接口
- POST /api/v1/auth/login - 用户登录
- POST /api/v1/auth/register - 用户注册
- GET /api/v1/health - 健康检查
- GET /api/v1/photos - 照片列表
- POST /api/v1/photos - 上传照片
- GET /api/v1/categories - 分类列表
- POST /api/v1/categories - 创建分类
- GET /api/v1/users - 用户列表

## 配置完成
- 开发环境配置 (SQLite)
- 生产环境支持 (PostgreSQL)
- JWT 认证配置
- 文件上传配置
- Makefile 构建脚本

服务已验证可正常构建和启动。
This commit is contained in:
xujiang
2025-07-10 16:12:12 +08:00
parent 39a42695d3
commit 317dc170f9
145 changed files with 3669 additions and 20721 deletions

View File

@ -1,793 +0,0 @@
# Shared Package Layer - CLAUDE.md
本文件为 Claude Code 在共享包模块中工作时提供指导。
## 🎯 模块概览
pkg 包提供可复用的工具和组件,供整个应用使用,遵循 Go 语言的包设计哲学。
### 主要职责
- 📦 提供通用工具和实用函数
- 📝 提供日志记录组件
- 🔄 提供统一响应格式
- ✅ 提供数据验证工具
- 🛠️ 提供辅助功能函数
## 📁 模块结构
```
pkg/
├── CLAUDE.md # 📋 当前文件 - 公共工具包指导
├── logger/ # 📝 日志工具
│ ├── logger.go # 日志接口和实现
│ ├── zap_logger.go # Zap 日志实现
│ ├── config.go # 日志配置
│ └── logger_test.go # 日志测试
├── response/ # 🔄 响应格式
│ ├── response.go # 统一响应结构
│ ├── error.go # 错误响应
│ ├── success.go # 成功响应
│ └── pagination.go # 分页响应
├── validator/ # ✅ 数据验证
│ ├── validator.go # 验证器接口
│ ├── gin_validator.go # Gin 验证器实现
│ ├── custom_validators.go # 自定义验证规则
│ └── validator_test.go # 验证器测试
├── utils/ # 🛠️ 通用工具
│ ├── string.go # 字符串工具
│ ├── time.go # 时间工具
│ ├── crypto.go # 加密工具
│ ├── file.go # 文件工具
│ ├── uuid.go # UUID 生成
│ └── utils_test.go # 工具测试
├── middleware/ # 🔗 中间件
│ ├── cors.go # CORS 中间件
│ ├── rate_limit.go # 限流中间件
│ ├── request_id.go # 请求ID中间件
│ └── recovery.go # 错误恢复中间件
├── database/ # 🗄️ 数据库工具
│ ├── connection.go # 数据库连接
│ ├── migrate.go # 数据库迁移
│ └── health.go # 健康检查
└── config/ # ⚙️ 配置工具
├── config.go # 配置管理
├── env.go # 环境变量处理
└── validation.go # 配置验证
```
## 📝 日志组件
### 日志接口设计
```go
// logger/logger.go - 日志接口
package logger
import (
"context"
)
// Logger 日志接口
type Logger interface {
// 基础日志方法
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
Fatal(msg string, fields ...Field)
// 上下文日志方法
DebugContext(ctx context.Context, msg string, fields ...Field)
InfoContext(ctx context.Context, msg string, fields ...Field)
WarnContext(ctx context.Context, msg string, fields ...Field)
ErrorContext(ctx context.Context, msg string, fields ...Field)
// 结构化字段方法
With(fields ...Field) Logger
WithError(err error) Logger
WithContext(ctx context.Context) Logger
// 日志级别控制
SetLevel(level Level)
GetLevel() Level
}
// Field 日志字段
type Field struct {
Key string
Value interface{}
}
// Level 日志级别
type Level int
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// 便捷字段构造函数
func String(key, value string) Field {
return Field{Key: key, Value: value}
}
func Int(key string, value int) Field {
return Field{Key: key, Value: value}
}
func Uint(key string, value uint) Field {
return Field{Key: key, Value: value}
}
func Error(err error) Field {
return Field{Key: "error", Value: err}
}
func Any(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
```
### Zap 日志实现
```go
// logger/zap_logger.go - Zap 日志实现
package logger
import (
"context"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ZapLogger Zap 日志实现
type ZapLogger struct {
logger *zap.Logger
config *Config
}
// Config 日志配置
type Config struct {
Level string `mapstructure:"level" json:"level"`
Format string `mapstructure:"format" json:"format"` // json, console
OutputPath string `mapstructure:"output_path" json:"output_path"`
ErrorPath string `mapstructure:"error_path" json:"error_path"`
MaxSize int `mapstructure:"max_size" json:"max_size"`
MaxAge int `mapstructure:"max_age" json:"max_age"`
MaxBackups int `mapstructure:"max_backups" json:"max_backups"`
Compress bool `mapstructure:"compress" json:"compress"`
}
// NewZapLogger 创建 Zap 日志实例
func NewZapLogger(config *Config) (Logger, error) {
zapConfig := zap.NewProductionConfig()
// 设置日志级别
level, err := zapcore.ParseLevel(config.Level)
if err != nil {
level = zapcore.InfoLevel
}
zapConfig.Level.SetLevel(level)
// 设置输出格式
if config.Format == "console" {
zapConfig.Encoding = "console"
zapConfig.EncoderConfig = zap.NewDevelopmentEncoderConfig()
} else {
zapConfig.Encoding = "json"
zapConfig.EncoderConfig = zap.NewProductionEncoderConfig()
}
// 设置输出路径
if config.OutputPath != "" {
zapConfig.OutputPaths = []string{config.OutputPath}
}
if config.ErrorPath != "" {
zapConfig.ErrorOutputPaths = []string{config.ErrorPath}
}
logger, err := zapConfig.Build()
if err != nil {
return nil, err
}
return &ZapLogger{
logger: logger,
config: config,
}, nil
}
// Debug 调试日志
func (l *ZapLogger) Debug(msg string, fields ...Field) {
l.logger.Debug(msg, l.convertFields(fields...)...)
}
// Info 信息日志
func (l *ZapLogger) Info(msg string, fields ...Field) {
l.logger.Info(msg, l.convertFields(fields...)...)
}
// Warn 警告日志
func (l *ZapLogger) Warn(msg string, fields ...Field) {
l.logger.Warn(msg, l.convertFields(fields...)...)
}
// Error 错误日志
func (l *ZapLogger) Error(msg string, fields ...Field) {
l.logger.Error(msg, l.convertFields(fields...)...)
}
// Fatal 致命错误日志
func (l *ZapLogger) Fatal(msg string, fields ...Field) {
l.logger.Fatal(msg, l.convertFields(fields...)...)
}
// With 添加字段
func (l *ZapLogger) With(fields ...Field) Logger {
return &ZapLogger{
logger: l.logger.With(l.convertFields(fields...)...),
config: l.config,
}
}
// WithError 添加错误字段
func (l *ZapLogger) WithError(err error) Logger {
return l.With(Error(err))
}
// convertFields 转换字段格式
func (l *ZapLogger) convertFields(fields ...Field) []zap.Field {
zapFields := make([]zap.Field, len(fields))
for i, field := range fields {
zapFields[i] = zap.Any(field.Key, field.Value)
}
return zapFields
}
// NewNoop 创建空日志实例(用于测试)
func NewNoop() Logger {
return &ZapLogger{
logger: zap.NewNop(),
config: &Config{},
}
}
```
## 🔄 响应格式
### 统一响应结构
```go
// response/response.go - 统一响应结构
package response
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Response 基础响应结构
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error *Error `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// Error 错误信息结构
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
// PaginatedResponse 分页响应结构
type PaginatedResponse struct {
Response
Pagination *Pagination `json:"pagination,omitempty"`
}
// Pagination 分页信息
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Success 成功响应
func Success(c *gin.Context, data interface{}, message ...string) {
msg := "Success"
if len(message) > 0 {
msg = message[0]
}
response := Response{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
}
c.JSON(http.StatusOK, response)
}
// Error 错误响应
func Error(c *gin.Context, code string, message string, details ...string) {
detail := ""
if len(details) > 0 {
detail = details[0]
}
response := Response{
Success: false,
Error: &Error{
Code: code,
Message: message,
Details: detail,
},
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
}
c.JSON(getHTTPStatusFromCode(code), response)
}
// BadRequest 400 错误
func BadRequest(c *gin.Context, message string, details ...string) {
Error(c, "BAD_REQUEST", message, details...)
}
// Unauthorized 401 错误
func Unauthorized(c *gin.Context, message string, details ...string) {
Error(c, "UNAUTHORIZED", message, details...)
}
// Forbidden 403 错误
func Forbidden(c *gin.Context, message string, details ...string) {
Error(c, "FORBIDDEN", message, details...)
}
// NotFound 404 错误
func NotFound(c *gin.Context, message string, details ...string) {
Error(c, "NOT_FOUND", message, details...)
}
// InternalError 500 错误
func InternalError(c *gin.Context, message string, details ...string) {
Error(c, "INTERNAL_ERROR", message, details...)
}
// Paginated 分页响应
func Paginated(c *gin.Context, data interface{}, page, limit int, total int64, message ...string) {
msg := "Success"
if len(message) > 0 {
msg = message[0]
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
pagination := &Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
}
response := PaginatedResponse{
Response: Response{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
},
Pagination: pagination,
}
c.JSON(http.StatusOK, response)
}
// getRequestID 获取请求ID
func getRequestID(c *gin.Context) string {
if requestID, exists := c.Get("X-Request-ID"); exists {
if id, ok := requestID.(string); ok {
return id
}
}
return ""
}
// getHTTPStatusFromCode 根据错误码获取HTTP状态码
func getHTTPStatusFromCode(code string) int {
switch code {
case "BAD_REQUEST", "INVALID_PARAMETER", "VALIDATION_ERROR":
return http.StatusBadRequest
case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED":
return http.StatusUnauthorized
case "FORBIDDEN", "PERMISSION_DENIED":
return http.StatusForbidden
case "NOT_FOUND", "USER_NOT_FOUND", "PHOTO_NOT_FOUND":
return http.StatusNotFound
case "CONFLICT", "USER_EXISTS", "DUPLICATE_KEY":
return http.StatusConflict
case "TOO_MANY_REQUESTS":
return http.StatusTooManyRequests
default:
return http.StatusInternalServerError
}
}
```
## ✅ 数据验证
### 自定义验证器
```go
// validator/custom_validators.go - 自定义验证规则
package validator
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
// RegisterCustomValidators 注册自定义验证器
func RegisterCustomValidators(v *validator.Validate) {
v.RegisterValidation("username", validateUsername)
v.RegisterValidation("password", validatePassword)
v.RegisterValidation("phone", validatePhone)
v.RegisterValidation("slug", validateSlug)
v.RegisterValidation("file_ext", validateFileExtension)
}
// validateUsername 验证用户名
func validateUsername(fl validator.FieldLevel) bool {
username := fl.Field().String()
// 3-50个字符只允许字母、数字、下划线、短横线
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, username)
return matched
}
// validatePassword 验证密码强度
func validatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 至少8个字符包含字母、数字和特殊字符
if len(password) < 8 {
return false
}
hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]`).MatchString(password)
return hasLetter && hasNumber && hasSpecial
}
// validatePhone 验证手机号
func validatePhone(fl validator.FieldLevel) bool {
phone := fl.Field().String()
// 支持多种手机号格式
patterns := []string{
`^1[3-9]\d{9}$`, // 中国大陆
`^\+1[2-9]\d{9}$`, // 美国
`^\+44[1-9]\d{8,9}$`, // 英国
}
for _, pattern := range patterns {
if matched, _ := regexp.MatchString(pattern, phone); matched {
return true
}
}
return false
}
// validateSlug 验证 URL 友好字符串
func validateSlug(fl validator.FieldLevel) bool {
slug := fl.Field().String()
matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug)
return matched
}
// validateFileExtension 验证文件扩展名
func validateFileExtension(fl validator.FieldLevel) bool {
filename := fl.Field().String()
ext := strings.ToLower(getFileExtension(filename))
// 允许的图片文件扩展名
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
for _, allowedExt := range allowedExts {
if ext == allowedExt {
return true
}
}
return false
}
// getFileExtension 获取文件扩展名
func getFileExtension(filename string) string {
lastDot := strings.LastIndex(filename, ".")
if lastDot == -1 {
return ""
}
return filename[lastDot:]
}
// ValidationError 验证错误结构
type ValidationError struct {
Field string `json:"field"`
Value string `json:"value"`
Tag string `json:"tag"`
Message string `json:"message"`
}
// FormatValidationErrors 格式化验证错误
func FormatValidationErrors(err error) []ValidationError {
var errors []ValidationError
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
errors = append(errors, ValidationError{
Field: e.Field(),
Value: e.Value().(string),
Tag: e.Tag(),
Message: getErrorMessage(e),
})
}
}
return errors
}
// getErrorMessage 获取错误消息
func getErrorMessage(e validator.FieldError) string {
switch e.Tag() {
case "required":
return e.Field() + " is required"
case "email":
return e.Field() + " must be a valid email"
case "min":
return e.Field() + " must be at least " + e.Param() + " characters"
case "max":
return e.Field() + " must be at most " + e.Param() + " characters"
case "username":
return e.Field() + " must be 3-50 characters and contain only letters, numbers, underscores, and hyphens"
case "password":
return e.Field() + " must be at least 8 characters and contain letters, numbers, and special characters"
case "phone":
return e.Field() + " must be a valid phone number"
case "slug":
return e.Field() + " must be a valid URL slug"
case "file_ext":
return e.Field() + " must have a valid image file extension"
default:
return e.Field() + " is invalid"
}
}
```
## 🛠️ 通用工具
### 字符串工具
```go
// utils/string.go - 字符串工具
package utils
import (
"crypto/rand"
"math/big"
"regexp"
"strings"
"unicode"
)
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
result[i] = charset[index.Int64()]
}
return string(result), nil
}
// ToSnakeCase 转换为蛇形命名
func ToSnakeCase(str string) string {
var result []rune
for i, r := range str {
if unicode.IsUpper(r) {
if i > 0 {
result = append(result, '_')
}
result = append(result, unicode.ToLower(r))
} else {
result = append(result, r)
}
}
return string(result)
}
// ToCamelCase 转换为驼峰命名
func ToCamelCase(str string) string {
parts := strings.Split(str, "_")
for i := 1; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return strings.Join(parts, "")
}
// Truncate 截断字符串
func Truncate(str string, length int) string {
if len(str) <= length {
return str
}
return str[:length] + "..."
}
// IsValidEmail 验证邮箱格式
func IsValidEmail(email string) bool {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}
// SanitizeFilename 清理文件名
func SanitizeFilename(filename string) string {
// 移除或替换不安全的字符
reg := regexp.MustCompile(`[<>:"/\\|?*]`)
return reg.ReplaceAllString(filename, "_")
}
// Contains 检查字符串切片是否包含指定字符串
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
```
### 加密工具
```go
// utils/crypto.go - 加密工具
package utils
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), nil
}
// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// MD5Hash 计算MD5哈希
func MD5Hash(data string) string {
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:])
}
// SHA256Hash 计算SHA256哈希
func SHA256Hash(data string) string {
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
```
## 🔗 中间件
### CORS 中间件
```go
// middleware/cors.go - CORS 中间件
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// CORS 创建CORS中间件
func CORS() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowOrigins = []string{"*"}
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"}
config.ExposeHeaders = []string{"X-Request-ID"}
config.AllowCredentials = true
return cors.New(config)
}
```
### 请求ID中间件
```go
// middleware/request_id.go - 请求ID中间件
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RequestID 请求ID中间件
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("X-Request-ID", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
```
## 💡 最佳实践
### 包设计原则
1. **单一职责**: 每个包只负责一个明确的功能
2. **接口导向**: 优先定义接口,便于测试和替换
3. **零依赖**: 包应尽量减少外部依赖
4. **文档完善**: 提供清晰的使用文档和示例
5. **向后兼容**: 保持API的向后兼容性
### 代码质量
1. **测试覆盖**: 为所有公共函数编写测试
2. **错误处理**: 合理处理和传播错误
3. **性能考虑**: 关注内存分配和性能影响
4. **线程安全**: 确保并发使用的安全性
5. **资源管理**: 及时释放资源
### 使用建议
1. **导入路径**: 使用清晰的导入路径
2. **命名规范**: 遵循 Go 语言命名约定
3. **版本管理**: 使用语义化版本管理
4. **依赖管理**: 合理管理第三方依赖
5. **配置化**: 支持配置化使用
本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。

View File

@ -0,0 +1,28 @@
package constants
const (
// 用户状态
UserStatusActive = 1
UserStatusInactive = 0
// 文件上传
MaxFileSize = 10 << 20 // 10MB
// 图片类型
ImageTypeJPEG = "image/jpeg"
ImageTypePNG = "image/png"
ImageTypeGIF = "image/gif"
ImageTypeWEBP = "image/webp"
// 缩略图尺寸
ThumbnailWidth = 300
ThumbnailHeight = 300
// JWT 过期时间
TokenExpireDuration = 24 * 60 * 60 // 24小时
// 分页默认值
DefaultPage = 1
DefaultPageSize = 10
MaxPageSize = 100
)

View File

@ -0,0 +1,96 @@
package errorx
import (
"fmt"
"net/http"
)
const (
// 通用错误代码
Success = 0
ServerError = 500
ParamError = 400
AuthError = 401
NotFound = 404
Forbidden = 403
// 业务错误代码
UserNotFound = 1001
UserExists = 1002
InvalidPassword = 1003
TokenExpired = 1004
TokenInvalid = 1005
PhotoNotFound = 2001
PhotoUploadFail = 2002
CategoryNotFound = 3001
CategoryExists = 3002
)
var codeText = map[int]string{
Success: "Success",
ServerError: "Server Error",
ParamError: "Parameter Error",
AuthError: "Authentication Error",
NotFound: "Not Found",
Forbidden: "Forbidden",
UserNotFound: "User Not Found",
UserExists: "User Already Exists",
InvalidPassword: "Invalid Password",
TokenExpired: "Token Expired",
TokenInvalid: "Token Invalid",
PhotoNotFound: "Photo Not Found",
PhotoUploadFail: "Photo Upload Failed",
CategoryNotFound: "Category Not Found",
CategoryExists: "Category Already Exists",
}
type CodeError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e *CodeError) Error() string {
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
}
func New(code int, msg string) *CodeError {
return &CodeError{
Code: code,
Msg: msg,
}
}
func NewWithCode(code int) *CodeError {
msg, ok := codeText[code]
if !ok {
msg = codeText[ServerError]
}
return &CodeError{
Code: code,
Msg: msg,
}
}
func GetHttpStatus(code int) int {
switch code {
case Success:
return http.StatusOK
case ParamError:
return http.StatusBadRequest
case AuthError, TokenExpired, TokenInvalid:
return http.StatusUnauthorized
case NotFound, UserNotFound, PhotoNotFound, CategoryNotFound:
return http.StatusNotFound
case Forbidden:
return http.StatusForbidden
case UserExists, CategoryExists:
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@ -1,76 +0,0 @@
package logger
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"photography-backend/internal/config"
)
// InitLogger 初始化日志记录器
func InitLogger(cfg *config.LoggerConfig) (*zap.Logger, error) {
// 设置日志级别
var level zapcore.Level
switch cfg.Level {
case "debug":
level = zapcore.DebugLevel
case "info":
level = zapcore.InfoLevel
case "warn":
level = zapcore.WarnLevel
case "error":
level = zapcore.ErrorLevel
default:
level = zapcore.InfoLevel
}
// 创建编码器配置
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 创建编码器
var encoder zapcore.Encoder
if cfg.Format == "json" {
encoder = zapcore.NewJSONEncoder(encoderConfig)
} else {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
}
// 创建写入器
var writers []zapcore.WriteSyncer
// 控制台输出
writers = append(writers, zapcore.AddSync(os.Stdout))
// 文件输出
if cfg.Output == "file" && cfg.Filename != "" {
// 确保日志目录存在
if err := os.MkdirAll("logs", 0755); err != nil {
return nil, err
}
fileWriter := &lumberjack.Logger{
Filename: cfg.Filename,
MaxSize: cfg.MaxSize,
MaxAge: cfg.MaxAge,
MaxBackups: 10,
LocalTime: true,
Compress: cfg.Compress,
}
writers = append(writers, zapcore.AddSync(fileWriter))
}
// 合并写入器
writer := zapcore.NewMultiWriteSyncer(writers...)
// 创建核心
core := zapcore.NewCore(encoder, writer, level)
// 创建日志记录器
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
return logger, nil
}

View File

@ -2,164 +2,41 @@ package response
import (
"net/http"
"time"
"github.com/zeromicro/go-zero/rest/httpx"
"photography-backend/pkg/errorx"
)
// Response 统一响应结构
type Response struct {
Success bool `json:"success"`
type Body struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// Meta 元数据
type Meta struct {
Timestamp string `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// PaginatedResponse 分页响应
type PaginatedResponse struct {
Success bool `json:"success"`
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Pagination *Pagination `json:"pagination"`
Meta *Meta `json:"meta,omitempty"`
}
// Pagination 分页信息
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Success 成功响应
func Success(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Success",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
func Response(w http.ResponseWriter, resp interface{}, err error) {
var body Body
if err != nil {
if e, ok := err.(*errorx.CodeError); ok {
body.Code = e.Code
body.Message = e.Msg
httpx.WriteJson(w, errorx.GetHttpStatus(e.Code), body)
} else {
body.Code = errorx.ServerError
body.Message = err.Error()
httpx.WriteJson(w, http.StatusInternalServerError, body)
}
} else {
body.Code = errorx.Success
body.Message = "success"
body.Data = resp
httpx.OkJson(w, body)
}
}
// Error 错误响应
func Error(code int, message string) *Response {
return &Response{
Success: false,
Code: code,
Message: message,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
func Success(w http.ResponseWriter, data interface{}) {
Response(w, data, nil)
}
// Created 创建成功响应
func Created(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusCreated,
Message: "Created successfully",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Updated 更新成功响应
func Updated(data interface{}) *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Updated successfully",
Data: data,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Deleted 删除成功响应
func Deleted() *Response {
return &Response{
Success: true,
Code: http.StatusOK,
Message: "Deleted successfully",
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// Paginated 分页响应
func Paginated(data interface{}, page, limit int, total int64) *PaginatedResponse {
totalPages := int((total + int64(limit) - 1) / int64(limit))
return &PaginatedResponse{
Success: true,
Code: http.StatusOK,
Message: "Success",
Data: data,
Pagination: &Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
},
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
}
// BadRequest 400错误
func BadRequest(message string) *Response {
return Error(http.StatusBadRequest, message)
}
// Unauthorized 401错误
func Unauthorized(message string) *Response {
return Error(http.StatusUnauthorized, message)
}
// Forbidden 403错误
func Forbidden(message string) *Response {
return Error(http.StatusForbidden, message)
}
// NotFound 404错误
func NotFound(message string) *Response {
return Error(http.StatusNotFound, message)
}
// InternalServerError 500错误
func InternalServerError(message string) *Response {
return Error(http.StatusInternalServerError, message)
}
// ValidationError 验证错误
func ValidationError(errors map[string]string) *Response {
return &Response{
Success: false,
Code: http.StatusUnprocessableEntity,
Message: "Validation failed",
Data: errors,
Meta: &Meta{
Timestamp: time.Now().Format(time.RFC3339),
},
}
func Error(w http.ResponseWriter, err error) {
Response(w, nil, err)
}

View File

@ -0,0 +1,77 @@
package database
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Config struct {
Driver string `json:"driver"` // mysql, postgres, sqlite
Host string `json:"host,optional"`
Port int `json:"port,optional"`
Username string `json:"username,optional"`
Password string `json:"password,optional"`
Database string `json:"database,optional"`
Charset string `json:"charset,optional"`
SSLMode string `json:"ssl_mode,optional"`
FilePath string `json:"file_path,optional"` // for sqlite
}
func NewDB(config Config) (*gorm.DB, error) {
var db *gorm.DB
var err error
// 配置日志
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Silent, // 日志级别
Colorful: false, // 禁用彩色打印
},
)
switch config.Driver {
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
config.Username, config.Password, config.Host, config.Port, config.Database, config.Charset)
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
case "postgres":
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Shanghai",
config.Host, config.Username, config.Password, config.Database, config.Port, config.SSLMode)
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
case "sqlite":
db, err = gorm.Open(sqlite.Open(config.FilePath), &gorm.Config{
Logger: newLogger,
})
default:
return nil, fmt.Errorf("unsupported database driver: %s", config.Driver)
}
if err != nil {
return nil, err
}
// 设置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}

View File

@ -0,0 +1,34 @@
package hash
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 使用 bcrypt 加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPassword 验证密码
func CheckPassword(password, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// MD5 生成 MD5 哈希
func MD5(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
// SHA256 生成 SHA256 哈希
func SHA256(str string) string {
h := sha256.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}

View File

@ -0,0 +1,45 @@
package jwt
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserId int64 `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func GenerateToken(userId int64, username string, secret string, expires time.Duration) (string, error) {
now := time.Now()
claims := Claims{
UserId: userId,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(expires)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
func ParseToken(tokenString string, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrInvalidKey
}