feat: Implement core backend infrastructure including configuration management, SQLite database with team owners and app settings, and initial owner-related APIs and frontend components.

This commit is contained in:
2026-01-31 03:16:24 +08:00
parent 634b493524
commit f590fe0c7a
6 changed files with 810 additions and 20 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
import { useState, useEffect, useCallback } from 'react'
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
interface TeamOwner {
id: number
@@ -8,6 +8,25 @@ interface TeamOwner {
account_id: string
status: string
created_at: string
last_checked_at?: string
}
interface BanCheckTaskState {
running: boolean
started_at: string
total: number
checked: number
banned: number
valid: number
failed: number
}
interface BanCheckSettings {
enabled: boolean
interval: number
check_hours: number
service_running: boolean
task_state: BanCheckTaskState
}
const statusColors: Record<string, string> = {
@@ -42,6 +61,12 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
const [deleting, setDeleting] = useState(false)
const limit = 20
// 封禁检查相关状态
const [banCheckSettings, setBanCheckSettings] = useState<BanCheckSettings | null>(null)
const [showBanCheckSettings, setShowBanCheckSettings] = useState(false)
const [banCheckRunning, setBanCheckRunning] = useState(false)
const [checkHours, setCheckHours] = useState(24)
const loadOwners = async () => {
setLoading(true)
try {
@@ -72,6 +97,99 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
setSelectedIds(new Set())
}, [page, filter])
// 加载封禁检查配置
const loadBanCheckSettings = useCallback(async () => {
try {
const res = await fetch('/api/db/owners/ban-check/settings')
const data = await res.json()
if (data.code === 0) {
setBanCheckSettings(data.data)
setCheckHours(data.data.check_hours || 24)
setBanCheckRunning(data.data.task_state?.running || false)
}
} catch (e) {
console.error('Failed to load ban check settings:', e)
}
}, [])
useEffect(() => {
loadBanCheckSettings()
// 轮询检查状态
const interval = setInterval(() => {
if (banCheckRunning) {
loadBanCheckSettings()
}
}, 2000)
return () => clearInterval(interval)
}, [loadBanCheckSettings, banCheckRunning])
// 手动触发封禁检查
const handleBanCheck = async (forceCheck = false) => {
if (banCheckRunning) return
setBanCheckRunning(true)
try {
const body: { ids?: number[], force_check?: boolean } = {}
if (selectedIds.size > 0) {
body.ids = Array.from(selectedIds)
}
if (forceCheck) {
body.force_check = true
}
const res = await fetch('/api/db/owners/ban-check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
if (data.code === 0) {
// 开始轮询状态
const pollInterval = setInterval(async () => {
const statusRes = await fetch('/api/db/owners/ban-check/status')
const statusData = await statusRes.json()
if (statusData.code === 0) {
if (!statusData.data.running) {
clearInterval(pollInterval)
setBanCheckRunning(false)
loadOwners()
onStatsChange?.()
loadBanCheckSettings()
}
}
}, 2000)
} else {
alert(data.message || '启动检查失败')
setBanCheckRunning(false)
}
} catch (e) {
console.error('Failed to start ban check:', e)
alert('启动检查失败')
setBanCheckRunning(false)
}
}
// 保存封禁检查配置
const handleSaveBanCheckSettings = async (enabled: boolean) => {
try {
const res = await fetch('/api/db/owners/ban-check/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled,
check_hours: checkHours,
}),
})
const data = await res.json()
if (data.code === 0) {
loadBanCheckSettings()
} else {
alert(data.message || '保存失败')
}
} catch (e) {
console.error('Failed to save ban check settings:', e)
alert('保存失败')
}
}
// 单个删除
const handleDelete = async (id: number) => {
if (!confirm('确认删除此账号?')) return
@@ -247,6 +365,29 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
>
{refetching ? '获取中...' : '重新获取ID'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleBanCheck(false)}
disabled={banCheckRunning}
icon={<ShieldCheck className={`h-4 w-4 ${banCheckRunning ? 'animate-pulse' : ''}`} />}
className="text-emerald-500 hover:text-emerald-600"
>
{banCheckRunning
? `检查中 (${banCheckSettings?.task_state?.checked || 0}/${banCheckSettings?.task_state?.total || 0})`
: selectedIds.size > 0
? `检查封禁 (${selectedIds.size})`
: '检查封禁'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowBanCheckSettings(!showBanCheckSettings)}
icon={<Settings className="h-4 w-4" />}
className={showBanCheckSettings ? 'text-blue-500' : 'text-slate-500 hover:text-slate-600'}
>
</Button>
{selectedIds.size > 0 && (
<Button
variant="ghost"
@@ -280,6 +421,62 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
</div>
</div>
</CardHeader>
{/* 封禁检查设置面板 */}
{showBanCheckSettings && (
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<label className="text-sm text-slate-600 dark:text-slate-400">:</label>
<button
onClick={() => handleSaveBanCheckSettings(!banCheckSettings?.enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
banCheckSettings?.enabled ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
banCheckSettings?.enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-slate-400" />
<label className="text-sm text-slate-600 dark:text-slate-400">:</label>
<Input
type="number"
value={checkHours}
onChange={(e) => setCheckHours(parseInt(e.target.value) || 24)}
className="w-20 h-8 text-sm"
min={1}
max={168}
/>
<span className="text-sm text-slate-500"></span>
<Button
size="sm"
variant="outline"
onClick={() => handleSaveBanCheckSettings(banCheckSettings?.enabled || false)}
>
</Button>
</div>
<div className="flex items-center gap-2 ml-auto text-xs text-slate-500">
{banCheckSettings?.task_state?.running ? (
<span className="text-emerald-500">
: {banCheckSettings.task_state.checked}/{banCheckSettings.task_state.total}
(: {banCheckSettings.task_state.valid}, : {banCheckSettings.task_state.banned})
</span>
) : banCheckSettings?.enabled ? (
<span></span>
) : (
<span></span>
)}
</div>
</div>
</div>
)}
<CardContent className="flex-1 overflow-hidden p-0">
<div className="h-full overflow-auto">
<table className="w-full text-sm">
@@ -298,19 +495,20 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">Account ID</th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400"></th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="text-center py-8 text-slate-500">
<td colSpan={7} className="text-center py-8 text-slate-500">
...
</td>
</tr>
) : owners.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-8 text-slate-500">
<td colSpan={7} className="text-center py-8 text-slate-500">
</td>
</tr>
@@ -336,6 +534,9 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
</span>
</td>
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td>
<td className="p-3 text-slate-500 text-xs">
{owner.last_checked_at ? formatTime(owner.last_checked_at) : '-'}
</td>
<td className="p-3 text-center">
<button
onClick={() => handleDelete(owner.id)}