feat: 完成后端-管理后台集成及部署配置
🚀 主要功能: - 完善后端API服务层,实现完整的CRUD操作 - 开发管理后台所有核心页面 (仪表板、照片、分类、标签、用户、设置) - 完成前后端完全集成,所有API接口正常对接 - 配置完整的CI/CD流水线,支持自动化部署 🎯 后端完善: - 实现PhotoService, CategoryService, TagService, UserService - 添加完整的API处理器和路由配置 - 支持Docker容器化部署 - 添加数据库迁移和健康检查 🎨 管理后台完成: - 仪表板: 实时统计数据展示 - 照片管理: 完整的CRUD操作,支持批量处理 - 分类管理: 树形结构展示和管理 - 标签管理: 颜色标签和统计信息 - 用户管理: 角色权限控制 - 系统设置: 多标签配置界面 - 添加pre-commit代码质量检查 🔧 部署配置: - Docker Compose完整配置 - 后端CI/CD流水线 (Docker部署) - 管理后台CI/CD流水线 (静态文件部署) - 前端CI/CD流水线优化 - 自动化脚本: 部署、备份、监控 - 完整的部署文档和运维指南 ✅ 集成完成: - 所有API接口正常连接 - 认证系统完整集成 - 数据获取和状态管理 - 错误处理和用户反馈 - 响应式设计优化
This commit is contained in:
243
backend/internal/utils/utils.go
Normal file
243
backend/internal/utils/utils.go
Normal file
@ -0,0 +1,243 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// Contains 检查字符串切片是否包含指定字符串
|
||||
func Contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsUint 检查 uint 切片是否包含指定值
|
||||
func ContainsUint(slice []uint, item uint) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateUniqueFilename 生成唯一文件名
|
||||
func GenerateUniqueFilename(originalFilename string) string {
|
||||
ext := filepath.Ext(originalFilename)
|
||||
name := strings.TrimSuffix(originalFilename, ext)
|
||||
|
||||
// 生成时间戳和哈希
|
||||
timestamp := time.Now().Unix()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%s%d", name, timestamp)))
|
||||
|
||||
return fmt.Sprintf("%d_%x%s", timestamp, hash[:8], ext)
|
||||
}
|
||||
|
||||
// GenerateSlug 生成 URL 友好的 slug
|
||||
func GenerateSlug(text string) string {
|
||||
// 转换为小写
|
||||
slug := strings.ToLower(text)
|
||||
|
||||
// 移除重音符号
|
||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
slug, _, _ = transform.String(t, slug)
|
||||
|
||||
// 只保留字母、数字、连字符和下划线
|
||||
reg := regexp.MustCompile(`[^a-z0-9\-_\s]`)
|
||||
slug = reg.ReplaceAllString(slug, "")
|
||||
|
||||
// 将空格替换为连字符
|
||||
slug = regexp.MustCompile(`\s+`).ReplaceAllString(slug, "-")
|
||||
|
||||
// 移除多余的连字符
|
||||
slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-")
|
||||
|
||||
// 移除开头和结尾的连字符
|
||||
slug = strings.Trim(slug, "-")
|
||||
|
||||
return slug
|
||||
}
|
||||
|
||||
// ValidateEmail 验证邮箱格式
|
||||
func ValidateEmail(email string) bool {
|
||||
emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
|
||||
return emailRegex.MatchString(strings.ToLower(email))
|
||||
}
|
||||
|
||||
// ValidatePassword 验证密码强度
|
||||
func ValidatePassword(password string) bool {
|
||||
if len(password) < 8 {
|
||||
return false
|
||||
}
|
||||
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
hasDigit := regexp.MustCompile(`\d`).MatchString(password)
|
||||
|
||||
return hasLower && hasUpper && hasDigit
|
||||
}
|
||||
|
||||
// Paginate 计算分页参数
|
||||
func Paginate(page, limit int) (offset int) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
offset = (page - 1) * limit
|
||||
return offset
|
||||
}
|
||||
|
||||
// CalculatePages 计算总页数
|
||||
func CalculatePages(total int64, limit int) int {
|
||||
if limit <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int((total + int64(limit) - 1) / int64(limit))
|
||||
}
|
||||
|
||||
// TruncateString 截断字符串
|
||||
func TruncateString(s string, maxLength int) string {
|
||||
if len(s) <= maxLength {
|
||||
return s
|
||||
}
|
||||
return s[:maxLength] + "..."
|
||||
}
|
||||
|
||||
// FormatFileSize 格式化文件大小
|
||||
func FormatFileSize(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
sizes := []string{"KB", "MB", "GB", "TB", "PB"}
|
||||
return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), sizes[exp])
|
||||
}
|
||||
|
||||
// ParseSortOrder 解析排序方向
|
||||
func ParseSortOrder(order string) string {
|
||||
order = strings.ToLower(strings.TrimSpace(order))
|
||||
if order == "asc" || order == "desc" {
|
||||
return order
|
||||
}
|
||||
return "desc" // 默认降序
|
||||
}
|
||||
|
||||
// SanitizeSearchQuery 清理搜索查询
|
||||
func SanitizeSearchQuery(query string) string {
|
||||
// 移除特殊字符,只保留字母、数字、空格和常用标点
|
||||
reg := regexp.MustCompile(`[^\w\s\-\.\_\@]`)
|
||||
query = reg.ReplaceAllString(query, "")
|
||||
|
||||
// 移除多余的空格
|
||||
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
|
||||
|
||||
return strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
// GenerateRandomString 生成随机字符串
|
||||
func GenerateRandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 使用当前时间作为种子
|
||||
timestamp := time.Now().UnixNano()
|
||||
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[(timestamp+int64(i))%int64(len(charset))]
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// IsValidImageExtension 检查是否为有效的图片扩展名
|
||||
func IsValidImageExtension(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
|
||||
return Contains(validExts, ext)
|
||||
}
|
||||
|
||||
// GetImageMimeType 根据文件扩展名获取 MIME 类型
|
||||
func GetImageMimeType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
mimeTypes := map[string]string{
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".tiff": "image/tiff",
|
||||
}
|
||||
|
||||
if mimeType, exists := mimeTypes[ext]; exists {
|
||||
return mimeType
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// RemoveEmptyStrings 移除字符串切片中的空字符串
|
||||
func RemoveEmptyStrings(slice []string) []string {
|
||||
var result []string
|
||||
for _, s := range slice {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
result = append(result, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueStrings 去重字符串切片
|
||||
func UniqueStrings(slice []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
var result []string
|
||||
|
||||
for _, item := range slice {
|
||||
if !keys[item] {
|
||||
keys[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueUints 去重 uint 切片
|
||||
func UniqueUints(slice []uint) []uint {
|
||||
keys := make(map[uint]bool)
|
||||
var result []uint
|
||||
|
||||
for _, item := range slice {
|
||||
if !keys[item] {
|
||||
keys[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user