feat: 完成后端服务核心业务逻辑实现
## 主要功能 - ✅ 用户认证模块 (登录/注册/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:
@ -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. **配置化**: 支持配置化使用
|
||||
|
||||
本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。
|
||||
28
backend/pkg/constants/constants.go
Normal file
28
backend/pkg/constants/constants.go
Normal 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
|
||||
)
|
||||
96
backend/pkg/errorx/errorx.go
Normal file
96
backend/pkg/errorx/errorx.go
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
77
backend/pkg/utils/database/database.go
Normal file
77
backend/pkg/utils/database/database.go
Normal 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
|
||||
}
|
||||
34
backend/pkg/utils/hash/hash.go
Normal file
34
backend/pkg/utils/hash/hash.go
Normal 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))
|
||||
}
|
||||
45
backend/pkg/utils/jwt/jwt.go
Normal file
45
backend/pkg/utils/jwt/jwt.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user