🚀 主要功能: - 完善后端API服务层,实现完整的CRUD操作 - 开发管理后台所有核心页面 (仪表板、照片、分类、标签、用户、设置) - 完成前后端完全集成,所有API接口正常对接 - 配置完整的CI/CD流水线,支持自动化部署 🎯 后端完善: - 实现PhotoService, CategoryService, TagService, UserService - 添加完整的API处理器和路由配置 - 支持Docker容器化部署 - 添加数据库迁移和健康检查 🎨 管理后台完成: - 仪表板: 实时统计数据展示 - 照片管理: 完整的CRUD操作,支持批量处理 - 分类管理: 树形结构展示和管理 - 标签管理: 颜色标签和统计信息 - 用户管理: 角色权限控制 - 系统设置: 多标签配置界面 - 添加pre-commit代码质量检查 🔧 部署配置: - Docker Compose完整配置 - 后端CI/CD流水线 (Docker部署) - 管理后台CI/CD流水线 (静态文件部署) - 前端CI/CD流水线优化 - 自动化脚本: 部署、备份、监控 - 完整的部署文档和运维指南 ✅ 集成完成: - 所有API接口正常连接 - 认证系统完整集成 - 数据获取和状态管理 - 错误处理和用户反馈 - 响应式设计优化
243 lines
5.6 KiB
Go
243 lines
5.6 KiB
Go
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
|
|
} |