Files
backend_v2/internal/database/postgres.go
toom1996 f7ae917603 update
2026-09-17 22:51:00 +08:00

158 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package database 负责数据库连接的建立与生命周期管理。
//
// 这里刻意不提供包级全局 DB 变量:*gorm.DB 由 main 装配后显式注入各 repository,
// 依赖关系清晰可见,也让 repository 可以在测试中替换为独立的数据库实例。
//
// 数据库已迁移至 PostgreSQL(见 scripts/pgvector 的本地 Docker 环境)。
// 表结构由 GORM AutoMigrate 在启动时幂等创建,不再依赖手写的 MySQL 迁移脚本。
package database
import (
"fmt"
"time"
"fashionapi/internal/config"
"fashionapi/internal/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// New 建立 PostgreSQL 连接并完成连接池设置。
func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(cfg.DSN()), &gorm.Config{
Logger: logger.Default.LogMode(parseLogLevel(cfg.LogLevel)),
// 关闭默认事务可显著提升只读接口的吞吐;写操作按需显式开启事务
SkipDefaultTransaction: true,
})
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取底层连接池失败: %w", err)
}
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
// 空闲回收:定期关闭空闲连接,避免持有被服务端回收的死连接。
// 仅当配置 > 0 时启用(0 = 沿用旧行为,不回收)。
if cfg.ConnMaxIdleTime > 0 {
sqlDB.SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTime) * time.Second)
}
// 立刻探活,避免"启动成功但首个请求才报错"
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("数据库 Ping 失败: %w", err)
}
return db, nil
}
// AutoMigrate 幂等创建 / 更新全部表结构。
//
// 表结构由 GORM 模型定义托管(替代原 scripts/sql、db/migrations 下的手写 MySQL 脚本)。
// 仅在缺少列/索引时增量变更,已存在的表不会被重建;对存量大表(如 brand_runway_images)
// 的加列操作可能短暂加锁,属一次性开销。
func AutoMigrate(db *gorm.DB) error {
// 确保 pgvector 扩展存在(dHash 近重复 HNSW 索引与语义 embedding 向量列均依赖它)。
if err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector").Error; err != nil {
return fmt.Errorf("create extension vector: %w", err)
}
return db.AutoMigrate(
&model.Brand{},
&model.User{},
&model.RefreshToken{},
&model.IngestNonce{},
&model.IngestJob{},
&model.StreetSnap{},
&model.StreetSnapImage{},
&model.StreetSnapDraft{},
&model.StreetSnapDraftImage{},
&model.Favorite{},
&model.History{},
&model.BrandRunway{},
&model.BrandRunwayImage{},
&model.BrandRunwayDraft{},
&model.BrandRunwayDraftImage{},
)
}
// ensureDedupSchema 在 GORM AutoMigrate 之外,补充去重所需的索引与语义 embedding 表。
// 全部幂等(IF NOT EXISTS),可随服务启动重复执行。
//
// - 各 image 表的 content_sha1 部分唯一索引(排除迁移前遗留的空串行,避免唯一约束冲突);
// - phash 的 HNSW 索引(bit_hamming_ops,汉明距离近邻检索,替代原先的全表扫描);
// - image_embeddings 表 + cosine HNSW 索引:语义 embedding(CLIP/DINOv2)落库位,
// 待后续接入推理填充,当前不写入。
func ensureDedupSchema(db *gorm.DB) error {
tables := []struct {
name string
idx string
}{
{"brand_runway_images", "uq_br_imgs_sha1"},
{"brand_runway_draft_images", "uq_br_draft_imgs_sha1"},
{"street_snap_images", "uq_ss_imgs_sha1"},
{"street_snap_draft_images", "uq_ss_draft_imgs_sha1"},
}
for _, t := range tables {
// 精确去重:content_sha1 唯一(排除空串遗留行)。
if err := db.Exec(fmt.Sprintf(
"CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (content_sha1) WHERE content_sha1 <> ''",
t.idx, t.name,
)).Error; err != nil {
return fmt.Errorf("create unique index %s: %w", t.idx, err)
}
// 近重复检索:dHash 向量(vector(64) 二进制)汉明 HNSW 索引。
if err := db.Exec(fmt.Sprintf(
"CREATE INDEX IF NOT EXISTS %s_phash_hnsw ON %s USING hnsw (phash bit_hamming_ops)",
t.idx, t.name,
)).Error; err != nil {
return fmt.Errorf("create hnsw index %s: %w", t.idx, err)
}
}
// 语义 embedding 表(CLIP/DINOv2 等 512 维向量):待后续推理接入填充;当前不写入。
if err := db.Exec(`
CREATE TABLE IF NOT EXISTS image_embeddings (
id BIGSERIAL PRIMARY KEY,
image_id INTEGER NOT NULL,
kind SMALLINT NOT NULL,
embedding vector(512),
created_at INTEGER NOT NULL DEFAULT 0
)`).Error; err != nil {
return fmt.Errorf("create image_embeddings: %w", err)
}
if err := db.Exec(
"CREATE INDEX IF NOT EXISTS image_embeddings_vec_idx ON image_embeddings USING hnsw (embedding vector_cosine_ops)",
).Error; err != nil {
return fmt.Errorf("create embeddings hnsw: %w", err)
}
return nil
}
// Close 关闭数据库连接池。
func Close(db *gorm.DB) error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func parseLogLevel(s string) logger.LogLevel {
switch s {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "info":
return logger.Info
default:
return logger.Warn
}
}