First commit
This commit is contained in:
268
backend/internal/repository/account_repo.go
Normal file
268
backend/internal/repository/account_repo.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountRepository(db *gorm.DB) *AccountRepository {
|
||||
return &AccountRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *AccountRepository) Create(ctx context.Context, account *model.Account) error {
|
||||
return r.db.WithContext(ctx).Create(account).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) GetByID(ctx context.Context, id int64) (*model.Account, error) {
|
||||
var account model.Account
|
||||
err := r.db.WithContext(ctx).Preload("Proxy").Preload("AccountGroups").First(&account, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 填充 GroupIDs 虚拟字段
|
||||
account.GroupIDs = make([]int64, 0, len(account.AccountGroups))
|
||||
for _, ag := range account.AccountGroups {
|
||||
account.GroupIDs = append(account.GroupIDs, ag.GroupID)
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (r *AccountRepository) Update(ctx context.Context, account *model.Account) error {
|
||||
return r.db.WithContext(ctx).Save(account).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) Delete(ctx context.Context, id int64) error {
|
||||
// 先删除账号与分组的绑定关系
|
||||
if err := r.db.WithContext(ctx).Where("account_id = ?", id).Delete(&model.AccountGroup{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 再删除账号
|
||||
return r.db.WithContext(ctx).Delete(&model.Account{}, id).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) List(ctx context.Context, params PaginationParams) ([]model.Account, *PaginationResult, error) {
|
||||
return r.ListWithFilters(ctx, params, "", "", "", "")
|
||||
}
|
||||
|
||||
// ListWithFilters lists accounts with optional filtering by platform, type, status, and search query
|
||||
func (r *AccountRepository) ListWithFilters(ctx context.Context, params PaginationParams, platform, accountType, status, search string) ([]model.Account, *PaginationResult, error) {
|
||||
var accounts []model.Account
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.Account{})
|
||||
|
||||
// Apply filters
|
||||
if platform != "" {
|
||||
db = db.Where("platform = ?", platform)
|
||||
}
|
||||
if accountType != "" {
|
||||
db = db.Where("type = ?", accountType)
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("name ILIKE ?", searchPattern)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Preload("Proxy").Preload("AccountGroups").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&accounts).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 填充每个 Account 的 GroupIDs 虚拟字段
|
||||
for i := range accounts {
|
||||
accounts[i].GroupIDs = make([]int64, 0, len(accounts[i].AccountGroups))
|
||||
for _, ag := range accounts[i].AccountGroups {
|
||||
accounts[i].GroupIDs = append(accounts[i].GroupIDs, ag.GroupID)
|
||||
}
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return accounts, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *AccountRepository) ListByGroup(ctx context.Context, groupID int64) ([]model.Account, error) {
|
||||
var accounts []model.Account
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN account_groups ON account_groups.account_id = accounts.id").
|
||||
Where("account_groups.group_id = ? AND accounts.status = ?", groupID, model.StatusActive).
|
||||
Preload("Proxy").
|
||||
Order("account_groups.priority ASC, accounts.priority ASC").
|
||||
Find(&accounts).Error
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
func (r *AccountRepository) ListActive(ctx context.Context) ([]model.Account, error) {
|
||||
var accounts []model.Account
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ?", model.StatusActive).
|
||||
Preload("Proxy").
|
||||
Order("priority ASC").
|
||||
Find(&accounts).Error
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
func (r *AccountRepository) UpdateLastUsed(ctx context.Context, id int64) error {
|
||||
now := time.Now()
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).Update("last_used_at", now).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) SetError(ctx context.Context, id int64, errorMsg string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": model.StatusError,
|
||||
"error_message": errorMsg,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) AddToGroup(ctx context.Context, accountID, groupID int64, priority int) error {
|
||||
ag := &model.AccountGroup{
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Priority: priority,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(ag).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) RemoveFromGroup(ctx context.Context, accountID, groupID int64) error {
|
||||
return r.db.WithContext(ctx).Where("account_id = ? AND group_id = ?", accountID, groupID).
|
||||
Delete(&model.AccountGroup{}).Error
|
||||
}
|
||||
|
||||
func (r *AccountRepository) GetGroups(ctx context.Context, accountID int64) ([]model.Group, error) {
|
||||
var groups []model.Group
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN account_groups ON account_groups.group_id = groups.id").
|
||||
Where("account_groups.account_id = ?", accountID).
|
||||
Find(&groups).Error
|
||||
return groups, err
|
||||
}
|
||||
|
||||
func (r *AccountRepository) ListByPlatform(ctx context.Context, platform string) ([]model.Account, error) {
|
||||
var accounts []model.Account
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("platform = ? AND status = ?", platform, model.StatusActive).
|
||||
Preload("Proxy").
|
||||
Order("priority ASC").
|
||||
Find(&accounts).Error
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
func (r *AccountRepository) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||
// 删除现有绑定
|
||||
if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Delete(&model.AccountGroup{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加新绑定
|
||||
if len(groupIDs) > 0 {
|
||||
accountGroups := make([]model.AccountGroup, 0, len(groupIDs))
|
||||
for i, groupID := range groupIDs {
|
||||
accountGroups = append(accountGroups, model.AccountGroup{
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Priority: i + 1, // 使用索引作为优先级
|
||||
})
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(&accountGroups).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSchedulable 获取所有可调度的账号
|
||||
func (r *AccountRepository) ListSchedulable(ctx context.Context) ([]model.Account, error) {
|
||||
var accounts []model.Account
|
||||
now := time.Now()
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ? AND schedulable = ?", model.StatusActive, true).
|
||||
Where("(overload_until IS NULL OR overload_until <= ?)", now).
|
||||
Where("(rate_limit_reset_at IS NULL OR rate_limit_reset_at <= ?)", now).
|
||||
Preload("Proxy").
|
||||
Order("priority ASC").
|
||||
Find(&accounts).Error
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
// ListSchedulableByGroupID 按组获取可调度的账号
|
||||
func (r *AccountRepository) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]model.Account, error) {
|
||||
var accounts []model.Account
|
||||
now := time.Now()
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN account_groups ON account_groups.account_id = accounts.id").
|
||||
Where("account_groups.group_id = ?", groupID).
|
||||
Where("accounts.status = ? AND accounts.schedulable = ?", model.StatusActive, true).
|
||||
Where("(accounts.overload_until IS NULL OR accounts.overload_until <= ?)", now).
|
||||
Where("(accounts.rate_limit_reset_at IS NULL OR accounts.rate_limit_reset_at <= ?)", now).
|
||||
Preload("Proxy").
|
||||
Order("account_groups.priority ASC, accounts.priority ASC").
|
||||
Find(&accounts).Error
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
// SetRateLimited 标记账号为限流状态(429)
|
||||
func (r *AccountRepository) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error {
|
||||
now := time.Now()
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"rate_limited_at": now,
|
||||
"rate_limit_reset_at": resetAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// SetOverloaded 标记账号为过载状态(529)
|
||||
func (r *AccountRepository) SetOverloaded(ctx context.Context, id int64, until time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||
Update("overload_until", until).Error
|
||||
}
|
||||
|
||||
// ClearRateLimit 清除账号的限流状态
|
||||
func (r *AccountRepository) ClearRateLimit(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"rate_limited_at": nil,
|
||||
"rate_limit_reset_at": nil,
|
||||
"overload_until": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateSessionWindow 更新账号的5小时时间窗口信息
|
||||
func (r *AccountRepository) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error {
|
||||
updates := map[string]interface{}{
|
||||
"session_window_status": status,
|
||||
}
|
||||
if start != nil {
|
||||
updates["session_window_start"] = start
|
||||
}
|
||||
if end != nil {
|
||||
updates["session_window_end"] = end
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// SetSchedulable 设置账号的调度开关
|
||||
func (r *AccountRepository) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||
Update("schedulable", schedulable).Error
|
||||
}
|
||||
149
backend/internal/repository/api_key_repo.go
Normal file
149
backend/internal/repository/api_key_repo.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApiKeyRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewApiKeyRepository(db *gorm.DB) *ApiKeyRepository {
|
||||
return &ApiKeyRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) Create(ctx context.Context, key *model.ApiKey) error {
|
||||
return r.db.WithContext(ctx).Create(key).Error
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) GetByID(ctx context.Context, id int64) (*model.ApiKey, error) {
|
||||
var key model.ApiKey
|
||||
err := r.db.WithContext(ctx).Preload("User").Preload("Group").First(&key, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) GetByKey(ctx context.Context, key string) (*model.ApiKey, error) {
|
||||
var apiKey model.ApiKey
|
||||
err := r.db.WithContext(ctx).Preload("User").Preload("Group").Where("key = ?", key).First(&apiKey).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apiKey, nil
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) Update(ctx context.Context, key *model.ApiKey) error {
|
||||
return r.db.WithContext(ctx).Model(key).Select("name", "group_id", "status", "updated_at").Updates(key).Error
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.ApiKey{}, id).Error
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) ListByUserID(ctx context.Context, userID int64, params PaginationParams) ([]model.ApiKey, *PaginationResult, error) {
|
||||
var keys []model.ApiKey
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("user_id = ?", userID)
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Preload("Group").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&keys).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return keys, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) ExistsByKey(ctx context.Context, key string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("key = ?", key).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *ApiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, params PaginationParams) ([]model.ApiKey, *PaginationResult, error) {
|
||||
var keys []model.ApiKey
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("group_id = ?", groupID)
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Preload("User").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&keys).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return keys, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchApiKeys searches API keys by user ID and/or keyword (name)
|
||||
func (r *ApiKeyRepository) SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error) {
|
||||
var keys []model.ApiKey
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.ApiKey{})
|
||||
|
||||
if userID > 0 {
|
||||
db = db.Where("user_id = ?", userID)
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
searchPattern := "%" + keyword + "%"
|
||||
db = db.Where("name ILIKE ?", searchPattern)
|
||||
}
|
||||
|
||||
if err := db.Limit(limit).Order("id DESC").Find(&keys).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// ClearGroupIDByGroupID 将指定分组的所有 API Key 的 group_id 设为 nil
|
||||
func (r *ApiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
result := r.db.WithContext(ctx).Model(&model.ApiKey{}).
|
||||
Where("group_id = ?", groupID).
|
||||
Update("group_id", nil)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// CountByGroupID 获取分组的 API Key 数量
|
||||
func (r *ApiKeyRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("group_id = ?", groupID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
137
backend/internal/repository/group_repo.go
Normal file
137
backend/internal/repository/group_repo.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GroupRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewGroupRepository(db *gorm.DB) *GroupRepository {
|
||||
return &GroupRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *GroupRepository) Create(ctx context.Context, group *model.Group) error {
|
||||
return r.db.WithContext(ctx).Create(group).Error
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GetByID(ctx context.Context, id int64) (*model.Group, error) {
|
||||
var group model.Group
|
||||
err := r.db.WithContext(ctx).First(&group, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (r *GroupRepository) Update(ctx context.Context, group *model.Group) error {
|
||||
return r.db.WithContext(ctx).Save(group).Error
|
||||
}
|
||||
|
||||
func (r *GroupRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.Group{}, id).Error
|
||||
}
|
||||
|
||||
func (r *GroupRepository) List(ctx context.Context, params PaginationParams) ([]model.Group, *PaginationResult, error) {
|
||||
return r.ListWithFilters(ctx, params, "", "", nil)
|
||||
}
|
||||
|
||||
// ListWithFilters lists groups with optional filtering by platform, status, and is_exclusive
|
||||
func (r *GroupRepository) ListWithFilters(ctx context.Context, params PaginationParams, platform, status string, isExclusive *bool) ([]model.Group, *PaginationResult, error) {
|
||||
var groups []model.Group
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.Group{})
|
||||
|
||||
// Apply filters
|
||||
if platform != "" {
|
||||
db = db.Where("platform = ?", platform)
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if isExclusive != nil {
|
||||
db = db.Where("is_exclusive = ?", *isExclusive)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id ASC").Find(&groups).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 获取每个分组的账号数量
|
||||
for i := range groups {
|
||||
count, _ := r.GetAccountCount(ctx, groups[i].ID)
|
||||
groups[i].AccountCount = count
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return groups, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *GroupRepository) ListActive(ctx context.Context) ([]model.Group, error) {
|
||||
var groups []model.Group
|
||||
err := r.db.WithContext(ctx).Where("status = ?", model.StatusActive).Order("id ASC").Find(&groups).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取每个分组的账号数量
|
||||
for i := range groups {
|
||||
count, _ := r.GetAccountCount(ctx, groups[i].ID)
|
||||
groups[i].AccountCount = count
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (r *GroupRepository) ListActiveByPlatform(ctx context.Context, platform string) ([]model.Group, error) {
|
||||
var groups []model.Group
|
||||
err := r.db.WithContext(ctx).Where("status = ? AND platform = ?", model.StatusActive, platform).Order("id ASC").Find(&groups).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取每个分组的账号数量
|
||||
for i := range groups {
|
||||
count, _ := r.GetAccountCount(ctx, groups[i].ID)
|
||||
groups[i].AccountCount = count
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (r *GroupRepository) ExistsByName(ctx context.Context, name string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Group{}).Where("name = ?", name).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GetAccountCount(ctx context.Context, groupID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.AccountGroup{}).Where("group_id = ?", groupID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DeleteAccountGroupsByGroupID 删除分组与账号的关联关系
|
||||
func (r *GroupRepository) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
result := r.db.WithContext(ctx).Where("group_id = ?", groupID).Delete(&model.AccountGroup{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// DB 返回底层数据库连接,用于事务处理
|
||||
func (r *GroupRepository) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
161
backend/internal/repository/proxy_repo.go
Normal file
161
backend/internal/repository/proxy_repo.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProxyRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProxyRepository(db *gorm.DB) *ProxyRepository {
|
||||
return &ProxyRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) Create(ctx context.Context, proxy *model.Proxy) error {
|
||||
return r.db.WithContext(ctx).Create(proxy).Error
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) GetByID(ctx context.Context, id int64) (*model.Proxy, error) {
|
||||
var proxy model.Proxy
|
||||
err := r.db.WithContext(ctx).First(&proxy, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proxy, nil
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) Update(ctx context.Context, proxy *model.Proxy) error {
|
||||
return r.db.WithContext(ctx).Save(proxy).Error
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.Proxy{}, id).Error
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) List(ctx context.Context, params PaginationParams) ([]model.Proxy, *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 PaginationParams, protocol, status, search string) ([]model.Proxy, *PaginationResult, error) {
|
||||
var proxies []model.Proxy
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.Proxy{})
|
||||
|
||||
// Apply filters
|
||||
if protocol != "" {
|
||||
db = db.Where("protocol = ?", protocol)
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("name ILIKE ?", searchPattern)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&proxies).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return proxies, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) ListActive(ctx context.Context) ([]model.Proxy, error) {
|
||||
var proxies []model.Proxy
|
||||
err := r.db.WithContext(ctx).Where("status = ?", model.StatusActive).Find(&proxies).Error
|
||||
return proxies, err
|
||||
}
|
||||
|
||||
// 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(&model.Proxy{}).
|
||||
Where("host = ? AND port = ? AND username = ? AND password = ?", host, port, username, password).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// CountAccountsByProxyID returns the number of accounts using a specific proxy
|
||||
func (r *ProxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Account{}).
|
||||
Where("proxy_id = ?", proxyID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 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).
|
||||
Model(&model.Account{}).
|
||||
Select("proxy_id, COUNT(*) as count").
|
||||
Where("proxy_id IS NOT NULL").
|
||||
Group("proxy_id").
|
||||
Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[int64]int64)
|
||||
for _, r := range results {
|
||||
counts[r.ProxyID] = r.Count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// ListActiveWithAccountCount returns all active proxies with account count, sorted by creation time descending
|
||||
func (r *ProxyRepository) ListActiveWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) {
|
||||
var proxies []model.Proxy
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ?", model.StatusActive).
|
||||
Order("created_at DESC").
|
||||
Find(&proxies).Error
|
||||
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([]model.ProxyWithAccountCount, len(proxies))
|
||||
for i, proxy := range proxies {
|
||||
result[i] = model.ProxyWithAccountCount{
|
||||
Proxy: proxy,
|
||||
AccountCount: counts[proxy.ID],
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
133
backend/internal/repository/redeem_code_repo.go
Normal file
133
backend/internal/repository/redeem_code_repo.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemCodeRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRedeemCodeRepository(db *gorm.DB) *RedeemCodeRepository {
|
||||
return &RedeemCodeRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) Create(ctx context.Context, code *model.RedeemCode) error {
|
||||
return r.db.WithContext(ctx).Create(code).Error
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) CreateBatch(ctx context.Context, codes []model.RedeemCode) error {
|
||||
return r.db.WithContext(ctx).Create(&codes).Error
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) GetByID(ctx context.Context, id int64) (*model.RedeemCode, error) {
|
||||
var code model.RedeemCode
|
||||
err := r.db.WithContext(ctx).First(&code, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &code, nil
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) GetByCode(ctx context.Context, code string) (*model.RedeemCode, error) {
|
||||
var redeemCode model.RedeemCode
|
||||
err := r.db.WithContext(ctx).Where("code = ?", code).First(&redeemCode).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &redeemCode, nil
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.RedeemCode{}, id).Error
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) List(ctx context.Context, params PaginationParams) ([]model.RedeemCode, *PaginationResult, error) {
|
||||
return r.ListWithFilters(ctx, params, "", "", "")
|
||||
}
|
||||
|
||||
// ListWithFilters lists redeem codes with optional filtering by type, status, and search query
|
||||
func (r *RedeemCodeRepository) ListWithFilters(ctx context.Context, params PaginationParams, codeType, status, search string) ([]model.RedeemCode, *PaginationResult, error) {
|
||||
var codes []model.RedeemCode
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.RedeemCode{})
|
||||
|
||||
// Apply filters
|
||||
if codeType != "" {
|
||||
db = db.Where("type = ?", codeType)
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("code ILIKE ?", searchPattern)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Preload("User").Preload("Group").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&codes).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return codes, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) Update(ctx context.Context, code *model.RedeemCode) error {
|
||||
return r.db.WithContext(ctx).Save(code).Error
|
||||
}
|
||||
|
||||
func (r *RedeemCodeRepository) Use(ctx context.Context, id, userID int64) error {
|
||||
now := time.Now()
|
||||
result := r.db.WithContext(ctx).Model(&model.RedeemCode{}).
|
||||
Where("id = ? AND status = ?", id, model.StatusUnused).
|
||||
Updates(map[string]interface{}{
|
||||
"status": model.StatusUsed,
|
||||
"used_by": userID,
|
||||
"used_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound // 兑换码不存在或已被使用
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListByUser returns all redeem codes used by a specific user
|
||||
func (r *RedeemCodeRepository) ListByUser(ctx context.Context, userID int64, limit int) ([]model.RedeemCode, error) {
|
||||
var codes []model.RedeemCode
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("used_by = ?", userID).
|
||||
Order("used_at DESC").
|
||||
Limit(limit).
|
||||
Find(&codes).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
74
backend/internal/repository/repository.go
Normal file
74
backend/internal/repository/repository.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Repositories 所有仓库的集合
|
||||
type Repositories struct {
|
||||
User *UserRepository
|
||||
ApiKey *ApiKeyRepository
|
||||
Group *GroupRepository
|
||||
Account *AccountRepository
|
||||
Proxy *ProxyRepository
|
||||
RedeemCode *RedeemCodeRepository
|
||||
UsageLog *UsageLogRepository
|
||||
Setting *SettingRepository
|
||||
UserSubscription *UserSubscriptionRepository
|
||||
}
|
||||
|
||||
// NewRepositories 创建所有仓库
|
||||
func NewRepositories(db *gorm.DB) *Repositories {
|
||||
return &Repositories{
|
||||
User: NewUserRepository(db),
|
||||
ApiKey: NewApiKeyRepository(db),
|
||||
Group: NewGroupRepository(db),
|
||||
Account: NewAccountRepository(db),
|
||||
Proxy: NewProxyRepository(db),
|
||||
RedeemCode: NewRedeemCodeRepository(db),
|
||||
UsageLog: NewUsageLogRepository(db),
|
||||
Setting: NewSettingRepository(db),
|
||||
UserSubscription: NewUserSubscriptionRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
// PaginationParams 分页参数
|
||||
type PaginationParams struct {
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// PaginationResult 分页结果
|
||||
type PaginationResult struct {
|
||||
Total int64
|
||||
Page int
|
||||
PageSize int
|
||||
Pages int
|
||||
}
|
||||
|
||||
// DefaultPagination 默认分页参数
|
||||
func DefaultPagination() PaginationParams {
|
||||
return PaginationParams{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
}
|
||||
|
||||
// Offset 计算偏移量
|
||||
func (p PaginationParams) Offset() int {
|
||||
if p.Page < 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
return (p.Page - 1) * p.PageSize
|
||||
}
|
||||
|
||||
// Limit 获取限制数
|
||||
func (p PaginationParams) Limit() int {
|
||||
if p.PageSize < 1 {
|
||||
return 20
|
||||
}
|
||||
if p.PageSize > 100 {
|
||||
return 100
|
||||
}
|
||||
return p.PageSize
|
||||
}
|
||||
108
backend/internal/repository/setting_repo.go
Normal file
108
backend/internal/repository/setting_repo.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// SettingRepository 系统设置数据访问层
|
||||
type SettingRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSettingRepository 创建系统设置仓库实例
|
||||
func NewSettingRepository(db *gorm.DB) *SettingRepository {
|
||||
return &SettingRepository{db: db}
|
||||
}
|
||||
|
||||
// Get 根据Key获取设置值
|
||||
func (r *SettingRepository) Get(ctx context.Context, key string) (*model.Setting, error) {
|
||||
var setting model.Setting
|
||||
err := r.db.WithContext(ctx).Where("key = ?", key).First(&setting).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &setting, nil
|
||||
}
|
||||
|
||||
// GetValue 获取设置值字符串
|
||||
func (r *SettingRepository) GetValue(ctx context.Context, key string) (string, error) {
|
||||
setting, err := r.Get(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
// Set 设置值(存在则更新,不存在则创建)
|
||||
func (r *SettingRepository) Set(ctx context.Context, key, value string) error {
|
||||
setting := &model.Setting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}),
|
||||
}).Create(setting).Error
|
||||
}
|
||||
|
||||
// GetMultiple 批量获取设置
|
||||
func (r *SettingRepository) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||
var settings []model.Setting
|
||||
err := r.db.WithContext(ctx).Where("key IN ?", keys).Find(&settings).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for _, s := range settings {
|
||||
result[s.Key] = s.Value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetMultiple 批量设置值
|
||||
func (r *SettingRepository) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range settings {
|
||||
setting := &model.Setting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}),
|
||||
}).Create(setting).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll 获取所有设置
|
||||
func (r *SettingRepository) GetAll(ctx context.Context) (map[string]string, error) {
|
||||
var settings []model.Setting
|
||||
err := r.db.WithContext(ctx).Find(&settings).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for _, s := range settings {
|
||||
result[s.Key] = s.Value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Delete 删除设置
|
||||
func (r *SettingRepository) Delete(ctx context.Context, key string) error {
|
||||
return r.db.WithContext(ctx).Where("key = ?", key).Delete(&model.Setting{}).Error
|
||||
}
|
||||
1006
backend/internal/repository/usage_log_repo.go
Normal file
1006
backend/internal/repository/usage_log_repo.go
Normal file
File diff suppressed because it is too large
Load Diff
130
backend/internal/repository/user_repo.go
Normal file
130
backend/internal/repository/user_repo.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sub2api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(ctx context.Context, user *model.User) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.User{}, id).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) List(ctx context.Context, params PaginationParams) ([]model.User, *PaginationResult, error) {
|
||||
return r.ListWithFilters(ctx, params, "", "", "")
|
||||
}
|
||||
|
||||
// ListWithFilters lists users with optional filtering by status, role, and search query
|
||||
func (r *UserRepository) ListWithFilters(ctx context.Context, params PaginationParams, status, role, search string) ([]model.User, *PaginationResult, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&model.User{})
|
||||
|
||||
// Apply filters
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if role != "" {
|
||||
db = db.Where("role = ?", role)
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("email ILIKE ?", searchPattern)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&users).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return users, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateBalance(ctx context.Context, id int64, amount float64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).
|
||||
Update("balance", gorm.Expr("balance + ?", amount)).Error
|
||||
}
|
||||
|
||||
// DeductBalance 扣减用户余额,仅当余额充足时执行
|
||||
func (r *UserRepository) DeductBalance(ctx context.Context, id int64, amount float64) error {
|
||||
result := r.db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("id = ? AND balance >= ?", id, amount).
|
||||
Update("balance", gorm.Expr("balance - ?", amount))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound // 余额不足或用户不存在
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateConcurrency(ctx context.Context, id int64, amount int) error {
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).
|
||||
Update("concurrency", gorm.Expr("concurrency + ?", amount)).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// RemoveGroupFromAllowedGroups 从所有用户的 allowed_groups 数组中移除指定的分组ID
|
||||
// 使用 PostgreSQL 的 array_remove 函数
|
||||
func (r *UserRepository) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) {
|
||||
result := r.db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("? = ANY(allowed_groups)", groupID).
|
||||
Update("allowed_groups", gorm.Expr("array_remove(allowed_groups, ?)", groupID))
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
322
backend/internal/repository/user_subscription_repo.go
Normal file
322
backend/internal/repository/user_subscription_repo.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"sub2api/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserSubscriptionRepository 用户订阅仓库
|
||||
type UserSubscriptionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserSubscriptionRepository 创建用户订阅仓库
|
||||
func NewUserSubscriptionRepository(db *gorm.DB) *UserSubscriptionRepository {
|
||||
return &UserSubscriptionRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建订阅
|
||||
func (r *UserSubscriptionRepository) Create(ctx context.Context, sub *model.UserSubscription) error {
|
||||
return r.db.WithContext(ctx).Create(sub).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取订阅
|
||||
func (r *UserSubscriptionRepository) GetByID(ctx context.Context, id int64) (*model.UserSubscription, error) {
|
||||
var sub model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("User").
|
||||
Preload("Group").
|
||||
Preload("AssignedByUser").
|
||||
First(&sub, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// GetByUserIDAndGroupID 根据用户ID和分组ID获取订阅
|
||||
func (r *UserSubscriptionRepository) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) {
|
||||
var sub model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("user_id = ? AND group_id = ?", userID, groupID).
|
||||
First(&sub).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// GetActiveByUserIDAndGroupID 获取用户对特定分组的有效订阅
|
||||
func (r *UserSubscriptionRepository) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) {
|
||||
var sub model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("user_id = ? AND group_id = ? AND status = ? AND expires_at > ?",
|
||||
userID, groupID, model.SubscriptionStatusActive, time.Now()).
|
||||
First(&sub).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// Update 更新订阅
|
||||
func (r *UserSubscriptionRepository) Update(ctx context.Context, sub *model.UserSubscription) error {
|
||||
sub.UpdatedAt = time.Now()
|
||||
return r.db.WithContext(ctx).Save(sub).Error
|
||||
}
|
||||
|
||||
// Delete 删除订阅
|
||||
func (r *UserSubscriptionRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.UserSubscription{}, id).Error
|
||||
}
|
||||
|
||||
// ListByUserID 获取用户的所有订阅
|
||||
func (r *UserSubscriptionRepository) ListByUserID(ctx context.Context, userID int64) ([]model.UserSubscription, error) {
|
||||
var subs []model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Find(&subs).Error
|
||||
return subs, err
|
||||
}
|
||||
|
||||
// ListActiveByUserID 获取用户的所有有效订阅
|
||||
func (r *UserSubscriptionRepository) ListActiveByUserID(ctx context.Context, userID int64) ([]model.UserSubscription, error) {
|
||||
var subs []model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("user_id = ? AND status = ? AND expires_at > ?",
|
||||
userID, model.SubscriptionStatusActive, time.Now()).
|
||||
Order("created_at DESC").
|
||||
Find(&subs).Error
|
||||
return subs, err
|
||||
}
|
||||
|
||||
// ListByGroupID 获取分组的所有订阅(分页)
|
||||
func (r *UserSubscriptionRepository) ListByGroupID(ctx context.Context, groupID int64, params PaginationParams) ([]model.UserSubscription, *PaginationResult, error) {
|
||||
var subs []model.UserSubscription
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.UserSubscription{}).Where("group_id = ?", groupID)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err := query.
|
||||
Preload("User").
|
||||
Preload("Group").
|
||||
Order("created_at DESC").
|
||||
Offset(params.Offset()).
|
||||
Limit(params.Limit()).
|
||||
Find(&subs).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return subs, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List 获取所有订阅(分页,支持筛选)
|
||||
func (r *UserSubscriptionRepository) List(ctx context.Context, params PaginationParams, userID, groupID *int64, status string) ([]model.UserSubscription, *PaginationResult, error) {
|
||||
var subs []model.UserSubscription
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.UserSubscription{})
|
||||
|
||||
if userID != nil {
|
||||
query = query.Where("user_id = ?", *userID)
|
||||
}
|
||||
if groupID != nil {
|
||||
query = query.Where("group_id = ?", *groupID)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err := query.
|
||||
Preload("User").
|
||||
Preload("Group").
|
||||
Preload("AssignedByUser").
|
||||
Order("created_at DESC").
|
||||
Offset(params.Offset()).
|
||||
Limit(params.Limit()).
|
||||
Find(&subs).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pages := int(total) / params.Limit()
|
||||
if int(total)%params.Limit() > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return subs, &PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: params.Limit(),
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IncrementUsage 增加使用量
|
||||
func (r *UserSubscriptionRepository) IncrementUsage(ctx context.Context, id int64, costUSD float64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"daily_usage_usd": gorm.Expr("daily_usage_usd + ?", costUSD),
|
||||
"weekly_usage_usd": gorm.Expr("weekly_usage_usd + ?", costUSD),
|
||||
"monthly_usage_usd": gorm.Expr("monthly_usage_usd + ?", costUSD),
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ResetDailyUsage 重置日使用量
|
||||
func (r *UserSubscriptionRepository) ResetDailyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"daily_usage_usd": 0,
|
||||
"daily_window_start": newWindowStart,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ResetWeeklyUsage 重置周使用量
|
||||
func (r *UserSubscriptionRepository) ResetWeeklyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"weekly_usage_usd": 0,
|
||||
"weekly_window_start": newWindowStart,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ResetMonthlyUsage 重置月使用量
|
||||
func (r *UserSubscriptionRepository) ResetMonthlyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"monthly_usage_usd": 0,
|
||||
"monthly_window_start": newWindowStart,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ActivateWindows 激活所有窗口(首次使用时)
|
||||
func (r *UserSubscriptionRepository) ActivateWindows(ctx context.Context, id int64, activateTime time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"daily_window_start": activateTime,
|
||||
"weekly_window_start": activateTime,
|
||||
"monthly_window_start": activateTime,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新订阅状态
|
||||
func (r *UserSubscriptionRepository) UpdateStatus(ctx context.Context, id int64, status string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ExtendExpiry 延长订阅过期时间
|
||||
func (r *UserSubscriptionRepository) ExtendExpiry(ctx context.Context, id int64, newExpiresAt time.Time) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"expires_at": newExpiresAt,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateNotes 更新订阅备注
|
||||
func (r *UserSubscriptionRepository) UpdateNotes(ctx context.Context, id int64, notes string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"notes": notes,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ListExpired 获取所有已过期但状态仍为active的订阅
|
||||
func (r *UserSubscriptionRepository) ListExpired(ctx context.Context) ([]model.UserSubscription, error) {
|
||||
var subs []model.UserSubscription
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ? AND expires_at <= ?", model.SubscriptionStatusActive, time.Now()).
|
||||
Find(&subs).Error
|
||||
return subs, err
|
||||
}
|
||||
|
||||
// BatchUpdateExpiredStatus 批量更新过期订阅状态
|
||||
func (r *UserSubscriptionRepository) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) {
|
||||
result := r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("status = ? AND expires_at <= ?", model.SubscriptionStatusActive, time.Now()).
|
||||
Updates(map[string]interface{}{
|
||||
"status": model.SubscriptionStatusExpired,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// ExistsByUserIDAndGroupID 检查用户是否已有该分组的订阅
|
||||
func (r *UserSubscriptionRepository) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("user_id = ? AND group_id = ?", userID, groupID).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// CountByGroupID 获取分组的订阅数量
|
||||
func (r *UserSubscriptionRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("group_id = ?", groupID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountActiveByGroupID 获取分组的有效订阅数量
|
||||
func (r *UserSubscriptionRepository) CountActiveByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.UserSubscription{}).
|
||||
Where("group_id = ? AND status = ? AND expires_at > ?",
|
||||
groupID, model.SubscriptionStatusActive, time.Now()).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DeleteByGroupID 删除分组相关的所有订阅记录
|
||||
func (r *UserSubscriptionRepository) DeleteByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
||||
result := r.db.WithContext(ctx).Where("group_id = ?", groupID).Delete(&model.UserSubscription{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
Reference in New Issue
Block a user