opt: 优化数据库连接池,配置管理,错误处理

This commit is contained in:
Kerwin
2025-10-27 18:21:59 +08:00
parent 013fe71925
commit 40e3350a4b
12 changed files with 1902 additions and 41 deletions

View File

@@ -2,7 +2,9 @@ package db
import (
"fmt"
"log"
"os"
"strconv"
"time"
"github.com/ctwj/urldb/db/entity"
@@ -45,8 +47,22 @@ func InitDB() error {
host, port, user, password, dbname)
var err error
// 配置慢查询日志
slowThreshold := getEnvInt("DB_SLOW_THRESHOLD_MS", 200)
logLevel := logger.Info
if os.Getenv("ENV") == "production" {
logLevel = logger.Warn
}
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Duration(slowThreshold) * time.Millisecond,
LogLevel: logLevel,
Colorful: true,
},
),
})
if err != nil {
return err
@@ -58,10 +74,17 @@ func InitDB() error {
return err
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
// 优化数据库连接池参数
maxOpenConns := getEnvInt("DB_MAX_OPEN_CONNS", 50)
maxIdleConns := getEnvInt("DB_MAX_IDLE_CONNS", 20)
connMaxLifetime := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 30)
sqlDB.SetMaxOpenConns(maxOpenConns) // 最大打开连接数
sqlDB.SetMaxIdleConns(maxIdleConns) // 最大空闲连接数
sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Minute) // 连接最大生命周期
utils.Info("数据库连接池配置 - 最大连接: %d, 空闲连接: %d, 生命周期: %d分钟",
maxOpenConns, maxIdleConns, connMaxLifetime)
// 检查是否需要迁移(只在开发环境或首次启动时)
if shouldRunMigration() {
@@ -300,3 +323,19 @@ func insertDefaultDataIfEmpty() error {
utils.Info("默认数据插入完成")
return nil
}
// getEnvInt 获取环境变量中的整数值,如果不存在则返回默认值
func getEnvInt(key string, defaultValue int) int {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
intValue, err := strconv.Atoi(value)
if err != nil {
utils.Warn("环境变量 %s 的值 '%s' 不是有效的整数,使用默认值 %d", key, value, defaultValue)
return defaultValue
}
return intValue
}

114
db/repo/pagination.go Normal file
View File

@@ -0,0 +1,114 @@
package repo
import (
"gorm.io/gorm"
)
// PaginationResult 分页查询结果
type PaginationResult[T any] struct {
Data []T `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// PaginationOptions 分页查询选项
type PaginationOptions struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
OrderBy string `json:"order_by"`
OrderDir string `json:"order_dir"` // asc or desc
Preloads []string `json:"preloads"` // 需要预加载的关联
Filters map[string]interface{} `json:"filters"` // 过滤条件
}
// DefaultPaginationOptions 默认分页选项
func DefaultPaginationOptions() *PaginationOptions {
return &PaginationOptions{
Page: 1,
PageSize: 20,
OrderBy: "id",
OrderDir: "desc",
Preloads: []string{},
Filters: make(map[string]interface{}),
}
}
// PaginatedQuery 通用分页查询函数
func PaginatedQuery[T any](db *gorm.DB, options *PaginationOptions) (*PaginationResult[T], error) {
// 验证分页参数
if options.Page < 1 {
options.Page = 1
}
if options.PageSize < 1 || options.PageSize > 1000 {
options.PageSize = 20
}
// 应用预加载
query := db.Model(new(T))
for _, preload := range options.Preloads {
query = query.Preload(preload)
}
// 应用过滤条件
for key, value := range options.Filters {
// 处理特殊过滤条件
switch key {
case "search":
// 搜索条件需要特殊处理
if searchStr, ok := value.(string); ok && searchStr != "" {
query = query.Where("title ILIKE ? OR description ILIKE ?", "%"+searchStr+"%", "%"+searchStr+"%")
}
case "category_id":
if categoryID, ok := value.(uint); ok {
query = query.Where("category_id = ?", categoryID)
}
case "pan_id":
if panID, ok := value.(uint); ok {
query = query.Where("pan_id = ?", panID)
}
case "is_valid":
if isValid, ok := value.(bool); ok {
query = query.Where("is_valid = ?", isValid)
}
case "is_public":
if isPublic, ok := value.(bool); ok {
query = query.Where("is_public = ?", isPublic)
}
default:
// 通用过滤条件
query = query.Where(key+" = ?", value)
}
}
// 应用排序
orderClause := options.OrderBy + " " + options.OrderDir
query = query.Order(orderClause)
// 计算偏移量
offset := (options.Page - 1) * options.PageSize
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, err
}
// 查询数据
var data []T
if err := query.Offset(offset).Limit(options.PageSize).Find(&data).Error; err != nil {
return nil, err
}
// 计算总页数
totalPages := int((total + int64(options.PageSize) - 1) / int64(options.PageSize))
return &PaginationResult[T]{
Data: data,
Total: total,
Page: options.Page,
PageSize: options.PageSize,
TotalPages: totalPages,
}, nil
}

View File

@@ -68,38 +68,21 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
// FindWithRelationsPaginated 分页查找包含关联关系的资源
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
var resources []entity.Resource
var total int64
offset := (page - 1) * limit
// 优化查询:只预加载必要的关联,并添加排序
db := r.db.Model(&entity.Resource{}).
Preload("Category").
Preload("Pan").
Order("updated_at DESC") // 按更新时间倒序,显示最新内容
// 获取总数(使用缓存键)
cacheKey := fmt.Sprintf("resources_total_%d_%d", page, limit)
if cached, exists := r.cache[cacheKey]; exists {
if totalCached, ok := cached.(int64); ok {
total = totalCached
}
} else {
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 缓存总数5分钟
r.cache[cacheKey] = total
go func() {
time.Sleep(5 * time.Minute)
delete(r.cache, cacheKey)
}()
// 使用新的分页查询功能
options := &PaginationOptions{
Page: page,
PageSize: limit,
OrderBy: "updated_at",
OrderDir: "desc",
Preloads: []string{"Category", "Pan"},
}
// 获取分页数据
err := db.Offset(offset).Limit(limit).Find(&resources).Error
return resources, total, err
result, err := PaginatedQuery[entity.Resource](r.db, options)
if err != nil {
return nil, 0, err
}
return result.Data, result.Total, nil
}
// FindByCategoryID 根据分类ID查找