From b9760abe3608c696d64709b02a12c7f9bb3b3d23 Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Tue, 30 Dec 2025 10:16:34 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=BF=BD=E7=95=A5openspec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 51583259..d58b3587 100644 --- a/.gitignore +++ b/.gitignore @@ -111,4 +111,4 @@ CLAUDE.md .claude scripts .code-review-state - +openspec/ From 0ea373d9d5b7958d5ebff35d5571fa54c4a1b91c Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Tue, 30 Dec 2025 14:04:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0git=20=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d58b3587..9542699c 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,5 @@ CLAUDE.md scripts .code-review-state openspec/ +docs/ +code-reviews/ \ No newline at end of file From daf0e883aee70a1382c4cd5456a8537859c8fc3e Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Tue, 30 Dec 2025 14:29:43 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=E5=BF=BD=E7=95=A5=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index de90b2a0..5a611909 100644 --- a/.gitignore +++ b/.gitignore @@ -108,9 +108,11 @@ backend/.installed # =================== tests CLAUDE.md +AGENTS.md .claude scripts .code-review-state openspec/ docs/ code-reviews/ +AGENTS.md From b9a753cd04771aabf992bb03d12b3ce649446bd8 Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Tue, 30 Dec 2025 14:35:29 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(=E4=BB=93=E5=BA=93):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20ent=20=E5=AE=9E=E7=8E=B0=E8=B4=A6=E5=8F=B7=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 替换 gorm 查询并复用分组过滤逻辑,避免编译错误 --- backend/internal/repository/account_repo.go | 63 ++++++++------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 9a5cfca8..19dff447 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -457,10 +457,11 @@ func (r *accountRepository) ListSchedulableByPlatform(ctx context.Context, platf } func (r *accountRepository) ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]service.Account, error) { + // 单平台查询复用多平台逻辑,保持过滤条件与排序策略一致。 return r.queryAccountsByGroup(ctx, groupID, accountGroupQueryOptions{ status: service.StatusActive, schedulable: true, - platform: platform, + platforms: []string{platform}, }) } @@ -468,50 +469,35 @@ func (r *accountRepository) ListSchedulableByPlatforms(ctx context.Context, plat if len(platforms) == 0 { return nil, nil } - var accounts []accountModel + // 仅返回可调度的活跃账号,并过滤处于过载/限流窗口的账号。 + // 代理与分组信息统一在 accountsToService 中批量加载,避免 N+1 查询。 now := time.Now() - err := r.db.WithContext(ctx). - Where("platform IN ?", platforms). - Where("status = ? AND schedulable = ?", service.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 + accounts, err := r.client.Account.Query(). + Where( + dbaccount.PlatformIn(platforms...), + dbaccount.StatusEQ(service.StatusActive), + dbaccount.SchedulableEQ(true), + dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)), + dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)), + ). + Order(dbent.Asc(dbaccount.FieldPriority)). + All(ctx) if err != nil { return nil, err } - outAccounts := make([]service.Account, 0, len(accounts)) - for i := range accounts { - outAccounts = append(outAccounts, *accountModelToService(&accounts[i])) - } - return outAccounts, nil + return r.accountsToService(ctx, accounts) } func (r *accountRepository) ListSchedulableByGroupIDAndPlatforms(ctx context.Context, groupID int64, platforms []string) ([]service.Account, error) { if len(platforms) == 0 { return nil, nil } - var accounts []accountModel - 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.platform IN ?", platforms). - Where("accounts.status = ? AND accounts.schedulable = ?", service.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 - if err != nil { - return nil, err - } - outAccounts := make([]service.Account, 0, len(accounts)) - for i := range accounts { - outAccounts = append(outAccounts, *accountModelToService(&accounts[i])) - } - return outAccounts, nil + // 复用按分组查询逻辑,保证分组优先级 + 账号优先级的排序与筛选一致。 + return r.queryAccountsByGroup(ctx, groupID, accountGroupQueryOptions{ + status: service.StatusActive, + schedulable: true, + platforms: platforms, + }) } func (r *accountRepository) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error { @@ -666,20 +652,21 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates type accountGroupQueryOptions struct { status string schedulable bool - platform string + platforms []string // 允许的多个平台,空切片表示不进行平台过滤 } func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID int64, opts accountGroupQueryOptions) ([]service.Account, error) { q := r.client.AccountGroup.Query(). Where(dbaccountgroup.GroupIDEQ(groupID)) + // 通过 account_groups 中间表查询账号,并按需叠加状态/平台/调度能力过滤。 preds := make([]dbpredicate.Account, 0, 6) preds = append(preds, dbaccount.DeletedAtIsNil()) if opts.status != "" { preds = append(preds, dbaccount.StatusEQ(opts.status)) } - if opts.platform != "" { - preds = append(preds, dbaccount.PlatformEQ(opts.platform)) + if len(opts.platforms) > 0 { + preds = append(preds, dbaccount.PlatformIn(opts.platforms...)) } if opts.schedulable { now := time.Now()