新增账号凭证邮箱查询接口
This commit is contained in:
@@ -129,6 +129,13 @@ type BulkUpdateAccountsRequest struct {
|
|||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
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
|
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||||
type AccountWithConcurrency struct {
|
type AccountWithConcurrency struct {
|
||||||
*dto.Account
|
*dto.Account
|
||||||
@@ -258,6 +265,87 @@ func (h *AccountHandler) List(c *gin.Context) {
|
|||||||
response.Paginated(c, result, total, page, pageSize)
|
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
|
// GetByID handles getting an account by ID
|
||||||
// GET /api/v1/admin/accounts/:id
|
// GET /api/v1/admin/accounts/:id
|
||||||
func (h *AccountHandler) GetByID(c *gin.Context) {
|
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)
|
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 {
|
func (r *accountRepository) UpdateLastUsed(ctx context.Context, id int64) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err := r.client.Account.Update().
|
_, err := r.client.Account.Update().
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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",
|
name: "POST /api/v1/admin/accounts/bulk-update",
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
@@ -387,6 +426,7 @@ type contractDeps struct {
|
|||||||
apiKeyRepo *stubApiKeyRepo
|
apiKeyRepo *stubApiKeyRepo
|
||||||
usageRepo *stubUsageLogRepo
|
usageRepo *stubUsageLogRepo
|
||||||
settingRepo *stubSettingRepo
|
settingRepo *stubSettingRepo
|
||||||
|
accountRepo *stubAccountRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContractDeps(t *testing.T) *contractDeps {
|
func newContractDeps(t *testing.T) *contractDeps {
|
||||||
@@ -482,6 +522,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
v1Admin.Use(adminAuth)
|
v1Admin.Use(adminAuth)
|
||||||
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
||||||
v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate)
|
v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate)
|
||||||
|
v1Admin.POST("/accounts/lookup", adminAccountHandler.Lookup)
|
||||||
|
|
||||||
return &contractDeps{
|
return &contractDeps{
|
||||||
now: now,
|
now: now,
|
||||||
@@ -489,6 +530,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
apiKeyRepo: apiKeyRepo,
|
apiKeyRepo: apiKeyRepo,
|
||||||
usageRepo: usageRepo,
|
usageRepo: usageRepo,
|
||||||
settingRepo: settingRepo,
|
settingRepo: settingRepo,
|
||||||
|
accountRepo: &accountRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,7 +715,8 @@ func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID i
|
|||||||
}
|
}
|
||||||
|
|
||||||
type stubAccountRepo struct {
|
type stubAccountRepo struct {
|
||||||
bulkUpdateIDs []int64
|
bulkUpdateIDs []int64
|
||||||
|
lookupAccounts []service.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error {
|
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")
|
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 {
|
func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error {
|
||||||
return errors.New("not implemented")
|
return errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
accounts := admin.Group("/accounts")
|
accounts := admin.Group("/accounts")
|
||||||
{
|
{
|
||||||
accounts.GET("", h.Admin.Account.List)
|
accounts.GET("", h.Admin.Account.List)
|
||||||
|
accounts.POST("/lookup", h.Admin.Account.Lookup)
|
||||||
accounts.GET("/:id", h.Admin.Account.GetByID)
|
accounts.GET("/:id", h.Admin.Account.GetByID)
|
||||||
accounts.POST("", h.Admin.Account.Create)
|
accounts.POST("", h.Admin.Account.Create)
|
||||||
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
|
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type AccountRepository interface {
|
|||||||
ListByGroup(ctx context.Context, groupID int64) ([]Account, error)
|
ListByGroup(ctx context.Context, groupID int64) ([]Account, error)
|
||||||
ListActive(ctx context.Context) ([]Account, error)
|
ListActive(ctx context.Context) ([]Account, error)
|
||||||
ListByPlatform(ctx context.Context, platform string) ([]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
|
UpdateLastUsed(ctx context.Context, id int64) error
|
||||||
BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) 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")
|
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 {
|
func (s *accountRepoStub) UpdateLastUsed(ctx context.Context, id int64) error {
|
||||||
panic("unexpected UpdateLastUsed call")
|
panic("unexpected UpdateLastUsed call")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type AdminService interface {
|
|||||||
CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error)
|
CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error)
|
||||||
UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*Account, error)
|
UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*Account, error)
|
||||||
DeleteAccount(ctx context.Context, id int64) 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)
|
RefreshAccountCredentials(ctx context.Context, id int64) (*Account, error)
|
||||||
ClearAccountError(ctx context.Context, id int64) (*Account, error)
|
ClearAccountError(ctx context.Context, id int64) (*Account, error)
|
||||||
SetAccountError(ctx context.Context, id int64, errorMsg string) 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)
|
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) {
|
func (s *adminServiceImpl) GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return []*Account{}, nil
|
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) {
|
func (m *mockAccountRepoForPlatform) ListByPlatform(ctx context.Context, platform string) ([]Account, error) {
|
||||||
return nil, nil
|
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 {
|
func (m *mockAccountRepoForPlatform) UpdateLastUsed(ctx context.Context, id int64) error {
|
||||||
return nil
|
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) {
|
func (m *mockAccountRepoForGemini) ListByPlatform(ctx context.Context, platform string) ([]Account, error) {
|
||||||
return nil, nil
|
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) UpdateLastUsed(ctx context.Context, id int64) error { return nil }
|
||||||
func (m *mockAccountRepoForGemini) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
|
func (m *mockAccountRepoForGemini) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user