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:
xujiang
2025-07-09 16:23:18 +08:00
parent c57ec3aa82
commit 72414d0979
62 changed files with 12416 additions and 262 deletions

View 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
}