新增账号凭证邮箱查询接口

This commit is contained in:
song
2026-01-20 14:17:10 +08:00
parent 86d63f919d
commit 64795a03e3
9 changed files with 213 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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().

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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