上个月有个准备做毕业设计的朋友跑来找我,题目写着"基于AI应用 + Spring Boot + Vue的电竞椅赛事中心设计"。他说自己最担心两件事:一是AI到底怎么"应用"进系统里才不算挂羊头卖狗肉,二是这类综合项目最后怎么讲清楚代码、怎么把设计说明书写到能过审。
我跟他说,这个题目骨架其实很清晰——难点不在技术栈本身,而在三件事怎么咬合:AI要落在真实业务上,Spring Boot后端要有说得通的分层逻辑,Vue前端要把赛事数据展示和实时交互撑起来。这篇文章就按我自己做这类全栈项目时的完整思路,从需求拆解、数据库设计、后端编码、前端落地,一直讲到设计说明书和部署踩坑。你按这个顺序走一遍,基本就能把一个"看着像PPT"的选题,做成一个真能跑、真能讲、真能交差的完整系统。
1. 为什么这套技术栈几乎是"标准答案":需求拆解与选型理由
1.1 电竞赛事中心到底是个什么系统
很多人看到"电竞赛事中心"这个名字,第一反应是做一个展示英雄联盟或CS2比赛信息的门户网页。但实际上,这类项目在课程设计、毕业设计或小型团队对外交付里,通常被定义为:一个围绕赛事全流程的数据管理系统。
从业务上说,它至少要覆盖以下能力:
- 赛事信息发布:一场比赛什么时候打、什么项目、在哪里、什么阶段。
- 战队与选手资料:队伍所属赛事、选手名单、位置、历史战绩。
- 赛程与比分管理:支持后台录入、前台实时展示,以及进行中的比分变化推送。
- 互动与个性化:用户登录后可以收藏战队、参与赛果预测、查看AI生成的赛前分析和赛后总结。
- 数据可视化:通过大屏或图表展示赛事分布、战队胜率、选手数据等。
也就是说,它不是一个静态官网,而是一个典型的"管理端 + 前台展示 + 实时推送 + 智能化分析"的组合型系统。搞清楚了这一点,技术选型就顺理成章了。
1.2 为什么选Spring Boot + Vue,而不是Python Flask + React
我的建议是:如果这不是纯生产级项目而是教学/交付性质,Spring Boot + Vue 三件套就是最不容易翻车的组合。
先看后端。电竞赛事中心天然适合Spring Boot,因为它的核心操作大多围绕"实体数据"展开——队伍、选手、场次、预测记录,这些都是经典的CRUD+业务状态流转。Spring Boot在实体建模、数据访问、事务管理和权限控制上非常成熟,生态里MyBatis-Plus、Hutool、Sa-Token这些工具能让你少写大量样板代码。更重要的是,这类项目几乎必然要求你写数据库设计文档和接口文档,Spring Boot在这方面的资料多到不行,踩坑时随便一搜就有答案。
再看前端。Vue生态对"信息展示型应用"的匹配度很高,组件化开发方式让赛事卡片、战队头像墙、比分面板这类重复度高的模块可以复用。Vue的响应式机制在处理赛程列表筛选、比分实时刷新时逻辑非常直观,配合第三方组件库(Element Plus、Vant等)能快速搭建出还算精致的界面。
再说为什么不建议在这个题目里用React或Flask。不是因为它们不行,而是因为风险不对称——React生态虽然也很好,但在国内教学评审场景里,Vue的资料和问答密度明显更高;Flask写轻量接口确实快,但到了要写"用户权限、定时任务、WebSocket/SSE推送"这些模块时,Spring Boot的现成方案更省事,文档写起来也更像正规软件工程项目。
1.3 AI应用在系统里的三个真实落点,而不是"挂羊头"
标题里带"AI应用",这是整个项目最容易被评审追问的地方。我见过太多方案,所谓AI就是在页面里放一个调ChatGPT接口的聊天框,输入"你好"输出"你好",跟赛事系统毫无关系。这个做法基本等于告诉评审:我对AI没有理解。
我在实际项目里通常会安排三个AI落点,既不会过度复杂,又能体现智能化价值:
- 赛前预测Bot:用户输入一场比赛的对阵双方,系统把战队近期战绩、历史交手数据、当前状态拼接成结构化Prompt,让大模型生成胜率预测、关键选手对位分析和一句话结论,并且返回带理由的分析报告。
- 赛事问答助手:用户可以用自然语言问"这个赛季哪支队伍获胜最多""有哪些选手擅长刺客英雄",系统先通过SQL或检索逻辑从数据库里查出候选数据,再交给大模型组织成通顺中文回答。
- 赛后简报自动生成:比赛结束后,根据比分和选手数据自动生成一段赛后简报,推送到赛事详情页。这个功能实现成本不高,但演示效果极好。
这三个落点有一个共同特点:AI不是在空转,而是阅读系统里的真实数据后再做推理。这样一来,"AI应用"就从包装变成一个实打实的业务模块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前先把数据模型想清楚:从用户角色到表结构
我见过太多人写这种项目,一上来就拖出几百行SQL,最后表之间的关系自己都讲不明白。正确顺序应该是:先画业务脑图,再设计接口,最后敲表结构。因为表结构是业务的投影,业务没理清,表就是空中楼阁。
2.1 角色与功能清单
我在这个系统里划分了三个角色:
- 游客:浏览赛事列表、战队资料、赛程安排和赛事大屏。
- 注册用户:游客的全部能力 + 收藏战队 + 参与赛果预测 + 使用AI预测和分析助手。
- 管理员:用户管理 + 赛事/战队/选手/比赛信息的增删改 + 查看所有预测记录。
这个角色划分不复杂,但已经足够撑起完整的权限逻辑。管理员用Spring Boot的拦截器或Sa-Token做接口级别鉴权,前端用路由守卫控制页面可见性。
2.2 核心数据表设计
数据库我建议至少设计以下7张表,足以覆盖所有业务闭环:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| sys_user | id, username, password, nickname, role | 用户表 |
| t_event | id, event_name, game_type, start_date, end_date, status, location | 赛事表,game_type区分LOL/CS2等 |
| t_team | id, team_name, logo_url, event_id, rating, win_count, lose_count | 战队表,外键关联赛事 |
| t_player | id, player_name, nickname, avatar, team_id, position, stats_json | 选手表,stats_json存KDA等扩展数据 |
| t_match | id, event_id, team_a_id, team_b_id, match_time, stage, score_a, score_b, status | 比赛表,status区分未开始/进行中/已结束 |
| t_prediction | id, match_id, user_id, predict_winner, result, create_time | 预测表,result标记预测是否正确 |
| t_ai_chat | id, user_id, session_id, question, answer, create_time | AI问答记录表 |
这里有两个设计上的小心得。
第一,选手数据不要把所有统计字段都做成列。因为电竞赛事的统计数据变化频繁,今天加个"场均压刀",明天加个"首杀率",不断加列会把自己折腾死。用stats_json存JSON,前端按需解析,非常灵活。
第二,比赛表里直接冗余存对阵双方的队名,而不是只存team_a_id让前端每次去联表查。因为赛事列表页动不动就要显示"RNG vs EDG",冗余一对名称字段可以少写很多关联查询,这就是典型的空间换时间。当然,冗余的前提是保证数据一致性,团队信息修改时需要同步比赛表,或用定时任务兜底。
2.3 数据流的完整业务闭环
拿"用户发起AI预测"这个场景举例,整个链路是这样的:
- 用户在比赛详情页点击"AI赛前分析"按钮。
- 前端把 matchId 发给后端
/api/ai/predict接口。 - 后端根据 matchId 查出两支队伍及其近期战绩。
- 后端把比分、排名、近期胜负、历史交手记录拼接成结构化Prompt。
- 通过HTTP请求调用大模型API,并设置超时和重试策略。
- 模型返回JSON格式预测结果,后端解析后返回给前端。
- 前端渲染预测报告卡片,同时把结果存进t_ai_chat作为历史记录。
你只要能在答辩时把这条链路画出来、说清楚,就已经比大多数只写"点击调用AI接口"的方案扎实很多了。
3. Spring Boot后端编码:把AI服务封装得和普通Service一样好用
3.1 工程结构与分层
我不建议把所有代码堆在controller里。一个能讲得出口的项目,分包要让人一眼看出边界:
code复制com.example.esport
├── controller // 接口层
├── service // 业务层
│ └── impl
├── mapper // MyBatis-Plus数据访问
├── entity // 数据库实体
├── dto // 前后端交互对象
├── config // 跨域、拦截器等配置
├── common // 统一返回体、异常处理、工具类
└── ai // AI服务封装
这套结构没什么新意,但它符合最主流的软件工程习惯。评审问起来你可以直接回答:controller只做参数接收和结果包装,业务逻辑在service层校验,mapper层不写业务SQL只做数据访问。这一句话就能说明白你懂分层。
3.2 赛事模块:一个典型Controller+Service实现
以一个赛事列表接口为例,常规但完整。Controller层尽量做得干净:
java复制@RestController
@RequestMapping("/api/match")
public class MatchController {
@Resource
private MatchService matchService;
@GetMapping("/list")
public Result<List<MatchVO>> list(@RequestParam(required = false) String status,
@RequestParam(required = false) Long eventId) {
return Result.success(matchService.getMatchList(status, eventId));
}
@GetMapping("/{id}")
public Result<MatchDetailVO> detail(@PathVariable Long id) {
return Result.success(matchService.getMatchDetail(id));
}
}
Service层做真正的业务处理:
java复制@Service
public class MatchServiceImpl implements MatchService {
@Resource
private MatchMapper matchMapper;
@Resource
private TeamMapper teamMapper;
@Override
public List<MatchVO> getMatchList(String status, Long eventId) {
LambdaQueryWrapper<Match> wrapper = new LambdaQueryWrapper<>();
// 状态筛选,status为空则查全部
wrapper.eq(StringUtils.hasText(status), Match::getStatus, status);
wrapper.eq(eventId != null, Match::getEventId, eventId);
// 按比赛时间排序
wrapper.orderByAsc(Match::getMatchTime);
List<Match> matches = matchMapper.selectList(wrapper);
// 组装VO
return matches.stream().map(m -> {
MatchVO vo = new MatchVO();
BeanUtils.copyProperties(m, vo);
Team teamA = teamMapper.selectById(m.getTeamAId());
Team teamB = teamMapper.selectById(m.getTeamBId());
vo.setTeamAName(teamA != null ? teamA.getTeamName() : "待定");
vo.setTeamBName(teamB != null ? teamB.getTeamName() : "待定");
return vo;
}).collect(Collectors.toList());
}
}
这段代码技术含量不高,但胜在逻辑完整。你用MyBatis-Plus的LambdaQueryWrapper演示了条件构造、排序和组件装配,这就已经覆盖了CRUD项目的大部分考察点。真正要体现深度的地方,是AI预测服务的设计。
3.3 AI预测服务:HttpClient + Prompt模板 + 超时兜底
这一节是整个项目技术含金量最高的部分,我详细说。
我们的目标是让大模型返回结构化JSON,而不是废话连篇的自然语言。因此在Prompt里要高强度约束输出格式。我采用的模式是:
- 先拼接系统提示词(system prompt)。
- 再把数据库里查到的队伍数据填充进模板。
- 要求模型只输出JSON,并给出JSON的字段说明和示例。
- 设置连接超时和读取超时,防止模型接口卡死拖垮整个后端。
- 对模型返回的JSON做容错解析,因为模型偶尔会输出markdown代码块包裹的JSON,或者多出几个字段。
核心代码如下:
java复制@Service
public class AiPredictService {
private final RestTemplate restTemplate;
private final Gson gson = new Gson();
public AiPredictResult predict(Match match, Team teamA, Team teamB) {
// 1. 组装Prompt模板
String prompt = buildPrompt(match, teamA, teamB);
// 2. 构造请求体
Map<String, Object> messages = new HashMap<>();
messages.put("model", "your-model-name");
messages.put("response_format", Map.of("type", "json_object"));
List<Map<String, String>> msgList = new ArrayList<>();
msgList.add(Map.of("role", "system", "content", "你是一名资深电竞数据分析师,只能输出JSON,不要输出任何多余文字。"));
msgList.add(Map.of("role", "user", "content", prompt));
messages.put("messages", msgList);
// 3. 设置超时
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(15000);
RestTemplate rt = new RestTemplate(factory);
// 4. 调用模型服务
String response = rt.postForObject(modelUrl, messages, String.class);
// 5. 解析JSON并兜底
return parseResponse(response);
}
private AiPredictResult parseResponse(String response) {
// 处理模型偶尔附带markdown代码块的情况
String jsonStr = response.replace("```json", "").replace("```", "").trim();
return gson.fromJson(jsonStr, AiPredictResult.class);
}
}
这里有几个关键的工程细节:
- 超时设置必须有。我曾经因为没设超时,AI接口挂了导致整个赛事列表接口都跟着卡死。线上系统一定不能让外部依赖拖垮自身核心链路。
- 解析必须有容错机制。大模型不是数据库,它可能返回"
json\n{...}\n",如果你直接gson.fromJson,会直接炸。在这一步做个replace是低成本高收益的防御。 - 如果追求稳定性,可以加本地缓存。比如同一场比赛的预测在1小时内不重复调用模型,直接返回上次结果,既省钱又提速。这也是可以在答辩时讲的优化点。
AI聊天模块同理,只是数据结构不同,Prompt也不用那么强约束。注意一点:如果前端要"打字机"效果,后端接口就不要用普通POST一次性返回,而是采用SSE流式输出,逐段推送模型的token。
3.4 实时比分推送:SSE实现
赛季中心里"比赛进行中比分实时刷新"是一个非常亮眼的功能。实现方式有两种:WebSocket和SSE。我这里推荐SSE,原因很直接:比分推送是单向的,从服务端推向客户端,SSE不需要客户端回复,而且Spring Boot原生支持SseEmitter,代码量远小于WebSocket。
java复制@RestController
@RequestMapping("/api/match")
public class MatchSseController {
// 简单的内存管理器,生产环境可用Redis发布订阅替代
private final Map<Long, CopyOnWriteArrayList<SseEmitter>> emitterMap = new ConcurrentHashMap<>();
@GetMapping("/score/stream/{matchId}")
public SseEmitter streamScore(@PathVariable Long matchId) {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
emitterMap.computeIfAbsent(matchId, k -> new CopyOnWriteArrayList<>()).add(emitter);
emitter.onCompletion(() -> removeEmitter(matchId, emitter));
emitter.onTimeout(() -> removeEmitter(matchId, emitter));
return emitter;
}
public void pushScore(Long matchId, MatchScoreData data) {
List<SseEmitter> emitters = emitterMap.get(matchId);
if (emitters == null) return;
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event().name("score").data(data, MediaType.APPLICATION_JSON));
} catch (IOException e) {
emitter.completeWithError(e);
removeEmitter(matchId, emitter);
}
}
}
}
这段代码演示了SseEmitter的基本用法,用内存Map管理每个比赛对应的连接列表。实际项目里如果比赛数据被管理员修改,触发pushScore方法后,所有订阅该比赛的前端页面就会即时收到新的比分。
不过要注意,这个方案是单机版内存实现。如果系统部署在多台服务器上,或者你希望更专业的架构,就得把Emitter列表搬到Redis的Pub/Sub上。不过在毕设/课程设计这个层级,内存版本已经足够,而且更易讲解。
4. Vue 3前端落地:从页面骨架到AI聊天面板
4.1 工程初始化与目录设计
前端我建议直接上Vue 3 + Vite + Element Plus + ECharts + Pinia,这一套是目前国内最主流、资料也最全的组合。Vite创建工程非常快:
bash复制npm create vite@latest esport-web -- --template vue
cd esport-web
npm install element-plus @element-plus/icons-vue axios vue-router pinia echarts
目录上不要搞得太花哨,但要做到一眼能看懂:
code复制src
├── api // 所有axios请求封装
├── components // 赛事卡片、比分面板、AI预测卡片等公共组件
├── views // 页面:首页、赛事列表、赛事详情、赛事大屏、登录、后台管理
├── router // 路由配置
├── store // Pinia状态
└── utils // 请求拦截器、日期格式化等工具
一个最容易忽略但实际很重要的点:axios请求拦截器和响应拦截器一定要写。统一处理BaseURL、Token注入和错误提示,能让你在联调时少掉一半头发。代码很简单,但价值很高:
javascript复制import axios from 'axios';
const service = axios.create({ baseURL: '/api', timeout: 15000 });
service.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
service.interceptors.response.use(
res => res.data.code === 200 ? res.data.data : Promise.reject(new Error(res.data.msg)),
err => {
ElMessage.error('请求失败:' + err.message);
return Promise.reject(err);
}
);
4.2 赛事大屏与数据可视化的实现
赛事大屏是电竞赛事中心项目的视觉担当,也是答辩时的加分项。我建议做一个全屏页面,上半部分是当前重点比赛的实时比分,中间是赛程时间轴,下方用ECharts图表展示战队胜率分布和选手数据雷达图。
ECharts在Vue里的用法比较标准,我重点说两个踩坑经验:
第一,图表容器必须有固定高度。ECharts初始化时如果给的是百分比高度,在容器还没渲染完成时会直接得到0,图就看不见。我的习惯是给容器直接height: 400px或通过CSS calc计算。
第二,组件销毁时要调用dispose方法。如果页面来回切换,ECharts实例不销毁会造成内存泄漏,图表还会出现渲染异常。在onUnmounted钩子里调echarts.dispose(instance)就行。
大屏上的"比赛状态"可以做成自动轮询更新,也可以直接接4.3说的SSE推送。轮询是兜底方案,SSE是真正实时方案,两者的结合能让系统既稳定又实时。
4.3 与后端SSE对接的实时更新
浏览器原生有EventSource对象,可以直接监听SSE。不过EventSource有一个限制:默认情况下它无法自定义Header,所以如果有鉴权需要,通常是通过Cookie传递session,或者把token放在query参数里(注意,放query参数有泄露风险,演示项目可以直接用Cookie方案或者简化成无需鉴权的订阅接口)。
我的前端对接代码大致如下:
javascript复制const es = new EventSource('/api/match/score/stream/' + matchId);
es.addEventListener('score', (event) => {
const data = JSON.parse(event.data);
state.scoreA = data.scoreA;
state.scoreB = data.scoreB;
});
es.onerror = () => {
// 断线自动重连,SSE内置了重连机制
console.log('score stream disconnected, retrying...');
};
这里有个细节:SSE断线后浏览器会自动重连,但服务端原来的Emitter连接也已经失效了,所以后端存储Emitter的Map必须维护好"连接失效即清理",否则积累大量僵尸连接会内存溢出。我在后端代码里已经用onCompletion和onTimeout做了清理,对应前端只需要正确处理事件和控制生命周期即可。
4.4 AI对话面板与流式输出效果
AI赛前分析和问答助手的界面,我建议不要用传统的一问一答框,而是做成卡片式对话面板——用户输入问题后,流式输出的文字逐字出现在聊天区域,很有科技感。
流式输出的背后是SSE逐段推送。大模型API本身支持stream=true参数,服务端拿到模型的流式响应后转发给前端;如果不想处理流式,也可以用非流式接口,等模型全部返回完一次性推给前端。两者差别在于用户体验,流式更接近ChatGPT的观感,但服务端代码会相对复杂一点。
前端处理流式的一个简单做法是fetch + ReadableStream:
javascript复制async function streamChat(question) {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ question })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let answer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
answer += decoder.decode(value);
state.chatContent = answer; // 绑定到响应式变量,实现打字机效果
}
}
如果后端是SSE,同样可以用EventSource监听message事件逐段拼字符串。这两种方式都能实现"边生成边显示"的效果,你可以根据自己的后端实现选择。
5. 代码讲解与设计说明书:交付材料的"话术"也是技术活
5.1 代码讲解的顺序,比代码本身更重要
很多人写完代码后,到答辩或做技术分享的时候不知道从哪里讲起,上来就讲某个Controller里的一行if判断,结果听众一头雾水。我的经验是:从数据流讲,而不是从代码行讲。
一个顺滑的讲解顺序是:
- 先讲系统整体架构:Vue发请求给后端Controller,Controller交给Service,Service通过Mapper操作MySQL,AI模块额外调用大模型API。
- 然后挑一个完整业务闭环,比如"赛事列表加载":前端调用/api/match/list,后端如何处理条件查询,如何返回VO。
- 再挑一个有技术含量的点,比如"AI预测":讲清楚Prompt怎么组装、超时怎么设置、返回JSON如何容错解析。
- 最后讲优化和不足:哪些地方用了缓存,哪些后续可以改进。
这样讲下来,评审跟着你的思路走,会觉得你对整个系统是真的有掌控力的。
5.2 设计说明书的标准骨架与"加分区"
设计说明书不需要写得像产品需求文档那样宏大,但一定要结构完整。我建议的章节如下:
- 摘要:系统是什么,用什么技术实现,有哪些功能。
- 需求分析:功能需求、用户角色、用例描述。
- 系统设计:总体架构图、技术选型理由、模块划分。
- 数据库设计:ER图、表结构说明、字段解释。
- 接口设计:每个核心接口的方法、URL、入参、出参、鉴权要求。
- 核心功能实现说明:结合代码解释AI预测、SSE推送等模块的实现思路。
- 系统测试:测试用例表、测试结果、边界情况处理。
- 总结与展望:遇到的问题、解决方法、后续优化方向。
其中数据库设计和接口设计是最容易显得专业、但很多人写得潦草的两个部分。数据库设计最好不要直接贴一大张建表SQL,而是先用表格列清楚每张表的核心字段和作用,再说字段之间如何关联。接口设计我通常会做一张表:
| 接口 | 方法 | 说明 | 鉴权 |
|---|---|---|---|
| /api/match/list | GET | 按状态/赛事查询赛程列表 | 无 |
| /api/match/ | GET | 赛事详情 | 无 |
| /api/ai/predict | POST | 生成赛前AI分析 | 登录 |
| /api/ai/chat | POST | 赛事问答流式输出 | 登录 |
| /api/match/score/stream/ | GET | SSE比分实时推送 | 无 |
这张表一放出来,评审就知道你的后端边界是很清晰的。
5.3 答辩/评审常见的追问点与应对思路
根据我带项目的经验,评审最爱问下面几个问题,提前准备好答案会让场面舒服很多:
-
"AI接口如果挂了,你的系统还能正常用吗?"
回答思路:AI模块在Service层有超时控制,并try-catch兜底;比赛列表、选手资料等核心功能不依赖AI,调用失败只影响分析功能,不影响主流程。 -
"为什么用SSE不用WebSocket?"
回答思路:比分推送是单向数据流,SSE语义上更匹配;Spring Boot对SseEmitter有原生支持,代码侵入小;如果以后要双向交互(比如弹幕聊天),再升级成WebSocket。 -
"设计说明书里的架构图画得很漂亮,系统里真的有这么分层吗?"
回答思路:带着现场翻代码,从controller一路指到mapper,展示真实的目录结构。这一条只要代码不乱,基本就是送分题。
6. 部署上线与踩坑记录:本地能跑,不等于线上能跑
6.1 打包与部署的两种常用方式
这类项目交付时,通常有两种部署方式。
方式一是前后端分离部署:后端打jar包,前端打包成静态文件交给Nginx托管,反向代理/api请求到后端端口。这种方式专业,但要配Nginx。
方式二是前后端合并部署:Vue打包后的dist目录里所有文件,直接放到Spring Boot的resources/static下,然后打成一个jar包运行。这种方式简单,一个java -jar就能跑起来,非常适合演示和验收。
我这里推荐方式二,它能让你的项目交付门槛降到最低。具体操作:
- 前端执行
npm run build,得到dist目录。 - 把dist内容复制到后端
src/main/resources/static。 - 后端打jar包,启动后通过
http://服务器IP:8080直接访问页面。
记住一个点:如果前端用了BrowserRouter形式的路由,直接访问某个子页面路径会404,需要配置一个转发规则把未知路径转发到index.html。不过大多数项目采用Hash模式路由,不会遇到这个问题,图省事就用hash。
6.2 我实际踩过的坑:SSE、跨域、AI接口超时
老实说,这个项目的代码写起来不复杂,真正磨人的是各种环境问题。我挑几个有代表性的记录一下。
第一个坑:SSE连不上,前端一直触发error。排查半天发现是后端接口触发了Controller异常,返回的是一个JSON错误体,但EventSource拿到非SSE格式数据时不会显示错误内容,只会静默触发onerror。定位方法是用浏览器直接访问SSE地址看返回头是否包含Content-Type: text/event-stream。这个问题告诉我们,SSE接口的异常处理和普通接口完全是两码事。
第二个坑:跨域配置写错导致登录失效。前后端分离开发时,Vue的devServer代理和后端CORS配置经常要配合。如果后端配了@CrossOrigin又在前端配了代理,有时候会出现请求发出去但拿不到cookie的情况。现在的解决方案是尽量只在Nginx或Vite代理层解决跨域,后端配一次WebMvcConfigurer的CORS映射做兜底即可。
第三个坑:AI接口响应慢,前端等太久直接超时。布局卡死、页面一直转圈,体验极差。这个问题的根子在于我给axios设置了15秒超时,但大模型推理在某些情况下就是要跑20秒以上。解决方案是对AI相关接口单独设立更长的超时时间,甚至直接走流式返回,让用户看到首字就心安。
第四个坑:部署环境JDK版本对不上。Spring Boot 3.x要求JDK 17+,很多人的公网服务器还是JDK 8。解决办法要么用Spring Boot 2.7版本配套JDK 8,要么在服务器上装好对应的JDK。我强烈建议做项目前先确认目标演示环境的Java版本,再决定Spring Boot大版本,不然后期改版本号会连锁引发一堆依赖兼容问题。
6.3 如果时间富余,下一步还能加什么
如果按照上面这些步骤做完,系统已经是一个能正常演示的完整项目了。如果评审要求更高,还有几个低成本高收益的扩展方向:
- 选手数据雷达图:把比赛数据可视化,展示选手在KDA、伤害占比、视野控制等维度的表现。
- AI赛果预测正确率统计:把用户预测的结果和真实赛果对比,统计预测准确率。这个功能一加,AI模块就从"玩具"变成"有数据闭环"的功能。
- 定时任务:定时从外部赛事平台采集赛程数据(如果有公开数据源的话),替代手工录入。
- 本地模型缓存:对AI分析结果做Redis缓存,同一场比赛的分析不重复调用模型API。
最后分享一点我自己的习惯:做这类技术交付型项目,最值钱的从来不是代码量,而是你能不能讲清楚每一个技术决策背后的为什么。哪怕是一张表为什么冗余了两个队名字段,都能体现项目和Demo之间的本质区别。按这篇文章的顺序把需求、数据库、后端、前端、文档、部署各过一遍,你会对这个系统从里到外都有底,答辩也好、对外讲解也好,自然是水到渠成的事。
