从0到1开始我的全栈之路(第六天)AI文生图Web应用全栈开发保姆级全流程

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");
    }
} 

3. AI文生图功能自测

3.1 页面功能完整性自测

3.2 文生图功能自测

4. 打完收工

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值