From 64795a03e387f18d7e34d3e8c32fe055a7a79670 Mon Sep 17 00:00:00 2001 From: song Date: Tue, 20 Jan 2026 14:17:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B4=A6=E5=8F=B7=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E9=82=AE=E7=AE=B1=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/handler/admin/account_handler.go | 88 +++++++++++++++++++ backend/internal/repository/account_repo.go | 31 +++++++ backend/internal/server/api_contract_test.go | 75 +++++++++++++++- backend/internal/server/routes/admin.go | 1 + backend/internal/service/account_service.go | 1 + .../service/account_service_delete_test.go | 4 + backend/internal/service/admin_service.go | 8 ++ .../service/gateway_multiplatform_test.go | 3 + .../service/gemini_multiplatform_test.go | 3 + 9 files changed, 213 insertions(+), 1 deletion(-) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 16fc86bb..579ada14 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -129,6 +129,13 @@ type BulkUpdateAccountsRequest struct { ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险 } +// AccountLookupRequest 用于凭证身份信息查找账号 +type AccountLookupRequest struct { + Platform string `json:"platform" binding:"required"` + Emails []string `json:"emails" binding:"required,min=1"` + IdentityType string `json:"identity_type"` +} + // AccountWithConcurrency extends Account with real-time concurrency info type AccountWithConcurrency struct { *dto.Account @@ -258,6 +265,87 @@ func (h *AccountHandler) List(c *gin.Context) { response.Paginated(c, result, total, page, pageSize) } +// Lookup 根据凭证身份信息查找账号 +// POST /api/v1/admin/accounts/lookup +func (h *AccountHandler) Lookup(c *gin.Context) { + var req AccountLookupRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + identityType := strings.TrimSpace(req.IdentityType) + if identityType == "" { + identityType = "credential_email" + } + if identityType != "credential_email" { + response.BadRequest(c, "Unsupported identity_type") + return + } + + platform := strings.TrimSpace(req.Platform) + if platform == "" { + response.BadRequest(c, "Platform is required") + return + } + + normalized := make([]string, 0, len(req.Emails)) + seen := make(map[string]struct{}) + for _, email := range req.Emails { + cleaned := strings.ToLower(strings.TrimSpace(email)) + if cleaned == "" { + continue + } + if _, ok := seen[cleaned]; ok { + continue + } + seen[cleaned] = struct{}{} + normalized = append(normalized, cleaned) + } + if len(normalized) == 0 { + response.BadRequest(c, "Emails is required") + return + } + + accounts, err := h.adminService.LookupAccountsByCredentialEmail(c.Request.Context(), platform, normalized) + if err != nil { + response.ErrorFrom(c, err) + return + } + + matchedMap := make(map[string]service.Account) + for _, account := range accounts { + email := strings.ToLower(strings.TrimSpace(account.GetCredential("email"))) + if email == "" { + continue + } + if _, ok := matchedMap[email]; ok { + continue + } + matchedMap[email] = account + } + + matched := make([]gin.H, 0, len(matchedMap)) + missing := make([]string, 0) + for _, email := range normalized { + if account, ok := matchedMap[email]; ok { + matched = append(matched, gin.H{ + "email": email, + "account_id": account.ID, + "platform": account.Platform, + "name": account.Name, + }) + continue + } + missing = append(missing, email) + } + + response.Success(c, gin.H{ + "matched": matched, + "missing": missing, + }) +} + // GetByID handles getting an account by ID // GET /api/v1/admin/accounts/:id func (h *AccountHandler) GetByID(c *gin.Context) { diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 8aa487ac..6fcb5290 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -473,6 +473,37 @@ func (r *accountRepository) ListByPlatform(ctx context.Context, platform string) return r.accountsToService(ctx, accounts) } +func (r *accountRepository) ListByPlatformAndCredentialEmails( + ctx context.Context, + platform string, + emails []string, +) ([]service.Account, error) { + if len(emails) == 0 { + return []service.Account{}, nil + } + args := make([]any, 0, len(emails)) + for _, email := range emails { + if email == "" { + continue + } + args = append(args, email) + } + if len(args) == 0 { + return []service.Account{}, nil + } + + accounts, err := r.client.Account.Query(). + Where(dbaccount.PlatformEQ(platform)). + Where(func(s *entsql.Selector) { + s.Where(sqljson.ValueIn(dbaccount.FieldCredentials, args, sqljson.Path("email"))) + }). + All(ctx) + if err != nil { + return nil, err + } + return r.accountsToService(ctx, accounts) +} + func (r *accountRepository) UpdateLastUsed(ctx context.Context, id int64) error { now := time.Now() _, err := r.client.Account.Update(). diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 80f3e408..81b94305 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "sort" + "strings" "testing" "time" @@ -341,6 +342,44 @@ func TestAPIContracts(t *testing.T) { } }`, }, + { + name: "POST /api/v1/admin/accounts/lookup", + setup: func(t *testing.T, deps *contractDeps) { + t.Helper() + deps.accountRepo.lookupAccounts = []service.Account{ + { + ID: 101, + Name: "Alice Account", + Platform: "antigravity", + Credentials: map[string]any{ + "email": "alice@example.com", + }, + }, + } + }, + method: http.MethodPost, + path: "/api/v1/admin/accounts/lookup", + body: `{"platform":"antigravity","emails":["Alice@Example.com","bob@example.com"]}`, + headers: map[string]string{ + "Content-Type": "application/json", + }, + wantStatus: http.StatusOK, + wantJSON: `{ + "code": 0, + "message": "success", + "data": { + "matched": [ + { + "email": "alice@example.com", + "account_id": 101, + "platform": "antigravity", + "name": "Alice Account" + } + ], + "missing": ["bob@example.com"] + } + }`, + }, { name: "POST /api/v1/admin/accounts/bulk-update", method: http.MethodPost, @@ -387,6 +426,7 @@ type contractDeps struct { apiKeyRepo *stubApiKeyRepo usageRepo *stubUsageLogRepo settingRepo *stubSettingRepo + accountRepo *stubAccountRepo } func newContractDeps(t *testing.T) *contractDeps { @@ -482,6 +522,7 @@ func newContractDeps(t *testing.T) *contractDeps { v1Admin.Use(adminAuth) v1Admin.GET("/settings", adminSettingHandler.GetSettings) v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate) + v1Admin.POST("/accounts/lookup", adminAccountHandler.Lookup) return &contractDeps{ now: now, @@ -489,6 +530,7 @@ func newContractDeps(t *testing.T) *contractDeps { apiKeyRepo: apiKeyRepo, usageRepo: usageRepo, settingRepo: settingRepo, + accountRepo: &accountRepo, } } @@ -673,7 +715,8 @@ func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID i } type stubAccountRepo struct { - bulkUpdateIDs []int64 + bulkUpdateIDs []int64 + lookupAccounts []service.Account } func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error { @@ -724,6 +767,36 @@ func (s *stubAccountRepo) ListByPlatform(ctx context.Context, platform string) ( return nil, errors.New("not implemented") } +func (s *stubAccountRepo) ListByPlatformAndCredentialEmails(ctx context.Context, platform string, emails []string) ([]service.Account, error) { + if len(s.lookupAccounts) == 0 { + return nil, nil + } + emailSet := make(map[string]struct{}, len(emails)) + for _, email := range emails { + normalized := strings.ToLower(strings.TrimSpace(email)) + if normalized == "" { + continue + } + emailSet[normalized] = struct{}{} + } + var matches []service.Account + for i := range s.lookupAccounts { + account := &s.lookupAccounts[i] + if account.Platform != platform { + continue + } + accountEmail := strings.ToLower(strings.TrimSpace(account.GetCredential("email"))) + if accountEmail == "" { + continue + } + if _, ok := emailSet[accountEmail]; !ok { + continue + } + matches = append(matches, *account) + } + return matches, nil +} + func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error { return errors.New("not implemented") } diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index ff05b32a..cf6fa942 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -197,6 +197,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) { accounts := admin.Group("/accounts") { accounts.GET("", h.Admin.Account.List) + accounts.POST("/lookup", h.Admin.Account.Lookup) accounts.GET("/:id", h.Admin.Account.GetByID) accounts.POST("", h.Admin.Account.Create) accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS) diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go index 90365d2f..72c5c5f8 100644 --- a/backend/internal/service/account_service.go +++ b/backend/internal/service/account_service.go @@ -33,6 +33,7 @@ type AccountRepository interface { ListByGroup(ctx context.Context, groupID int64) ([]Account, error) ListActive(ctx context.Context) ([]Account, error) ListByPlatform(ctx context.Context, platform string) ([]Account, error) + ListByPlatformAndCredentialEmails(ctx context.Context, platform string, emails []string) ([]Account, error) UpdateLastUsed(ctx context.Context, id int64) error BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error diff --git a/backend/internal/service/account_service_delete_test.go b/backend/internal/service/account_service_delete_test.go index e5eabfc6..08b0d5b6 100644 --- a/backend/internal/service/account_service_delete_test.go +++ b/backend/internal/service/account_service_delete_test.go @@ -87,6 +87,10 @@ func (s *accountRepoStub) ListByPlatform(ctx context.Context, platform string) ( panic("unexpected ListByPlatform call") } +func (s *accountRepoStub) ListByPlatformAndCredentialEmails(ctx context.Context, platform string, emails []string) ([]Account, error) { + panic("unexpected ListByPlatformAndCredentialEmails call") +} + func (s *accountRepoStub) UpdateLastUsed(ctx context.Context, id int64) error { panic("unexpected UpdateLastUsed call") } diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 0afa0716..1b2c7ff4 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -40,6 +40,7 @@ type AdminService interface { CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error) UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*Account, error) DeleteAccount(ctx context.Context, id int64) error + LookupAccountsByCredentialEmail(ctx context.Context, platform string, emails []string) ([]Account, error) RefreshAccountCredentials(ctx context.Context, id int64) (*Account, error) ClearAccountError(ctx context.Context, id int64) (*Account, error) SetAccountError(ctx context.Context, id int64, errorMsg string) error @@ -793,6 +794,13 @@ func (s *adminServiceImpl) GetAccount(ctx context.Context, id int64) (*Account, return s.accountRepo.GetByID(ctx, id) } +func (s *adminServiceImpl) LookupAccountsByCredentialEmail(ctx context.Context, platform string, emails []string) ([]Account, error) { + if platform == "" || len(emails) == 0 { + return []Account{}, nil + } + return s.accountRepo.ListByPlatformAndCredentialEmails(ctx, platform, emails) +} + func (s *adminServiceImpl) GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error) { if len(ids) == 0 { return []*Account{}, nil diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index 4d17d5e1..ccae80fe 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -96,6 +96,9 @@ func (m *mockAccountRepoForPlatform) ListActive(ctx context.Context) ([]Account, func (m *mockAccountRepoForPlatform) ListByPlatform(ctx context.Context, platform string) ([]Account, error) { return nil, nil } +func (m *mockAccountRepoForPlatform) ListByPlatformAndCredentialEmails(ctx context.Context, platform string, emails []string) ([]Account, error) { + return nil, nil +} func (m *mockAccountRepoForPlatform) UpdateLastUsed(ctx context.Context, id int64) error { return nil } diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index fc009873..20640b01 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -81,6 +81,9 @@ func (m *mockAccountRepoForGemini) ListActive(ctx context.Context) ([]Account, e func (m *mockAccountRepoForGemini) ListByPlatform(ctx context.Context, platform string) ([]Account, error) { return nil, nil } +func (m *mockAccountRepoForGemini) ListByPlatformAndCredentialEmails(ctx context.Context, platform string, emails []string) ([]Account, error) { + return nil, nil +} func (m *mockAccountRepoForGemini) UpdateLastUsed(ctx context.Context, id int64) error { return nil } func (m *mockAccountRepoForGemini) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error { return nil