2026山东大学软件学院项目实训(四)
一、基本信息
组号:69
组员:李重昊
负责模块:前端开发和库表设计
二、任务概述
应用模块:前端开发未完成部分
-
主页应用创建、我的应用/精选应用展示
-
应用对话页 SSE 流式输出与网页预览
-
应用管理页(管理员)操作完善
-
菜单可见性与权限提示修复
对话历史模块:库表设计相关部分
-
设计并落地
chat_history表 -
增加游标分页关键复合索引
(appId, createTime)
三、主要技术方案与实现
1、目标
1.1 应用生成预览能力
根据文档要求,AI 生成过程需要“边输出边展示”,并在生成结束后能在页面右侧直接预览网页。 我的方案是:
-
前端对话页用
EventSource对接/app/chat/gen/code -
实时拼接 AI 返回 chunk 到聊天气泡
-
收到
done事件后,自动尝试预览地址/static/{codeGenType}_{appId}/ -
若连接关闭但无 chunk,给出明确提示,避免空白气泡误导
1.2 应用管理页面可用性
应用管理、用户管理菜单不应“看起来消失”。 处理方式:保留菜单可见,点击时做权限提示(未登录/非管理员)。
1.3 对话历史库表设计
为后续“游标分页加载历史消息”做准备,在 chat_history 表中加入:
-
messageType区分 user/ai -
parentId预留扩展 -
核心索引 idx_appId_createTime (appId, createTime)`
2、对话历史库表设计与落库
我在 SQL 初始化脚本中新增 chat_history 表结构,并补齐单列索引与复合索引,满足后续按 createTime 游标向前翻页的性能需求。
-- 对话历史表(用于对话记忆与游标分页)
create table if not exists chat_history
(
id bigint auto_increment comment 'id' primary key,
message text not null comment '消息',
messageType varchar(32) not null comment 'user/ai',
appId bigint not null comment '应用id',
userId bigint not null comment '创建用户id',
parentId bigint null comment '父消息id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
INDEX idx_appId (appId),
INDEX idx_createTime (createTime),
INDEX idx_appId_createTime (appId, createTime)
) comment '对话历史' collate = utf8mb4_unicode_ci;
3、应用管理页新增“预览生成应用”能力
在管理员应用列表中增加“预览生成”按钮,直接打开后端静态预览路径;若已有部署 key,则可打开部署作品地址。
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" type="primary" ghost @click="previewGenerated(record)">
预览生成
</a-button>
<a-button v-if="record.deployKey" size="small" @click="openDeployed(record)">查看作品</a-button>
<a-button size="small" @click="goEdit(record.id)">编辑</a-button>
<a-button size="small" @click="setFeatured(record, true)">精选</a-button>
<a-button size="small" @click="setFeatured(record, false)">取消精选</a-button>
<a-popconfirm title="确定删除该应用吗?" @confirm="doDelete(record.id)">
<a-button danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
预览URL构建逻辑
const getApiBaseUrl = () => {
const defaults = (request as unknown as { defaults?: { baseURL?: string } }).defaults
return defaults?.baseURL || 'http://localhost:8080/api'
}
const previewGenerated = (record: API.AppVO) => {
if (!record?.id) return
const codeGenType = record.codeGenType || 'multi_file'
const url = `${getApiBaseUrl()}/static/${codeGenType}_${record.id}/`
window.open(url, '_blank')
}
4、 菜单补全与权限设置
“应用管理/用户管理”菜单显示,对非管理员访问做前端提示拦截。
const originItems = [
{
key: '/',
icon: () => h(HomeOutlined),
label: '主页',
title: '主页',
},
{
key: '/admin/appManage',
label: '应用管理',
title: '应用管理',
},
{
key: '/admin/userManage',
label: '用户管理',
title: '用户管理',
},
// ...
]
const handleMenuClick: MenuProps['onClick'] = (e) => {
const key = e.key as string
selectedKeys.value = [key]
const isAdminMenu = key.startsWith('/admin')
if (isAdminMenu) {
const loginUser = loginUserStore.loginUser
if (!loginUser?.id) {
message.warning('请先登录')
router.push('/user/login')
return
}
if (loginUser.userRole !== 'admin') {
message.warning('该页面仅管理员可访问')
return
}
}
if (key.startsWith('/')) {
router.push(key)
}
}
5、 应用对话页 SSE 输出与预览链路增强
实现了更稳健的流式展示与预览逻辑:
-
兼容 JSON chunk / 纯文本 chunk
-
done后刷新预览 -
进入页面时自动探测是否存在预览文件
-
无输出时显示明确提示,避免空白 AI 气泡
const refreshPreviewIfExists = async () => {
if (!appId.value) return
const codeGenType = appInfo.value?.codeGenType || 'multi_file'
const candidate = `${baseURL.value}/static/${codeGenType}_${appId.value}/index.html`
try {
const res = await fetch(candidate, {
method: 'GET',
credentials: 'include',
})
if (res.ok) {
previewUrl.value = `${baseURL.value}/static/${codeGenType}_${appId.value}/`
} else {
previewUrl.value = ''
}
} catch {
previewUrl.value = ''
}
}
es = new EventSource(url, { withCredentials: true } as unknown as EventSourceInit)
es.onmessage = (evt) => {
if (completed) return
try {
let chunk = ''
try {
const parsed = JSON.parse(evt.data)
chunk = parsed?.d ?? ''
} catch {
// 兼容后端异常情况下直接返回纯文本 data
chunk = evt.data ?? ''
}
if (chunk === undefined || chunk === null) {
chunk = ''
}
full += chunk
if (chunk.trim().length > 0) {
hasValidChunk = true
}
messages.value[aiIndex].content = full
messages.value[aiIndex].loading = false
scrollBottom()
} catch {
messages.value[aiIndex].loading = false
messages.value[aiIndex].content = '解析流式响应失败,请重试'
completed = true
isGenerating.value = false
es?.close()
es = null
}
}
es.addEventListener('done', async () => {
if (completed) return
completed = true
isGenerating.value = false
messages.value[aiIndex].loading = false
if (!hasValidChunk && !messages.value[aiIndex].content) {
messages.value[aiIndex].content = 'AI 暂无输出,请重试或检查后端日志。'
}
es?.close()
es = null
await fetchApp()
await refreshPreviewIfExists()
})
6、 后端 AI 门面注入修复
在服务层补齐 AiCodeGeneratorFacade 注入,保证ai能正确输出到前端
@Autowired
private UserService userService;
@Autowired
private AiCodeGeneratorFacade aiCodeGeneratorFacade;
四、结果与验证
4.1 功能结果
-
已完成应用管理页中的“预览生成应用”入口
-
应用对话页支持流式输出与预览自动刷新
-
菜单项可见性恢复,权限提示更明确
-
chat_history表已创建,复合索引可用
4.2 验证结果
-
前端
lint/build通过(已在开发环境执行) -
数据库验证通过:
DESCRIBE chat_history可返回字段,索引包含idx_appId_createTime
五、问题与解决
-
管理菜单“消失”
-
原因:前端按角色直接过滤隐藏
-
处理:改为可见 + 点击时提示权限
-
-
AI 无输出空白气泡
-
原因:流关闭时无有效 chunk 没有反馈
-
处理:增加
hasValidChunk与结束兜底提示;兼容纯文本流
-
-
后端流式调用潜在空指针
-
原因:
AiCodeGeneratorFacade未注入 -
处理:补齐
@Autowired
-
六、经验总结
本次任务中我需要将同组同学制作的后端接口,完成相应的前端显示。主要完成的是主页的展示以及应用管理和用户管理。在本次实现过程中,在写好前端页面时与ai的对话一直不能正常实现,并多次出现空白或者报错。经过多次检查输出后,发现可能是SSE在解析失败的情况下被当成了正常结束,先修正了在无返回条件下无输出的问题,然后发现后端AppServiceImpl没有注入,返回为空指针。
本次修正的难度略大于前几次,总体来说有不少收获。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)