关键词生成故事
前端
<template>
<div class="story-manager-container cartoon-theme">
<div class="hero-banner">
<img src="@/image/story.png" class="hero-image" alt="故事管理" />
</div>
<div class="split-layout">
<div class="left-pane">
<el-card class="story-card fixed-panel">
<template #header>
<div class="card-header">
<div class="card-title"><span class="card-icon">📚</span><span>儿童故事管理系统</span></div>
</div>
</template>
<!-- 故事生成区域 -->
<el-form :model="generateForm" class="generate-form" label-width="80px">
<el-form-item label="关键词">
<el-input v-model="generateForm.keywords" placeholder="请输入故事关键词(例如:魔法森林、勇敢的小兔子等)" />
</el-form-item>
<el-form-item>
<div class="actions-row">
<el-button type="primary" @click="generateStory" :loading="isGenerating">生成故事</el-button>
<el-button @click="resetGenerateForm">重置</el-button>
</div>
</el-form-item>
</el-form>
<div v-if="isGenerating || (genProgress>0 && genProgress<100)" style="margin:8px 0">
<el-progress :percentage="genProgress" :status="genProgress===100 ? 'success' : ''" :stroke-width="10" />
</div>
<!-- 故事列表搜索 -->
<el-divider>故事列表</el-divider>
<el-form :model="searchForm" class="search-form" label-width="80px">
<el-form-item label="关键词">
<el-input v-model="searchForm.keywords" placeholder="搜索关键词" />
</el-form-item>
<el-form-item>
<div class="actions-row">
<el-button type="primary" @click="searchStories">搜索</el-button>
<el-button @click="resetSearchForm">重置</el-button>
</div>
</el-form-item>
</el-form>
<!-- 故事列表 -->
<el-table
v-loading="isLoading"
element-loading-text="加载中..."
:data="storyList"
style="width: 100%"
@selection-change="handleSelectionChange"
empty-text="暂无数据"
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" :index="indexMethod" label="序号" width="80" />
<el-table-column prop="title" label="标题" show-overflow-tooltip min-width="200">
<template #default="scope">
<div class="story-title-cell" @click="viewStory(scope.row)">
{{ scope.row.title }}
</div>
</template>
</el-table-column>
<el-table-column prop="keywords" label="关键词" width="150" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<el-tag v-if="scope.row.status === 1">正常</el-tag>
<el-tag type="danger" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<div class="row-actions">
<el-button type="primary" size="small" @click="viewStory(scope.row)">查看</el-button>
<el-button type="warning" size="small" @click="editStory(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteStory(scope.row.id)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 批量操作 -->
<div class="batch-actions">
<el-button
type="danger"
@click="batchDelete"
:disabled="selectedStories.length === 0"
>
批量删除
</el-button>
</div>
</el-card>
</div>
<div class="right-pane">
<el-card class="fixed-panel">
<template #header>
<div class="card-header">
<div class="card-title"><span class="card-icon">📚</span><span>编辑与查看</span></div>
</div>
</template>
<div v-if="panelMode==='edit'">
<el-form :model="editForm" :rules="rules" ref="editFormRef" label-width="80px" class="edit-form">
<el-form-item label="ID" prop="id">
<el-input v-model="editForm.id" disabled />
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="editForm.title" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="关键词" prop="keywords">
<el-input v-model="editForm.keywords" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="内容" prop="body">
<el-input v-model="editForm.body" type="textarea" :rows="12" maxlength="5000" show-word-limit />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="editForm.status">
<el-radio :value="1">正常</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<div class="edit-actions">
<el-button @click="handleEditClose">取消</el-button>
<el-button type="primary" @click="saveEdit" :loading="isLoading">保存</el-button>
</div>
</el-form>
</div>
<div v-else-if="panelMode==='view' && selectedStory">
<h2 class="story-detail-title">{{ selectedStory.title }}</h2>
<div class="story-meta">
<el-tag class="story-tags" v-for="keyword in selectedStory.keywords?.split(',') || []" :key="keyword">{{ keyword.trim() }}</el-tag>
<div class="story-status">
状态:
<el-tag :type="selectedStory.status === 1 ? 'success' : 'danger'">{{ selectedStory.status === 1 ? '正常' : '禁用' }}</el-tag>
</div>
<div class="story-id">ID: {{ selectedStory.id }}</div>
</div>
<div class="story-detail-body" v-html="formatStoryBody(selectedStory.body)"></div>
<div class="edit-actions">
<el-button type="warning" @click="editStory(selectedStory)">编辑</el-button>
</div>
</div>
<div v-else class="placeholder">
选择左侧列表中的故事进行查看,或生成后在此编辑并保存
</div>
</el-card>
</div>
</div>
<!-- 故事详情对话框 -->
<el-dialog
v-model="storyDetailVisible"
title="故事详情"
width="800px"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="story-detail-dialog"
>
<div v-if="selectedStory" class="story-detail-content">
<h2 class="story-detail-title">{{ selectedStory.title }}</h2>
<div class="story-meta">
<el-tag size="small" class="story-tags" v-for="keyword in selectedStory.keywords?.split(',') || []" :key="keyword">
{{ keyword.trim() }}
</el-tag>
<div class="story-status">
状态:
<el-tag size="small" :type="selectedStory.status === 1 ? 'success' : 'danger'">
{{ selectedStory.status === 1 ? '正常' : '禁用' }}
</el-tag>
</div>
<div class="story-id">ID: {{ selectedStory.id }}</div>
</div>
<div class="story-detail-body" v-html="formatStoryBody(selectedStory.body)"></div>
</div>
<div v-else class="no-data">正在加载故事详情...</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="storyDetailVisible = false">关闭</el-button>
<el-button type="primary" @click="editStory(selectedStory)">编辑</el-button>
</span>
</template>
</el-dialog>
<!-- 编辑故事对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑故事"
width="800px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="handleEditClose"
>
<el-form
:model="editForm"
:rules="rules"
ref="editFormRef"
label-width="80px"
size="medium"
>
<el-form-item label="ID" prop="id">
<el-input v-model="editForm.id" disabled />
</el-form-item>
<el-form-item label="标题" prop="title" :required="true">
<el-input
v-model="editForm.title"
placeholder="请输入故事标题"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="关键词" prop="keywords">
<el-input
v-model="editForm.keywords"
placeholder="请输入关键词,用逗号分隔"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="内容" prop="body" :required="true">
<el-input
v-model="editForm.body"
type="textarea"
:rows="12"
placeholder="请输入故事内容"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="editForm.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleEditClose">取消</el-button>
<el-button type="primary" @click="saveEdit" :loading="isLoading">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 语音播放对话框 -->
<el-dialog
v-model="soundDialogVisible"
title="播放语音"
width="700px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div v-if="latestSoundRecord">
<div style="margin-bottom:8px">文件:{{ latestSoundRecord.filePath }}</div>
<audio v-if="latestSoundUrl" :src="latestSoundUrl" controls style="width:100%"></audio>
</div>
<div v-else>暂无语音文件</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="soundDialogVisible=false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'StoryManager',
data() {
return {
// 生成故事表单
generateForm: {
keywords: ''
},
// 搜索表单
searchForm: {
keywords: ''
},
// 编辑表单
editForm: {
id: null,
title: '',
body: '',
keywords: '',
status: 1
},
// 表单验证规则
rules: {
title: [
{ required: true, message: '请输入故事标题', trigger: 'blur' },
{ min: 1, max: 100, message: '标题长度在 1 到 100 个字符', trigger: 'blur' }
],
body: [
{ required: true, message: '请输入故事内容', trigger: 'blur' },
{ min: 1, max: 5000, message: '内容长度在 1 到 5000 个字符', trigger: 'blur' }
]
},
// 生成的故事
generatedStory: null,
// 故事列表
storyList: [],
// 选中的故事
selectedStories: [],
selectedStory: null,
// 分页信息
currentPage: 1,
pageSize: 10,
total: 0,
// 加载状态
isGenerating: false,
isLoading: false,
// 对话框显示状态
storyDetailVisible: false,
editDialogVisible: false
,soundDialogVisible: false
,latestSoundUrl: ''
,latestSoundRecord: null
,audioLoading: false
,panelMode: 'idle'
,debug: true
,genProgress: 0
,genTimer: null
}
},
mounted() {
// 页面加载时获取故事列表
this.getStoryList()
},
methods: {
dlog(...args) {
if (this.debug) console.log('[StoryManager]', ...args)
},
indexMethod(i) {
return (this.currentPage - 1) * this.pageSize + i + 1
},
startGenProgress() {
if (this.genTimer) { clearInterval(this.genTimer); this.genTimer = null }
this.genProgress = 10
this.genTimer = setInterval(() => {
const next = this.genProgress + Math.max(1, Math.floor(Math.random()*7))
this.genProgress = next >= 90 ? 90 : next
}, 300)
},
endGenProgress(success) {
if (this.genTimer) { clearInterval(this.genTimer); this.genTimer = null }
this.genProgress = success ? 100 : 0
},
// 生成故事
generateStory() {
if (!this.generateForm.keywords.trim()) {
this.$message.error('请输入关键词')
return
}
this.isGenerating = true
this.dlog('generateStory:start', this.generateForm.keywords)
this.startGenProgress()
this.$axios.post('/story/preview', {
keywords: this.generateForm.keywords
})
.then(response => {
if (response.data.code === 200) {
this.generatedStory = response.data.data
this.editForm = JSON.parse(JSON.stringify(this.generatedStory))
this.selectedStory = JSON.parse(JSON.stringify(this.generatedStory))
this.panelMode = 'edit'
this.$message.success('故事生成成功')
this.dlog('generateStory:success', response.data)
this.endGenProgress(true)
} else {
this.$message.error(response.data.msg || '故事生成失败')
this.dlog('generateStory:fail', response.data)
this.endGenProgress(false)
}
})
.catch(error => {
console.error('生成故事失败:', error)
this.$message.error('生成故事失败,请稍后重试')
this.dlog('generateStory:error', error)
this.endGenProgress(false)
})
.finally(() => {
this.isGenerating = false
this.dlog('generateStory:end')
})
},
// 获取故事列表
getStoryList() {
this.isLoading = true
const params = {
pageNum: this.currentPage,
pageSize: this.pageSize,
param: {
keywords: this.searchForm.keywords
}
}
this.dlog('getStoryList:start', params)
this.$axios.post('/story/list', params)
.then(response => {
this.isLoading = false
if (response.data.code === 200) {
this.storyList = response.data.data || []
this.total = response.data.total || 0
this.dlog('getStoryList:success', { total: this.total, listCount: this.storyList.length })
} else {
this.$message.error(response.data.msg || '获取故事列表失败')
this.storyList = []
this.total = 0
this.dlog('getStoryList:fail', response.data)
}
})
.catch(error => {
this.isLoading = false
console.error('获取故事列表失败:', error)
this.$message.error('获取故事列表失败,请稍后重试')
this.storyList = []
this.total = 0
this.dlog('getStoryList:error', error)
})
},
// 搜索故事
searchStories() {
this.currentPage = 1
this.dlog('searchStories', this.searchForm)
this.getStoryList()
},
// 查看故事详情
viewStory(row) {
this.selectedStory = JSON.parse(JSON.stringify(row))
this.storyDetailVisible = false
this.panelMode = 'view'
this.dlog('viewStory', row)
},
// 格式化故事内容,将换行符转换为<br>
formatStoryBody(body) {
if (!body) return ''
return body.replace(/\n/g, '<br>').replace(/\n\n/g, '<br><br>')
},
// 编辑故事
editStory(row) {
if (!row) return
this.editForm = JSON.parse(JSON.stringify(row))
this.editDialogVisible = false
this.panelMode = 'edit'
this.dlog('editStory', row)
},
// 保存编辑
saveEdit() {
this.$refs.editFormRef.validate((valid) => {
if (valid) {
this.isLoading = true
const isNew = !this.editForm.id
if (isNew && !this.editForm.userId) {
const stored = localStorage.getItem('auth_user') || sessionStorage.getItem('auth_user')
const user = stored ? JSON.parse(stored) : null
if (!user || !user.id) { this.$message.error('请先登录'); this.isLoading = false; return }
this.editForm.userId = user.id
}
const req = isNew ? this.$axios.post('/story/save', this.editForm) : this.$axios.put('/story/update', this.editForm)
this.dlog('saveEdit:start', { isNew, payload: this.editForm })
req
.then(response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message.success('保存成功')
this.editDialogVisible = false
const saved = response.data.data || this.editForm
this.selectedStory = JSON.parse(JSON.stringify(saved))
this.panelMode = 'view'
this.getStoryList()
this.dlog('saveEdit:success', response.data)
} else {
this.$message.error(response.data.msg || '保存失败')
this.dlog('saveEdit:fail', response.data)
}
})
.catch(error => {
this.isLoading = false
console.error('保存故事失败:', error)
this.$message.error('保存失败,请稍后重试')
this.dlog('saveEdit:error', error)
})
}
})
},
// 删除故事
deleteStory(id) {
if (!id) return
this.dlog('deleteStory:start', id)
this.$confirm('确定要删除这个故事吗?此操作不可撤销!', '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.isLoading = true
this.$axios.delete(`/story/${id}`)
.then(response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message.success('删除成功')
this.getStoryList()
this.dlog('deleteStory:success', id)
} else {
this.$message.error(response.data.msg || '删除失败')
this.dlog('deleteStory:fail', response.data)
}
})
.catch(error => {
this.isLoading = false
console.error('删除故事失败:', error)
this.$message.error('删除失败,请稍后重试')
this.dlog('deleteStory:error', error)
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
// 批量删除
batchDelete() {
if (this.selectedStories.length === 0) {
this.$message.warning('请选择要删除的故事')
return
}
const ids = this.selectedStories.map(story => story.id)
this.dlog('batchDelete:start', ids)
this.$confirm(`确定要删除选中的 ${this.selectedStories.length} 个故事吗?此操作不可撤销!`, '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.isLoading = true
this.$axios.post('/story/batch-delete', ids)
.then(response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message.success(`成功删除 ${this.selectedStories.length} 个故事`)
this.selectedStories = []
this.getStoryList()
this.dlog('batchDelete:success')
} else {
this.$message.error(response.data.msg || '批量删除失败')
this.dlog('batchDelete:fail', response.data)
}
})
.catch(error => {
this.isLoading = false
console.error('批量删除故事失败:', error)
this.$message.error('批量删除失败,请稍后重试')
this.dlog('batchDelete:error', error)
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
// 处理选择变化
handleSelectionChange(val) {
this.selectedStories = val
this.dlog('selectionChange', val)
},
// 处理编辑对话框关闭
handleEditClose() {
this.$refs.editFormRef?.resetFields()
this.editDialogVisible = false
this.panelMode = 'idle'
this.dlog('editClose')
},
// 分页大小变化
handleSizeChange(val) {
this.pageSize = val
this.getStoryList()
this.dlog('pageSizeChange', val)
},
// 当前页变化
handleCurrentChange(val) {
this.currentPage = val
this.getStoryList()
this.dlog('currentPageChange', val)
},
// 重置生成表单
resetGenerateForm() {
this.generateForm.keywords = ''
this.generatedStory = null
},
// 重置搜索表单
resetSearchForm() {
this.searchForm.keywords = ''
this.currentPage = 1
this.getStoryList()
}
}
,generateSound(row) {
if (!row || !row.id) return
this.audioLoading = true
this.dlog('generateSound:start', row.id)
this.$axios.post('/sound/generate', { storyId: row.id }, { timeout: 180000 })
.then(res => {
this.audioLoading = false
if (res.data.code === 200) {
const s = res.data.data
this.latestSoundRecord = s
this.latestSoundUrl = s && s.id ? `/sound/file/${s.id}` : ''
this.soundDialogVisible = true
this.$message.success('语音生成成功')
this.dlog('generateSound:success', s)
} else {
this.$message.error(res.data.msg || '生成失败')
this.dlog('generateSound:fail', res.data)
}
})
.catch(err => {
this.audioLoading = false
const isTimeout = err?.code === 'ECONNABORTED' || /timeout/i.test(err?.message || '')
const msg = isTimeout ? '请求超时,请稍后重试或增大超时' : (err?.response?.data?.msg || '生成失败')
this.$message.error(msg)
this.dlog('generateSound:error', err)
})
}
,playLatest(row) {
if (!row || !row.id) return
this.audioLoading = true
this.dlog('playLatest:start', row.id)
this.$axios.get(`/sound/by-story/${row.id}`)
.then(res => {
this.audioLoading = false
if (res.data.code === 200) {
const list = res.data.data || []
if (list.length === 0) {
this.$message.info('暂无语音文件')
this.dlog('playLatest:empty')
return
}
const latest = list.reduce((a,b) => (a.id > b.id ? a : b))
this.latestSoundRecord = latest
this.latestSoundUrl = latest && latest.id ? `/sound/file/${latest.id}` : ''
this.soundDialogVisible = true
this.dlog('playLatest:success', latest)
} else {
this.$message.error(res.data.msg || '查询失败')
this.dlog('playLatest:fail', res.data)
}
})
.catch(() => {
this.audioLoading = false
this.$message.error('查询失败')
this.dlog('playLatest:error')
})
}
}
</script>
<style scoped>
.page-hero { background: linear-gradient(90deg,#fff3e0,#ffe0b2); border: 1px solid #fde0a6; border-radius: 16px; padding: 18px; box-shadow: 0 6px 16px rgba(255,143,0,0.12); margin-bottom: 16px; }
.hero-story { background: linear-gradient(90deg,#e3f2fd,#bbdefb); border-color: #bbdefb; }
.hero-banner { margin-bottom: 16px; height: 180px; border-radius: 16px; overflow: hidden; box-shadow: 0 6px 16px rgba(33,150,243,0.12); border: 1px solid #bbdefb; }
.hero-image { width: 100%; height: 100%; display: block; object-fit: cover; }
.hero-title { font-size: 22px; font-weight: 700; color: #3e2723; }
.hero-sub { color: #6d4c41; margin-top: 6px; }
.story-manager-container {
padding: 20px;
}
.split-layout { display: flex; gap: 16px; align-items: flex-start; }
.left-pane { flex: 1; min-width: 420px; }
.right-pane { width: 48%; }
.edit-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.placeholder { color: #909399; padding: 20px; text-align: center; }
.actions-row { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
.row-actions { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
.fixed-panel { height: 900px; display: flex; flex-direction: column; }
.fixed-panel .el-card__body { overflow: auto; }
.story-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title { display: flex; align-items: center; gap: 10px; font-weight: 700; color: #303133; font-size: 18px; }
.card-icon { font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 999px; background: rgba(255,255,255,0.92); box-shadow: 0 2px 6px rgba(0,0,0,0.15); }
.generate-form,
.search-form,
.edit-form {
margin-bottom: 20px;
}
.generated-result {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.story-title {
color: #303133;
margin-bottom: 10px;
}
.story-title-cell {
cursor: pointer;
color: #409eff;
}
.story-title-cell:hover {
color: #66b1ff;
text-decoration: underline;
}
.story-content {
color: #606266;
line-height: 1.8;
white-space: pre-wrap;
}
/* 故事详情对话框样式 */
.story-detail-dialog .el-dialog__header {
padding: 20px 25px 15px;
}
.story-detail-dialog .el-dialog__body {
padding: 0 25px 20px;
max-height: 500px;
overflow-y: auto;
}
.story-detail-content {
padding: 10px 0;
}
.story-detail-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin: 0 0 15px 0;
text-align: center;
}
.story-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ebeef5;
}
.story-tags {
margin-right: 5px;
}
.story-id {
color: #909399;
font-size: 12px;
margin-left: auto;
}
.story-detail-body {
font-size: 14px;
line-height: 1.8;
color: #606266;
white-space: pre-wrap;
}
.no-data {
text-align: center;
color: #909399;
padding: 40px 0;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.batch-actions {
margin-top: 10px;
}
.dialog-footer {
text-align: right;
}
.cartoon-theme { background: url('@/image/storyBG.png') center/cover no-repeat; }
</style>
.cartoon-theme .el-card { border-radius: 16px; border: 1px solid rgba(0,0,0,0.06); box-shadow: 0 10px 20px rgba(0,0,0,0.08); }
.cartoon-theme .el-dialog { border-radius: 16px; overflow: hidden; }
.cartoon-theme .el-dialog__header { background: linear-gradient(90deg,#fff3e0,#ffe0b2); color: #3e2723; }
.cartoon-theme .el-button { border-radius: 18px; font-weight: 600; }
.cartoon-theme .el-button--primary { background: linear-gradient(90deg,#ff9a9e,#fecfef); border-color: #ff9a9e; }
后端调用大模型——豆包
package com.example.demo.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.*;
/**
* API调用服务,用于与外部大语言模型API交互生成故事内容
*
* @author qi
* @since 2025-03-13
*/
@Service
public class ApiService {
@Autowired
private RestTemplate restTemplate;
// API基础URL
private static final String API_URL = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
// API密钥
private static final String API_KEY = "";
// 使用的模型
private static final String MODEL = "doubao-seed-1-6-251015";
/**
* 根据关键词生成故事内容
*
* @param keywords 关键词
* @return 包含标题和正文的Map
*/
public Map<String, String> generateStoryByKeywords(String keywords) {
try {
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + API_KEY);
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", MODEL);
requestBody.put("max_completion_tokens", 1600);
requestBody.put("reasoning_effort", "medium");
// 构建messages数组,加入system指令提高稳定性
List<Map<String, Object>> messages = new ArrayList<>();
Map<String, Object> sysMsg = new HashMap<>();
sysMsg.put("role", "system");
List<Map<String, Object>> sysContent = new ArrayList<>();
Map<String, Object> sysText = new HashMap<>();
sysText.put("type", "text");
sysText.put("text", "你是儿童故事生成助手。严格遵循输出格式要求,不得添加额外解释或说明。");
sysContent.add(sysText);
sysMsg.put("content", sysContent);
messages.add(sysMsg);
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
// 构建content数组(只包含text部分,因为我们不需要图像)
List<Map<String, Object>> content = new ArrayList<>();
Map<String, Object> textContent = new HashMap<>();
textContent.put("type", "text");
// 构建提示词,要求生成标题和正文
String prompt = String.format(
"请基于关键词'%s'创作一个儿童故事,并仅以JSON格式输出:{\\\"title\\\":\\\"[故事标题]\\\",\\\"body\\\":\\\"[故事内容]\\\"}。" +
"要求:正文字数在300到600字之间,结构完整(开端/发展/结尾),语言通俗积极,适合儿童阅读;不要输出除该JSON外的任何内容。",
keywords
);
textContent.put("text", prompt);
content.add(textContent);
message.put("content", content);
messages.add(message);
requestBody.put("messages", messages);
// 创建请求实体
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(response.getBody());
JsonNode choices = rootNode.path("choices");
JsonNode messageNode = choices.get(0).path("message");
JsonNode contentNode = messageNode.path("content");
String generatedContent = "";
if (contentNode.isArray()) {
StringBuilder sb = new StringBuilder();
for (JsonNode node : contentNode) {
if (node.has("text")) {
sb.append(node.path("text").asText(""));
}
if (node.has("output_text")) {
sb.append(node.path("output_text").asText(""));
}
}
generatedContent = sb.toString();
} else {
// 某些版本可能返回字符串或对象,尽量提取文本
if (contentNode.has("text")) {
generatedContent = contentNode.path("text").asText("");
} else if (contentNode.has("output_text")) {
generatedContent = contentNode.path("output_text").asText("");
} else {
generatedContent = contentNode.asText("");
}
}
// 兼容直接位于 message 的 output_text 字段
if (generatedContent == null || generatedContent.trim().isEmpty()) {
String ot = messageNode.path("output_text").asText("");
if (ot != null && !ot.trim().isEmpty()) {
generatedContent = ot;
}
}
Map<String, String> jsonParsed = tryParseJson(generatedContent);
if (jsonParsed != null) {
// 若解析到JSON格式,直接返回
return jsonParsed;
}
return parseStoryContent(generatedContent);
} else {
throw new RuntimeException("API调用失败: " + response.getStatusCodeValue());
}
} catch (Exception e) {
// 记录错误并返回默认故事
e.printStackTrace();
Map<String, String> defaultStory = new HashMap<>();
defaultStory.put("title", "默认故事标题 - " + keywords);
String body = "由于API调用失败,这是一个基于关键词'" + keywords + "'的默认故事内容。请检查API连接和密钥配置。";
int limit = 2000;
if (body.length() > limit) {
body = body.substring(0, limit);
}
defaultStory.put("body", body);
return defaultStory;
}
}
/**
* 解析API返回的故事内容,提取标题和正文
*
* @param content API返回的完整内容
* @return 包含标题和正文的Map
*/
private Map<String, String> parseStoryContent(String content) {
Map<String, String> result = new HashMap<>();
try {
String text = content == null ? "" : content.trim();
String title = null;
String body = null;
// 兼容 "标题:[xxx]\n正文:[yyy]" 或不带方括号场景
java.util.regex.Pattern p = java.util.regex.Pattern.compile("标题\\s*[::]\\s*\\[?(.*?)\\]?\\s*正文\\s*[::]\\s*(.*)", java.util.regex.Pattern.DOTALL);
java.util.regex.Matcher m = p.matcher(text);
if (m.find()) {
title = m.group(1).trim();
body = m.group(2).trim();
} else {
int idx = text.indexOf('\n');
if (idx > 0 && idx < 50) {
title = text.substring(0, idx).replaceFirst("^标题[::]?", "").trim();
body = text.substring(idx + 1).trim();
} else {
title = "错误";
body = text;
}
}
if (body == null) body = "";
// 若正文为空,使用关键词构造一个兜底文本,避免前端出现空白
if (body.isBlank()) {
body = "这是一个故事草稿,正文暂缺。请稍后重试或编辑补充正文。";
}
result.put("title", title);
result.put("body", body);
} catch (Exception e) {
result.put("title", "解析失败的故事");
String body = content == null ? "" : content;
int limit = 2000;
if (body.length() > limit) {
body = body.substring(0, limit);
}
result.put("body", body);
}
return result;
}
// 优先按JSON解析,减少格式不一致导致的空正文
private Map<String, String> tryParseJson(String text) {
if (text == null) return null;
String s = text.trim();
if (!(s.startsWith("{") && s.endsWith("}"))) return null;
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(s);
String title = node.path("title").asText("");
String body = node.path("body").asText("");
if (!title.isBlank() && !body.isBlank()) {
Map<String, String> map = new HashMap<>();
map.put("title", title.trim());
map.put("body", body.trim());
return map;
}
} catch (Exception ignore) {}
return null;
}
}
浙公网安备 33010602011771号