初春图床v3.2

This commit is contained in:
onexru committed 2025-12-24 16:51:01 +08:00
1 parent 74171ddc75
commit f764100eb9
32 files changed
+3910 -1716

No files matched your search

+4
View File
@@ -17,6 +17,10 @@ type System struct {
}
func Init() *System {
// 生成默认.env文件
if !config.EnvExists() {
config.CreateDefaultEnv()
}
// 创建配置实例
config.NewConfig()
cfg := config.App
+81 -110
View File
@@ -3,7 +3,6 @@ package config
import (
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
@@ -13,7 +12,6 @@ import (
"github.com/joho/godotenv"
)
// Config 应用配置结构体
type Config struct {
// 服务器配置
Port string
@@ -21,13 +19,15 @@ type Config struct {
// Sqlite3数据库
SqlitePath string
// Mysql数据库
IsMysql bool
DbHost string
DbPort int
DbUser string
DbPassword string
DbName string
// 数据库配置
DbType string
DbHost string
DbPort int
DbUser string
DbPassword string
DbName string
DbCaCertPath string
DbSkipCertVerify bool
// 上传文件配置
MaxFileSize int64
@@ -47,195 +47,166 @@ type Config struct {
// 全局配置实例
var App *Config
// EnvExists 检查.env文件是否存在(若为目录则清理并返回false)
// 检查.env文件/目录状态
func EnvExists() bool {
info, err := os.Stat(".env")
// 场景1:文件不存在
if os.IsNotExist(err) {
return false
}
// 场景2:存在但不是文件(是目录),清理目录后返回false
// 如果存在但不是文件,先删除目录
if err == nil && info.IsDir() {
log.Println("发现.env是目录,正在自动清理...")
if rmErr := os.RemoveAll(".env"); rmErr != nil {
log.Fatalf("清理.env目录失败:%v", rmErr)
log.Printf("发现.env是目录,正在删除...")
if err := os.RemoveAll(".env"); err != nil {
log.Fatalf("删除.env目录失败:%v", err)
}
return false
}
// 场景3:正常文件
return true
}
// CreateDefaultEnv 创建默认.env文件(仅当文件不存在时执行)
func CreateDefaultEnv() error {
// 双重检查:避免并发场景下重复创建
if EnvExists() {
return nil
}
// 创建data目录(SQLite存储目录)
// 创建默认.env文件
func CreateDefaultEnv() {
// 创建data目录(避免SQLite路径报错)
if err := os.MkdirAll("./data", 0755); err != nil {
return fmt.Errorf("创建data目录失败:%w", err)
log.Fatalf("创建data目录失败:%v", err)
}
// 生成32位随机Session密钥(base64编码)
// 生成随机的SESSION_SECRET(32位base64编码)
sessionSecret := generateRandomSecret(32)
// 定义.env模板
// 直接定义.env模板内容
envTemplate := `# 服务器配置
SERVER_PORT=8080
# 数据库配置 - 二选一(IS_MYSQL=true则使用MySQL,否则使用SQLite)
# 数据库配置
DB_TYPE=sqlite
# 类型可选:sqlite | mysql | postgres
SQLITE_PATH=./data/data.db
IS_MYSQL=false
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_NAME=oneimgxru
# CA证书路径,如果不需要TLS加密连接则将其注释
DB_CA_CERT_PATH=./ca/isrgrootx1.pem
DB_SKIP_CERT_VERIFY=false
# 文件上传配置
# 最大文件大小(字节):10485760 = 10MB
MAX_FILE_SIZE=10485760
# 允许上传的文件类型(逗号分隔)
ALLOWED_TYPES=image/jpeg,image/png,image/gif,image/webp
# 默认管理员账户(首次启动初始化)
# 默认用户配置
DEFAULT_USER=admin
DEFAULT_PASS=123456
# Session配置(自动生成随机值,请勿手动修改)
# Session配置
SESSION_SECRET=
`
// 替换Session密钥占位符
// 替换模板中的SESSION_SECRET占位符
envContent := strings.Replace(envTemplate, "SESSION_SECRET=", "SESSION_SECRET="+sessionSecret, 1)
// 获取当前工作目录,拼接.env路径
// 写入.env文件
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("获取当前工作目录失败:%w", err)
log.Fatalf("获取当前工作目录失败:%v", err)
}
envPath := filepath.Join(wd, ".env")
// 写入.env文件
if err := os.WriteFile(envPath, []byte(envContent), 0644); err != nil {
return fmt.Errorf("写入.env文件失败:%w", err)
// 确保目标路径不是目录
if info, err := os.Stat(envPath); err == nil && info.IsDir() {
log.Fatalf("无法写入.env文件:%s 是一个目录", envPath)
}
log.Printf("✅ 首次启动:自动生成默认.env文件(路径:%s)", envPath)
return nil
if err := os.WriteFile(envPath, []byte(envContent), 0644); err != nil {
log.Fatalf("生成默认.env文件失败:%v", err)
}
log.Printf("✅ 首次启动:自动生成.env文件(路径:%s)", envPath)
}
// generateRandomSecret 生成指定长度的随机密钥(base64 URL安全编码)
// 生成指定长度的随机密钥(base64编码)
func generateRandomSecret(length int) string {
bytes := make([]byte, length)
// 读取加密安全的随机数
n, err := rand.Read(bytes)
_, err := rand.Read(bytes)
if err != nil {
log.Fatalf("生成随机密钥失败:%v", err)
}
if n != length {
log.Fatalf("生成随机密钥长度不足:期望%d字节,实际%d字节", length, n)
}
return base64.URLEncoding.EncodeToString(bytes)
}
// NewConfig 初始化全局配置
// 初始化配置(优先加载外部.env,无则生成默认)
func NewConfig() {
// 检查.env文件,不存在则创建
// 1. 检查.env文件,不存在则生成
if !EnvExists() {
if err := CreateDefaultEnv(); err != nil {
log.Fatalf("创建默认.env文件失败:%v", err)
}
CreateDefaultEnv()
}
// 加载.env文件到环境变量
if err := godotenv.Load(); err != nil {
// 2. 加载.env文件(此时必存在)
err := godotenv.Load()
if err != nil {
log.Fatalf("加载.env文件失败:%v", err)
}
// 服务器端口:优先读SERVER_PORT,其次PORT,默认8080
// 3. 解析配置项
maxFileSize, _ := strconv.ParseInt(getEnv("MAX_FILE_SIZE", "10485760"), 10, 64)
allowedTypes := strings.Split(getEnv("ALLOWED_TYPES", "image/jpeg,image/png,image/gif,image/webp"), ",")
port := getEnv("SERVER_PORT", getEnv("PORT", "8080"))
// SQLite配置
// Sqlite3配置
sqlitePath := getEnv("SQLITE_PATH", "./data/data.db")
// 确保SQLite目录存在(防止路径不存在导致数据库初始化失败)
// 确保SQLite目录存在
if err := os.MkdirAll(filepath.Dir(sqlitePath), 0755); err != nil {
log.Printf("⚠️ 警告:创建SQLite目录失败(%s):%v", filepath.Dir(sqlitePath), err)
log.Printf("警告:创建SQLite目录失败:%v", err)
}
// MySQL配置
isMysql := getEnv("IS_MYSQL", "false") == "true"
// Mysql配置
dbType := getEnv("DB_TYPE", "sqlite3")
dbHost := getEnv("DB_HOST", "localhost")
dbPort, _ := strconv.Atoi(getEnv("DB_PORT", "3306"))
dbUser := getEnv("DB_USER", "root")
dbPassword := getEnv("DB_PASSWORD", "")
dbName := getEnv("DB_NAME", "oneimgxru")
// 文件上传配置
maxFileSize, err := strconv.ParseInt(getEnv("MAX_FILE_SIZE", "10485760"), 10, 64)
if err != nil || maxFileSize <= 0 {
log.Printf("MAX_FILE_SIZE配置无效(%s),使用默认值10MB", getEnv("MAX_FILE_SIZE", "10485760"))
maxFileSize = 10485760
}
allowedTypes := strings.Split(getEnv("ALLOWED_TYPES", "image/jpeg,image/png,image/gif,image/webp"), ",")
// 清理空的文件类型(防止配置错误)
allowedTypes = cleanEmptyStrings(allowedTypes)
dbCaCertPath := getEnv("DB_CA_CERT_PATH", "")
dbSkipCertVerify := getEnv("DB_SKIP_CERT_VERIFY", "false") == "true"
// 默认用户配置
defaultUser := getEnv("DEFAULT_USER", "admin")
defaultPass := getEnv("DEFAULT_PASS", "123456")
// JWT配置:优先读环境变量,无则生成随机密钥(每次启动无配置时都会生成新的,生产环境建议手动配置)
// JWT配置(默认生成随机密钥,避免硬编码)
jwtSecret := getEnv("JWT_SECRET", generateRandomSecret(32))
// Session配置:优先读环境变量,无则生成随机密钥
// Session配置(读取.env中的值,无则生成)
sessionSecret := getEnv("SESSION_SECRET", generateRandomSecret(32))
// 初始化全局配置实例
// 初始化全局配置
App = &Config{
Port: port,
SqlitePath: sqlitePath,
IsMysql: isMysql,
DbHost: dbHost,
DbPort: dbPort,
DbUser: dbUser,
DbPassword: dbPassword,
DbName: dbName,
MaxFileSize: maxFileSize,
AllowedTypes: allowedTypes,
DefaultUser: defaultUser,
DefaultPass: defaultPass,
JWTSecret: jwtSecret,
SessionSecret: sessionSecret,
Port: port,
SqlitePath: sqlitePath,
DbType: dbType,
DbHost: dbHost,
DbPort: dbPort,
DbUser: dbUser,
DbPassword: dbPassword,
DbName: dbName,
MaxFileSize: maxFileSize,
AllowedTypes: allowedTypes,
DefaultUser: defaultUser,
DefaultPass: defaultPass,
JWTSecret: jwtSecret,
SessionSecret: sessionSecret,
DbCaCertPath: dbCaCertPath,
DbSkipCertVerify: dbSkipCertVerify,
}
// 日志提示当前使用的数据库类型
if App.IsMysql {
log.Printf("数据库配置:使用MySQL(%s:%d/%s)", App.DbHost, App.DbPort, App.DbName)
} else {
log.Printf("数据库配置:使用SQLite(路径:%s)", App.SqlitePath)
}
log.Println("✅ 应用配置初始化完成")
log.Println("✅ 配置初始化完成")
}
// getEnv 获取环境变量,若不存在则返回默认值
// 获取环境变量
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if strings.TrimSpace(value) == "" {
return defaultValue
if value := os.Getenv(key); value != "" {
return value
}
return value
}
func cleanEmptyStrings(slice []string) []string {
var result []string
for _, s := range slice {
trimmed := strings.TrimSpace(s)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
return defaultValue
}
+3
View File
@@ -94,6 +94,9 @@ func DeleteImage(c *gin.Context) {
return
}
// 删除关联tag
db.Where("image_id = ?", image.Id).Delete(models.ImageToTags{})
if !deleteStatus {
c.JSON(http.StatusOK, result.Success(
"记录删除成功,物理删除失败",
+148
View File
@@ -0,0 +1,148 @@
package controllers
import (
"math/rand"
"net/http"
"oneimg/backend/database"
"oneimg/backend/models"
"oneimg/backend/utils/result"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// 初始化随机数种子
var randomGenerator = rand.New(rand.NewSource(time.Now().UnixNano()))
// 定义返回的图片结构体
type RandomImageResponse struct {
Image string `json:"image"` // 图片文件名
Url string `json:"url"` // 图片完整访问地址
}
func GetRandomImages(c *gin.Context) {
// 解析并校验参数
tag := c.Query("tag")
model := c.DefaultQuery("model", "json")
limitStr := c.DefaultQuery("limit", "1")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 1 || limit > 20 {
c.JSON(http.StatusBadRequest, result.Error(400, "limit参数错误,必须是1-20之间的整数"))
return
}
// 初始化数据库连接
db := database.GetDB().DB
var images []models.Image
var total int64
var respData []RandomImageResponse
if tag == "" {
// 统计总图片数
if err := db.Model(&models.Image{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取图片总数失败"))
return
}
// 无图片时返回空数据
if total == 0 {
c.JSON(http.StatusOK, result.Success("ok", []RandomImageResponse{}))
return
}
// 计算随机偏移量
var offset int
if total > int64(limit) {
offset = randomGenerator.Intn(int(total) - limit + 1)
} else {
offset = 0
}
// 查询随机图片
if err := db.Model(&models.Image{}).
Offset(offset).
Limit(limit).
Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取图片失败"))
return
}
} else {
// 统计该标签下的图片数
if err := db.Model(&models.Image{}).
Joins("JOIN image_to_tags ON images.id = image_to_tags.image_id").
Joins("JOIN tags ON image_to_tags.tag_id = tags.id").
Where("tags.name = ?", tag).
Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取标签图片总数失败"))
return
}
// 无该标签图片时返回空数据
if total == 0 {
c.JSON(http.StatusOK, result.Success("ok", []RandomImageResponse{}))
return
}
// 计算随机偏移量
var offset int
if total > int64(limit) {
offset = randomGenerator.Intn(int(total) - limit + 1)
} else {
offset = 0
}
// 查询该标签下的随机图片
if err := db.Model(&models.Image{}).
Joins("JOIN image_to_tags ON images.id = image_to_tags.image_id").
Joins("JOIN tags ON image_to_tags.tag_id = tags.id").
Where("tags.name = ?", tag).
Offset(offset).
Limit(limit).
Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取标签图片失败"))
return
}
}
if model == "image" {
imageUrl := strings.ReplaceAll(images[0].Url, "/uploads/", "/")
c.AddParam("path", imageUrl)
ImageProxy(c)
return
}
for _, img := range images {
fullUrl := getFullImageUrl(c, img.Url)
respData = append(respData, RandomImageResponse{
Image: img.FileName,
Url: fullUrl,
})
}
c.JSON(http.StatusOK, result.Success("ok", respData))
}
func getFullImageUrl(c *gin.Context, path string) string {
var scheme string
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else if c.Request.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
var host string
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
} else {
host = c.Request.Host
}
baseUrl := scheme + "://" + host
baseUrl = strings.TrimSuffix(baseUrl, "/")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
fullUrl := baseUrl + path
return fullUrl
}
+187 -80
View File
@@ -3,6 +3,8 @@ package controllers
import (
"net/http"
"strconv"
"strings"
"time"
"oneimg/backend/database"
"oneimg/backend/models"
@@ -11,110 +13,215 @@ import (
"github.com/gin-gonic/gin"
)
// GetImageList 获取图片列表
func GetImageList(c *gin.Context) {
// 获取分页参数
pageStr := c.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "20")
type ImageWithTags struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
Url string `json:"url" gorm:"column:url"`
Thumbnail string `json:"thumbnail" gorm:"column:thumbnail"`
Filename string `json:"filename" gorm:"column:file_name"`
FileSize int64 `json:"file_size" gorm:"column:file_size"`
MimeType string `json:"mimeType" gorm:"column:mime_type"`
Width int `json:"width" gorm:"column:width"`
Height int `json:"height" gorm:"column:height"`
Storage string `json:"storage" gorm:"column:storage"`
UserId int `json:"user_id" gorm:"column:user_id"`
Md5 string `json:"md5" gorm:"column:md5"`
Uuid string `json:"uuid" gorm:"column:uuid"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
Tags []models.Tags `json:"tags" gorm:"-"`
}
page, err := strconv.Atoi(pageStr)
// 映射到数据库表
func (ImageWithTags) TableName() string {
return "images"
}
// GetImageList 获取图片列表(解决字段歧义+消除SA4010+完整功能)
func GetImageList(c *gin.Context) {
// 基础参数解析
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(limitStr)
limit, err := strconv.Atoi(c.DefaultQuery("limit", "20"))
if err != nil || limit < 1 || limit > 100 {
limit = 20
}
// 获取排序参数
sortBy := c.DefaultQuery("sort_by", "created_at")
sortOrder := c.DefaultQuery("sort_order", "desc")
// 获取搜索参数
search := c.Query("search")
// 计算偏移量
offset := (page - 1) * limit
db := database.GetDB().DB
var images []models.Image
var total int64
// 构建查询
query := db.Model(&models.Image{})
// 获取角色参数
role := c.Query("role")
if role != "" {
switch role {
case "admin":
query = query.Where("user_id == 1")
case "guest":
query = query.Where("user_id != 1")
}
// 排序参数
sortBy := c.DefaultQuery("sort_by", "created_at")
sortOrder := c.DefaultQuery("sort_order", "desc")
fieldMapping := map[string]string{
"created_at": "created_at",
"file_size": "file_size",
"filename": "file_name",
}
if c.GetInt("user_role") != 1 || role == "" {
query = query.Where("uuid = ?", GetUUID(c))
dbSortField := fieldMapping[sortBy]
if dbSortField == "" {
dbSortField = "created_at"
}
// 添加搜索条件
if search != "" {
query = query.Where("file_name LIKE ?", "%"+search+"%")
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "获取图片总数失败",
})
return
}
// 验证排序字段
validSortFields := map[string]bool{
"created_at": true,
"file_size": true,
"filename": true,
}
if !validSortFields[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
orderClause := dbSortField + " " + sortOrder
// 映射前端字段名到数据库字段名
fieldMapping := map[string]string{
"filename": "file_name",
"created_at": "created_at",
"file_size": "file_size",
// 搜索/角色参数
search := c.Query("search")
role := c.Query("role")
// 标签参数解析
var hasZeroTag bool
var filterTagIds []int
tagIdStr := c.Query("tags")
if tagIdStr != "" {
tagIdList := strings.Split(tagIdStr, ",")
for _, s := range tagIdList {
trimmed := strings.TrimSpace(s)
tid, err := strconv.Atoi(trimmed)
if err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "标签ID格式错误:"+trimmed))
return
}
if tid == 0 {
hasZeroTag = true
} else if tid > 0 {
filterTagIds = append(filterTagIds, tid)
}
}
}
dbField := fieldMapping[sortBy]
if dbField == "" {
dbField = "created_at"
// 筛选图片ID
db := database.GetDB().DB
idQuery := db.Model(&models.Image{}).Select("images.id")
// 基础筛选:角色+权限+搜索
if role != "" {
switch role {
case "admin":
idQuery = idQuery.Where("images.user_id = ?", 1)
case "guest":
idQuery = idQuery.Where("images.user_id != ?", 1)
}
}
if c.GetInt("user_role") != 1 || role == "" {
idQuery = idQuery.Where("images.uuid = ?", GetUUID(c))
}
if search != "" {
idQuery = idQuery.Where("images.file_name LIKE ?", "%"+search+"%")
}
// 获取图片列表
orderClause := dbField + " " + sortOrder
if err := query.Order(orderClause).Offset(offset).Limit(limit).Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "获取图片列表失败",
})
if hasZeroTag || len(filterTagIds) > 0 {
idQuery = idQuery.Joins("LEFT JOIN image_to_tags ON images.id = image_to_tags.image_id")
// 合并筛选条件
if hasZeroTag && len(filterTagIds) > 0 {
// 无标签 OR 关联指定标签
idQuery = idQuery.Where("image_to_tags.tag_id IS NULL OR image_to_tags.tag_id IN (?)", filterTagIds)
} else if hasZeroTag {
// 仅无标签
idQuery = idQuery.Where("image_to_tags.tag_id IS NULL")
} else {
// 仅指定标签(用IS NOT NULL确保关联有效)
idQuery = idQuery.Where("image_to_tags.tag_id IN (?) AND image_to_tags.tag_id IS NOT NULL", filterTagIds)
}
// 去重:避免重复的图片ID
idQuery = idQuery.Distinct("images.id")
}
// 获取图片ID列表
var imageIds []int
imageIds = make([]int, 0)
if err := idQuery.Find(&imageIds).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "筛选图片失败:"+err.Error()))
return
}
// 计算总页数
// 统计总数和分页
total := int64(len(imageIds))
totalPages := (total + int64(limit) - 1) / int64(limit)
if offset > len(imageIds) {
imageIds = []int{}
} else if offset+limit > len(imageIds) {
imageIds = imageIds[offset:]
} else {
imageIds = imageIds[offset : offset+limit]
}
c.JSON(http.StatusOK, result.Success("获取图片列表成功", gin.H{
// 查询图片详情
var images []ImageWithTags
if len(imageIds) > 0 {
imageFields := "id, url, thumbnail, file_name, file_size, mime_type, width, height, storage, user_id, md5, uuid, created_at"
if err := db.Model(&ImageWithTags{}).
Select(imageFields).
Where("id IN (?)", imageIds).
Order(orderClause).
Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询图片详情失败:"+err.Error()))
return
}
}
// 批量查询标签
defaultTags := []models.Tags{{Id: 0, Name: "默认"}}
imgIds := make([]int, 0, len(images))
for _, img := range images {
imgIds = append(imgIds, img.Id)
}
// 查询标签关联
var imageToTags []models.ImageToTags
if len(imgIds) > 0 {
if err := db.Where("image_id IN (?)", imgIds).Find(&imageToTags).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询标签关联失败:"+err.Error()))
return
}
}
// 构建图片ID->标签ID映射
imgTagMap := make(map[int][]int)
for _, it := range imageToTags {
tags := imgTagMap[it.ImageId]
tags = append(tags, it.TagId)
imgTagMap[it.ImageId] = tags
}
// 收集所有标签ID
var allTagIds []int
allTagIds = make([]int, 0)
for _, tids := range imgTagMap {
allTagIds = append(allTagIds, tids...)
}
// 查询标签信息
tagMap := make(map[int]models.Tags)
if len(allTagIds) > 0 {
var tags []models.Tags
if err := db.Where("id IN (?)", allTagIds).Find(&tags).Error; err == nil {
for _, t := range tags {
tagMap[t.Id] = t
}
}
}
// 组装标签
for i := range images {
tids := imgTagMap[images[i].Id]
if len(tids) == 0 {
images[i].Tags = defaultTags
} else {
tagList := make([]models.Tags, 0, len(tids))
for _, tid := range tids {
if t, ok := tagMap[tid]; ok {
tagList = append(tagList, t)
}
}
images[i].Tags = tagList
}
}
// 返回结果
c.JSON(http.StatusOK, result.Success("ok", gin.H{
"images": images,
"total": total,
"page": page,
-124
View File
@@ -1,124 +0,0 @@
package controllers
import (
"log"
"oneimg/backend/config"
"oneimg/backend/database"
"oneimg/backend/interfaces"
"oneimg/backend/models"
"oneimg/backend/utils/md5"
"oneimg/backend/utils/settings"
"oneimg/backend/utils/telegram"
"oneimg/backend/utils/uploads"
"time"
"github.com/gin-gonic/gin"
)
// UploadImages 图片上传主入口
func UploadImages(c *gin.Context) {
// 初始化上传上下文
uc := uploads.NewUploadContext(c)
// 获取系统配置
setting, err := settings.GetSettings()
if err != nil {
uc.Fail(500, "获取上传配置失败:%v", err)
return
}
// 解析并校验上传文件
files, err := uc.ParseAndValidateFiles()
if err != nil {
uc.Fail(400, "文件解析失败")
return
}
// 获取存储上传器
uploader, err := uc.GetStorageUploader(&setting)
if err != nil {
uc.Fail(400, "%s", err.Error())
return
}
// 获取全局配置
cfg, ok := c.MustGet("config").(*config.Config)
if !ok {
uc.Fail(500, "全局配置获取失败")
return
}
// 批量处理文件上传(参数匹配接口定义)
uploadResults := make([]interfaces.ImageUploadResult, 0, len(files))
successCount := 0
for _, file := range files {
fileResult, err := uploader.Upload(c, cfg, &setting, file)
if err != nil {
// 单个文件上传失败不影响其他文件
uc.Fail(500, "文件[%s]上传失败:%v", file.Filename, err)
return
}
// 保存图片信息到数据库
imageModel := models.Image{
Url: fileResult.URL,
Thumbnail: fileResult.ThumbnailURL,
FileName: fileResult.FileName,
FileSize: fileResult.FileSize,
MimeType: fileResult.MimeType,
Width: fileResult.Width,
Height: fileResult.Height,
Storage: fileResult.Storage,
UserId: c.GetInt("user_id"),
MD5: md5.Md5(c.GetString("username") + fileResult.FileName),
UUID: GetUUID(c),
}
db := database.GetDB()
if db != nil {
db.DB.Create(&imageModel)
}
uploadResults = append(uploadResults, *fileResult)
if setting.TGNotice {
placeholderData := telegram.PlaceholderData{
Username: c.GetString("username"),
Date: time.Now().Format("2006-01-02 15:04:05"),
Filename: fileResult.FileName,
StorageType: setting.StorageType,
URL: c.Request.Host + fileResult.URL,
}
err := telegram.SendSimpleMsg(
setting.TGBotToken, // 机器人Token
setting.TGReceivers, // 接收者ChatID
setting.TGNoticeText, // 模板文本
placeholderData, // 占位符数据
)
if err != nil {
log.Println(err)
// 忽略错误
}
}
successCount++
}
if successCount == 0 {
uc.Fail(500, "所有文件上传失败")
return
}
// 返回上传结果
uc.Success("上传成功", map[string]any{
"files": uploadResults,
"count": successCount,
})
}
// UploadImage 单文件上传
func UploadImage(c *gin.Context) {
UploadImages(c)
}
+409
View File
@@ -0,0 +1,409 @@
package controllers
import (
"encoding/json"
"log"
"net/http"
"oneimg/backend/config"
"oneimg/backend/database"
"oneimg/backend/interfaces"
"oneimg/backend/models"
"oneimg/backend/utils/md5"
"oneimg/backend/utils/result"
"oneimg/backend/utils/settings"
"oneimg/backend/utils/telegram"
"oneimg/backend/utils/uploads"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// UploadImages 图片上传主入口
func UploadImages(c *gin.Context) {
// 初始化上传上下文
uc := uploads.NewUploadContext(c)
// 获取数据库连接
db := database.GetDB()
var tags []string
var existingTags []models.Tags
tagsStr := c.PostForm("tags")
if tagsStr != "" {
err := json.Unmarshal([]byte(tagsStr), &tags)
if err != nil {
uc.Fail(400, "Tags参数格式错误:%v", err)
return
}
err = db.DB.Where("name IN ?", tags).Find(&existingTags).Error
if err != nil {
uc.Fail(500, "Tag查询失败:%v", err)
return
}
}
// 获取系统配置
setting, err := settings.GetSettings()
if err != nil {
uc.Fail(500, "获取上传配置失败:%v", err)
return
}
// 解析并校验上传文件
files, err := uc.ParseAndValidateFiles()
if err != nil {
uc.Fail(400, "文件解析失败")
return
}
// 获取存储上传器
uploader, err := uc.GetStorageUploader(&setting)
if err != nil {
uc.Fail(400, "%s", err.Error())
return
}
// 获取全局配置
cfg, ok := c.MustGet("config").(*config.Config)
if !ok {
uc.Fail(500, "全局配置获取失败")
return
}
// 批量处理文件上传(参数匹配接口定义)
uploadResults := make([]interfaces.ImageUploadResult, 0, len(files))
successCount := 0
for _, file := range files {
fileResult, err := uploader.Upload(c, cfg, &setting, file)
if err != nil {
// 单个文件上传失败不影响其他文件
uc.Fail(500, "文件[%s]上传失败:%v", file.Filename, err)
return
}
// 保存图片信息到数据库
imageModel := models.Image{
Url: fileResult.URL,
Thumbnail: fileResult.ThumbnailURL,
FileName: fileResult.FileName,
FileSize: fileResult.FileSize,
MimeType: fileResult.MimeType,
Width: fileResult.Width,
Height: fileResult.Height,
Storage: fileResult.Storage,
UserId: c.GetInt("user_id"),
MD5: md5.Md5(c.GetString("username") + fileResult.FileName),
UUID: GetUUID(c),
}
if db != nil {
db.DB.Create(&imageModel)
}
// 上传时关联图片标签
if len(existingTags) > 0 {
var imageTagRelations []models.ImageToTags
for _, tag := range existingTags {
imageTagRelations = append(imageTagRelations, models.ImageToTags{
ImageId: imageModel.Id,
TagId: tag.Id,
})
}
db.DB.Create(&imageTagRelations)
}
uploadResults = append(uploadResults, *fileResult)
if setting.TGNotice {
placeholderData := telegram.PlaceholderData{
Username: c.GetString("username"),
Date: time.Now().Format("2006-01-02 15:04:05"),
Filename: fileResult.FileName,
StorageType: setting.StorageType,
URL: c.Request.Host + fileResult.URL,
}
err := telegram.SendSimpleMsg(
setting.TGBotToken, // 机器人Token
setting.TGReceivers, // 接收者ChatID
setting.TGNoticeText, // 模板文本
placeholderData, // 占位符数据
)
if err != nil {
log.Println(err)
// 忽略错误
}
}
successCount++
}
if successCount == 0 {
uc.Fail(500, "所有文件上传失败")
return
}
// 返回上传结果
uc.Success("上传成功", map[string]any{
"files": uploadResults,
"count": successCount,
})
}
// UploadImage 单文件上传
func UploadImage(c *gin.Context) {
UploadImages(c)
}
func AddImageTag(c *gin.Context) {
// 获取请求参数
type TagRequest struct {
Id int `json:"id"` // 图片ID
Tag string `json:"tag"` // 标签ID(前端传字符串,后端转换)
}
var req TagRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "参数解析失败:"+err.Error()))
return
}
// 参数非空校验
if req.Id <= 0 || req.Tag == "" {
c.JSON(http.StatusBadRequest, result.Error(400, "参数错误"))
return
}
// 转换并校验图片ID
tagId, err := strconv.Atoi(req.Tag)
if err != nil || tagId <= 0 {
c.JSON(http.StatusBadRequest, result.Error(400, "标签ID无效"))
return
}
imageId := req.Id
// 获取数据库连接
db := database.GetDB().DB
// 查询图片是否存在
var image models.Image
if err := db.Where("id = ?", imageId).First(&image).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, result.Error(400, "图片不存在"))
} else {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询图片失败:"+err.Error()))
}
return
}
// 查询标签是否存在
var tag models.Tags
if err := db.Where("id = ?", tagId).First(&tag).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, result.Error(400, "标签不存在"))
} else {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询标签失败:"+err.Error()))
}
return
}
// 查询图片是否已经添加过该标签
var imageTag models.ImageToTags
if err := db.Where("image_id = ? AND tag_id = ?", imageId, tagId).First(&imageTag).Error; err == nil {
c.JSON(http.StatusBadRequest, result.Error(400, "图片已添加过该标签"))
return
} else if err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, result.Error(500, "检查标签关联失败:"+err.Error()))
return
}
// 添加图片标签关联
if err := db.Create(&models.ImageToTags{
ImageId: imageId,
TagId: tagId,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "添加标签失败:"+err.Error()))
return
}
c.JSON(http.StatusOK, result.Success("标签添加成功", nil))
}
func DeleteImageTag(c *gin.Context) {
// 获取请求参数
type TagRequest struct {
Id int `json:"id"` // 图片ID
Tag int `json:"tag"` // 标签ID
}
var req TagRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "参数解析失败:"+err.Error()))
return
}
// 参数非空校验
if req.Id <= 0 || req.Tag <= 0 {
c.JSON(http.StatusBadRequest, result.Error(400, "参数错误"))
return
}
// 转换并校验图片ID
tagId := req.Tag
imageId := req.Id
// 获取数据库连接
db := database.GetDB().DB
// 查询图片是否存在
var image models.Image
if err := db.Where("id = ?", imageId).First(&image).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, result.Error(400, "图片不存在"))
} else {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询图片失败:"+err.Error()))
}
return
}
// 检查标签是否已经添加过该图片
var imageTag models.ImageToTags
if err := db.Where("image_id = ? AND tag_id = ?", imageId, tagId).First(&imageTag).Error; err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "关联不存在"))
return
}
// 删除图片标签关联
if err := db.Delete(&imageTag).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "删除标签失败:"+err.Error()))
return
}
c.JSON(http.StatusOK, result.Success("标签删除成功", nil))
}
// 批量删除tag
func DeleteImageTags(c *gin.Context) {
type Request struct {
Images []int `json:"image_ids"`
Tag string `json:"tag_id"`
}
var req Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "参数解析失败:"+err.Error()))
return
}
tagID, err := strconv.Atoi(req.Tag)
if err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "tag_id必须是有效数字"))
return
}
if len(req.Images) <= 0 || tagID <= 0 {
c.JSON(http.StatusBadRequest, result.Error(400, "参数错误"))
return
}
// 直接执行删除操作,不返回结果
db := database.GetDB().DB
for _, imageId := range req.Images {
db.Where("image_id = ? AND tag_id = ?", imageId, tagID).Delete(&models.ImageToTags{})
}
c.JSON(http.StatusOK, result.Success("批量删除标签成功", nil))
}
// 批量添加tag
func AddImageTags(c *gin.Context) {
type Request struct {
Images []int `json:"image_ids"`
Tag string `json:"tag_id"`
}
var req Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "参数解析失败:"+err.Error()))
return
}
tagID, err := strconv.Atoi(req.Tag)
if err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "tag_id必须是有效数字"))
return
}
if len(req.Images) <= 0 || tagID <= 0 {
c.JSON(http.StatusBadRequest, result.Error(400, "参数错误"))
return
}
db := database.GetDB().DB
// 检查标签是否存在
var tag models.Tags
if err := db.Where("id = ?", tagID).First(&tag).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, result.Error(400, "标签不存在"))
} else {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询标签失败:"+err.Error()))
}
return
}
var existImageIDs []int
if err := db.Model(&models.Image{}).Where("id IN (?)", req.Images).Pluck("id", &existImageIDs).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询图片列表失败:"+err.Error()))
return
}
var existRelations []int
if err := db.Model(&models.ImageToTags{}).
Where("image_id IN (?) AND tag_id = ?", req.Images, tagID).
Pluck("image_id", &existRelations).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "检查标签关联失败:"+err.Error()))
return
}
var insertData []models.ImageToTags
existRelationMap := make(map[int]bool)
for _, id := range existRelations {
existRelationMap[id] = true
}
for _, imageID := range req.Images {
if existRelationMap[imageID] {
continue
}
insertData = append(insertData, models.ImageToTags{
ImageId: imageID,
TagId: tagID,
})
}
if len(insertData) > 0 {
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.CreateInBatches(&insertData, 100).Error; err != nil {
return err
}
return nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "批量添加标签失败:"+err.Error()))
return
}
} else {
c.JSON(http.StatusOK, result.Success("没有需要添加的标签", nil))
return
}
c.JSON(http.StatusOK, result.Success("批量添加标签成功", nil))
}
+20
View File
@@ -60,6 +60,26 @@ func GetLoginSettings(c *gin.Context) {
))
}
// 返回网站SEO信息
func GetSEOSettings(c *gin.Context) {
settings, err := settings.GetSettings()
if err != nil {
c.JSON(500, result.Error(500, "获取设置失败"))
return
}
c.JSON(200, result.Success("ok",
map[string]any{
"seo_title": settings.SEOTitle,
"seo_description": settings.SEODescription,
"seo_keywords": settings.SEOKeywords,
"seo_icp": settings.SEOICP,
"public_security": settings.PublicSecurity,
"seo_icon": settings.SEOicon,
},
))
}
func UpdateSettings(c *gin.Context) {
var req UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
+122
View File
@@ -0,0 +1,122 @@
package controllers
import (
"net/http"
"strconv"
"unicode/utf8"
"oneimg/backend/database"
"oneimg/backend/models"
"oneimg/backend/utils/result"
"github.com/gin-gonic/gin"
)
func AddTag(c *gin.Context) {
var tag models.Tags
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "参数错误"))
return
}
if tag.Name == "" {
c.JSON(http.StatusBadRequest, result.Error(400, "标签名称不能为空"))
return
}
if utf8.RuneCountInString(tag.Name) > 10 {
c.JSON(http.StatusBadRequest, result.Error(400, "标签名称过长"))
return
}
db := database.GetDB().DB
// 查询标签是否存在
var count int64
if err := db.Model(&models.Tags{}).Where("name = ?", tag.Name).Count(&count).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "查询标签失败"))
return
}
// 标签已存在
if count > 0 {
c.JSON(http.StatusConflict, result.Error(409, "标签已存在"))
return
}
// 插入标签
if err := db.Create(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "插入标签失败"))
return
}
c.JSON(http.StatusOK, result.Success("ok", tag))
}
func GetTags(c *gin.Context) {
var tags []models.Tags
var total int64
db := database.GetDB().DB
query := db.Model(&models.Tags{})
// 获取总数
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取标签总数失败"))
return
}
// 获取标签列表
if err := query.Find(&tags).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "获取标签列表失败"))
return
}
c.JSON(http.StatusOK, result.Success("ok", map[string]any{
"total": total,
"list": tags,
}))
}
func DeleteTag(c *gin.Context) {
idStr := c.Param("id")
if idStr == "" {
c.JSON(http.StatusBadRequest, result.Error(400, "标签ID不能为空"))
return
}
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, result.Error(400, "标签ID无效"))
return
}
if id == 0 {
c.JSON(http.StatusForbidden, result.Error(403, "默认标签不能删除"))
return
}
db := database.GetDB().DB
var tag models.Tags
// 查询标签信息
if err := db.First(&tag, uint(id)).Error; err != nil {
c.JSON(http.StatusNotFound, result.Error(404, "标签不存在"))
return
}
// 删除图片关联
if err := db.Where("tag_id = ?", id).Delete(&models.ImageToTags{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "删除标签关联失败"))
return
}
// 标签存在且用户有权限
if err := db.Delete(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, result.Error(500, "删除标签失败"))
return
}
c.JSON(http.StatusOK, result.Success("ok", nil))
}
+130 -38
View File
@@ -1,20 +1,27 @@
package database
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"oneimg/backend/config"
"oneimg/backend/models"
// MySQL底层驱动
sqlmysql "github.com/go-sql-driver/mysql"
// GORM驱动
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// 数据库操作类,支持Mysql+SQLite3
// Database 数据库操作类
type Database struct {
DB *gorm.DB
}
@@ -23,9 +30,24 @@ var db *Database
// NewDB 创建新的数据库连接
func NewDB(dialector gorm.Dialector) (*Database, error) {
gormDB, err := gorm.Open(dialector, &gorm.Config{})
gormConfig := &gorm.Config{
SkipDefaultTransaction: true,
DisableForeignKeyConstraintWhenMigrating: true,
}
gormDB, err := gorm.Open(dialector, gormConfig)
if err != nil {
return nil, err
return nil, fmt.Errorf("gorm连接失败: %w", err)
}
sqlDB, err := gormDB.DB()
if err != nil {
return nil, fmt.Errorf("获取SQL连接失败: %w", err)
}
// 验证连接
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("连接验证失败: %w", err)
}
return &Database{DB: gormDB}, nil
@@ -41,56 +63,126 @@ func InitDB(cfg *config.Config) {
var err error
var dialector gorm.Dialector
// 根据配置选择数据库类型
if cfg.IsMysql {
// 使用 MySQL
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.DbUser,
cfg.DbPassword,
cfg.DbHost,
cfg.DbPort,
cfg.DbName)
dialector = mysql.Open(dsn)
log.Println("使用 MySQL 数据库")
} else {
// 使用 SQLite
// 检查路径是否存在
switch cfg.DbType {
case "mysql":
dialector, err = initMysqlWithTLS(cfg)
if err != nil {
log.Fatalf("❌ MySQL初始化失败: %v", err)
}
log.Println("✅ MySQL 数据库连接成功")
case "postgres":
dialector, err = initPostgreSQLWithTLS(cfg)
if err != nil {
log.Fatalf("❌ PostgreSQL初始化失败: %v", err)
}
log.Println("✅ PostgreSQL 数据库连接成功")
default:
ensureDirExists(cfg.SqlitePath)
dialector = sqlite.Open(cfg.SqlitePath)
log.Printf("使用 SQLite 数据库: %s", cfg.SqlitePath)
log.Printf("✅ SQLite 数据库连接成功: %s", cfg.SqlitePath)
}
// 创建数据库实例
db, err = NewDB(dialector)
if err != nil {
log.Fatal("数据库连接失败:", err)
log.Fatalf("❌ 数据库实例创建失败: %v", err)
}
log.Println("数据库连接成功")
// 自动迁移数据表
err = db.DB.AutoMigrate(&models.User{}, &models.Image{}, &models.Settings{}, &models.ImageTeleGram{})
err = db.DB.AutoMigrate(
&models.Tags{},
&models.User{},
&models.Image{},
&models.Settings{},
&models.ImageTeleGram{},
&models.ImageToTags{},
)
if err != nil {
log.Fatal("数据库迁移失败:", err)
log.Fatalf("❌ 数据库表迁移失败: %v", err)
}
log.Println("✅ 数据库表迁移完成")
}
log.Println("数据库表迁移完成")
// initMysqlWithTLS 初始化MySQL
func initMysqlWithTLS(cfg *config.Config) (gorm.Dialector, error) {
dsnTemplate := "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s"
dsn := fmt.Sprintf(dsnTemplate, cfg.DbUser, cfg.DbPassword, cfg.DbHost, cfg.DbPort, cfg.DbName)
tlsName := "custom_tls"
tlsConfig, err := buildTLSConfig(cfg)
if err != nil {
return nil, err
}
if err := sqlmysql.RegisterTLSConfig(tlsName, tlsConfig); err != nil {
return nil, err
}
dsn += "&tls=" + tlsName
return mysql.New(mysql.Config{DSN: dsn}), nil
}
// initPostgreSQLWithTLS 初始化 PG 数据库
func initPostgreSQLWithTLS(cfg *config.Config) (gorm.Dialector, error) {
// 对特殊字符进行编码
user := url.QueryEscape(cfg.DbUser)
pass := url.QueryEscape(cfg.DbPassword)
// 构建基础 DSN
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s", user, pass, cfg.DbHost, cfg.DbPort, cfg.DbName)
// 处理 SSL/TLS 参数
queryParams := url.Values{}
queryParams.Add("timezone", "Asia/Shanghai")
if cfg.DbCaCertPath != "" && fileExists(cfg.DbCaCertPath) {
absPath, _ := filepath.Abs(cfg.DbCaCertPath)
if cfg.DbSkipCertVerify {
queryParams.Add("sslmode", "require")
} else {
queryParams.Add("sslmode", "verify-full")
queryParams.Add("sslrootcert", absPath)
}
} else {
queryParams.Add("sslmode", "require")
}
dsn = dsn + "?" + queryParams.Encode()
return postgres.New(postgres.Config{
DSN: dsn,
PreferSimpleProtocol: true, // 增加兼容性
}), nil
}
// buildTLSConfig 构建 TLS 配置
func buildTLSConfig(cfg *config.Config) (*tls.Config, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: cfg.DbSkipCertVerify,
}
if cfg.DbCaCertPath != "" && fileExists(cfg.DbCaCertPath) {
caCert, err := os.ReadFile(cfg.DbCaCertPath)
if err != nil {
return nil, fmt.Errorf("读取CA证书失败: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("解析CA证书失败")
}
tlsConfig.RootCAs = caCertPool
}
return tlsConfig, nil
}
// 辅助函数,如果数据库目录不存在则创建
func ensureDirExists(path string) {
dir := filepath.Dir(path)
// 检查目录状态
_, err := os.Stat(dir)
if os.IsNotExist(err) {
// 创建目录(0755:生产环境安全权限)
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatalf("创建数据库目录失败 [路径: %s]: %v", dir, err)
}
return
}
// 处理其他错误(如权限不足)
if err != nil {
log.Fatalf("检查数据库目录失败 [路径: %s]: %v", dir, err)
if _, err := os.Stat(dir); os.IsNotExist(err) {
os.MkdirAll(dir, 0755)
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
+24 -1
View File
@@ -2,6 +2,8 @@ package middlewares
import (
"net/http"
"oneimg/backend/utils/settings"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -16,12 +18,28 @@ type AuthResponse struct {
// AuthMiddleware Session认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取请求头中的token
authHeader := c.Request.Header.Get("Authorization")
apiToken := ""
parts := strings.SplitN(authHeader, "=", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) == "oneimg_token" {
apiToken = strings.TrimSpace(parts[1])
}
if validateToken(apiToken) {
c.Set("user_id", 1)
c.Set("user_role", 1)
c.Set("username", "admin")
c.Next()
}
// 获取session
session := sessions.Default(c)
// 检查是否已登录
loggedIn := session.Get("logged_in")
if loggedIn == nil || loggedIn != true {
if (loggedIn == nil || loggedIn != true) && apiToken == "" {
c.JSON(http.StatusUnauthorized, AuthResponse{
Code: 401,
Message: "用户未登录",
@@ -56,6 +74,11 @@ func AuthMiddleware() gin.HandlerFunc {
}
}
func validateToken(token string) bool {
setting, _ := settings.GetSettings()
return token == setting.APIToken
}
func AdminOnlyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取用户ID
+1 -1
View File
@@ -4,7 +4,7 @@ import "time"
// 图片模型
type Image struct {
Id int `json:"id" gorm:"primaryKey"`
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Url string `json:"url" gorm:"not null"`
Thumbnail string `json:"thumbnail"`
FileName string `json:"filename" gorm:"not null"`
+6
View File
@@ -0,0 +1,6 @@
package models
type ImageToTags struct {
ImageId int `json:"image_id" gorm:"primaryKey"`
TagId int `json:"tag_id" gorm:"primaryKey"`
}
+11 -1
View File
@@ -7,7 +7,7 @@ import (
// Settings 系统配置模型(全局唯一配置)
// 注意:该表应只有一条记录(ID=1),所有配置项存储在同一条记录中
type Settings struct {
ID int `gorm:"primarykey;column:id" json:"id"`
ID int `gorm:"primarykey;column:id;autoIncrement" json:"id"`
OriginalImage bool `gorm:"column:original_image;default:false" json:"original_image"` // 是否保存原图(默认保存)
SaveWebp bool `gorm:"column:save_webp;default:true" json:"save_webp"` // 是否保存webp格式(默认保存)
Thumbnail bool `gorm:"column:thumbnail;default:true" json:"thumbnail"` // 是否生成缩略图(默认生成)
@@ -17,6 +17,8 @@ type Settings struct {
TGBotToken string `gorm:"column:tg_bot_token;default:''" json:"tg_bot_token"` // TG机器人Token
TGReceivers string `gorm:"column:tg_receivers;default:''" json:"tg_receivers"` // TG接收者(多个用逗号分隔)
TGNoticeText string `gorm:"column:tg_notice_text;default:''" json:"tg_notice_text"` // TG通知文本
StartAPI bool `gorm:"column:start_api;default:false" json:"start_api"` // 是否启用API(默认关闭)
APIToken string `gorm:"column:api_token;default:''" json:"api_token"` // API Token
// 水印设置
WatermarkEnable bool `gorm:"column:watermark_enable;default:false" json:"watermark_enable"` // 是否启用水印(默认不启用)
@@ -34,6 +36,14 @@ type Settings struct {
StorageType string `gorm:"column:storage_type;default:'default'" json:"storage_type"` // 存储类型:default/s3/r2/webdav
StoragePath string `gorm:"column:storage_path;default:'./uploads'" json:"storage_path"` // 本地存储路径(默认./uploads)
// SEO 设置
SEOTitle string `gorm:"column:seo_title;default:'初春图床'" json:"seo_title"` // SEO标题(默认为初春图床)
SEODescription string `gorm:"column:seo_description;default:'初春图床,一个免费、稳定、高效的图床服务'" json:"seo_description"` // SEO描述(默认为初春图床,一个免费、稳定、高效的图床服务)
SEOKeywords string `gorm:"column:seo_keywords;default:'初春网络,雾创岛,初春图床,图床,免费,稳定,高效'" json:"seo_keywords"` // SEO关键词(默认为初春网络,雾创岛,初春图床,图床,免费,稳定,高效)
SEOICP string `gorm:"column:seo_icp;default:''" json:"seo_icp"` // SEO ICP备案(默认为空)
PublicSecurity string `gorm:"column:public_security;default:''" json:"public_security"` // SEO 公安备案(默认为空)
SEOicon string `gorm:"column:seo_icon;default:''" json:"seo_icon"` // SEO ICON(默认为空)
// S3配置(兼容S3协议的对象存储)
S3Endpoint string `gorm:"column:s3_endpoint;default:''" json:"s3_endpoint"`
S3AccessKey string `gorm:"column:s3_access_key;default:''" json:"s3_access_key"`
+6
View File
@@ -0,0 +1,6 @@
package models
type Tags struct {
Id int `json:"id" gorm:"type:integer;primaryKey;autoIncrement"`
Name string `json:"name" gorm:"not null;default:'';uniqueIndex:name;size:50"`
}
+1 -1
View File
@@ -2,7 +2,7 @@ package models
// 用户模型
type User struct {
Id int `gorm:"primarykey;column:id" json:"id"`
Id int `gorm:"primarykey;column:id;autoIncrement" json:"id"`
Role int `gorm:"default:1;uniqueIndex:unique_idx" json:"role"`
Username string `json:"username"`
Password string `json:"password"`
+16
View File
@@ -58,6 +58,10 @@ func SetupRoutes(frontendFS embed.FS) *gin.Engine {
api.GET("/logout", controllers.Logout)
// 返回登录设置
api.GET("/settings/login", controllers.GetLoginSettings)
// 返回SEO设置
api.GET("/settings/seo", controllers.GetSEOSettings)
// 随机图片
api.GET("/images/random", controllers.GetRandomImages)
// 需要认证的接口分组(应用AuthMiddleware)
auth := api.Group("")
@@ -66,6 +70,10 @@ func SetupRoutes(frontendFS embed.FS) *gin.Engine {
// 用户信息接口
auth.GET("/user/status", controllers.CheckLoginStatus)
// 标签管理接口
auth.GET("/tags", controllers.GetTags)
auth.DELETE("/tags/:id", controllers.DeleteTag)
// 统计数据
auth.GET("/stats/dashboard", controllers.GetDashboardStats)
auth.GET("/stats/images", controllers.GetImageStats)
@@ -76,10 +84,17 @@ func SetupRoutes(frontendFS embed.FS) *gin.Engine {
auth.DELETE("/images/:id", controllers.DeleteImage)
auth.GET("/images", controllers.GetImageList)
auth.GET("/images/:id", controllers.GetImageDetail)
auth.POST("/images/tag", controllers.AddImageTag)
auth.DELETE("/images/tag", controllers.DeleteImageTag)
auth.DELETE("/images/tags", controllers.DeleteImageTags) // 批量删除图片标签
auth.POST("/images/tags", controllers.AddImageTags)
// 需要管理员权限
auth.Use(middlewares.AdminOnlyMiddleware())
{
// 新增标签
auth.POST("/tags", controllers.AddTag)
// 账户管理接口
auth.POST("/account/change", controllers.ChangeAccountInfo)
auth.POST("/sessions/clear", controllers.ClearAllSessions)
@@ -104,6 +119,7 @@ func SetupRoutes(frontendFS embed.FS) *gin.Engine {
for _, f := range files {
fileNames = append(fileNames, f.Name())
}
log.Printf("读取distFS文件列表成功:%v", fileNames)
} else {
log.Printf("读取distFS文件列表失败:%s", err)
}
+2 -2
View File
@@ -61,7 +61,7 @@ func ParseWatermarkParams(c *gin.Context) WatermarkConfig {
FontSize: 24,
FontSizeRatio: 0.02,
MinFontSize: 10,
MaxFontSize: 50,
MaxFontSize: 500,
FontColor: "FFFFFF",
Opacity: 1.0,
FontPath: "jyhphy.ttf",
@@ -150,7 +150,7 @@ func WatermarkSetting(setting models.Settings) WatermarkConfig {
var (
ratio = 0.02
minSize = 10
maxSize = 50
maxSize = 500
dynamicOn = true
fontSize = 24
)
+31
View File
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
+68 -5
View File
@@ -1,5 +1,5 @@
<template>
<div id="app">
<div id="app" class="relative min-h-screen">
<!-- 背景网格 -->
<div class="fixed inset-0 bg-grid opacity-70 dark:opacity-50"></div>
@@ -24,17 +24,80 @@
<!-- 主内容区 -->
<main class="flex-grow pt-24 pb-16 px-4 relative z-10">
<router-view></router-view>
<router-view class="mb-8"></router-view>
</main>
<!-- 底部版权信息 -->
<footer class="absolute bottom-0 left-0 right-0 min-h-16 border-light-200 dark:border-dark-100 shadow-md dark:shadow-dark-md z-40">
<div class="px-4 py-2 text-center text-xs text-gray-500 dark:text-gray-400">
© {{ year }} <a href="/" class="hover:underline">{{ seoSetting.seo_title || '初春图床'}}</a>. All rights reserved.
<div class="md:flex items-center justify-center mt-1 gap-2">
<!-- 设置备案信息 -->
<p v-if="seoSetting.seo_icp" class="mt-1">
<img class="inline-block h-6 w-6" :src="icpImg" alt="ICP"/>
<a href="http://www.beian.miit.gov.cn/" target="_blank" class="hover:underline">{{seoSetting.seo_icp}}</a>
</p>
<!-- 设置公安备案信息 -->
<p v-if="seoSetting.public_security" class="mt-1">
<img class="inline-block h-6 w-6" :src="securityImg" alt="公安部备案"/>
<a href="https://beian.mps.gov.cn/#/query/webSearch" target="_blank" class="hover:underline">{{seoSetting.public_security}}</a>
</p>
</div>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted, inject } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import Navbar from "@/components/NavBar.vue";
import icpImg from '@/assets/images/icp.svg';
import securityImg from '@/assets/images/gongan.png';
const seoSetting = ref({
seo_title: '初春图床',
seo_description: '',
seo_keywords: '',
seo_icp: '',
public_security: '',
seo_icon: ''
});
const year = new Date().getFullYear();
const handleSeoUpdate = (data) => {
if (!data || typeof data !== 'object') return;
// 更新组件内SEO数据
seoSetting.value = { ...seoSetting.value, ...data };
// 封装元标签设置函数
const setMetaTag = (name, content) => {
// 查找现有标签,找不到则创建
let tag = document.querySelector(`meta[name="${name}"]`);
if (!tag) {
tag = document.createElement('meta');
tag.setAttribute('name', name);
document.head.appendChild(tag);
}
if (content && content.trim()) {
tag.setAttribute('content', content.trim());
} else {
tag.removeAttribute('content');
}
};
setMetaTag('description', seoSetting.value.seo_description);
setMetaTag('keywords', seoSetting.value.seo_keywords);
};
onMounted(() => {
})
window.seoBus?.onUpdate(handleSeoUpdate);
if (window.seoStting) {
seoSetting.value = window.seoStting;
}
});
onUnmounted(() => {
window.seoBus.callbacks = window.seoBus.callbacks.filter(cb => cb !== handleSeoUpdate);
});
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1610794200367" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11672" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"/></defs><path d="M778.24 163.84c-76.8-40.96-165.888-61.44-269.312-61.44s-192.512 20.48-269.312 61.44h-133.12l23.552 337.92c8.192 113.664 67.584 217.088 162.816 280.576l215.04 144.384 215.04-144.384c96.256-63.488 155.648-166.912 163.84-280.576l23.552-337.92H778.24z m47.104 333.824c-7.168 94.208-56.32 181.248-135.168 233.472L508.928 851.968 327.68 731.136c-78.848-53.248-129.024-139.264-135.168-233.472L173.056 225.28h136.192v-26.624c58.368-23.552 124.928-34.816 199.68-34.816s141.312 12.288 199.68 34.816v26.624H844.8l-19.456 272.384z" fill="#2468F2" p-id="11673"/><path d="M685.056 328.704v-46.08H455.68c2.048-4.096 6.144-9.216 11.264-15.36 5.12-7.168 9.216-12.288 11.264-15.36L419.84 240.64c-31.744 46.08-75.776 87.04-133.12 123.904 4.096 4.096 10.24 11.264 18.432 21.504l17.408 17.408c23.552-15.36 45.056-31.744 63.488-50.176 26.624 25.6 49.152 43.008 67.584 51.2-46.08 15.36-104.448 27.648-175.104 35.84 2.048 5.12 6.144 13.312 9.216 24.576 4.096 11.264 6.144 19.456 7.168 24.576l39.936-7.168v218.112h54.272V680.96h238.592v19.456h54.272V481.28H348.16c60.416-12.288 114.688-27.648 163.84-46.08 49.152 19.456 118.784 34.816 210.944 46.08 5.12-17.408 10.24-34.816 17.408-51.2-62.464-4.096-116.736-12.288-161.792-24.576 38.912-20.48 74.752-46.08 106.496-76.8z m-150.528 194.56h94.208v41.984h-94.208v-41.984z m0 78.848h94.208v41.984h-94.208v-41.984z m-144.384-78.848h94.208v41.984h-94.208v-41.984z m0 78.848h94.208v41.984h-94.208v-41.984z m34.816-275.456h182.272c-26.624 22.528-57.344 41.984-94.208 57.344-31.744-15.36-61.44-34.816-88.064-57.344z" fill="#2468F2" p-id="11674"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+39 -12
View File
@@ -12,8 +12,8 @@
<i class="ri-align-justify"></i>
</button>
<div class="flex items-center gap-2 font-semibold text-xl">
<div class="w-10 h-10 rounded-md bg-gradient-to-br from-primary to-primary-dark flex items-center justify-center text-white font-bold">雾</div>
<span>初春图床</span>
<div class="w-10 h-10 rounded-md bg-gradient-to-br from-primary to-primary-dark flex items-center justify-center text-white font-bold">{{getFirstWord(seoTitle)}}</div>
<span>{{ seoTitle }}</span>
</div>
</div>
@@ -79,17 +79,19 @@ import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 1. 定义 ref 引用
// 定义 ref 引用
const themeToggleRef = ref(null)
const sidebarToggleRef = ref(null)
const sidebarRef = ref(null)
const sidebarOverlayRef = ref(null)
const logoutRef = ref(null)
const seoTitle = ref('初春图床');
// 2. 导航菜单数据
// 导航菜单数据
const navItems = [
{ path: '/', icon: 'home-line', name: '首页' },
{ path: '/gallery', icon: 'nft-line', name: '画廊' },
{ path: '/tags', icon: 'bookmark-line', name: 'Tags' },
{ path: '/stats', icon: 'numbers-fill', name: '统计' }
]
@@ -109,12 +111,12 @@ const isRouteActive = (targetPath) => {
return route.path.startsWith(targetPath)
}
// 3. 导航点击事件
// 导航点击事件
const handleNavClick = () => {
closeSidebar()
}
// 4. 主题切换功能
// 主题切换功能
const storageKey = 'theme-preference'
const detectUserThemePreference = () => {
@@ -136,7 +138,7 @@ const applyTheme = (theme) => {
}
}
// 5. 侧边栏控制功能
// 侧边栏控制功能
const openSidebar = () => {
if (sidebarRef.value) {
sidebarRef.value.classList.remove('sidebar-closed')
@@ -161,7 +163,7 @@ const closeSidebar = () => {
document.body.style.overflow = ''
}
// 6. 登出功能
// 登出功能
const handleLogout = async () => {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('token')
@@ -181,14 +183,32 @@ const handleLogout = async () => {
} catch (error) {
Message.error('登出失败')
}
}
}
// 7. 组件挂载时初始化
// 获取标题第一个字
const getFirstWord = (title) => {
if (!title) return ''
return title.split('')[0]
}
const handleSeoUpdate = (data) => {
if (data?.seo_title) {
seoTitle.value = data.seo_title;
}
};
// 组件挂载时初始化
onMounted(() => {
// 初始化主题
const initialTheme = detectUserThemePreference()
applyTheme(initialTheme)
// 绑定 SEO 更新事件
window.seoBus?.onUpdate(handleSeoUpdate);
if (window.seoStting?.seo_title) {
seoTitle.value = window.seoStting.seo_title;
}
// 绑定主题切换事件
if (themeToggleRef.value) {
themeToggleRef.value.addEventListener('click', () => {
@@ -222,7 +242,7 @@ onMounted(() => {
window.addEventListener('resize', handleResize)
})
// 8. 组件卸载时清理
// 组件卸载时清理
onUnmounted(() => {
// 移除主题切换事件
if (themeToggleRef.value) {
@@ -244,6 +264,9 @@ onUnmounted(() => {
logoutRef.value.removeEventListener('click', handleLogout)
}
// 移除SEO更新事件
window.seoBus.callbacks = window.seoBus.callbacks.filter(cb => cb !== handleSeoUpdate);
// 移除窗口 resize 事件
window.removeEventListener('resize', () => {})
@@ -251,11 +274,15 @@ onUnmounted(() => {
document.body.style.overflow = ''
})
// 9. 初始化侧边栏状态
// 初始化侧边栏状态
onMounted(() => {
if (window.innerWidth >= 1024) {
closeSidebar()
}
// 加载SEO标题
if (window.seoStting?.seo_title) {
seoTitle.value = window.seoStting.seo_title;
}
})
</script>
+133 -23
View File
@@ -1,5 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'
let seoStting = {
seo_title: '初春图床',
seo_description: '',
seo_keywords: '',
seo_icp: '',
public_security: '',
seo_icon: ''
};
const seoBus = {
callbacks: [],
onUpdate: (cb) => seoBus.callbacks.push(cb),
triggerUpdate: (data) => seoBus.callbacks.forEach(cb => cb(data))
};
let seoRequestPromise = null;
const routes = [
{
path: '/login',
@@ -7,7 +24,7 @@ const routes = [
component: () => import('@/views/Login.vue'),
meta: {
title: '登录',
public: true // 标记为公开路由,不需要登录即可访问
public: true
}
},
{
@@ -15,7 +32,7 @@ const routes = [
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '初春图床'
title: '首页'
}
},
{
@@ -26,6 +43,14 @@ const routes = [
title: '图库'
}
},
{
path: '/tags',
name: 'Tags',
component: () => import('@/views/Tags.vue'),
meta: {
title: '标签'
}
},
{
path: '/stats',
name: 'Stats',
@@ -54,34 +79,119 @@ const routes = [
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // 使用环境变量中的基础URL
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 全局前置守卫 - 处理页面标题和登录验证
router.beforeEach(async (to, from, next) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo'));
document.title = to.meta.title || '初春图床';
const isPublic = to.meta.public;
if (isPublic) {
return next();
}
try {
const response = await fetch('/api/user/status', {
method: 'GET'
});
const result = await response.json()
if (result.code === 200 && result.data.logged_in == true) {
if (userInfo.username !== result.data.username) {
return next('/login');
// 获取SEO配置
const getSeoSetting = async () => {
if (seoRequestPromise) return seoRequestPromise;
seoRequestPromise = new Promise(async (resolve) => {
try {
const response = await fetch('/api/settings/seo', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error(`请求失败:${response.status} ${response.statusText}`);
}
const result = await response.json();
if (result.code === 200 && result.data) {
seoStting = { ...seoStting, ...result.data };
window.seoStting = seoStting; // 挂载到全局
seoBus.triggerUpdate(seoStting); // 更新SEO设置
// 设置网站图标
if (seoStting.seo_icon) {
let favicon = document.querySelector('link[rel="icon"]');
if (!favicon) {
favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/x-icon';
document.head.appendChild(favicon);
}
favicon.href = seoStting.seo_icon;
}
} else {
ElMessage.error(result.message || '获取SEO设置失败:无数据');
}
} catch (error) {
console.error('获取SEO设置失败:', error);
ElMessage.error(error.message || '获取SEO设置失败:网络异常');
} finally {
resolve(seoStting);
}
});
return seoRequestPromise;
};
// 封装动态标题计算函数
const getPageTitle = (to) => {
if (to.meta.title === '首页') {
return seoStting.seo_title;
}
return to.meta.title ? `${to.meta.title} - ${seoStting.seo_title}` : seoStting.seo_title;
};
//全局前置守卫
router.beforeEach(async (to, from, next) => {
try {
// 等待SEO接口完成
await getSeoSetting();
// 设置页面标题
document.title = getPageTitle(to);
// 处理公开路由
const isPublic = to.meta.public;
if (isPublic) {
return next();
}
next('/login');
// 验证本地用户信息
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
if (!userInfo.username) {
return next('/login');
}
// 验证登录状态
const response = await fetch('/api/user/status');
if (!response.ok) {
throw new Error(`登录状态验证失败:${response.status}`);
}
const result = await response.json();
if (result.code !== 200 || !result.data.logged_in) {
return next('/login');
}
// 验证用户名一致性
if (userInfo.username !== result.data.username) {
return next('/login');
}
// 所有验证通过,放行
next();
} catch (error) {
console.error('验证登录状态失败:', error);
next('/login');
// 避免多次调用next的警告
if (!to.fullPath.includes('/login')) {
next('/login');
} else {
next();
}
}
});
export default router
// 全局后置守卫
router.afterEach((to) => {
document.title = getPageTitle(to);
});
window.seoBus = seoBus;
export default router
+1 -1
View File
@@ -6,7 +6,7 @@ class Message {
duration: 3000, // 自动关闭时长(毫秒),0表示不自动关闭
position: 'top-right', // 显示位置:top-left/top-center/top-right/bottom-left/bottom-center/bottom-right
offset:75, // 距离边界的偏移量(像素)
zIndex: 9999, // 层级(高于普通元素,低于PopupModal)
zIndex: 99999, // 层级(高于普通元素,低于PopupModal)
showClose: false, // 是否显示关闭按钮
onClose: null // 关闭回调函数
};
+2 -2
View File
@@ -92,8 +92,8 @@ class PopupModal {
// 标题
this.titleEl = document.createElement('h3');
this.titleEl.className = 'font-semibold text-lg text-dark-300 dark:text-light-100';
this.titleEl.textContent = this.config.title;
this.titleEl.className = 'font-semibold text-lg text-dark-300 dark:text-light-100 w-[50%] truncate';
this.titleEl.innerHTML = this.config.title;
this.header.appendChild(this.titleEl);
// 关闭按钮
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+198 -26
View File
@@ -11,7 +11,6 @@
<!-- 主要内容 -->
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 系统配置卡片 -->
<div class="order-1 md:order-2 w-full p-0 mx-auto">
<div class="panel-content p-6 md:p-8 bg-white dark:bg-gray-800 rounded-xl shadow-md">
@@ -77,30 +76,6 @@
</div>
</div>
<!-- 存储类型:下拉框变更保存 -->
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="storage_type">
存储类型
</label>
<select
id="storage_type"
v-model="systemSettings.storage_type"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
@change="handleSelectChange('storage_type', systemSettings.storage_type)"
>
<option value="" disabled>请选择存储类型</option>
<option value="default">本地存储</option>
<option value="s3">S3</option>
<option value="r2">R2</option>
<option value="webdav">WebDav</option>
<option value="ftp">FTP</option>
<option value="telegram">Telegram</option>
</select>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
选择Telegram存储必须使用海外服务器,否则无法正常上传、查看、删除图片
</div>
</div>
<!-- 水印文本:失去焦点保存 -->
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="watermark_text">
@@ -210,6 +185,56 @@
</div>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="api_token">
API Token
</label>
<div class="relative w-full">
<input
id="api_token"
v-model="systemSettings.api_token"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
placeholder="API Token"
@blur="handleFieldBlur('api_token', systemSettings.api_token)"
/>
<button
type="button"
class="bg-primary absolute right-0 top-0 h-full hover:bg-primary-dark text-white px-3 py-[7px] rounded-r-lg transition-colors duration-200 flex items-center justify-center"
@click="generateApiToken"
>
生成
</button>
</div>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
1. 用于调用 API 接口,在请求头 Authorization 字段中添加 oneimg_token={API Token}
</div>
</div>
<!-- 存储类型:下拉框变更保存 -->
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="storage_type">
存储类型
</label>
<select
id="storage_type"
v-model="systemSettings.storage_type"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
@change="handleSelectChange('storage_type', systemSettings.storage_type)"
>
<option value="" disabled>请选择存储类型</option>
<option value="default">本地存储</option>
<option value="s3">S3</option>
<option value="r2">R2</option>
<option value="webdav">WebDav</option>
<option value="ftp">FTP</option>
<option value="telegram">Telegram</option>
</select>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
选择Telegram存储必须使用海外服务器,否则无法正常上传、查看、删除图片
</div>
</div>
<!-- S3/R2配置:失去焦点保存 -->
<div v-if="['s3', 'r2'].includes(systemSettings.storage_type)" class="space-y-4 pt-2 border-t border-gray-200 dark:border-gray-700">
<h3 class="font-bold text-sm text-gray-800 dark:text-gray-200">S3/R2 配置</h3>
@@ -382,6 +407,106 @@
<!-- 系统设置卡片(开关部分不变) -->
<div class="order-2 md:order-1 w-full p-0 mx-auto">
<!-- SEO设置卡片 -->
<div class="panel-content p-6 mb-4 md:p-8 bg-white dark:bg-gray-800 rounded-xl shadow-md space-y-6">
<h2 class="panel-title flex items-center text-xl font-semibold mb-8">
<span class="panel-icon mr-2 text-2xl">
<i class="ri-seo-line"></i>
</span>
SEO 设置
</h2>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="seo_title">
网站标题
</label>
<input
id="seo_title"
v-model="systemSettings.seo_title"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
placeholder="请输入网站标题"
@blur="handleFieldBlur('seo_title', systemSettings.seo_title)"
/>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="seo_description">
网站标题
</label>
<textarea
id="seo_description"
v-model="systemSettings.seo_description"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
rows="3"
placeholder="请输入网站描述"
@blur="handleFieldBlur('seo_description', systemSettings.seo_description)"
></textarea>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="seo_keywords">
网站关键词
</label>
<textarea
id="seo_keywords"
v-model="systemSettings.seo_keywords"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
rows="3"
placeholder="请输入网站关键词"
@blur="handleFieldBlur('seo_keywords', systemSettings.seo_keywords)"
></textarea>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="seo_icp">
网站备案号
</label>
<input
id="seo_icp"
v-model="systemSettings.seo_icp"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
placeholder="请输入网站备案号"
@blur="handleFieldBlur('seo_icp', systemSettings.seo_icp)"
/>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
输入网站备案号会在页面底部显示备案信息
</div>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="public_security">
网站公安备案号
</label>
<input
id="public_security"
v-model="systemSettings.public_security"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
placeholder="请输入网站公安备案号"
@blur="handleFieldBlur('public_security', systemSettings.public_security)"
/>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
输入网站公安备案号会在页面底部显示公安备案信息
</div>
</div>
<div class="setting-group">
<label class="setting-label block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="seo_icon">
网站小图标
</label>
<input
id="seo_icon"
v-model="systemSettings.seo_icon"
type="text"
class="setting-input w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary dark:focus:ring-primary/70 dark:focus:border-primary/70 transition-colors outline-none"
placeholder="请输入网站小图标"
@blur="handleFieldBlur('seo_icon', systemSettings.seo_icon)"
/>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">
输入网站小图标URL会替换默认的小图标
</div>
</div>
</div>
<div class="panel-content p-6 md:p-8 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<h2 class="panel-title flex items-center text-xl font-semibold mb-8">
<span class="panel-icon mr-2 text-2xl">
@@ -517,6 +642,22 @@
<div class="absolute left-1 top-1 bg-white dark:bg-gray-200 w-4 h-4 rounded-full switch-transition switch-antialias peer-checked:translate-x-6"></div>
</label>
</div>
<div class="setting-group flex items-center justify-between py-2">
<label class="setting-label text-sm font-medium text-gray-700 dark:text-gray-300">
启用API
</label>
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
v-model="systemSettings.start_api"
class="sr-only peer"
@change="handleSwitchChange('start_api', systemSettings.start_api)"
>
<div class="w-12 h-6 bg-gray-200 dark:bg-gray-700 rounded-full peer-checked:bg-green-500 dark:peer-checked:bg-green-600 switch-transition switch-antialias"></div>
<div class="absolute left-1 top-1 bg-white dark:bg-gray-200 w-4 h-4 rounded-full switch-transition switch-antialias peer-checked:translate-x-6"></div>
</label>
</div>
<div class="mt-1 text-gray-500 dark:text-gray-400 text-xs">启用API必须设置API Token。</div>
</div>
</div>
</div>
@@ -560,7 +701,15 @@ const systemSettings = reactive({
watermark_color: '',
watermark_opac: '',
referer_white_list: '',
referer_white_enable: false
referer_white_enable: false,
seo_title: '',
seo_description: '',
seo_keywords: '',
seo_icp: '',
public_security: '',
seo_icon: '',
api_token: '',
start_api: false
})
const updateSetting = reactive({})
@@ -617,8 +766,31 @@ const saveSetting = async (key, value) => {
}, 300)
}
const generateApiToken = () => {
const token = generate32BitTokenMixCase();
systemSettings.api_token = token;
saveSetting('api_token', token)
}
const generate32BitTokenMixCase = () => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let token = '';
for (let i = 0; i < 32; i++) {
const randomIndex = Math.floor(Math.random() * chars.length);
token += chars[randomIndex];
}
return token;
}
// 开关状态变更统一处理方法
const handleSwitchChange = (key, value) => {
if (key == "start_api") {
if (systemSettings.api_token == '') {
message.warning('请先填写API Token')
systemSettings.start_api = false
return
}
}
saveSetting(key, value)
}
+221
View File
@@ -0,0 +1,221 @@
<template>
<div class="pt-6 md:px-6 lg:px-8 xl:container xl:mx-auto">
<!-- 标签管理卡片 -->
<div class="bg-white dark:bg-dark-200 rounded-xl shadow-md dark:shadow-dark-md p-5 transition-all duration-300 hover:shadow-lg dark:hover:shadow-dark-lg">
<h2 class="section-title text-lg font-semibold mb-4 flex items-center gap-2">
<i class="ri-bookmark-line text-primary"></i>
标签管理
</h2>
<!-- 标签添加区域 -->
<div class="flex justify-between items-center mb-6">
<input type="text"
v-model="tagInput"
@keyup.enter="handleAddTag"
class="flex-1 px-6 py-4 border min-w-[100px] border-light-300 dark:border-dark-100 rounded-lg bg-white dark:bg-dark-200 text-sm outline-none focus:ring-1 focus:ring-primary focus:border-primary transition-all"
placeholder="请输入标签(最多10个字符)..."
maxlength="10"/>
<button class="px-6 py-4 ml-3 text-sm font-semibold text-white bg-primary rounded-lg hover:bg-primary/80 transition-all"
@click="handleAddTag"
:disabled="isAdding">
<span v-if="!isAdding" class="truncate text-overflow">添加</span>
<span v-else class="flex items-center gap-1">
<i class="ri-loader-2-line animate-spin"></i>
提交中
</span>
</button>
</div>
<!-- 错误提示 -->
<div v-if="errorMsg" class="mb-4 px-4 py-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg">
{{ errorMsg }}
</div>
<!-- 标签列表区域 -->
<div class="tag-list-container">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300 mb-3">已创建标签</h3>
<!-- 标签列表 -->
<div class="flex flex-wrap gap-3">
<!-- 默认标签 -->
<div class="flex items-center px-4 py-2 bg-primary/10 dark:bg-primary/20 text-primary rounded-lg text-sm">
<span>默认</span>
<button class="ml-2 text-primary/70 hover:text-red-500 transition-colors"
@click="handleDeleteTag(0, 0)"
:disabled="isDeleting">
<i class="ri-close-line"></i>
</button>
</div>
<!-- 标签项 -->
<div v-for="(tag, index) in tagList" :key="index" class="flex items-center px-4 py-2 bg-primary/10 dark:bg-primary/20 text-primary rounded-lg text-sm">
<span>{{ tag?.name || '未知'}}</span>
<button class="ml-2 text-primary/70 hover:text-red-500 transition-colors"
@click="handleDeleteTag(tag.id, index)"
:disabled="isDeleting">
<i class="ri-close-line"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 响应式数据
const tagInput = ref(''); // 标签输入框
const tagList = ref([]); // 标签列表
const errorMsg = ref(''); // 错误提示
const isAdding = ref(false); // 添加标签加载状态
const isDeleting = ref(false); // 删除标签加载状态
// 初始化:加载已有标签
onMounted(() => {
fetchTagList();
});
// 获取标签列表
const fetchTagList = async () => {
try {
// 替换为你的实际接口地址
const response = await fetch('/api/tags', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
}
});
const result = await response.json();
if (response.ok && result.code === 200) {
tagList.value = result.data?.list || [];
} else {
throw new Error(result.message || '获取标签列表失败');
}
} catch (error) {
console.error('获取标签失败:', error);
Message.error(error.message || '获取标签列表失败');
}
};
// 处理添加标签
const handleAddTag = async () => {
// 清空之前的错误提示
errorMsg.value = '';
// 1. 输入校验
const tagName = tagInput.value.trim();
if (!tagName) {
errorMsg.value = '标签名称不能为空';
return;
}
if (tagName.length > 10) {
errorMsg.value = '标签名称不能超过10个字符';
return;
}
// 2. 重复校验
const isDuplicate = tagList.value.some(tag => tag.name === tagName);
if (isDuplicate) {
errorMsg.value = '该标签已存在,请勿重复添加';
return;
}
// 3. 提交添加
try {
isAdding.value = true;
// 替换为你的实际接口地址
const response = await fetch('/api/tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify({ name: tagName })
});
const result = await response.json();
if (response.ok && result.code === 200) {
// 添加成功,更新列表
tagList.value.push(result.data);
tagInput.value = ''; // 清空输入框
Message.success('标签添加成功');
} else {
throw new Error(result.message || '添加标签失败');
}
} catch (error) {
console.error('添加标签失败:', error);
errorMsg.value = error.message || '添加标签失败';
Message.error(error.message || '添加标签失败');
} finally {
isAdding.value = false;
}
};
// 处理删除标签
const handleDeleteTag = async (tagId, index) => {
// 确认删除
const modal = new PopupModal({
title: '确认删除',
content: `
<div class="flex gap-3">
<i class="fa fa-exclamation-triangle text-warning text-xl mt-1"></i>
<div>
<p>确定要删除这个标签吗?</p>
<p class="mt-1 text-secondary text-sm">删除后无法恢复,请谨慎操作</p>
</div>
</div>
`,
buttons: [
{
text: '取消',
type: 'default',
callback: (modal) => modal.close()
},
{
text: '确认删除',
type: 'danger',
callback: async (modal) => {
modal.close()
await deleteAsync(tagId, index)
}
}
],
maskClose: true
})
modal.open()
};
const deleteAsync = async (tagId, index) => {
try {
isDeleting.value = true;
// 替换为你的实际接口地址
const response = await fetch(`/api/tags/${tagId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
}
});
const result = await response.json();
if (response.ok && result.code === 200) {
// 删除成功,更新列表
tagList.value.splice(index, 1);
Message.success('标签删除成功');
} else {
throw new Error(result.message || '删除标签失败');
}
} catch (error) {
console.error('删除标签失败:', error);
Message.error(error.message || '删除标签失败');
} finally {
isDeleting.value = false;
}
}
</script>
+10 -22
View File
@@ -10,36 +10,23 @@ require (
github.com/chai2010/webp v1.4.0
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-contrib/sessions v1.0.4
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
gorm.io/driver/mysql v1.5.2
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.4
)
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
require (
github.com/gobuffalo/logger v1.0.3 // indirect
github.com/gobuffalo/packd v1.0.0 // indirect
github.com/gobuffalo/packr/v2 v2.8.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/karrick/godirwalk v1.15.3 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/markbates/errx v1.1.0 // indirect
github.com/markbates/oncer v1.0.0 // indirect
github.com/markbates/safe v1.0.1 // indirect
github.com/mjibson/esc v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/spf13/cobra v0.0.6 // indirect
github.com/spf13/pflag v1.0.3 // indirect
golang.org/x/term v0.38.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
)
require (
@@ -73,7 +60,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
@@ -85,7 +72,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -104,4 +91,5 @@ require (
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gorm.io/driver/postgres v1.5.2
)
+51 -190
View File
@@ -1,11 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
@@ -44,78 +39,58 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfx
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc=
github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM=
github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM=
github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zNVIY=
github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -123,24 +98,26 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0+incompatible/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -149,127 +126,62 @@ github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.15.3 h1:0a2pXOgtB16CqIqXTiT7+K9L73f74n/aNQUnH6Ortew=
github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3+incompatible/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mjibson/esc v0.2.0 h1:k96hdaR9Z+nMcnDwNrOvhdBqtjyMrbVyxLpsRCdP2mA=
github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
@@ -277,92 +189,41 @@ golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWI
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=