refactor(数据库): 迁移持久层到 Ent 并清理 GORM
将仓储层/基础设施改为 Ent + 原生 SQL 执行路径,并移除 AutoMigrate 与 GORM 依赖。 重构内容包括: - 仓储层改用 Ent/SQL(含 usage_log/account 等复杂查询),统一错误映射 - 基础设施与 setup 初始化切换为 Ent + SQL migrations - 集成测试与 fixtures 迁移到 Ent 事务模型 - 清理遗留 GORM 模型/依赖,补充迁移与文档说明 - 增加根目录 Makefile 便于前后端编译 测试: - go test -tags unit ./... - go test -tags integration ./...
This commit is contained in:
@@ -2,52 +2,97 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"database/sql"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type sqlQuerier interface {
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||
}
|
||||
|
||||
type proxyRepository struct {
|
||||
db *gorm.DB
|
||||
client *dbent.Client
|
||||
sql sqlQuerier
|
||||
}
|
||||
|
||||
func NewProxyRepository(db *gorm.DB) service.ProxyRepository {
|
||||
return &proxyRepository{db: db}
|
||||
func NewProxyRepository(client *dbent.Client, sqlDB *sql.DB) service.ProxyRepository {
|
||||
return newProxyRepositoryWithSQL(client, sqlDB)
|
||||
}
|
||||
|
||||
func (r *proxyRepository) Create(ctx context.Context, proxy *service.Proxy) error {
|
||||
m := proxyModelFromService(proxy)
|
||||
err := r.db.WithContext(ctx).Create(m).Error
|
||||
func newProxyRepositoryWithSQL(client *dbent.Client, sqlq sqlQuerier) *proxyRepository {
|
||||
return &proxyRepository{client: client, sql: sqlq}
|
||||
}
|
||||
|
||||
func (r *proxyRepository) Create(ctx context.Context, proxyIn *service.Proxy) error {
|
||||
builder := r.client.Proxy.Create().
|
||||
SetName(proxyIn.Name).
|
||||
SetProtocol(proxyIn.Protocol).
|
||||
SetHost(proxyIn.Host).
|
||||
SetPort(proxyIn.Port).
|
||||
SetStatus(proxyIn.Status)
|
||||
if proxyIn.Username != "" {
|
||||
builder.SetUsername(proxyIn.Username)
|
||||
}
|
||||
if proxyIn.Password != "" {
|
||||
builder.SetPassword(proxyIn.Password)
|
||||
}
|
||||
|
||||
created, err := builder.Save(ctx)
|
||||
if err == nil {
|
||||
applyProxyModelToService(proxy, m)
|
||||
applyProxyEntityToService(proxyIn, created)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *proxyRepository) GetByID(ctx context.Context, id int64) (*service.Proxy, error) {
|
||||
var m proxyModel
|
||||
err := r.db.WithContext(ctx).First(&m, id).Error
|
||||
m, err := r.client.Proxy.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrProxyNotFound, nil)
|
||||
if dbent.IsNotFound(err) {
|
||||
return nil, service.ErrProxyNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return proxyModelToService(&m), nil
|
||||
return proxyEntityToService(m), nil
|
||||
}
|
||||
|
||||
func (r *proxyRepository) Update(ctx context.Context, proxy *service.Proxy) error {
|
||||
m := proxyModelFromService(proxy)
|
||||
err := r.db.WithContext(ctx).Save(m).Error
|
||||
func (r *proxyRepository) Update(ctx context.Context, proxyIn *service.Proxy) error {
|
||||
builder := r.client.Proxy.UpdateOneID(proxyIn.ID).
|
||||
SetName(proxyIn.Name).
|
||||
SetProtocol(proxyIn.Protocol).
|
||||
SetHost(proxyIn.Host).
|
||||
SetPort(proxyIn.Port).
|
||||
SetStatus(proxyIn.Status)
|
||||
if proxyIn.Username != "" {
|
||||
builder.SetUsername(proxyIn.Username)
|
||||
} else {
|
||||
builder.ClearUsername()
|
||||
}
|
||||
if proxyIn.Password != "" {
|
||||
builder.SetPassword(proxyIn.Password)
|
||||
} else {
|
||||
builder.ClearPassword()
|
||||
}
|
||||
|
||||
updated, err := builder.Save(ctx)
|
||||
if err == nil {
|
||||
applyProxyModelToService(proxy, m)
|
||||
applyProxyEntityToService(proxyIn, updated)
|
||||
return nil
|
||||
}
|
||||
if dbent.IsNotFound(err) {
|
||||
return service.ErrProxyNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *proxyRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&proxyModel{}, id).Error
|
||||
_, err := r.client.Proxy.Delete().Where(proxy.IDEQ(id)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *proxyRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.Proxy, *pagination.PaginationResult, error) {
|
||||
@@ -56,104 +101,111 @@ func (r *proxyRepository) List(ctx context.Context, params pagination.Pagination
|
||||
|
||||
// ListWithFilters lists proxies with optional filtering by protocol, status, and search query
|
||||
func (r *proxyRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.Proxy, *pagination.PaginationResult, error) {
|
||||
var proxies []proxyModel
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&proxyModel{})
|
||||
|
||||
// Apply filters
|
||||
q := r.client.Proxy.Query()
|
||||
if protocol != "" {
|
||||
db = db.Where("protocol = ?", protocol)
|
||||
q = q.Where(proxy.ProtocolEQ(protocol))
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
q = q.Where(proxy.StatusEQ(status))
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("name ILIKE ?", searchPattern)
|
||||
q = q.Where(proxy.NameContainsFold(search))
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&proxies).Error; err != nil {
|
||||
proxies, err := q.
|
||||
Offset(params.Offset()).
|
||||
Limit(params.Limit()).
|
||||
Order(dbent.Desc(proxy.FieldID)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
outProxies := make([]service.Proxy, 0, len(proxies))
|
||||
for i := range proxies {
|
||||
outProxies = append(outProxies, *proxyModelToService(&proxies[i]))
|
||||
outProxies = append(outProxies, *proxyEntityToService(proxies[i]))
|
||||
}
|
||||
|
||||
return outProxies, paginationResultFromTotal(total, params), nil
|
||||
return outProxies, paginationResultFromTotal(int64(total), params), nil
|
||||
}
|
||||
|
||||
func (r *proxyRepository) ListActive(ctx context.Context) ([]service.Proxy, error) {
|
||||
var proxies []proxyModel
|
||||
err := r.db.WithContext(ctx).Where("status = ?", service.StatusActive).Find(&proxies).Error
|
||||
proxies, err := r.client.Proxy.Query().
|
||||
Where(proxy.StatusEQ(service.StatusActive)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outProxies := make([]service.Proxy, 0, len(proxies))
|
||||
for i := range proxies {
|
||||
outProxies = append(outProxies, *proxyModelToService(&proxies[i]))
|
||||
outProxies = append(outProxies, *proxyEntityToService(proxies[i]))
|
||||
}
|
||||
return outProxies, nil
|
||||
}
|
||||
|
||||
// ExistsByHostPortAuth checks if a proxy with the same host, port, username, and password exists
|
||||
func (r *proxyRepository) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&proxyModel{}).
|
||||
Where("host = ? AND port = ? AND username = ? AND password = ?", host, port, username, password).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
q := r.client.Proxy.Query().
|
||||
Where(proxy.HostEQ(host), proxy.PortEQ(port))
|
||||
|
||||
if username == "" {
|
||||
q = q.Where(proxy.Or(proxy.UsernameIsNil(), proxy.UsernameEQ("")))
|
||||
} else {
|
||||
q = q.Where(proxy.UsernameEQ(username))
|
||||
}
|
||||
return count > 0, nil
|
||||
if password == "" {
|
||||
q = q.Where(proxy.Or(proxy.PasswordIsNil(), proxy.PasswordEQ("")))
|
||||
} else {
|
||||
q = q.Where(proxy.PasswordEQ(password))
|
||||
}
|
||||
|
||||
count, err := q.Count(ctx)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// CountAccountsByProxyID returns the number of accounts using a specific proxy
|
||||
func (r *proxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
|
||||
row := r.sql.QueryRowContext(ctx, "SELECT COUNT(*) FROM accounts WHERE proxy_id = $1", proxyID)
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Table("accounts").
|
||||
Where("proxy_id = ?", proxyID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetAccountCountsForProxies returns a map of proxy ID to account count for all proxies
|
||||
func (r *proxyRepository) GetAccountCountsForProxies(ctx context.Context) (map[int64]int64, error) {
|
||||
type result struct {
|
||||
ProxyID int64 `gorm:"column:proxy_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
var results []result
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("accounts").
|
||||
Select("proxy_id, COUNT(*) as count").
|
||||
Where("proxy_id IS NOT NULL").
|
||||
Group("proxy_id").
|
||||
Scan(&results).Error
|
||||
rows, err := r.sql.QueryContext(ctx, "SELECT proxy_id, COUNT(*) AS count FROM accounts WHERE proxy_id IS NOT NULL GROUP BY proxy_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
counts := make(map[int64]int64)
|
||||
for _, r := range results {
|
||||
counts[r.ProxyID] = r.Count
|
||||
for rows.Next() {
|
||||
var proxyID, count int64
|
||||
if err := rows.Scan(&proxyID, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts[proxyID] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// ListActiveWithAccountCount returns all active proxies with account count, sorted by creation time descending
|
||||
func (r *proxyRepository) ListActiveWithAccountCount(ctx context.Context) ([]service.ProxyWithAccountCount, error) {
|
||||
var proxies []proxyModel
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ?", service.StatusActive).
|
||||
Order("created_at DESC").
|
||||
Find(&proxies).Error
|
||||
proxies, err := r.client.Proxy.Query().
|
||||
Where(proxy.StatusEQ(service.StatusActive)).
|
||||
Order(dbent.Desc(proxy.FieldCreatedAt)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -167,76 +219,47 @@ func (r *proxyRepository) ListActiveWithAccountCount(ctx context.Context) ([]ser
|
||||
// Build result with account counts
|
||||
result := make([]service.ProxyWithAccountCount, 0, len(proxies))
|
||||
for i := range proxies {
|
||||
proxy := proxyModelToService(&proxies[i])
|
||||
if proxy == nil {
|
||||
proxyOut := proxyEntityToService(proxies[i])
|
||||
if proxyOut == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, service.ProxyWithAccountCount{
|
||||
Proxy: *proxy,
|
||||
AccountCount: counts[proxy.ID],
|
||||
Proxy: *proxyOut,
|
||||
AccountCount: counts[proxyOut.ID],
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type proxyModel struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100;not null"`
|
||||
Protocol string `gorm:"size:20;not null"`
|
||||
Host string `gorm:"size:255;not null"`
|
||||
Port int `gorm:"not null"`
|
||||
Username string `gorm:"size:100"`
|
||||
Password string `gorm:"size:100"`
|
||||
Status string `gorm:"size:20;default:active;not null"`
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
UpdatedAt time.Time `gorm:"not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (proxyModel) TableName() string { return "proxies" }
|
||||
|
||||
func proxyModelToService(m *proxyModel) *service.Proxy {
|
||||
func proxyEntityToService(m *dbent.Proxy) *service.Proxy {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &service.Proxy{
|
||||
out := &service.Proxy{
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
Protocol: m.Protocol,
|
||||
Host: m.Host,
|
||||
Port: m.Port,
|
||||
Username: m.Username,
|
||||
Password: m.Password,
|
||||
Status: m.Status,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
if m.Username != nil {
|
||||
out.Username = *m.Username
|
||||
}
|
||||
if m.Password != nil {
|
||||
out.Password = *m.Password
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func proxyModelFromService(p *service.Proxy) *proxyModel {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &proxyModel{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Protocol: p.Protocol,
|
||||
Host: p.Host,
|
||||
Port: p.Port,
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
Status: p.Status,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func applyProxyModelToService(proxy *service.Proxy, m *proxyModel) {
|
||||
if proxy == nil || m == nil {
|
||||
func applyProxyEntityToService(dst *service.Proxy, src *dbent.Proxy) {
|
||||
if dst == nil || src == nil {
|
||||
return
|
||||
}
|
||||
proxy.ID = m.ID
|
||||
proxy.CreatedAt = m.CreatedAt
|
||||
proxy.UpdatedAt = m.UpdatedAt
|
||||
dst.ID = src.ID
|
||||
dst.CreatedAt = src.CreatedAt
|
||||
dst.UpdatedAt = src.UpdatedAt
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user