山东大学创新实训——诈骗克星个人博客六

山东大学创新实训——诈骗克星个人博客六 山东大学创新实训——诈骗克星个人博客六前言前五篇博客分别介绍了诈骗克星小程序的页面构建、检测能力、模拟对话、知识库和结果展示。然而这些前端能力一直依赖本地 Mock 数据运行——就像建好了精美的仪表盘却没有接入真正的传感器。本文记录我们首次将前端与 Java Spring Boot 后端SafeGuard打通的全过程涵盖 17 个 API 的对齐、三大核心技术决策以及联调中暴露的真实 Bug 修复。一、联调全景从 0 到 17 个 API前后端分离项目的第一次联调本质上是一次契约对齐。前端期望的数据结构和后端实际返回的 JSON 有时候会存在差异我们通过一份接口文档逐条核对最终梳理出 17 个 API 的完整映射。改造分三个阶段推进阶段目标改造Phase A最小可行联调前后端路由对齐Phase B视频检测异步化任务管理 SSE 进度推送Phase CLLM 流式输出DeepSeek SSE 透传至小程序二、技术决策为什么用 ConcurrentHashMap 而不是 CaffeinePhase B 需要一个内存级的任务管理器来跟踪异步检测任务的状态。常见方案有两种——ConcurrentHashMap和 Caffeine 缓存。我们最终选择了前者原因涉及三个层面的考量。生命周期语义不匹配Caffeine 的核心抽象是缓存——数据写入后自动过期、按策略驱逐调用者只关心存和取不关心条目何时消失。但我们的任务管理器管理的是有状态工作流create → updateProgress → complete/fail → cleanup每个任务持有一个SseEmitter长连接实例需要在其生命周期内主动推送事件。如果交给 Caffeine 的 TTL 机制自动驱逐SseEmitter会被静默关闭前端收到的是无预警的连接中断。Caffeine 的能力过剩异步任务的并发量极低1-5 个同时运行Caffeine 引以为傲的 LRU 淘汰、软引用、命中率统计等能力完全用不上。引入 Caffeine 意味着多一个强制依赖com.github.ben-manes.caffeine对于这个需求来说过于沉重。显式清理更可控ConcurrentHashMap配合ScheduledExecutorService实现了精确的 30 秒延迟清理——任务完成后才开始倒计时而非从创建时计算 TTL。这确保了用户查看结果期间数据始终可用// 30 秒后清理从 complete/fail 开始计时privatevoidscheduleCleanup(StringtaskId){cleanupScheduler.schedule(()-{tasks.remove(taskId);},30_000L,TimeUnit.MILLISECONDS);}如果用 Caffeine需要组合expireAfterWrite和expireAfter自定义策略才能模拟类似行为代码复杂度反而更高。适用场景速览维度ConcurrentHashMap 手动清理Caffeine生命周期显式 create→complete→remove自动过期/驱逐并发度低1-5 个任务高数千条目状态耦合持有 SseEmitter 等资源纯数据缓存清理时机精确控制完成后再计时TTL 从创建/访问算起依赖开销JDK 内置零依赖需引入第三方包结论Caffeine 是优秀的本地缓存但任务管理器需要的是状态机而非缓存。ConcurrentHashMap 手动清理的简单组合更符合这个场景的精确需求。三、前端双模监控SSE 优先 Polling 回退SSE 细节前篇已述本文只讲核心设计视频检测涉及逐帧推理单次请求耗时数十秒。前端TaskWatcher采用双模策略优先建立 SSE 长连接接收实时进度若不可用则自动降级为轮询每 1.5 秒查询一次任务状态。两种模式共享同一套onProgress/onDone/onError回调接口对业务代码完全透明classTaskWatcher{watch(taskId,callbacks{}){// SSE 优先失败则自动回退轮询if(!this._startSSE(taskId,callbacks)){this._startPolling(taskId,callbacks);}}}关键点不在 SSE 本身而在于双模设计——小程序环境下wx.request的enableChunked支持因基础库版本而异双模方案确保了兼容性零妥协。四、零缓冲透传 本地打字机回退SSE 流式透传细节前篇已述本文聚焦降级策略后端收到 DeepSeek 流式响应后透传至前端若 SSE 链路受阻前端自动降级为本地打字机效果。这不是简单的直接显示完整文本而是按字符逐字吐出——中文字符间隔 40ms、英文 30ms、标点停顿 120ms最大化保留流式体验asyncstreamContent(messageId,fullContent){for(leti0;ifullContent.length;i){this.updateStreamingMessage(messageId,fullContent.slice(0,i));constcharfullContent[i-1];letdelay30;if(char/[一-龥]/.test(char))delay40;elseif(/[.,!?;:。]/.test(char))delay120;awaitthis.sleep(delay);}}核心思路不把网络降级视为失败而是视为传输协议切换。用户在任何网络条件下都能获得一致的逐字展示体验。五、PromptLoader 复合占位符解析多模态检测需要将音频、视频的分析结果注入 LLM 提示词模板。传统方案使用String.format()或简单键值替换但遇到{audio.fakeProbability}这样的点分隔复合占位符就无能为力了。我们实现了一个支持反射的提示词加载器通过正则匹配{对象.属性}格式的占位符运行时通过反射递归获取嵌套属性值publicclassPromptLoader{publicStringloadPrompt(StringtemplateName,MapString,Objectparams){StringtemplateloadTemplate(templateName);PatternpatternPattern.compile(\\{([\\w.])\\});Matchermatcherpattern.matcher(template);StringBuffersbnewStringBuffer();while(matcher.find()){Stringplaceholdermatcher.group(1);String[]partsplaceholder.split(\\.);ObjectvalueresolveValue(params,parts);matcher.appendReplacement(sb,Matcher.quoteReplacement(value!null?value.toString():));}matcher.appendTail(sb);returnsb.toString();}privateObjectresolveValue(Objectobj,String[]parts){Objectcurrentobj;for(Stringpart:parts){if(currentinstanceofMap){current((Map?,?)current).get(part);}else{currentinvokeGetter(current,part);// 反射 getter}}returncurrent;}}这使得提示词模板可以直接引用嵌套字段音频伪造概率为 {audio.fakeProbability}视频帧分析结果为 {video.frameAnalysis[0].label}大幅提升了模板的表达力和复用性。六、联调修复真实环境暴露的 6 个 Bug前后端首次联调6 个真实 Bug 浮出水面问题根因修复结果圆环只显示半圈CSSborder方案只能画 180°改用conic-gradient360° 渐变检测按钮始终可用canDetect是方法而非data字段新增data字段 输入变化时刷新文本检测置信度溢出 100%公式riskProb 0.1导致 1.05改为Math.max(real, fake)知识库搜索不命中单向匹配公检法 vs “冒充公检法”双向匹配 分类名对齐首页跳转失败知识库是 TabBar 页却用navigateTo改为switchTab视频选择语法错误try-catch 缺闭合括号补全括号这些 Bug 大部分在 Mock 环境下无法暴露只有真实数据流经完整链路时才会触发——这正是联调不可替代的价值。七、总结前后端联调不是简单的接口拼接而是契约的验证、异常的处理、体验的打磨。用ConcurrentHashMap而非 Caffeine 管理任务生命周期、双模监控保障兼容性、本地打字机作为 SSE 降级方案、反射式占位符提升模板表达力——这些技术决策加上真实联调中修复的 6 个 Bug让诈骗克星从一套精美的 Mock 原型蜕变为真正可用的反诈检测系统。