1. 首先第一步当然是我们的需求分析惹
但是这次不同的是!我们在需求分析前先要去看我们要调用的API模型的官方文档,不然很可能你的需求根本实现不了
1.1 调研要使用的大模型并阅读官方文档
我们采用通义万相,以下是官方文档链接和截图:
文生图,文本生成图像_模型服务灵积(DashScope)-阿里云帮助中心
1.2 分析通义万相的优势和特点
1.2.1 功能特点
支持中英文双语输入
提供多种预设风格:水彩、油画、中国画、素描、扁平插画、二次元、3D卡通等
支持自定义参数:尺寸、数量、种子值等
支持参考图功能(可以基于已有图片生成新风格)
1.2.2 技术优势
提供完整的SDK和API接口
支持异步任务处理
提供详细的错误处理机制
图片生成质量高
1.2.3 使用限制
单账户QPS限制为2
并发任务数量限制为1
每次请求最多生成4张图片
支持的图片尺寸有限制(1024*1024、720*1280、1280*720)
1.2.4 计费方式
需要阿里云账号和API-KEY
按生成图片数量计费
2. 开始逐步实现前后端功能
2.1 首先修改 Layout.vue 中的菜单标题和结构
我们把原来的“数据中台”修改为“Ai数据中台”,添加了"智能AI交互"菜单及其子菜单"AI文生图"
<!-- 将之前 App.vue 的内容移到这里 -->
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="200px">
<div class="menu-header">AI数据中台</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-sub-menu index="5">
<template #title>
<el-icon><Document /></el-icon>
<span>实时数据展示</span>
</template>
<el-menu-item index="5-1">指标配置</el-menu-item>
<el-menu-item index="5-2">数据大屏</el-menu-item>
</el-sub-menu>
<!-- 新增智能AI交互菜单 -->
<el-sub-menu index="6">
<template #title>
<el-icon><Document /></el-icon>
<span>智能AI交互</span>
</template>
<el-menu-item index="6-1">AI文生图</el-menu-item>
<el-menu-item index="6-2">AI图生图</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: '元数据分析' },
'5-1': { path: '/dashboard/metric-config', name: '指标配置' },
'5-2': { path: '/dashboard/data-screen', name: '数据大屏' },
'6-1': { path: '/dashboard/ai-image', name: 'AI文生图' },
'6-2': { path: '/dashboard/ai-style', name: 'AI图生图' }
}
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>
2.2 在 router/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')
},
{
path: 'metric-config',
name: 'MetricConfig',
component: () => import('@/views/realtime/MetricConfig.vue')
},
{
path: 'data-screen',
name: 'DataScreen',
component: () => import('@/views/realtime/DataScreen.vue')
},
{
path: 'ai-image',
name: 'AiImage',
component: () => import('@/views/ai/AiImage.vue')
},
{
path: 'ai-style',
name: 'AiStyle',
component: () => import('@/views/ai/AiStyle.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
2.3 创建新的 AI 文生图组件:AiImage.vue
<template>
<div class="ai-image">
<div class="header">
<h2>AI文生图</h2>
</div>
<el-row :gutter="20">
<!-- 左侧参数配置区 -->
<el-col :span="8">
<el-card class="config-panel">
<template #header>
<div class="card-header">
<span>生成参数</span>
</div>
</template>
<!-- 文本输入框 -->
<el-form :model="form" label-position="top">
<el-form-item label="图片描述">
<el-input
v-model="form.prompt"
type="textarea"
:rows="4"
placeholder="请输入图片描述(支持中英文)"
/>
</el-form-item>
<!-- 图片风格选择 -->
<el-form-item label="图片风格">
<el-select v-model="form.style" placeholder="请选择图片风格" style="width: 100%">
<el-option label="摄影" value="<photography>" />
<el-option label="人像写真" value="<portrait>" />
<el-option label="3D卡通" value="<3d cartoon>" />
<el-option label="动画" value="<anime>" />
<el-option label="油画" value="<oil painting>" />
<el-option label="水彩" value="<watercolor>" />
<el-option label="素描" value="<sketch>" />
<el-option label="中国画" value="<chinese painting>" />
<el-option label="扁平插画" value="<flat illustration>" />
</el-select>
</el-form-item>
<!-- 负面提示词 -->
<el-form-item label="负面提示词">
<el-input
v-model="form.negative_prompt"
type="textarea"
:rows="3"
placeholder="描述不希望在图片中出现的内容(可选)"
/>
</el-form-item>
<!-- 图片尺寸选择 -->
<el-form-item label="图片尺寸">
<el-select v-model="form.size" placeholder="请选择图片尺寸" style="width: 100%">
<el-option label="1024 x 1024" value="1024*1024" />
<el-option label="720 x 1280" value="720*1280" />
<el-option label="1280 x 720" value="1280*720" />
</el-select>
</el-form-item>
<!-- 生成数量选择 -->
<el-form-item label="生成数量">
<el-input-number
v-model="form.n"
:min="1"
:max="4"
style="width: 100%"
/>
</el-form-item>
<!-- 生成按钮 -->
<el-button
type="primary"
:loading="generating"
@click="handleGenerate"
style="width: 100%"
>
生成图片
</el-button>
</el-form>
</el-card>
</el-col>
<!-- 右侧图片展示区 -->
<el-col :span="16">
<el-card class="result-panel">
<template #header>
<div class="card-header">
<el-tabs v-model="activeTab">
<el-tab-pane label="生成结果" name="result">
<div v-if="generating" class="generating">
<el-icon class="is-loading"><Loading /></el-icon>
<p>正在生成中,请稍候...</p>
</div>
<template v-else-if="images.length > 0">
<!-- 缩略图列表 -->
<div class="thumbnail-list">
<div v-for="(image, index) in images" :key="index" class="thumbnail-item">
<el-image
:src="image.url"
fit="cover"
@click="selectImage(index)"
:class="{ 'active': selectedImageIndex === index }"
/>
</div>
</div>
<!-- 大图预览 -->
<div class="preview-container">
<el-image
v-if="selectedImage"
:src="selectedImage.url"
fit="contain"
:preview-src-list="[selectedImage.url]"
class="preview-image"
>
<template #error>
<div class="image-error">加载失败</div>
</template>
</el-image>
<div class="preview-actions">
<el-button type="primary" @click="downloadImage(selectedImage.url)">
下载图片
</el-button>
</div>
</div>
</template>
<div v-else class="empty-result">
<el-empty description="暂无生成结果" />
</div>
</el-tab-pane>
<el-tab-pane label="历史记录" name="history">
<div class="history-list">
<div v-if="historyList.length === 0" class="empty-result">
<el-empty description="暂无历史记录" />
</div>
<div v-else v-for="item in historyList" :key="item.id" class="history-item">
<div class="history-info">
<div class="history-prompt">{{ item.prompt }}</div>
<div v-if="item.negativePrompt" class="history-negative-prompt">
负面提示词:{{ item.negativePrompt }}
</div>
<div class="history-meta">
<span>风格:{{ item.style }}</span>
<span>尺寸:{{ item.size }}</span>
<span>时间:{{ formatDate(item.createdAt) }}</span>
</div>
</div>
<div class="history-images">
<template v-if="item.imageUrls">
<el-image
v-for="(url, index) in JSON.parse(item.imageUrls)"
:key="index"
:src="url"
fit="cover"
:preview-src-list="[url]"
class="history-image"
/>
</template>
</div>
<div class="history-actions">
<el-button type="primary" link @click="reusePrompt(item)">
重新生成
</el-button>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<div v-if="generating" class="generating">
<el-icon class="is-loading"><Loading /></el-icon>
<p>正在生成中,请稍候...</p>
</div>
<div v-else-if="images.length > 0" class="image-grid">
<div v-for="(image, index) in images" :key="index" class="image-item">
<el-image
:src="image.url"
fit="contain"
:preview-src-list="[image.url]"
>
<template #error>
<div class="image-error">加载失败</div>
</template>
</el-image>
<div class="image-actions">
<el-button type="primary" link @click="downloadImage(image.url)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="empty-result">
<el-empty description="暂无生成结果" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import request from '@/utils/request'
export default {
name: 'AiImage',
components: {
Loading
},
setup() {
const form = reactive({
prompt: '',
style: '<photography>',
size: '1024*1024',
n: 1
})
const generating = ref(false)
const images = ref([])
const activeTab = ref('result')
const historyList = ref([])
const selectedImageIndex = ref(0)
const selectedImage = computed(() => images.value[selectedImageIndex.value])
const selectImage = (index) => {
selectedImageIndex.value = index
}
const handleGenerate = async () => {
if (!form.prompt) {
ElMessage.warning('请输入图片描述')
return
}
generating.value = true
images.value = []
selectedImageIndex.value = 0
try {
// 调用生成API
const response = await request.post('/api/image/generate', {
prompt: form.prompt,
negative_prompt: form.negative_prompt,
style: form.style,
size: form.size,
n: form.n
})
// 获取任务ID
const taskId = response.output.task_id
let retryCount = 0
const maxRetries = 60
// 轮询任务状态
while (retryCount < maxRetries) {
const taskStatus = await request.get(`/api/image/task/${taskId}`)
if (taskStatus.output.task_status === 'SUCCEEDED') {
// 任务成功,获取图片URL
images.value = taskStatus.output.results
break
} else if (taskStatus.output.task_status === 'FAILED') {
throw new Error('生成失败:' + taskStatus.output.message)
}
// 等待1秒后继续查询
await new Promise(resolve => setTimeout(resolve, 1000))
retryCount++
}
if (retryCount >= maxRetries) {
throw new Error('生成超时,请重试')
}
// 生成成功后刷新历史记录
await fetchHistory()
} catch (error) {
console.error('生成失败:', error)
ElMessage.error(error.message || '生成失败,请重试')
} finally {
generating.value = false
}
}
const downloadImage = async (url) => {
try {
const response = await fetch(url)
const blob = await response.blob()
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = 'generated-image.png'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请重试')
}
}
const fetchHistory = async () => {
try {
const res = await request.get('/api/image/history')
console.log('历史记录数据:', res)
historyList.value = res.data || []
} catch (error) {
console.error('获取历史记录失败:', error)
ElMessage.error('获取历史记录失败')
}
}
const reusePrompt = (item) => {
form.prompt = item.prompt
form.style = item.style
form.size = item.size
if (item.negativePrompt) {
form.negative_prompt = item.negativePrompt
}
activeTab.value = 'result'
}
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
onMounted(() => {
console.log('组件挂载,获取历史记录')
fetchHistory()
})
return {
form,
generating,
images,
handleGenerate,
downloadImage,
activeTab,
historyList,
reusePrompt,
formatDate,
selectedImageIndex,
selectedImage,
selectImage
}
}
}
</script>
<style scoped>
.ai-image {
padding: 20px;
}
.config-panel, .result-panel {
height: calc(100vh - 140px);
overflow-y: auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.generating {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
padding: 20px;
}
.image-item {
position: relative;
}
.image-actions {
display: flex;
justify-content: center;
margin-top: 10px;
}
.empty-result {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.image-error {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #f5f7fa;
color: #909399;
}
.history-list {
padding: 20px;
}
.history-item {
border: 1px solid #eee;
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
}
.history-info {
margin-bottom: 10px;
}
.history-prompt {
font-weight: bold;
margin-bottom: 5px;
}
.history-meta {
color: #666;
font-size: 0.9em;
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 5px;
}
.history-images {
display: flex;
gap: 10px;
overflow-x: auto;
padding: 10px 0;
scrollbar-width: thin;
scrollbar-color: #ddd transparent;
}
.history-images::-webkit-scrollbar {
height: 6px;
}
.history-images::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 3px;
}
.history-image {
width: 120px;
height: 120px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
}
.history-actions {
margin-top: 10px;
text-align: right;
}
.history-negative-prompt {
color: #666;
font-size: 0.9em;
margin: 5px 0;
font-style: italic;
}
.thumbnail-list {
display: flex;
gap: 10px;
margin-bottom: 20px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.thumbnail-item {
width: 100px;
height: 100px;
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
overflow: hidden;
}
.thumbnail-item .el-image {
width: 100%;
height: 100%;
}
.thumbnail-item .el-image.active {
border-color: var(--el-color-primary);
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
}
.preview-image {
width: 100%;
height: 500px; /* 调整预览图的高度 */
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.preview-actions {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
}
/* 移除重复的图片展示区域 */
.result-panel > .image-grid,
.result-panel > .generating,
.result-panel > .empty-result {
display: none;
}
</style>
2.4 实现后端的API调用。首先创建一个图片生成的服务类:ImageGenerationService.java
package com.example.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.example.entity.ImageGenerationHistory;
import com.example.entity.User;
import com.example.repository.ImageGenerationHistoryRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ImageGenerationService {
private static final Logger logger = LoggerFactory.getLogger(ImageGenerationService.class);
@Value("${dashscope.api-key}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis";
@Autowired
private ImageGenerationHistoryRepository historyRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
public Map<String, Object> generateImage(Map<String, Object> requestData) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
headers.set("X-DashScope-Async", "enable");
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestData, headers);
logger.debug("发送请求到 {}", API_URL);
logger.debug("请求头: {}", headers);
logger.debug("请求体: {}", requestData);
ResponseEntity<Map> response = restTemplate.exchange(
API_URL,
HttpMethod.POST,
requestEntity,
Map.class
);
logger.debug("响应状态: {}", response.getStatusCode());
logger.debug("响应体: {}", response.getBody());
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
} else {
throw new RuntimeException("API调用失败: " + response.getStatusCode());
}
} catch (Exception e) {
logger.error("生成图片失败", e);
throw new RuntimeException("生成图片失败: " + e.getMessage());
}
}
public Map<String, Object> checkTaskStatus(String taskId) {
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
HttpEntity<?> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
"https://dashscope.aliyuncs.com/api/v1/tasks/" + taskId,
HttpMethod.GET,
requestEntity,
Map.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
} else {
throw new RuntimeException("获取任务状态失败: " + response.getStatusCode());
}
} catch (Exception e) {
logger.error("获取任务状态失败", e);
throw new RuntimeException("获取任务状态失败: " + e.getMessage());
}
}
public void saveHistory(Map<String, Object> request, Map<String, Object> response, User user) {
try {
ImageGenerationHistory history = new ImageGenerationHistory();
history.setPrompt(request.get("prompt").toString());
history.setNegativePrompt(request.get("negative_prompt") != null ?
request.get("negative_prompt").toString() : null);
history.setStyle(request.get("style").toString());
history.setSize(request.get("size").toString());
history.setImageUrls(objectMapper.writeValueAsString(
((Map<String, Object>)response.get("output")).get("results")
));
history.setUser(user);
historyRepository.save(history);
} catch (Exception e) {
logger.error("保存历史记录失败", e);
}
}
public List<ImageGenerationHistory> getUserHistory(Long userId) {
return historyRepository.findByUserIdOrderByCreatedAtDesc(userId);
}
}
2.5 然后创建对应的控制器:ImageGenerationController.java
package com.example.controller;
import com.example.service.ImageGenerationService;
import com.example.service.UserService;
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.HashMap;
import java.util.List;
import java.util.Map;
import com.example.entity.ImageGenerationHistory;
import com.example.entity.User;
@RestController
@RequestMapping("/api/image")
public class ImageGenerationController {
private static final Logger logger = LoggerFactory.getLogger(ImageGenerationController.class);
@Autowired
private ImageGenerationService imageGenerationService;
@Autowired
private UserService userService;
@PostMapping("/generate")
public ResponseEntity<?> generateImage(@RequestBody Map<String, Object> request,
@RequestHeader("Authorization") String token) {
try {
// 构建API请求数据
Map<String, Object> requestData = new HashMap<>();
requestData.put("model", "wanx-v1");
// 构建input
Map<String, Object> input = new HashMap<>();
input.put("prompt", request.get("prompt"));
if (request.get("negative_prompt") != null && !request.get("negative_prompt").toString().isEmpty()) {
input.put("negative_prompt", request.get("negative_prompt"));
}
requestData.put("input", input);
// 构建parameters
Map<String, Object> parameters = new HashMap<>();
parameters.put("style", request.get("style"));
parameters.put("size", request.get("size"));
parameters.put("n", request.get("n"));
requestData.put("parameters", parameters);
Map<String, Object> response = imageGenerationService.generateImage(requestData);
// 保存历史记录
User user = userService.getUserFromToken(token);
imageGenerationService.saveHistory(request, response, user);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("生成图片失败", e);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
@GetMapping("/task/{taskId}")
public ResponseEntity<?> checkTaskStatus(@PathVariable String taskId) {
try {
Map<String, Object> response = imageGenerationService.checkTaskStatus(taskId);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取任务状态失败", e);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
@GetMapping("/history")
public ResponseEntity<?> getHistory(@RequestHeader("Authorization") String token) {
try {
// 从token获取用户信息
User user = userService.getUserFromToken(token);
List<ImageGenerationHistory> history = imageGenerationService.getUserHistory(user.getId());
Map<String, Object> response = new HashMap<>();
response.put("data", history);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("获取历史记录失败", e);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
}
}
2.6 在 application.properties 中添加API密钥配置
这里换成你自己的key,申请在下面链接,有免费额度
如何获取通义千问API的KEY_模型服务灵积(DashScope)-阿里云帮助中心
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&characterEncoding=utf8&useUnicode=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=false
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.properties.hibernate.connection.characterEncoding=utf8
spring.jpa.properties.hibernate.connection.useUnicode=true
# HTTP编码配置
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
# 日志配置
logging.file.name=logs/application.log
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.hibernate=ERROR
# 日志格式
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 日志文件大小
logging.file.max-size=10MB
logging.file.max-history=10
# 阿里云通义万相API配置
dashscope.api-key=you_key
2.7 最后修改前端的生成方法:AiImage.vue
2.3已实现
2.8 添加生成历史记录功能。首先创建历史记录实体类:ImageGenerationHistory.java
package com.example.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "image_generation_history")
public class ImageGenerationHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String prompt;
@Column(name = "negative_prompt")
private String negativePrompt;
@Column(nullable = false)
private String style;
@Column(nullable = false)
private String size;
@Column(name = "image_urls", columnDefinition = "TEXT")
private String imageUrls;
@Column(name = "created_at")
private LocalDateTime createdAt;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getNegativePrompt() {
return negativePrompt;
}
public void setNegativePrompt(String negativePrompt) {
this.negativePrompt = negativePrompt;
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getImageUrls() {
return imageUrls;
}
public void setImageUrls(String imageUrls) {
this.imageUrls = imageUrls;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
2.9 首先创建历史记录的Repository:ImageGenerationHistoryRepository.java
package com.example.repository;
import com.example.entity.ImageGenerationHistory;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ImageGenerationHistoryRepository extends JpaRepository<ImageGenerationHistory, Long> {
List<ImageGenerationHistory> findByUserIdOrderByCreatedAtDesc(Long userId);
}
2.10 修改 ImageGenerationService,添加历史记录相关方法:ImageGenerationService.java
2.4已实现
2.11 修改 ImageGenerationController,添加历史记录相关接口:ImageGenerationController.java
2.5已实现
2.12 在前端 AiImage.vue 中添加历史记录展示:AiImage.vue
2.3已实现
2.9~2.12实现了:
- 后端历史记录的存储和查询
- 前端历史记录的展示
- 历史记录的重用功能
- 按时间倒序排列历史记录
2.13 在 UserService 中添加 getUserFromToken 方法。修改 UserService.java
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtil jwtUtil;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public User register(User user) {
// 验证用户名和邮箱是否已存在
if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("用户名已存在");
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("邮箱已存在");
}
// 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 保存用户
return userRepository.save(user);
}
public User login(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 验证密码
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
return user;
}
public User getUserFromToken(String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
String username = jwtUtil.getUsernameFromToken(token);
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
}
throw new RuntimeException("无效的token");
}
}