feat: Establish core backend services including SQLite database, structured logging, and initial owner management capabilities.
This commit is contained in:
@@ -133,6 +133,7 @@ func startServer(cfg *config.Config) {
|
|||||||
mux.HandleFunc("/api/db/owners/clear-used", api.CORS(handleClearUsedOwners)) // 清理已使用
|
mux.HandleFunc("/api/db/owners/clear-used", api.CORS(handleClearUsedOwners)) // 清理已使用
|
||||||
mux.HandleFunc("/api/db/owners/delete/", api.CORS(handleDeleteOwner)) // DELETE /api/db/owners/delete/{id}
|
mux.HandleFunc("/api/db/owners/delete/", api.CORS(handleDeleteOwner)) // DELETE /api/db/owners/delete/{id}
|
||||||
mux.HandleFunc("/api/db/owners/batch-delete", api.CORS(handleBatchDeleteOwners)) // POST 批量删除
|
mux.HandleFunc("/api/db/owners/batch-delete", api.CORS(handleBatchDeleteOwners)) // POST 批量删除
|
||||||
|
mux.HandleFunc("/api/db/owners/ids", api.CORS(handleGetOwnerIDs)) // GET 获取所有ID(全选用)
|
||||||
mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
|
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))
|
||||||
|
|
||||||
@@ -977,6 +978,31 @@ func handleBatchDeleteOwners(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetOwnerIDs GET /api/db/owners/ids?status=xxx - 获取所有符合条件的 owner ID(全选用)
|
||||||
|
func handleGetOwnerIDs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
api.Error(w, http.StatusMethodNotAllowed, "仅支持 GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if database.Instance == nil {
|
||||||
|
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
ids, err := database.Instance.GetTeamOwnerIDs(status)
|
||||||
|
if err != nil {
|
||||||
|
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Success(w, map[string]interface{}{
|
||||||
|
"ids": ids,
|
||||||
|
"total": len(ids),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// handleRegisterTest POST /api/register/test - 测试注册流程
|
// handleRegisterTest POST /api/register/test - 测试注册流程
|
||||||
func handleRegisterTest(w http.ResponseWriter, r *http.Request) {
|
func handleRegisterTest(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
|||||||
@@ -459,6 +459,33 @@ func (d *DB) UpdateOwnerAccountID(id int64, accountID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTeamOwnerIDs 获取所有符合条件的 owner ID(用于全选)
|
||||||
|
func (d *DB) GetTeamOwnerIDs(status string) ([]int64, error) {
|
||||||
|
query := "SELECT id FROM team_owners WHERE 1=1"
|
||||||
|
args := []interface{}{}
|
||||||
|
if status != "" {
|
||||||
|
query += " AND status = ?"
|
||||||
|
args = append(args, status)
|
||||||
|
}
|
||||||
|
query += " ORDER BY created_at DESC"
|
||||||
|
|
||||||
|
rows, err := d.db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var ids []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetOwnerStats 获取统计
|
// GetOwnerStats 获取统计
|
||||||
func (d *DB) GetOwnerStats() map[string]int {
|
func (d *DB) GetOwnerStats() map[string]int {
|
||||||
stats := map[string]int{
|
stats := map[string]int{
|
||||||
|
|||||||
@@ -238,7 +238,38 @@ func GetLogs(limit int) []LogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLogsByModule 按模块筛选日志并分页(最新的在前)
|
// GetLogsByModule 按模块筛选日志并分页(最新的在前)
|
||||||
|
// 优先从内存读取,内存无数据时回退到数据库
|
||||||
func GetLogsByModule(module string, page, pageSize int) ([]LogEntry, int) {
|
func GetLogsByModule(module string, page, pageSize int) ([]LogEntry, int) {
|
||||||
|
logsMu.RLock()
|
||||||
|
// 检查内存中是否有该模块的日志
|
||||||
|
hasMemoryLogs := false
|
||||||
|
for i := len(logs) - 1; i >= 0; i-- {
|
||||||
|
if logs[i].Module == module {
|
||||||
|
hasMemoryLogs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logsMu.RUnlock()
|
||||||
|
|
||||||
|
// 内存中无数据,回退到数据库查询
|
||||||
|
if !hasMemoryLogs && database.Instance != nil {
|
||||||
|
dbLogs, total, err := database.Instance.GetLogsByModule(module, page, pageSize)
|
||||||
|
if err == nil && total > 0 {
|
||||||
|
entries := make([]LogEntry, len(dbLogs))
|
||||||
|
for i, dl := range dbLogs {
|
||||||
|
entries[i] = LogEntry{
|
||||||
|
Timestamp: dl.Timestamp,
|
||||||
|
Level: dl.Level,
|
||||||
|
Message: dl.Message,
|
||||||
|
Email: dl.Email,
|
||||||
|
Module: dl.Module,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从内存读取
|
||||||
logsMu.RLock()
|
logsMu.RLock()
|
||||||
defer logsMu.RUnlock()
|
defer logsMu.RUnlock()
|
||||||
|
|
||||||
@@ -283,7 +314,40 @@ func ClearLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLogsByModuleAndLevel 按模块和级别筛选日志并分页(最新的在前)
|
// GetLogsByModuleAndLevel 按模块和级别筛选日志并分页(最新的在前)
|
||||||
|
// 优先从内存读取,内存无数据时回退到数据库
|
||||||
func GetLogsByModuleAndLevel(module, level string, page, pageSize int) ([]LogEntry, int) {
|
func GetLogsByModuleAndLevel(module, level string, page, pageSize int) ([]LogEntry, int) {
|
||||||
|
logsMu.RLock()
|
||||||
|
// 检查内存中是否有该模块+级别的日志
|
||||||
|
hasMemoryLogs := false
|
||||||
|
for i := len(logs) - 1; i >= 0; i-- {
|
||||||
|
if logs[i].Module == module {
|
||||||
|
if level == "" || logs[i].Level == level {
|
||||||
|
hasMemoryLogs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logsMu.RUnlock()
|
||||||
|
|
||||||
|
// 内存中无数据,回退到数据库查询
|
||||||
|
if !hasMemoryLogs && database.Instance != nil {
|
||||||
|
dbLogs, total, err := database.Instance.GetLogsByModuleAndLevel(module, level, page, pageSize)
|
||||||
|
if err == nil && total > 0 {
|
||||||
|
entries := make([]LogEntry, len(dbLogs))
|
||||||
|
for i, dl := range dbLogs {
|
||||||
|
entries[i] = LogEntry{
|
||||||
|
Timestamp: dl.Timestamp,
|
||||||
|
Level: dl.Level,
|
||||||
|
Message: dl.Message,
|
||||||
|
Email: dl.Email,
|
||||||
|
Module: dl.Module,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从内存读取
|
||||||
logsMu.RLock()
|
logsMu.RLock()
|
||||||
defer logsMu.RUnlock()
|
defer logsMu.RUnlock()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
|
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, MinusSquare, ShieldCheck, Settings, Clock } from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
|
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
|
||||||
|
|
||||||
interface TeamOwner {
|
interface TeamOwner {
|
||||||
@@ -58,6 +58,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const [filter, setFilter] = useState<string>('')
|
const [filter, setFilter] = useState<string>('')
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
|
||||||
|
const [selectAllMode, setSelectAllMode] = useState(false) // 是否全选所有页
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
const limit = 20
|
const limit = 20
|
||||||
|
|
||||||
@@ -93,8 +94,10 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOwners()
|
loadOwners()
|
||||||
// 清除选择
|
// 清除选择(全选所有页模式除外)
|
||||||
setSelectedIds(new Set())
|
if (!selectAllMode) {
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
}
|
||||||
}, [page, filter])
|
}, [page, filter])
|
||||||
|
|
||||||
// 加载封禁检查配置
|
// 加载封禁检查配置
|
||||||
@@ -295,6 +298,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
|
|
||||||
// 选择逻辑
|
// 选择逻辑
|
||||||
const toggleSelect = (id: number) => {
|
const toggleSelect = (id: number) => {
|
||||||
|
setSelectAllMode(false)
|
||||||
const newSet = new Set(selectedIds)
|
const newSet = new Set(selectedIds)
|
||||||
if (newSet.has(id)) {
|
if (newSet.has(id)) {
|
||||||
newSet.delete(id)
|
newSet.delete(id)
|
||||||
@@ -305,14 +309,40 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
if (selectedIds.size === owners.length) {
|
if (selectedIds.size === owners.length && !selectAllMode) {
|
||||||
|
// 当前页已全选,取消全选
|
||||||
setSelectedIds(new Set())
|
setSelectedIds(new Set())
|
||||||
} else {
|
} else {
|
||||||
|
// 选中当前页所有
|
||||||
|
setSelectAllMode(false)
|
||||||
setSelectedIds(new Set(owners.map(o => o.id)))
|
setSelectedIds(new Set(owners.map(o => o.id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAllSelected = owners.length > 0 && selectedIds.size === owners.length
|
// 全选所有页
|
||||||
|
const handleSelectAllPages = async () => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (filter) params.set('status', filter)
|
||||||
|
const res = await fetch(`/api/db/owners/ids?${params}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.code === 0 && data.data.ids) {
|
||||||
|
setSelectedIds(new Set(data.data.ids))
|
||||||
|
setSelectAllMode(true)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to select all:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消全选
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
setSelectAllMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllSelected = owners.length > 0 && selectedIds.size === owners.length && !selectAllMode
|
||||||
|
const isPagePartialSelected = selectedIds.size > 0 && owners.some(o => selectedIds.has(o.id)) && !isAllSelected && !selectAllMode
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit)
|
const totalPages = Math.ceil(total / limit)
|
||||||
|
|
||||||
@@ -478,14 +508,60 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<CardContent className="flex-1 overflow-hidden p-0">
|
<CardContent className="flex-1 overflow-hidden p-0">
|
||||||
|
{/* 全选提示条 */}
|
||||||
|
{selectedIds.size > 0 && (
|
||||||
|
<div className="px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-800 flex items-center gap-3 text-sm">
|
||||||
|
{selectAllMode ? (
|
||||||
|
<>
|
||||||
|
<span className="text-blue-700 dark:text-blue-300">
|
||||||
|
已选择全部 <strong>{selectedIds.size}</strong> 个{filter ? ` "${statusLabels[filter] || filter}" 状态的` : ''}母号
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleClearSelection}
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
取消选择
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : isAllSelected && total > owners.length ? (
|
||||||
|
<>
|
||||||
|
<span className="text-blue-700 dark:text-blue-300">
|
||||||
|
已选择当前页全部 <strong>{owners.length}</strong> 个母号
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleSelectAllPages}
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
选择全部 {total} 个{filter ? ` "${statusLabels[filter] || filter}" 状态的` : ''}母号
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-blue-700 dark:text-blue-300">
|
||||||
|
已选择 <strong>{selectedIds.size}</strong> 个母号
|
||||||
|
{total > owners.length && (
|
||||||
|
<button
|
||||||
|
onClick={handleSelectAllPages}
|
||||||
|
className="ml-3 text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
选择全部 {total} 个
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0">
|
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-center p-3 w-10">
|
<th className="text-center p-3 w-10">
|
||||||
<button onClick={toggleSelectAll} className="hover:text-blue-500">
|
<button onClick={toggleSelectAll} className="hover:text-blue-500">
|
||||||
{isAllSelected ? (
|
{selectAllMode ? (
|
||||||
<CheckSquare className="h-4 w-4 text-blue-500" />
|
<CheckSquare className="h-4 w-4 text-blue-500" />
|
||||||
|
) : isAllSelected ? (
|
||||||
|
<CheckSquare className="h-4 w-4 text-blue-500" />
|
||||||
|
) : isPagePartialSelected ? (
|
||||||
|
<MinusSquare className="h-4 w-4 text-blue-400" />
|
||||||
) : (
|
) : (
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user