新增账号凭证邮箱查询接口
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user