chore: 固定后台布局与全局滚动样式优化

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-12 11:08:41 +08:00
parent 30b9cd5e05
commit cfaca4a2aa
10 changed files with 459 additions and 455 deletions

View File

@ -12,6 +12,12 @@ const AConfigProvider = ConfigProvider;
</script>
<style>
html,
body,
#app {
height: 100%;
}
#app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
@ -24,4 +30,24 @@ body {
margin: 0;
padding: 0;
}
/* 全局滚动条样式(适用于页面与内部可滚动容器) */
* {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
*::-webkit-scrollbar {
width: 4px;
height: 4px;
}
*::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 999px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
</style>

View File

@ -15,10 +15,6 @@ declare module 'vue' {
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
@ -26,47 +22,33 @@ declare module 'vue' {
AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']

View File

@ -1,10 +1,12 @@
<template>
<a-layout class="admin-layout">
<a-layout class="admin-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="admin-sider"
:width="240"
:collapsed-width="80"
>
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
@ -14,6 +16,7 @@
</div>
</div>
<div class="sider-scroll">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
@ -71,9 +74,10 @@
<span>系统设置</span>
</a-menu-item>
</a-menu>
</div>
</a-layout-sider>
<a-layout>
<a-layout class="admin-main">
<a-layout-header class="admin-header">
<div class="header-left">
<MenuUnfoldOutlined
@ -218,6 +222,10 @@ $border-color: #E5E7EB;
$bg-light: #F9FAFB;
$bg-dark: #111827;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.admin-layout {
min-height: 100vh;
background: $bg-light;
@ -227,6 +235,19 @@ $bg-dark: #111827;
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo {
height: 72px;
@ -264,6 +285,12 @@ $bg-dark: #111827;
}
}
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu {
border-right: none !important;
padding: 12px 8px;
@ -318,6 +345,11 @@ $bg-dark: #111827;
}
}
.admin-main {
margin-left: $sider-width;
min-height: 100vh;
}
.admin-header {
background: white;
padding: 0 24px;
@ -326,6 +358,12 @@ $bg-dark: #111827;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger {
font-size: 18px;
@ -354,10 +392,21 @@ $bg-dark: #111827;
.admin-content {
margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px;
background: white;
border-radius: 16px;
min-height: calc(100vh - 64px - 40px);
min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.admin-layout.is-collapsed {
.admin-main {
margin-left: $sider-collapsed-width;
}
.admin-header {
left: $sider-collapsed-width;
}
}
</style>

View File

@ -4,14 +4,24 @@
<div class="welcome-banner">
<div class="banner-content">
<div class="banner-text">
<h1><HomeOutlined /> 校园阅读管理中心</h1>
<h1>
<HomeOutlined /> 校园阅读管理中心
</h1>
<p>让每一个孩子都能享受阅读的快乐智慧成长每一天</p>
</div>
<div class="banner-decorations">
<span class="decoration"><BookOutlined /></span>
<span class="decoration"><StarOutlined /></span>
<span class="decoration"><BgColorsOutlined /></span>
<span class="decoration"><SmileOutlined /></span>
<span class="decoration">
<BookOutlined />
</span>
<span class="decoration">
<StarOutlined />
</span>
<span class="decoration">
<BgColorsOutlined />
</span>
<span class="decoration">
<SmileOutlined />
</span>
</div>
</div>
</div>
@ -33,7 +43,9 @@
<div class="charts-grid">
<div class="content-card trend-card">
<div class="card-header">
<span class="card-icon"><LineChartOutlined /></span>
<span class="card-icon">
<LineChartOutlined />
</span>
<h3>授课趋势</h3>
</div>
<div class="card-body" :class="{ 'is-loading': trendLoading }">
@ -43,7 +55,9 @@
</div>
<div class="content-card distribution-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<span class="card-icon">
<BarChartOutlined />
</span>
<h3>课程分布</h3>
</div>
<div class="card-body" :class="{ 'is-loading': distributionLoading }">
@ -58,21 +72,21 @@
<!-- 近期活动 -->
<div class="content-card activities-card">
<div class="card-header">
<span class="card-icon"><CalendarOutlined /></span>
<span class="card-icon">
<CalendarOutlined />
</span>
<h3>近期课程活动</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="recentActivities.length === 0" class="empty-state">
<span class="empty-icon"><InboxOutlined /></span>
<span class="empty-icon">
<InboxOutlined />
</span>
<p>暂无近期活动</p>
</div>
<div v-else class="activity-list">
<div
v-for="item in recentActivities"
:key="item.id"
class="activity-item"
>
<div v-for="item in recentActivities" :key="item.id" class="activity-item">
<div class="activity-avatar">
<BookOutlined />
</div>
@ -88,28 +102,30 @@
<!-- 教师活跃度排行 -->
<div class="content-card teachers-card">
<div class="card-header">
<span class="card-icon"><TrophyOutlined /></span>
<span class="card-icon">
<TrophyOutlined />
</span>
<h3>教师活跃度排行</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span>
<span class="empty-icon">
<TeamOutlined />
</span>
<p>暂无数据</p>
</div>
<div v-else class="teacher-list">
<div
v-for="(item, index) in activeTeachers"
:key="item.id"
class="teacher-item"
>
<div v-for="(item, index) in activeTeachers" :key="item.id" class="teacher-item">
<div class="rank-badge" :class="'rank-' + (index + 1)">
{{ index + 1 }}
</div>
<div class="teacher-info">
<div class="teacher-name">{{ item.name }}</div>
<div class="teacher-lessons">
<span class="lesson-icon"><ReadOutlined /></span>
<span class="lesson-icon">
<ReadOutlined />
</span>
授课 {{ item.lessonCount }}
</div>
</div>
@ -125,29 +141,25 @@
<!-- 课程使用统计 -->
<div class="course-stats-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<span class="card-icon">
<BarChartOutlined />
</span>
<h3>课程使用统计</h3>
<div class="header-extra">
<a-range-picker
v-model:value="dateRange"
@change="loadCourseStats"
:placeholder="['开始日期', '结束日期']"
style="width: 240px;"
/>
<a-range-picker v-model:value="dateRange" @change="loadCourseStats" :placeholder="['开始日期', '结束日期']"
style="width: 240px;" />
</div>
</div>
<div class="card-body" :class="{ 'is-loading': courseStatsLoading }">
<a-spin v-if="courseStatsLoading" />
<div v-else-if="courseStats.length === 0" class="empty-state">
<span class="empty-icon"><LineChartOutlined /></span>
<span class="empty-icon">
<LineChartOutlined />
</span>
<p>暂无课程使用数据</p>
</div>
<div v-else class="course-list">
<div
v-for="(item, index) in courseStats"
:key="item.courseId"
class="course-item"
>
<div v-for="(item, index) in courseStats" :key="item.courseId" class="course-item">
<div class="course-rank" :class="'top-' + (index + 1)">
<TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" />
<span v-else>{{ index + 1 }}</span>
@ -155,13 +167,10 @@
<div class="course-name">{{ item.courseName }}</div>
<div class="course-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{
<div class="progress-fill" :style="{
width: getUsagePercent(item.usageCount) + '%',
background: getProgressGradient(index)
}"
></div>
}"></div>
</div>
<span class="progress-value">{{ item.usageCount }}</span>
</div>
@ -174,7 +183,9 @@
<div class="export-section">
<div class="content-card export-card">
<div class="card-header">
<span class="card-icon"><DownloadOutlined /></span>
<span class="card-icon">
<DownloadOutlined />
</span>
<h3>数据导出</h3>
</div>
<div class="card-body">
@ -717,13 +728,28 @@ onUnmounted(() => {
animation: float 3s ease-in-out infinite;
}
.decoration:nth-child(2) { animation-delay: 0.5s; }
.decoration:nth-child(3) { animation-delay: 1s; }
.decoration:nth-child(4) { animation-delay: 1.5s; }
.decoration:nth-child(2) {
animation-delay: 0.5s;
}
.decoration:nth-child(3) {
animation-delay: 1s;
}
.decoration:nth-child(4) {
animation-delay: 1.5s;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* 统计卡片 */
@ -956,9 +982,17 @@ onUnmounted(() => {
background: #B2BEC3;
}
.rank-badge.rank-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); }
.rank-badge.rank-2 { background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); }
.rank-badge.rank-3 { background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%); }
.rank-badge.rank-1 {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
.rank-badge.rank-2 {
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
}
.rank-badge.rank-3 {
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
}
.teacher-info {
flex: 1;

View File

@ -1,10 +1,12 @@
<template>
<a-layout class="school-layout">
<a-layout class="school-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="school-sider"
:width="240"
:collapsed-width="80"
>
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
@ -15,6 +17,7 @@
</div>
</div>
<div class="sider-scroll">
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
@ -114,9 +117,10 @@
</a-menu-item>
</a-sub-menu>
</a-menu>
</div>
</a-layout-sider>
<a-layout>
<a-layout class="school-main">
<a-layout-header class="school-header">
<div class="header-left">
<MenuUnfoldOutlined
@ -287,6 +291,10 @@ $text-secondary: #666666;
$border-color: #E8E8E8;
$bg-light: #FAFAFA;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.school-layout {
min-height: 100vh;
background: $bg-light;
@ -296,6 +304,19 @@ $bg-light: #FAFAFA;
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo {
height: 80px;
@ -344,6 +365,12 @@ $bg-light: #FAFAFA;
}
}
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu {
border-right: none !important;
padding: 8px 12px;
@ -423,6 +450,11 @@ $bg-light: #FAFAFA;
}
}
.school-main {
margin-left: $sider-width;
min-height: 100vh;
}
.school-header {
background: white;
padding: 0 24px;
@ -431,6 +463,12 @@ $bg-light: #FAFAFA;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger {
font-size: 18px;
@ -459,10 +497,21 @@ $bg-light: #FAFAFA;
.school-content {
margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px;
background: white;
border-radius: 12px;
min-height: calc(100vh - 64px - 40px);
min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.school-layout.is-collapsed {
.school-main {
margin-left: $sider-collapsed-width;
}
.school-header {
left: $sider-collapsed-width;
}
}
</style>

View File

@ -1,10 +1,12 @@
<template>
<a-layout class="teacher-layout">
<a-layout class="teacher-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="teacher-sider"
:width="240"
:collapsed-width="80"
>
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
@ -15,6 +17,7 @@
</div>
</div>
<div class="sider-scroll">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
@ -60,9 +63,10 @@
<span>成长档案</span>
</a-menu-item>
</a-menu>
</div>
</a-layout-sider>
<a-layout>
<a-layout class="teacher-main">
<a-layout-header class="teacher-header">
<div class="header-left">
<MenuUnfoldOutlined
@ -204,6 +208,10 @@ $text-secondary: #666666;
$border-color: #E8E8E8;
$bg-light: #FAFAFA;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.teacher-layout {
min-height: 100vh;
background: $bg-light;
@ -213,6 +221,19 @@ $bg-light: #FAFAFA;
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo {
height: 80px;
@ -261,6 +282,12 @@ $bg-light: #FAFAFA;
}
}
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu {
border-right: none !important;
padding: 8px 12px;
@ -303,6 +330,11 @@ $bg-light: #FAFAFA;
}
}
.teacher-main {
margin-left: $sider-width;
min-height: 100vh;
}
.teacher-header {
background: white;
padding: 0 24px;
@ -311,6 +343,12 @@ $bg-light: #FAFAFA;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger {
font-size: 18px;
@ -339,10 +377,21 @@ $bg-light: #FAFAFA;
.teacher-content {
margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px;
background: white;
border-radius: 12px;
min-height: calc(100vh - 64px - 40px);
min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.teacher-layout.is-collapsed {
.teacher-main {
margin-left: $sider-collapsed-width;
}
.teacher-header {
left: $sider-collapsed-width;
}
}
</style>

View File

@ -1,57 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
/**
* 数据库表结构检查工具
*/
@SpringBootTest
@ActiveProfiles("dev")
public class DatabaseInspectTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 检查 V20 迁移涉及的表和列
*/
@Test
public void checkV20Tables() {
System.out.println("=== 检查 V20 迁移涉及的表和列 ===\n");
String[] tables = {"student_records", "lesson_feedbacks", "lessons", "student_class_history", "class_teacher", "students"};
for (String table : tables) {
System.out.println("表:" + table);
try {
List<Map<String, Object>> columns = jdbcTemplate.queryForList(
"SHOW COLUMNS FROM " + table
);
System.out.println(" 列名:");
for (Map<String, Object> col : columns) {
System.out.println(" - " + col.get("Field") + " (" + col.get("Type") + ")");
}
} catch (Exception e) {
System.out.println(" 表不存在:" + e.getMessage());
}
System.out.println();
}
}
/**
* 清理 flyway_schema_history 表中的 V20 记录
*/
@Test
public void cleanFlywayHistory() {
System.out.println("=== 清理 Flyway V20 记录 ===");
int deleted = jdbcTemplate.update("DELETE FROM flyway_schema_history WHERE version = '20'");
System.out.println("已删除 " + deleted + " 条记录");
}
}

View File

@ -1,73 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
/**
* Flyway 迁移历史查看和清理工具
*/
@SpringBootTest
@ActiveProfiles("dev")
public class FlywayHistoryTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 查看迁移历史
*/
@Test
public void showHistory() {
System.out.println("=== Flyway 迁移历史 ===");
List<Map<String, Object>> history = jdbcTemplate.queryForList(
"SELECT version, description, type, script, checksum, installed_by, installed_on, execution_time, success " +
"FROM flyway_schema_history " +
"ORDER BY installed_rank DESC"
);
for (Map<String, Object> row : history) {
System.out.printf("Version: %-10s | Type: %-15s | Script: %-50s | Success: %s | Installed: %s%n",
row.get("version"),
row.get("type"),
row.get("script"),
row.get("success"),
row.get("installed_on")
);
}
}
/**
* 清理版本 20 的迁移记录
*/
@Test
public void cleanV20() {
System.out.println("=== 清理版本 20 的迁移记录 ===");
// 查看版本 20 的记录
List<Map<String, Object>> v20Records = jdbcTemplate.queryForList(
"SELECT * FROM flyway_schema_history WHERE version = '20'"
);
if (v20Records.isEmpty()) {
System.out.println("没有找到版本 20 的记录");
return;
}
System.out.println("找到 " + v20Records.size() + " 条版本 20 的记录:");
for (Map<String, Object> row : v20Records) {
System.out.printf(" - ID: %s, Script: %s, Success: %s%n",
row.get("installed_rank"), row.get("script"), row.get("success"));
}
// 删除版本 20 的所有记录
int deleted = jdbcTemplate.update("DELETE FROM flyway_schema_history WHERE version = '20'");
System.out.println("已删除 " + deleted + " 条记录");
System.out.println("请重启应用Flyway 会重新执行 V20 迁移");
}
}

View File

@ -1,55 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
/**
* Flyway 修复工具
* 用于修复失败的数据库迁移
*
* 使用方法
* 1. application-dev.yml 中添加spring.flyway.enabled=false
* 2. 运行此测试类mvn test -Dtest=FlywayRepairTool#repairV20
* 3. 恢复 Flyway 配置
* 4. 重启应用
*/
@SpringBootTest
@ActiveProfiles("dev")
public class FlywayRepairTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 修复 V20 失败的迁移
*/
@Test
public void repairV20() {
System.out.println("开始修复 Flyway V20 迁移...");
// 删除 V20 的失败记录
int rows = jdbcTemplate.update(
"DELETE FROM flyway_schema_history WHERE version = '20'"
);
System.out.println("已删除 " + rows + " 条失败记录");
System.out.println("修复完成!请重启应用。");
}
/**
* 查看当前的迁移历史
*/
@Test
public void showMigrationHistory() {
System.out.println("当前迁移历史:");
jdbcTemplate.queryForList("SELECT * FROM flyway_schema_history ORDER BY installed_on DESC")
.forEach(record -> {
System.out.println(" Version: " + record.get("version") +
", Status: " + record.get("success") +
", Installed: " + record.get("installed_on"));
});
}
}