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,所以只实现了添加数据源功能,困死了,睡觉😴