前八周把六大模块基本搭完了,这周开始把项目从"H5 能跑"推进到"真机能用"。一上真机就暴露出一堆细节问题——头像传上去刷新就没了、AI 对话里的图片真机根本加载不出来、换个 WiFi 整个图片链路就崩、学习计划每点一次"重新生成"任务就翻倍累积、推荐问题里显示的是知识点编号而不是名字……每一条真机都能复现,每条都得改。这周我主要做的就是前后端联调和多端适配,把这些坑一个个填平。今天把这批工作记一下。

一、这周干了什么?
这周主线是前后端联调 + 多端(H5/App)适配,具体做了这几件事:

图片资源访问链路重构:新增后端文件代理接口,把 MinIO 资源路径统一改为后端代理路径,前端用全局 $media 方法拼接完整地址,换网络只改一处配置。

多模态对话修复:修复头像上传、AI 聊天图片上传两个 bug,并让对话能保留之前上传过的图片(历史多模态消息)。

多端 SSE 流式实现:把智能问答的 SSE 流式通信适配到 H5 和 App 两端,新增稳定性处理(超时、断连、错误分类)。

练习改为简答自评:简答/编程题去掉填空框,改成"我已做完→查看参考答案→自己判定对错",并修复本次训练结果无法正常显示的问题。

学习计划修复:修复"重新生成计划"时旧任务在 task 表累积、首页今日任务出现大量重复条目的问题,并补全 CORS 对 PATCH 方法的支持。

若干联调 bug:推荐问题显示知识点编号、已掌握错题排序、底部输入与下一题页面优化等。

数据爬虫与向量构建:data_crawler 新增对剩余科目的爬取和向量构建支持,并实际爬取入库了相关语料。

二、图片资源访问链路重构(本周核心)
这是这周改动最大、也最有价值的一块。

2.1 问题:真机和换网络后图片全挂
之前图片(头像、错题图、对话图片)存到 MinIO 后,返回给前端的是一个直连 MinIO 的完整 URL,类似 http://127.0.0.1:9000/myapp-file/xxx.jpg。这在本机 H5 上没问题,但一上真机就全挂:

  • 真机访问不到后端机器的 127.0.0.1
  • 改成局域网 IP(如 10.37.19.184:9000)后,换个 WiFi 整套地址又失效;
  • 还得额外对外暴露 MinIO 的 9000 端口。

2.2 方案:后端文件代理 + 前端 $media 拼接
思路是让图片访问统一走后端 8080,而不是直连 MinIO。新增了一个 FileProxyController,把 MinIO 里的对象通过后端转发出去:

/**
 * 图片/文件代理:把 MinIO 中的对象通过后端(8080)转发出去。
 * 换网络时只需改前端 BASE_URL 一处,无需维护 MinIO 局域网 IP、也不必对外暴露 9000。
 * GET /api/file/{objectKey}   objectKey 可含多级路径,如 2026-06-20/uuid.jpeg
 */
@GetMapping("/**")
public void proxy(HttpServletRequest request, HttpServletResponse response) {
    String uri = request.getRequestURI();
    String objectKey = uri.substring(uri.indexOf("/api/file/") + "/api/file/".length());
    objectKey = URLDecoder.decode(objectKey, StandardCharsets.UTF_8);

    String bucket = minioConfig.getBucket();
    // 取 content-type 与长度
    StatObjectResponse stat = minioClient.statObject(
            StatObjectArgs.builder().bucket(bucket).object(objectKey).build());
    response.setContentType(stat.contentType());
    response.setHeader("Cache-Control", "public, max-age=86400"); // 缓存一天
    try (GetObjectResponse in = minioClient.getObject(
            GetObjectArgs.builder().bucket(bucket).object(objectKey).build());
         OutputStream out = response.getOutputStream()) {
        in.transferTo(out);
    }
}

对应地,MinioService.getFileUrl 不再返回 MinIO 直连地址,而是返回后端代理的相对路径:

public String getFileUrl(String objectKey) {
    if (objectKey == null || objectKey.isEmpty()) return "";
    if (objectKey.startsWith("http")) return objectKey; // 已是完整 URL 直接返回
    // 返回走后端代理的相对路径,前端用自己的 BASE_URL 拼成完整地址
    return "/api/file/" + objectKey;
}

这个代理接口在 MvcConfiguration 里对登录拦截器做了放行——因为 <img> 标签请求不会带 Token。

2.3 前端:全局 media方法后端给的是相对路径‘/api/file/xxx.jpg‘,前端需要拼上BASEURL才能访问。为了不在每个页面手动拼,我在‘main.js‘注册了一个全局方法‘media 方法 后端给的是相对路径 `/api/file/xxx.jpg`,前端需要拼上 BASE_URL 才能访问。为了不在每个页面手动拼,我在 `main.js` 注册了一个全局方法 `media方法后端给的是相对路径‘/api/file/xxx.jpg,前端需要拼上BASEURL才能访问。为了不在每个页面手动拼,我在main.js注册了一个全局方法media`,对各种来源的地址做归一化:

// main.js
import { toMediaUrl } from '@/utils/request.js'
app.config.globalProperties.$media = toMediaUrl

toMediaUrl 会判断:如果是 blob:/data:/本地临时路径就原样返回,如果是 /api/file/ 这类后端相对路径就拼上 BASE_URL。页面里只要 :src="$media(imageUrl)" 即可。

这样重构后,换网络环境只需要改前端 BASE_URL 一处,后端、MinIO 的地址完全不用动,真机图片读取问题彻底解决。

三、多模态对话修复
3.1 头像上传后刷新消失
联调时发现上传新头像后,页面刷新又变回旧头像。根因和上面是同一类问题:头像 URL 之前存的是带 127.0.0.1 的 MinIO 直连地址,真机渲染不出来。改用 /api/file/ 代理路径 + $media 拼接后解决。

3.2 AI 对话图片识别为空 & 历史图片丢失
两个问题:

  • 上传图片给 AI 后回复为空——排查到是多模态消息组装、图片字节流传输的链路问题,在 AlibabaOpenAiChatModelMultimodalChatServiceImpl 里做了修复;
  • 历史对话看不到当时发的图——给多模态接口补上了 sessionId,把用户消息(带图片 URL、msgType=2)和 AI 回复一起落库,回看历史时图片能正常带出来。

四、多端 SSE 流式实现
智能问答用的是 SSE 流式输出,但 H5 和 App 两端的网络能力不一样,需要分别适配。这周在 request.js 里完善了多端 SSE(一百多行改动),并配合 AppSseBridge.vue 处理 App 端的流式桥接,加上了超时控制、断连重连、错误分类提示等稳定性处理。知识库详情页"向 AI 提问"无响应的问题也一并修掉了。

五、练习改为简答自评
之前简答/编程题在前端也放了个填空框让用户输入,但这类题本来就没法用文本精确判分,体验很别扭。这周把交互改了:

简答/编程题不再有填空处,而是一个按钮"我已做完,查看参考答案",点击后展示参考答案,再由用户自己选择"做对了/做错了"。同时修复了练习结束后本次训练结果(正确数、得分)无法正常显示的问题。

六、学习计划修复
6.1 重新生成计划任务累积
这是个很典型的联调 bug:用户每点一次"重新生成计划",task 表里就多出一批任务,首页"今日任务"越积越多、全是重复条目。

根因是强制重新生成时,只把旧计划置为废弃,却没清理旧计划遗留的未完成任务。修复:

// 删除旧计划遗留的未完成任务(status 0/1),避免每次重新生成后任务在 task 表累积;
// 已完成任务(status=2)保留作为历史
long removed = taskService.count(new LambdaQueryWrapper<Task>()
        .eq(Task::getUserId, userId)
        .in(Task::getStatus, Arrays.asList(0, 1)));
taskService.remove(new LambdaQueryWrapper<Task>()
        .eq(Task::getUserId, userId)
        .in(Task::getStatus, Arrays.asList(0, 1)));
log.info("用户 {} 强制重新生成计划,清理未完成旧任务 {} 条", userId, removed);

6.2 CORS 缺 PATCH 方法
"保存并生成计划"用的是 PATCH 请求,但前端一直报错。这个 bug 很隐蔽——我用 PowerShell 直连后端能成功,浏览器却失败,最后定位到是全局 CORS 配置的 allowedMethods 里漏了 PATCH(预检请求被拦),补上即可:

// MvcConfiguration
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")

教训:跨域问题一定要在浏览器里复现,用命令行直连会绕过 CORS 预检,看不到问题。

七、若干联调小修复
推荐问题显示编号:聊天页的推荐问题里,知识点显示成了「2」「3」这样的数字。原因是画像里 weakPoints 存的是知识点 ID,没转成名称。在 ChatApiController 里加了一层 ID→名称的解析:

private String resolveKpName(String kpIdOrName) {
    if (kpIdOrName == null || kpIdOrName.isBlank()) return "";
    if (kpIdOrName.matches("\\d+")) {           // 纯数字才查
        KnowledgePoint kp = knowledgePointService.getById(Long.parseLong(kpIdOrName));
        if (kp != null && kp.getKpName() != null) return kp.getKpName();
    }
    return kpIdOrName;                            // 否则原样返回
}

已掌握错题后置:错题列表把已标记"已掌握"的题排到后面,优先复习还没掌握的。

页面优化:知识库列表删掉了意义不大的筛选项,学习计划界面去掉"观看视频"、调整学习次数选择,做题页底部输入和"下一题"的交互也优化了一下。

八、数据爬虫与向量构建
后端 RAG 需要语料支撑,这周给 data_crawler 补了对剩余科目的爬取和向量构建支持,并实际爬取、清洗、入库了多个科目的语料(cnblogs、csdn 等源,覆盖高数、算法、计算机网络、数据结构、数据库、操作系统等),为知识库检索增强提供数据基础。

九、踩坑与心得

  1. 本机能跑 ≠ 真机能用

这周最大的体会就是这句话。头像、对话图片、SSE,在本机 H5 上全都正常,一上真机各种挂。127.0.0.1、局域网 IP、端口暴露这些在本机根本意识不到,到真机就是致命问题。后端文件代理这套方案的价值就在于把"网络环境"这个变量收敛到前端一个 BASE_URL,一劳永逸。

  1. CORS 问题要用浏览器复现

PATCH 跨域那个 bug 卡了挺久,因为我习惯用命令行直连后端验证,而命令行不走 CORS 预检,永远是成功的。联调前端问题,必须在浏览器/真机里复现,看 Network 面板。

  1. 数据库累积类 bug 容易被忽略

"重新生成计划"任务累积,单次看功能是好的,多点几次才暴露。这类"会随时间/操作次数恶化"的 bug,联调时要专门多操作几遍来验证,不能点一次就过。

十、总结
这一周把项目从"演示能跑"推到了"真机可用":图片链路重构 + 多端 SSE 让真机体验立住了,练习自评、学习计划修复、推荐问题等一批联调 bug 也都收尾了,加上爬虫补齐了 RAG 语料。

现在在真机上可以跑通完整体验:登录 → 智能问答(流式 + 拍照识题 + 历史图片)→ 错题拍照录入 → 练习(选择/填空自动判分、简答自评)→ 生成个性化学习计划 → 首页今日任务联动。下一步打算把语料向量真正灌进库里,让 RAG 检索增强完整跑起来。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐