新增数据重置功能:管理员可一键清空所有行情数据,需输入确认文字防误操作
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -193,3 +193,10 @@ def list_candles(
|
|||||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/admin/reset-data")
|
||||||
|
def reset_data():
|
||||||
|
"""清空所有行情数据(仅限管理员调用)。"""
|
||||||
|
storage.truncate_all()
|
||||||
|
return {"status": "ok", "message": "已清空所有行情数据"}
|
||||||
|
|||||||
@@ -152,3 +152,14 @@ def get_latest_score(ts_code: str, db_url: str = DEFAULT_DB_URL) -> Optional[dic
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_all(db_url: str = DEFAULT_DB_URL):
|
||||||
|
"""清空所有行情数据(candles + scores),保留用户表。"""
|
||||||
|
conn = _get_conn(db_url)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("TRUNCATE TABLE candles, scores")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
22
web/backend/internal/handlers/reset.go
Normal file
22
web/backend/internal/handlers/reset.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Deps) AdminResetData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Post(d.TushareURL+"/api/v1/admin/reset-data", "application/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, http.StatusBadGateway, fmt.Sprintf("tushare service unavailable: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ func New(d *handlers.Deps, dist fs.FS) http.Handler {
|
|||||||
r.Post("/admin/users", d.AdminCreateUser)
|
r.Post("/admin/users", d.AdminCreateUser)
|
||||||
r.Patch("/admin/users/{id}", d.AdminPatchUser)
|
r.Patch("/admin/users/{id}", d.AdminPatchUser)
|
||||||
r.Delete("/admin/users/{id}", d.AdminDeleteUser)
|
r.Delete("/admin/users/{id}", d.AdminDeleteUser)
|
||||||
|
r.Post("/admin/reset-data", d.AdminResetData)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { useMobile } from '@/composables/useMobile'
|
import { useMobile } from '@/composables/useMobile'
|
||||||
|
import { resetAllData } from '@/api/admin'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
@@ -12,6 +14,7 @@ const route = useRoute()
|
|||||||
const { isMobile } = useMobile()
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
|
const resetting = ref(false)
|
||||||
|
|
||||||
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
||||||
|
|
||||||
@@ -29,6 +32,26 @@ function logout() {
|
|||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
drawerOpen.value = false
|
drawerOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleReset() {
|
||||||
|
closeDrawer()
|
||||||
|
try {
|
||||||
|
await ElMessageBox.prompt('此操作将清空所有行情数据(candles + scores),不可恢复。请输入"确认重置"后继续:', '数据重置', {
|
||||||
|
confirmButtonText: '确认重置',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPattern: /^确认重置$/,
|
||||||
|
inputErrorMessage: '请输入"确认重置"',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
resetting.value = true
|
||||||
|
await resetAllData()
|
||||||
|
ElMessage.success('已清空所有行情数据')
|
||||||
|
} catch {
|
||||||
|
// user cancelled
|
||||||
|
} finally {
|
||||||
|
resetting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,6 +70,9 @@ function closeDrawer() {
|
|||||||
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
|
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
|
||||||
<el-menu-item index="/run">手动打分</el-menu-item>
|
<el-menu-item index="/run">手动打分</el-menu-item>
|
||||||
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
|
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
|
||||||
|
<el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleReset" :disabled="resetting">
|
||||||
|
数据重置
|
||||||
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
|
|||||||
5
web/frontend/src/api/admin.ts
Normal file
5
web/frontend/src/api/admin.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
export function resetAllData() {
|
||||||
|
return client.post('/admin/reset-data').then((r) => r.data)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user