feat: Add owner account management feature with a new frontend list component and backend API/database integration.
This commit is contained in:
@@ -104,6 +104,7 @@ func startServer(cfg *config.Config) {
|
|||||||
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
|
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
|
||||||
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
|
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
|
||||||
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
|
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
|
||||||
|
mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
|
||||||
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
|
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
|
||||||
|
|
||||||
// 注册测试 API
|
// 注册测试 API
|
||||||
|
|||||||
@@ -128,6 +128,84 @@ func HandleUploadValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleRefetchAccountIDs 重新获取缺少 account_id 的母号
|
||||||
|
func HandleRefetchAccountIDs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if database.Instance == nil {
|
||||||
|
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有缺少 account_id 的 owners
|
||||||
|
owners, err := database.Instance.GetOwnersWithoutAccountID()
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusInternalServerError, fmt.Sprintf("查询数据库失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(owners) == 0 {
|
||||||
|
Success(w, map[string]interface{}{
|
||||||
|
"message": "所有母号都已有 account_id",
|
||||||
|
"total": 0,
|
||||||
|
"success": 0,
|
||||||
|
"fail": 0,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(fmt.Sprintf("开始重新获取 account_id: 共 %d 个", len(owners)), "", "upload")
|
||||||
|
|
||||||
|
// 并发获取 account_id
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
sem := make(chan struct{}, 20) // 20 并发
|
||||||
|
var mu sync.Mutex
|
||||||
|
successCount := 0
|
||||||
|
failCount := 0
|
||||||
|
|
||||||
|
for _, owner := range owners {
|
||||||
|
wg.Add(1)
|
||||||
|
sem <- struct{}{}
|
||||||
|
|
||||||
|
go func(o database.TeamOwner) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
accountID, err := fetchAccountID(o.Token)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
failCount++
|
||||||
|
logger.Warning(fmt.Sprintf("获取 account_id 失败 (%s): %v", o.Email, err), "", "upload")
|
||||||
|
} else {
|
||||||
|
// 更新数据库
|
||||||
|
if updateErr := database.Instance.UpdateOwnerAccountID(o.ID, accountID); updateErr != nil {
|
||||||
|
failCount++
|
||||||
|
logger.Error(fmt.Sprintf("更新 account_id 失败 (%s): %v", o.Email, updateErr), "", "upload")
|
||||||
|
} else {
|
||||||
|
successCount++
|
||||||
|
logger.Info(fmt.Sprintf("获取 account_id 成功: %s -> %s", o.Email, accountID), "", "upload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
logger.Info(fmt.Sprintf("重新获取 account_id 完成: 成功=%d, 失败=%d", successCount, failCount), "", "upload")
|
||||||
|
|
||||||
|
Success(w, map[string]interface{}{
|
||||||
|
"message": "重新获取 account_id 完成",
|
||||||
|
"total": len(owners),
|
||||||
|
"success": successCount,
|
||||||
|
"fail": failCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// fetchAccountIDsConcurrent 并发获取 account_id
|
// fetchAccountIDsConcurrent 并发获取 account_id
|
||||||
func fetchAccountIDsConcurrent(owners []database.TeamOwner, concurrency int) {
|
func fetchAccountIDsConcurrent(owners []database.TeamOwner, concurrency int) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|||||||
@@ -237,6 +237,36 @@ func (d *DB) ClearTeamOwners() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOwnersWithoutAccountID 获取缺少 account_id 的 owners
|
||||||
|
func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, email, password, token, account_id, status, created_at
|
||||||
|
FROM team_owners WHERE account_id = '' OR account_id IS NULL
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var owners []TeamOwner
|
||||||
|
for rows.Next() {
|
||||||
|
var owner TeamOwner
|
||||||
|
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
owners = append(owners, owner)
|
||||||
|
}
|
||||||
|
return owners, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOwnerAccountID 更新 owner 的 account_id
|
||||||
|
func (d *DB) UpdateOwnerAccountID(id int64, accountID string) error {
|
||||||
|
_, err := d.db.Exec("UPDATE team_owners SET account_id = ? WHERE id = ?", accountID, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetOwnerStats 获取统计
|
// GetOwnerStats 获取统计
|
||||||
func (d *DB) GetOwnerStats() map[string]int {
|
func (d *DB) GetOwnerStats() map[string]int {
|
||||||
stats := map[string]int{
|
stats := map[string]int{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react'
|
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key } from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
||||||
|
|
||||||
interface TeamOwner {
|
interface TeamOwner {
|
||||||
@@ -84,6 +84,30 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [refetching, setRefetching] = useState(false)
|
||||||
|
|
||||||
|
const handleRefetchAccountIds = async () => {
|
||||||
|
if (refetching) return
|
||||||
|
setRefetching(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/db/owners/refetch-account-ids', { method: 'POST' })
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.code === 0) {
|
||||||
|
const result = data.data
|
||||||
|
alert(`重新获取完成\n总数: ${result.total}\n成功: ${result.success}\n失败: ${result.fail}`)
|
||||||
|
loadOwners()
|
||||||
|
onStatsChange?.()
|
||||||
|
} else {
|
||||||
|
alert(`操作失败: ${data.message}`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to refetch account ids:', e)
|
||||||
|
alert('操作失败,请查看控制台')
|
||||||
|
} finally {
|
||||||
|
setRefetching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit)
|
const totalPages = Math.ceil(total / limit)
|
||||||
|
|
||||||
const formatTime = (ts: string) => {
|
const formatTime = (ts: string) => {
|
||||||
@@ -124,6 +148,16 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
<Button variant="ghost" size="sm" onClick={loadOwners} icon={<RefreshCw className="h-4 w-4" />}>
|
<Button variant="ghost" size="sm" onClick={loadOwners} icon={<RefreshCw className="h-4 w-4" />}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefetchAccountIds}
|
||||||
|
disabled={refetching}
|
||||||
|
icon={<Key className={`h-4 w-4 ${refetching ? 'animate-pulse' : ''}`} />}
|
||||||
|
className="text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{refetching ? '获取中...' : '重新获取ID'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user