## 数据完整性修复 (fix-critical-data-integrity) - 添加 error_translate.go 统一错误转换层 - 修复 nil 输入和 NotFound 错误处理 - 增强仓储层错误一致性 ## 仓储一致性修复 (fix-high-repository-consistency) - Group schema 添加 default_validity_days 字段 - Account schema 添加 proxy edge 关联 - 新增 UsageLog ent schema 定义 - 修复 UpdateBalance/UpdateConcurrency 受影响行数校验 ## 数据卫生修复 (fix-medium-data-hygiene) - UserSubscription 添加软删除支持 (SoftDeleteMixin) - RedeemCode/Setting 添加硬删除策略文档 - account_groups/user_allowed_groups 的 created_at 声明 timestamptz - 停止写入 legacy users.allowed_groups 列 - 新增迁移: 011-014 (索引优化、软删除、孤立数据审计、列清理) ## 测试补充 - 添加 UserSubscription 软删除测试 - 添加迁移回归测试 - 添加 NotFound 错误测试 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
7.1 KiB
Go
269 lines
7.1 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"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"
|
|
)
|
|
|
|
type sqlQuerier interface {
|
|
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
|
}
|
|
|
|
type proxyRepository struct {
|
|
client *dbent.Client
|
|
sql sqlQuerier
|
|
}
|
|
|
|
func NewProxyRepository(client *dbent.Client, sqlDB *sql.DB) service.ProxyRepository {
|
|
return newProxyRepositoryWithSQL(client, sqlDB)
|
|
}
|
|
|
|
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 {
|
|
applyProxyEntityToService(proxyIn, created)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *proxyRepository) GetByID(ctx context.Context, id int64) (*service.Proxy, error) {
|
|
m, err := r.client.Proxy.Get(ctx, id)
|
|
if err != nil {
|
|
if dbent.IsNotFound(err) {
|
|
return nil, service.ErrProxyNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return proxyEntityToService(m), nil
|
|
}
|
|
|
|
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 {
|
|
applyProxyEntityToService(proxyIn, updated)
|
|
return nil
|
|
}
|
|
if dbent.IsNotFound(err) {
|
|
return service.ErrProxyNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *proxyRepository) Delete(ctx context.Context, id int64) 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) {
|
|
return r.ListWithFilters(ctx, params, "", "", "")
|
|
}
|
|
|
|
// 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) {
|
|
q := r.client.Proxy.Query()
|
|
if protocol != "" {
|
|
q = q.Where(proxy.ProtocolEQ(protocol))
|
|
}
|
|
if status != "" {
|
|
q = q.Where(proxy.StatusEQ(status))
|
|
}
|
|
if search != "" {
|
|
q = q.Where(proxy.NameContainsFold(search))
|
|
}
|
|
|
|
total, err := q.Count(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
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, *proxyEntityToService(proxies[i]))
|
|
}
|
|
|
|
return outProxies, paginationResultFromTotal(int64(total), params), nil
|
|
}
|
|
|
|
func (r *proxyRepository) ListActive(ctx context.Context) ([]service.Proxy, 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, *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) {
|
|
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))
|
|
}
|
|
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) {
|
|
var count int64
|
|
if err := scanSingleRow(ctx, r.sql, "SELECT COUNT(*) FROM accounts WHERE proxy_id = $1", []any{proxyID}, &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) (counts map[int64]int64, err error) {
|
|
rows, err := r.sql.QueryContext(ctx, "SELECT proxy_id, COUNT(*) AS count FROM accounts WHERE proxy_id IS NOT NULL AND deleted_at IS NULL GROUP BY proxy_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if closeErr := rows.Close(); closeErr != nil && err == nil {
|
|
err = closeErr
|
|
counts = nil
|
|
}
|
|
}()
|
|
|
|
counts = make(map[int64]int64)
|
|
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) {
|
|
proxies, err := r.client.Proxy.Query().
|
|
Where(proxy.StatusEQ(service.StatusActive)).
|
|
Order(dbent.Desc(proxy.FieldCreatedAt)).
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get account counts
|
|
counts, err := r.GetAccountCountsForProxies(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build result with account counts
|
|
result := make([]service.ProxyWithAccountCount, 0, len(proxies))
|
|
for i := range proxies {
|
|
proxyOut := proxyEntityToService(proxies[i])
|
|
if proxyOut == nil {
|
|
continue
|
|
}
|
|
result = append(result, service.ProxyWithAccountCount{
|
|
Proxy: *proxyOut,
|
|
AccountCount: counts[proxyOut.ID],
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func proxyEntityToService(m *dbent.Proxy) *service.Proxy {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := &service.Proxy{
|
|
ID: m.ID,
|
|
Name: m.Name,
|
|
Protocol: m.Protocol,
|
|
Host: m.Host,
|
|
Port: m.Port,
|
|
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 applyProxyEntityToService(dst *service.Proxy, src *dbent.Proxy) {
|
|
if dst == nil || src == nil {
|
|
return
|
|
}
|
|
dst.ID = src.ID
|
|
dst.CreatedAt = src.CreatedAt
|
|
dst.UpdatedAt = src.UpdatedAt
|
|
}
|