Commit d57928c4 authored by wangteng's avatar wangteng

代码提交

parent 6d58b2e9
...@@ -5,3 +5,6 @@ ...@@ -5,3 +5,6 @@
# 总体要求 # 总体要求
- 每做一个任务就在工作记录中记录一次 - 每做一个任务就在工作记录中记录一次
# 服务器发版本
- deploy_server.py
...@@ -308,53 +308,12 @@ def list_operation_records( ...@@ -308,53 +308,12 @@ def list_operation_records(
operation: Optional[Literal["backup", "restore", "clear", "encrypt", "decrypt"]] = None, operation: Optional[Literal["backup", "restore", "clear", "encrypt", "decrypt"]] = None,
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
if operation in ("backup", "clear"):
_import_file_backed_history(db, operation)
query = db.query(OperationRecord) query = db.query(OperationRecord)
if operation: if operation:
query = query.filter(OperationRecord.operation == operation) query = query.filter(OperationRecord.operation == operation)
return [row.to_dict() for row in query.order_by(OperationRecord.created_at.desc(), OperationRecord.id.desc()).all()] return [row.to_dict() for row in query.order_by(OperationRecord.created_at.desc(), OperationRecord.id.desc()).all()]
def _import_file_backed_history(db: Session, operation: str) -> None:
"""将旧版已存在的备份文件补登记到历史表,仅首次扫描未登记文件。"""
known = {
row[0]
for row in db.query(OperationRecord.filename)
.filter(OperationRecord.operation == operation, OperationRecord.filename != "")
.all()
}
added = 0
for path in get_backup_dir().glob("*.json"):
if path.name in known:
continue
try:
package = json.loads(path.read_text(encoding="utf-8"))
file_operation = package.get("operation") or "backup"
if file_operation != operation:
continue
source = package.get("source") or {}
source_id = source.get("id")
if not isinstance(source_id, int) or not db.get(DataSource, source_id):
source_id = None
db.add(OperationRecord(
operation=operation,
source_id=source_id,
source_name=str(source.get("name") or ""),
database_name=str(source.get("database") or source.get("schema") or ""),
scope=str(source.get("scope") or ""),
tables_json=json.dumps([item.get("table_name") for item in package.get("tables", []) if item.get("table_name")], ensure_ascii=False),
filename=path.name,
execute_setting="由已有备份文件补登记",
created_at=datetime.fromtimestamp(path.stat().st_mtime),
))
added += 1
except (OSError, ValueError, TypeError, json.JSONDecodeError):
continue
if added:
db.commit()
@router.post("/records/import") @router.post("/records/import")
def import_legacy_operation_records( def import_legacy_operation_records(
operation: Literal["backup", "restore", "clear", "encrypt", "decrypt"], operation: Literal["backup", "restore", "clear", "encrypt", "decrypt"],
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<div class="clear-page"> <div class="clear-page">
<div class="config-toolbar clear-toolbar"> <div class="config-toolbar clear-toolbar">
<div class="left"> <div class="left">
<input v-model="keyword" class="text-input clear-search" placeholder="备份文件名/数据库/数据表" @keyup.enter="loadVersions" /> <input v-model="keyword" class="text-input clear-search" placeholder="备份文件名/数据库/数据表" @keyup.enter="onQuery" />
<button class="btn btn-primary" @click="loadVersions">查询</button> <button class="btn btn-primary" @click="onQuery">查询</button><button class="btn" @click="onReset">重置</button>
</div> </div>
<div class="right"><button class="btn btn-danger" @click="openCreate">清空数据表</button></div> <div class="right"><button class="btn btn-danger" @click="openCreate">清空数据表</button></div>
</div> </div>
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
<div class="q-card"> <div class="q-card">
<div class="table-scroll"> <div class="table-scroll">
<table class="config-list clear-list"> <table class="config-list clear-list">
<thead><tr><th>备份文件</th><th>数据源</th><th>数据库 / Schema</th><th>已清空数据表</th><th>备份时间</th><th>操作</th></tr></thead> <thead><tr><th>备份文件</th><th>数据源</th><th>数据库</th><th>已清空数据表</th><th>备份时间</th><th>操作</th></tr></thead>
<tbody> <tbody>
<tr v-if="!records.length"><td colspan="6" class="empty-cell">暂无清表记录</td></tr> <tr v-if="!recordsLoading && !pagedRecords.length"><td colspan="6" class="empty-cell">暂无清表记录</td></tr>
<tr v-for="record in records" :key="record.id"> <tr v-for="record in pagedRecords" :key="record.id">
<td class="filename">{{ record.filename }}</td><td>{{ record.source?.name || '—' }}</td> <td class="filename">{{ record.filename }}</td><td>{{ record.source?.name || '—' }}</td>
<td>{{ record.source?.database || record.source?.schema || '—' }}</td> <td>{{ record.source?.database || record.source?.schema || '—' }}</td>
<td class="tables">{{ (record.tables || []).join('、') || '—' }}</td> <td class="tables">{{ (record.tables || []).join('、') || '—' }}</td>
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div><div class="pagination-wrap" v-if="filteredRecords.length"><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 15, 20, 30]" :total="filteredRecords.length" layout="total, sizes, prev, pager, next" background small /></div>
</div> </div>
</div> </div>
...@@ -39,15 +39,20 @@ ...@@ -39,15 +39,20 @@
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { listDataSources } from '@/api/dataSources' import { listDataSources } from '@/api/dataSources'
import { listSchemas, listTables } from '@/api/connections' import { listSchemas, listTables } from '@/api/connections'
import { clearTables, listOperationRecords, restoreClearedTables } from '@/api/backup' import { clearTables, listOperationRecords, restoreClearedTables } from '@/api/backup'
const sources = ref([]), schemas = ref([]), tables = ref([]), records = ref([]) const sources = ref([]), schemas = ref([]), tables = ref([]), records = ref([])
const keyword = ref(''), createOpen = ref(false), loading = ref(false), clearing = ref(false), restoring = ref('') const keyword = ref(''), appliedKeyword = ref(''), currentPage = ref(1), pageSize = ref(10), createOpen = ref(false), loading = ref(false), recordsLoading = ref(true), clearing = ref(false), restoring = ref('')
const form = reactive({ source: null, database: '', scope: 'database', tables: [] }) const form = reactive({ source: null, database: '', scope: 'database', tables: [] })
const filteredRecords = computed(() => {
const q = appliedKeyword.value.trim().toLowerCase()
return q ? records.value.filter(item => [item.filename, item.source_name, item.database, ...(item.tables || [])].join(' ').toLowerCase().includes(q)) : records.value
})
const pagedRecords = computed(() => filteredRecords.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
function sourceForm(source, database = '') { function sourceForm(source, database = '') {
return { dbType: source.db_type, host: source.host, port: source.port, user: source.user, password: source.password || '', db: database || source.database || '', schema: database || source.schema || source.database || '' } return { dbType: source.db_type, host: source.host, port: source.port, user: source.user, password: source.password || '', db: database || source.database || '', schema: database || source.schema || source.database || '' }
...@@ -56,13 +61,12 @@ function formatDate(value) { return value ? new Date(value).toLocaleString() : ' ...@@ -56,13 +61,12 @@ function formatDate(value) { return value ? new Date(value).toLocaleString() : '
function resetForm() { Object.assign(form, { source: null, database: '', scope: 'database', tables: [] }); schemas.value = []; tables.value = [] } function resetForm() { Object.assign(form, { source: null, database: '', scope: 'database', tables: [] }); schemas.value = []; tables.value = [] }
function openCreate() { resetForm(); createOpen.value = true } function openCreate() { resetForm(); createOpen.value = true }
async function loadVersions() { async function loadVersions() {
recordsLoading.value = true
try { try {
const versions = await listOperationRecords('clear') const versions = await listOperationRecords('clear')
const q = keyword.value.trim().toLowerCase() records.value = versions.map(item => ({ ...item, source: { name: item.source_name, database: item.database } }))
records.value = versions currentPage.value = 1
.filter(item => !q || [item.filename, item.source_name, item.database, ...(item.tables || [])].join(' ').toLowerCase().includes(q)) } catch (error) { ElMessage.error(`加载清表记录失败:${error.message}`) } finally { recordsLoading.value = false }
.map(item => ({ ...item, source: { name: item.source_name, database: item.database } }))
} catch (error) { ElMessage.error(`加载清表记录失败:${error.message}`) }
} }
async function onSourceChange() { async function onSourceChange() {
form.database = ''; form.tables = []; schemas.value = []; tables.value = [] form.database = ''; form.tables = []; schemas.value = []; tables.value = []
...@@ -97,7 +101,13 @@ async function restore(record) { ...@@ -97,7 +101,13 @@ async function restore(record) {
restoring.value = record.filename restoring.value = record.filename
try { const result = await restoreClearedTables({ filename: record.filename, source_id: source.id, database, schema: database }); ElMessage.success(result.message) } catch (error) { ElMessage.error(error.message) } finally { restoring.value = '' } try { const result = await restoreClearedTables({ filename: record.filename, source_id: source.id, database, schema: database }); ElMessage.success(result.message) } catch (error) { ElMessage.error(error.message) } finally { restoring.value = '' }
} }
onMounted(async () => { try { sources.value = await listDataSources() } catch (_) { sources.value = [] } await loadVersions() }) function onQuery() { appliedKeyword.value = keyword.value; currentPage.value = 1 }
function onReset() { keyword.value = ''; appliedKeyword.value = ''; currentPage.value = 1 }
watch([filteredRecords, () => Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))], () => {
const pageCount = Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))
if (currentPage.value > pageCount) currentPage.value = pageCount
})
onMounted(async () => { await loadVersions(); try { sources.value = await listDataSources() } catch (_) { sources.value = [] } })
</script> </script>
<style scoped> <style scoped>
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="right"><button class="btn btn-success" @click="openAdd">+ 新增数据源</button></div> <div class="right"><button class="btn btn-success" @click="openAdd">+ 新增数据源</button></div>
</div> </div>
<div class="q-card"> <div class="q-card">
<div class="table-scroll"><table class="config-list"><thead><tr><th>名称</th><th>类型</th><th>地址</th><th>数据库 / Schema</th><th>状态</th><th>最近测试</th><th>操作</th></tr></thead> <div class="table-scroll"><table class="config-list"><thead><tr><th>名称</th><th>类型</th><th>地址</th><th>数据库</th><th>状态</th><th>最近测试</th><th>操作</th></tr></thead>
<tbody><tr v-if="!loading && !pagedRows.length"><td colspan="7" class="empty-cell">{{ filteredRows.length ? '当前页暂无数据' : '暂无数据源,请先新增' }}</td></tr> <tbody><tr v-if="!loading && !pagedRows.length"><td colspan="7" class="empty-cell">{{ filteredRows.length ? '当前页暂无数据' : '暂无数据源,请先新增' }}</td></tr>
<tr v-for="row in pagedRows" :key="row.id"><td><b>{{ row.name }}</b></td><td>{{ row.db_type }}</td><td>{{ row.host }}:{{ row.port }}</td><td>{{ row.database || '—' }}<span v-if="row.schema"> / {{ row.schema }}</span></td><td><span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span></td><td><span v-if="row.last_tested_at" :class="row.last_test_ok ? 'ok-text' : 'error-text'">{{ row.last_test_ok ? '成功' : '失败' }}</span><span v-else>—</span></td><td><button class="btn-text" @click="onTest(row)">测试</button><button class="btn-text" @click="openEdit(row)">编辑</button><button class="btn-text danger" @click="onDelete(row)">删除</button></td></tr></tbody> <tr v-for="row in pagedRows" :key="row.id"><td><b>{{ row.name }}</b></td><td>{{ row.db_type }}</td><td>{{ row.host }}:{{ row.port }}</td><td>{{ row.database || '—' }}<span v-if="row.schema"> / {{ row.schema }}</span></td><td><span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span></td><td><span v-if="row.last_tested_at" :class="row.last_test_ok ? 'ok-text' : 'error-text'">{{ row.last_test_ok ? '成功' : '失败' }}</span><span v-else>—</span></td><td><button class="btn-text" @click="onTest(row)">测试</button><button class="btn-text" @click="openEdit(row)">编辑</button><button class="btn-text danger" @click="onDelete(row)">删除</button></td></tr></tbody>
</table></div> </table></div>
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment