从0到1开始我的全栈之路(第四天)典型的后台管理系统页面设计与前后端实现全流程

1. 前端部分原型草图设计

2. 需求分析

2.1 整体布局

左侧:固定宽度的导航菜单栏

右侧:主要内容区域

可能需要顶部区域显示面包屑导航

2.2 左侧导航菜单结构

顶部标题:数据治理

主菜单项:

  数据源管理

  数据标准

  调度中心

  元数据管理

每个主菜单都有子菜单

支持展开/折叠功能

2.3 交互功能

菜单可以展开/折叠

点击子菜单在右侧显示对应内容

保持当前选中菜单的高亮状态

需要记住用户上次打开的菜单状态

2.4 路由设计

每个子菜单对应一个路由

URL要能反映当前位置

支持刷新页面保持当前视图

3. 架构和技术选型

3.1 布局

Element Plus 的 Container 组件

3.2 菜单

Element Plus 的 Menu 组件

3.3 路由

Vue Router 进行页面切换

3.4 状态管理

Vuex/Pinia 管理菜单状态

4. 逐步实现需求

4.1 安装 Element Plus,在 vue_web 目录下运行

npm install element-plus

4.2 在 main.js 中引入 Element Plus

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

4.3 修改 App.vue 为简单路由容器

因为我们要在登录后进入这个页面,所以原有App.vue 内容移动到新的 Layout.vue

<template>
  <router-view />
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  height: 100%;
}
</style>

4.4 Layout.vue代码

<!-- 将之前 App.vue 的内容移到这里 -->
<template>
  <el-container class="layout-container">
    <!-- 左侧菜单 -->
    <el-aside width="200px">
      <div class="menu-header">数据治理</div>
      <el-menu
        :default-active="activeMenu"
        class="el-menu-vertical"
        background-color="#304156"
        text-color="#fff"
        active-text-color="#ffd04b"
        @select="handleSelect"
      >
        <!-- 数据源管理 -->
        <el-sub-menu index="1">
          <template #title>
            <el-icon><Document /></el-icon>
            <span>数据源管理</span>
          </template>
          <el-menu-item index="1-1">数据源配置</el-menu-item>
          <el-menu-item index="1-2">连接测试</el-menu-item>
        </el-sub-menu>

        <!-- 数据标准 -->
        <el-sub-menu index="2">
          <template #title>
            <el-icon><Document /></el-icon>
            <span>数据标准</span>
          </template>
          <el-menu-item index="2-1">标准定义</el-menu-item>
          <el-menu-item index="2-2">标准管理</el-menu-item>
        </el-sub-menu>

        <!-- 调度中心 -->
        <el-sub-menu index="3">
          <template #title>
            <el-icon><Document /></el-icon>
            <span>调度中心</span>
          </template>
          <el-menu-item index="3-1">任务管理</el-menu-item>
          <el-menu-item index="3-2">调度配置</el-menu-item>
        </el-sub-menu>

        <!-- 元数据管理 -->
        <el-sub-menu index="4">
          <template #title>
            <el-icon><Document /></el-icon>
            <span>元数据管理</span>
          </template>
          <el-menu-item index="4-1">元数据采集</el-menu-item>
          <el-menu-item index="4-2">元数据分析</el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>

    <!-- 右侧内容区 -->
    <el-container>
      <el-header>
        <el-breadcrumb separator="/">
          <el-breadcrumb-item>数据治理</el-breadcrumb-item>
          <el-breadcrumb-item>{{ currentMenu }}</el-breadcrumb-item>
        </el-breadcrumb>
      </el-header>
      <el-main>
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import { ref } from 'vue'
import { Document } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'

export default {
  name: 'Layout',
  components: {
    Document
  },
  setup() {
    const router = useRouter()
    const activeMenu = ref('1-1')
    const currentMenu = ref('数据源配置')

    // 菜单项与路由路径的映射
    const menuRoutes = {
      '1-1': { path: '/dashboard/source-config', name: '数据源配置' },
      '1-2': { path: '/dashboard/connection-test', name: '连接测试' },
      '2-1': { path: '/dashboard/standard-definition', name: '标准定义' },
      '2-2': { path: '/dashboard/standard-management', name: '标准管理' },
      '3-1': { path: '/dashboard/task-management', name: '任务管理' },
      '3-2': { path: '/dashboard/schedule-config', name: '调度配置' },
      '4-1': { path: '/dashboard/metadata-collection', name: '元数据采集' },
      '4-2': { path: '/dashboard/metadata-analysis', name: '元数据分析' }
    }

    const handleSelect = (index) => {
      activeMenu.value = index
      const route = menuRoutes[index]
      if (route) {
        currentMenu.value = route.name
        router.push(route.path)
      }
    }

    return {
      activeMenu,
      currentMenu,
      handleSelect
    }
  }
}
</script>

<style scoped>
.layout-container {
  height: 100vh;
}

.el-aside {
  background-color: #304156;
  color: #fff;
}

.menu-header {
  height: 60px;
  line-height: 60px;
  text-align: center;
  font-size: 18px;
  font-weight: bold;
  background-color: #2b2f3a;
}

.el-menu-vertical {
  border-right: none;
}

.el-header {
  background-color: #fff;
  border-bottom: 1px solid #e6e6e6;
  display: flex;
  align-items: center;
  height: 60px;
}

.el-main {
  background-color: #f0f2f5;
  padding: 20px;
}

.el-menu-item.is-active {
  background-color: #263445 !important;
}

.el-sub-menu__title:hover {
  background-color: #263445 !important;
}

.el-menu-item:hover {
  background-color: #263445 !important;
}
</style> 

4.5 修改index.js路由配置

import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login.vue'
import Layout from '@/views/Layout.vue'

const routes = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'Layout',
    component: Layout,
    meta: { requiresAuth: true },
    children: [
      {
        path: 'source-config',
        name: 'SourceConfig',
        component: () => import('@/views/datasource/SourceConfig.vue')
      },
      {
        path: 'connection-test',
        name: 'ConnectionTest',
        component: () => import('@/views/datasource/ConnectionTest.vue')
      },
      {
        path: 'standard-definition',
        name: 'StandardDefinition',
        component: () => import('@/views/standard/StandardDefinition.vue')
      },
      {
        path: 'standard-management',
        name: 'StandardManagement',
        component: () => import('@/views/standard/StandardManagement.vue')
      },
      {
        path: 'task-management',
        name: 'TaskManagement',
        component: () => import('@/views/schedule/TaskManagement.vue')
      },
      {
        path: 'schedule-config',
        name: 'ScheduleConfig',
        component: () => import('@/views/schedule/ScheduleConfig.vue')
      },
      {
        path: 'metadata-collection',
        name: 'MetadataCollection',
        component: () => import('@/views/metadata/MetadataCollection.vue')
      },
      {
        path: 'metadata-analysis',
        name: 'MetadataAnalysis',
        component: () => import('@/views/metadata/MetadataAnalysis.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!token) {
      next('/login')
    } else {
      next()
    }
  } else {
    next()
  }
})

export default router 

4.6 修改Login.vue登录成功后的跳转路径

<template>
  <div class="gradient-bg">
    <div class="login-box">
      <div class="login-header">{{ isLogin ? '请你登录' : '用户注册' }}</div>
      <div class="form-content">
        <input 
          type="text" 
          v-model="form.username" 
          placeholder="账号" 
          class="input-field"
        >
        <input 
          type="password" 
          v-model="form.password" 
          placeholder="密码" 
          class="input-field"
        >
        <input 
          v-if="!isLogin"
          type="email" 
          v-model="form.email" 
          placeholder="邮箱" 
          class="input-field"
        >
        <div class="login-button" @click="handleSubmit">
          {{ isLogin ? '登录' : '注册' }}
        </div>
      </div>
      <div class="message">
        {{ isLogin ? '如果没有账户?' : '已有账户?' }}
        <a href="#" @click.prevent="isLogin = !isLogin">
          {{ isLogin ? '请先注册' : '去登录' }}
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import request from '@/utils/request'

export default {
  name: 'Login',
  data() {
    return {
      isLogin: true,
      form: {
        username: '',
        password: '',
        email: ''
      }
    }
  },
  methods: {
    async handleSubmit() {
      try {
        const url = this.isLogin ? '/api/auth/login' : '/api/auth/register'
        const response = await request.post(url, this.form)
        
        if (this.isLogin) {
          localStorage.setItem('token', response.token)
          localStorage.setItem('user', JSON.stringify(response.user))
          this.$router.push('/dashboard')
        } else {
          this.isLogin = true
          this.form = {
            username: '',
            password: '',
            email: ''
          }
        }
      } catch (error) {
        alert(error.response?.data?.error || '操作失败')
      }
    }
  }
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}

html {
  height: 100%;
}

body {
  height: 100%;
}

.gradient-bg {
  height: 100vh;
  background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);
}

.login-box {
  background-color: #fff;
  width: 358px;
  height: 588px;
  border-radius: 15px;
  padding: 0 50px;
  position: relative;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

.login-header {
  font-size: 38px;
  font-weight: bold;
  text-align: center;
  line-height: 200px;
}

.input-field {
  display: block;
  width: 100%;
  margin-bottom: 20px;
  border: 0;
  padding: 10px;
  border-bottom: 1px solid rgb(128, 125, 125);
  font-size: 15px;
  outline: none;
}

.input-field::placeholder {
  text-transform: uppercase;
}

.login-button {
  text-align: center;
  padding: 10px;
  width: 100%;
  margin-top: 40px;
  background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);
  color: #fff;
  border-radius: 8px;
  cursor: pointer;
}

.message {
  text-align: center;
  line-height: 88px;
}

a {
  text-decoration-line: none;
  color: #abc1ee;
  cursor: pointer;
}
</style> 

4.7 安装 Element Plus 的图标包

npm install @element-plus/icons-vue

4.8 创建数据源管理相关组件

SourceConfig.vue

代码位置

代码内容
<template>
  <div class="source-config">
    <div class="header">
      <h2>数据源配置</h2>
      <el-button type="primary" @click="handleAdd">添加数据源</el-button>
    </div>

    <!-- 数据源列表 -->
    <el-table :data="sourceList" style="width: 100%; margin-top: 20px">
      <el-table-column prop="name" label="数据源名称" />
      <el-table-column prop="type" label="数据源类型" />
      <el-table-column prop="host" label="主机地址" />
      <el-table-column prop="port" label="端口" />
      <el-table-column prop="database" label="数据库名" />
      <el-table-column prop="status" label="状态">
        <template #default="scope">
          <el-tag :type="scope.row.status === '正常' ? 'success' : 'danger'">
            {{ scope.row.status }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
          <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 添加/编辑数据源对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="500px"
    >
      <el-form :model="form" label-width="100px">
        <el-form-item label="数据源名称">
          <el-input v-model="form.name" placeholder="请输入数据源名称" />
        </el-form-item>
        <el-form-item label="数据源类型">
          <el-select v-model="form.type" placeholder="请选择数据源类型" style="width: 100%">
            <el-option label="MySQL" value="mysql" />
            <el-option label="PostgreSQL" value="postgresql" />
            <el-option label="Oracle" value="oracle" />
          </el-select>
        </el-form-item>
        <el-form-item label="主机地址">
          <el-input v-model="form.host" placeholder="请输入主机地址" />
        </el-form-item>
        <el-form-item label="端口">
          <el-input v-model="form.port" placeholder="请输入端口号" />
        </el-form-item>
        <el-form-item label="数据库名">
          <el-input v-model="form.database" placeholder="请输入数据库名" />
        </el-form-item>
        <el-form-item label="用户名">
          <el-input v-model="form.username" placeholder="请输入用户名" />
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="form.password" type="password" placeholder="请输入密码" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'

export default {
  name: 'SourceConfig',
  setup() {
    const sourceList = ref([])
    const dialogVisible = ref(false)
    const dialogTitle = ref('添加数据源')
    const form = reactive({
      id: null,
      name: '',
      type: '',
      host: '',
      port: '',
      database: '',
      username: '',
      password: ''
    })

    // 获取数据源列表
    const fetchSourceList = async () => {
      try {
        const res = await request.get('/api/datasource')
        sourceList.value = res.data || []
        console.log('获取到的数据源列表:', sourceList.value)
      } catch (error) {
        console.error('获取数据源列表失败:', error)
        ElMessage.error('获取数据源列表失败')
      }
    }

    // 添加数据源
    const handleAdd = () => {
      dialogTitle.value = '添加数据源'
      Object.keys(form).forEach(key => form[key] = '')
      dialogVisible.value = true
    }

    // 编辑数据源
    const handleEdit = (row) => {
      dialogTitle.value = '编辑数据源'
      Object.assign(form, row)
      dialogVisible.value = true
    }

    // 删除数据源
    const handleDelete = (row) => {
      ElMessageBox.confirm('确定要删除该数据源吗?', '提示', {
        type: 'warning'
      }).then(async () => {
        try {
          await request.delete(`/api/datasource/${row.id}`)
          ElMessage.success('删除成功')
          fetchSourceList()
        } catch (error) {
          ElMessage.error('删除失败')
        }
      })
    }

    // 提交表单
    const handleSubmit = async () => {
      try {
        // 表单验证
        if (!form.name || !form.type || !form.host || !form.port || 
            !form.database || !form.username || !form.password) {
          ElMessage.warning('请填写所有必填字段')
          return
        }

        const data = {
          name: form.name,
          type: form.type,
          host: form.host,
          port: form.port,
          database: form.database,
          username: form.username,
          password: form.password
        }

        if (form.id) {
          await request.put(`/api/datasource/${form.id}`, data)
        } else {
          await request.post('/api/datasource', data)
        }

        ElMessage.success(form.id ? '更新成功' : '添加成功')
        dialogVisible.value = false
        
        // 重新获取列表
        await fetchSourceList()
      } catch (error) {
        console.error('操作失败:', error)
        ElMessage.error(error.response?.data?.error || '操作失败')
      }
    }

    // 初始化获取数据源列表
    fetchSourceList()

    return {
      sourceList,
      dialogVisible,
      dialogTitle,
      form,
      handleAdd,
      handleEdit,
      handleDelete,
      handleSubmit
    }
  }
}
</script>

<style scoped>
.source-config {
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style> 

ConnectionTest.vue

代码位置

同上

代码内容
<template>
  <div class="connection-test">
    <div class="header">
      <h2>连接测试</h2>
    </div>

    <!-- 数据源选择和测试表单 -->
    <el-card class="test-form">
      <el-form :model="form" label-width="100px">
        <el-form-item label="数据源">
          <el-select 
            v-model="form.sourceId" 
            placeholder="请选择数据源"
            style="width: 100%"
            @change="handleSourceChange"
          >
            <el-option 
              v-for="source in sourceList" 
              :key="source.id" 
              :label="source.name" 
              :value="source.id"
            />
          </el-select>
        </el-form-item>

        <!-- 显示选中的数据源信息 -->
        <template v-if="selectedSource">
          <el-form-item label="类型">
            <span>{{ selectedSource.type }}</span>
          </el-form-item>
          <el-form-item label="主机地址">
            <span>{{ selectedSource.host }}</span>
          </el-form-item>
          <el-form-item label="端口">
            <span>{{ selectedSource.port }}</span>
          </el-form-item>
          <el-form-item label="数据库">
            <span>{{ selectedSource.database }}</span>
          </el-form-item>
        </template>

        <el-form-item>
          <el-button type="primary" @click="handleTest" :loading="testing">
            测试连接
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 测试结果展示 -->
    <el-card class="test-result" v-if="testResult">
      <template #header>
        <div class="card-header">
          <span>测试结果</span>
          <el-tag :type="testResult.success ? 'success' : 'danger'">
            {{ testResult.success ? '连接成功' : '连接失败' }}
          </el-tag>
        </div>
      </template>
      <div class="result-content">
        <pre>{{ testResult.message }}</pre>
      </div>
    </el-card>
  </div>
</template>

<script>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'

export default {
  name: 'ConnectionTest',
  setup() {
    const sourceList = ref([])
    const selectedSource = ref(null)
    const testing = ref(false)
    const testResult = ref(null)
    const form = reactive({
      sourceId: ''
    })

    // 获取数据源列表
    const fetchSourceList = async () => {
      try {
        const res = await request.get('/api/datasource')
        sourceList.value = res.data || []
        console.log('获取到的数据源列表:', sourceList.value)
      } catch (error) {
        console.error('获取数据源列表失败:', error)
        ElMessage.error('获取数据源列表失败')
      }
    }

    // 数据源选择变化
    const handleSourceChange = (sourceId) => {
      selectedSource.value = sourceList.value.find(s => s.id === sourceId)
      testResult.value = null
    }

    // 测试连接
    const handleTest = async () => {
      if (!form.sourceId) {
        ElMessage.warning('请选择数据源')
        return
      }

      testing.value = true
      try {
        const res = await request.post(`/api/datasource/test/${form.sourceId}`)
        testResult.value = res
        if (res.success) {
          ElMessage.success('连接成功')
        } else {
          ElMessage.error(res.message || '连接失败')
        }
      } catch (error) {
        console.error('测试失败:', error)
        testResult.value = {
          success: false,
          message: error.response?.data?.error || '测试失败'
        }
        ElMessage.error(testResult.value.message)
      } finally {
        testing.value = false
      }
    }

    // 初始化获取数据源列表
    fetchSourceList()

    return {
      sourceList,
      selectedSource,
      testing,
      testResult,
      form,
      handleSourceChange,
      handleTest
    }
  }
}
</script>

<style scoped>
.connection-test {
  padding: 20px;
}

.header {
  margin-bottom: 20px;
}

.test-form {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.result-content {
  background-color: #f5f7fa;
  padding: 15px;
  border-radius: 4px;
}

pre {
  margin: 0;
  white-space: pre-wrap;
  word-wrap: break-word;
}
</style> 

4.9 创建数据标准相关组件

StandardDefinition.vue

代码位置

代码内容
<template>
  <div class="standard-definition">
    <h2>标准定义</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>标准定义页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'StandardDefinition'
}
</script> 

StandardManagement.vue

代码位置

你懂的!

代码内容
<template>
  <div class="standard-management">
    <h2>标准管理</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>标准管理页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'StandardManagement'
}
</script> 

4.10 创建调度中心相关组件

TaskManagement.vue

代码位置

代码内容
<template>
  <div class="task-management">
    <h2>任务管理</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>任务管理页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'TaskManagement'
}
</script> 

ScheduleConfig.vue

代码位置

你懂

代码内容
<template>
  <div class="schedule-config">
    <h2>调度配置</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>调度配置页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'ScheduleConfig'
}
</script> 

4.11 创建元数据管理相关组件

MetadataCollection.vue

代码位置

代码内容
<template>
  <div class="metadata-collection">
    <h2>元数据采集</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>元数据采集页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'MetadataCollection'
}
</script> 

MetadataAnalysis.vue

代码位置

你懂

代码内容
<template>
  <div class="metadata-analysis">
    <h2>元数据分析</h2>
    <!-- 临时占位内容 -->
    <el-card class="box-card">
      <div>元数据分析页面内容</div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'MetadataAnalysis'
}
</script> 

5. 后端逻辑实现

5.1 实现后端数据源管理功能

5.1.1 创建数据源实体DataSource.java

代码位置

代码内容
package com.example.entity;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "data_sources")
public class DataSource {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @Column(nullable = false)
    private String type;

    @Column(nullable = false)
    private String host;

    @Column(nullable = false)
    private String port;

    @Column(name = "database_name", nullable = false)
    private String database;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Column
    private String status = "正常";

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public String getDatabase() {
        return database;
    }

    public void setDatabase(String database) {
        this.database = database;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
} 

5.1.2 创建数据源仓库接口DataSourceRepository.java

代码位置

代码内容
package com.example.repository;

import com.example.entity.DataSource;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface DataSourceRepository extends JpaRepository<DataSource, Long> {
    Optional<DataSource> findByName(String name);
    boolean existsByName(String name);
} 

5.1.3 创建数据源服务类DataSourceService.java

代码位置

代码内容
package com.example.service;

import com.example.entity.DataSource;
import com.example.repository.DataSourceRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

@Service
public class DataSourceService {
    
    private static final Logger logger = LoggerFactory.getLogger(DataSourceService.class);
    
    @Autowired
    private DataSourceRepository dataSourceRepository;

    public List<DataSource> findAll() {
        logger.debug("获取所有数据源");
        List<DataSource> sources = dataSourceRepository.findAll();
        logger.debug("找到 {} 个数据源", sources.size());
        return sources;
    }

    public DataSource findById(Long id) {
        return dataSourceRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("数据源不存在"));
    }

    public DataSource create(DataSource dataSource) {
        logger.debug("创建数据源: {}", dataSource.getName());
        if (dataSourceRepository.existsByName(dataSource.getName())) {
            logger.warn("数据源名称已存在: {}", dataSource.getName());
            throw new RuntimeException("数据源名称已存在");
        }
        DataSource saved = dataSourceRepository.save(dataSource);
        logger.debug("数据源创建成功: {}", saved.getId());
        return saved;
    }

    public DataSource update(Long id, DataSource dataSource) {
        DataSource existingSource = findById(id);
        
        // 如果名称变更了,需要检查新名称是否已存在
        if (!existingSource.getName().equals(dataSource.getName()) 
            && dataSourceRepository.existsByName(dataSource.getName())) {
            throw new RuntimeException("数据源名称已存在");
        }

        dataSource.setId(id);
        return dataSourceRepository.save(dataSource);
    }

    public void delete(Long id) {
        dataSourceRepository.deleteById(id);
    }

    public Map<String, Object> testConnection(Long id) {
        DataSource dataSource = findById(id);
        Map<String, Object> result = new HashMap<>();
        
        try {
            String url = buildJdbcUrl(dataSource);
            Class.forName(getDriverClass(dataSource.getType()));
            
            try (Connection conn = DriverManager.getConnection(
                    url, 
                    dataSource.getUsername(), 
                    dataSource.getPassword()
            )) {
                result.put("success", true);
                result.put("message", "连接成功");
                
                // 更新数据源状态
                dataSource.setStatus("正常");
                dataSourceRepository.save(dataSource);
            }
        } catch (Exception e) {
            logger.error("连接测试失败: {}", e.getMessage());
            result.put("success", false);
            result.put("message", "连接失败: " + e.getMessage());
            
            // 更新数据源状态
            dataSource.setStatus("异常");
            dataSourceRepository.save(dataSource);
        }
        
        return result;
    }

    private String buildJdbcUrl(DataSource dataSource) {
        switch (dataSource.getType().toLowerCase()) {
            case "mysql":
                return String.format("jdbc:mysql://%s:%s/%s?useSSL=false&allowPublicKeyRetrieval=true",
                        dataSource.getHost(),
                        dataSource.getPort(),
                        dataSource.getDatabase());
            case "postgresql":
                return String.format("jdbc:postgresql://%s:%s/%s",
                        dataSource.getHost(),
                        dataSource.getPort(),
                        dataSource.getDatabase());
            case "oracle":
                return String.format("jdbc:oracle:thin:@%s:%s:%s",
                        dataSource.getHost(),
                        dataSource.getPort(),
                        dataSource.getDatabase());
            default:
                throw new RuntimeException("不支持的数据源类型");
        }
    }

    private String getDriverClass(String type) {
        switch (type.toLowerCase()) {
            case "mysql":
                return "com.mysql.jdbc.Driver";
            case "postgresql":
                return "org.postgresql.Driver";
            case "oracle":
                return "oracle.jdbc.OracleDriver";
            default:
                throw new RuntimeException("不支持的数据源类型");
        }
    }
} 

5.1.4 创建控制器DataSourceController.java

代码位置

代码内容
package com.example.controller;

import com.example.entity.DataSource;
import com.example.service.DataSourceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.HashMap;

@RestController
@RequestMapping("/api/datasource")
public class DataSourceController {
    
    private static final Logger logger = LoggerFactory.getLogger(DataSourceController.class);
    
    @Autowired
    private DataSourceService dataSourceService;

    @GetMapping
    public ResponseEntity<?> getAllDataSources() {
        try {
            List<DataSource> sources = dataSourceService.findAll();
            Map<String, Object> response = new HashMap<>();
            response.put("data", sources);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("获取数据源列表失败", e);
            return ResponseEntity.badRequest().body(createErrorResponse(e));
        }
    }

    @PostMapping
    public ResponseEntity<?> createDataSource(@RequestBody DataSource dataSource) {
        try {
            logger.info("开始创建数据源: {}", dataSource.getName());
            DataSource created = dataSourceService.create(dataSource);
            Map<String, Object> response = new HashMap<>();
            response.put("data", created);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("创建数据源失败", e);
            return ResponseEntity.badRequest().body(createErrorResponse(e));
        }
    }

    @PutMapping("/{id}")
    public ResponseEntity<?> updateDataSource(
            @PathVariable Long id,
            @RequestBody DataSource dataSource
    ) {
        try {
            DataSource updated = dataSourceService.update(id, dataSource);
            return ResponseEntity.ok(updated);
        } catch (Exception e) {
            logger.error("更新数据源失败", e);
            return ResponseEntity.badRequest().body(createErrorResponse(e));
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteDataSource(@PathVariable Long id) {
        try {
            dataSourceService.delete(id);
            return ResponseEntity.ok().build();
        } catch (Exception e) {
            logger.error("删除数据源失败", e);
            return ResponseEntity.badRequest().body(createErrorResponse(e));
        }
    }

    @PostMapping("/test/{id}")
    public ResponseEntity<?> testConnection(@PathVariable Long id) {
        try {
            Map<String, Object> result = dataSourceService.testConnection(id);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            logger.error("测试连接失败", e);
            return ResponseEntity.badRequest().body(createErrorResponse(e));
        }
    }

    private Map<String, String> createErrorResponse(Exception e) {
        Map<String, String> response = new HashMap<>();
        response.put("error", e.getMessage());
        return response;
    }
} 

5.1.5 在pom.xml中添加数据库驱动依赖

<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

<!-- PostgreSQL驱动 -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.23</version>
</dependency>

5.1.6 修改 application.properties 配置

主要spring.jpa.hibernate.ddl-auto=update,改为update,修改后的文件如下

server.port=8081
spring.application.name=sprint-client

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/vue_sprint?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

# 日志配置
logging.level.com.example=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

6. 启动前后端服务,见证奇迹的时刻到了

6.1 登录后页面功能验证

6.2 添加数据源功能验证

添加成功

6.3 连接测试验证

7. 今日总结

因为今天被抓着解屎山代码的bug,所以只实现了添加数据源功能,困死了,睡觉😴

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值