SpringBoot + 微信小程序 + AI 智能校园导航系统的毕业设计项目,看起来功能很多,实际拆开只有三条业务线:校园地图数据管理、路线规划、AI 自然语言问答。只要把这三条线想清楚,源码、论文、PPT 和演示讲解都可以围绕它们组织,项目也才不会做成一张“只带几个标记点的地图”。
校园导航和普通地图导航有一个本质差异:普通地图软件面对的是成熟的市政路网,而校园导航面对的是“最后一公里”问题。新生报到要知道自己住哪栋宿舍楼,访客要知道图书馆入口朝哪边开,学生想知道第一食堂三层有没有开放。这些问题只有校内数据才能回答。加进 AI 大模型之后,用户不需要逐条筛选目的地,直接输入“我想去图书馆还书,怎么走”,系统就能理解意图、检索校内地点并返回路线。
这篇文章按这个系统的真实开发顺序展开,分为技术选型、数据库设计、SpringBoot 后端接口、微信小程序页面、AI 接入、联调验证、排错路径和答辩准备几个部分。文中代码用于说明实现思路,实际项目落地时,要根据自己的包名、依赖版本、数据库字段和 AI 服务商配置做调整。
1. 先弄清楚智能校园导航系统要解决什么问题
1.1 校园导航和普通城市导航的本质差异
普通地图软件有完整的路网数据,导航时系统会在道路级别计算最短路径。校园导航不同,校园内的道路等级低、楼栋密集、小路多,很多地点不在市政路网上,甚至同一栋楼的不同入口坐标都不一样。这时需要自己维护一份“校园 POI(兴趣点)”数据,把教学楼、食堂、宿舍、图书馆、体育馆、行政楼都记录成带经纬度的地点,再在 POI 之间建立可通行的路径边。
这条数据链是导航系统的基础。数据准确,路线才可信;数据不准,AI 回答得再流畅也没有用。所以设计系统时,第一个要做的不是写 AI 接口,而是把校园地图数据模型设计好。
还需要注意坐标系问题。微信小程序地图组件默认使用 WGS-84 或 GCJ-02 坐标,不同来源的坐标如果混用,会出现 marker 偏移几十米的现象。项目中应该统一记录并使用同一套坐标系,常见做法是统一使用 GCJ-02 坐标入库,前端直接展示。
1.2 AI 大模型在系统里的真实定位
AI 在校园导航系统里不是核心计算引擎,而是一个“自然语言入口层”。路线计算仍然靠后端图算法完成,AI 负责解决用户怎么把自己的需求表达出来的问题。
用户输入“我要去图书馆”,传统系统要用户先选择目的地类型,再在地图上点选。接入大模型后,系统可以把这句话拆解为:
- 意图:navigation
- 目标地点:图书馆
- 可能存在的附加信息:还书、自习、开放时间
然后 AI 从 POI 表里检索出候选地点,把候选地点信息和校园知识库内容组织成一句可读的回复,同时返回一个结构化的地点对象,小程序端拿到这个对象就能直接画路线。
在毕业设计里,这样的定位更容易写清楚。论文里可以写“本系统采用大模型完成意图识别与对话生成,路径规划由基于 Dijstra 算法的后端模块实现”,逻辑清楚,也避免了把大模型硬塞进路径计算导致的不可控。
1.3 角色、功能边界与毕业设计加分点
系统常见的角色划分是普通用户和管理员。普通用户在小程序端使用地图浏览、POI 检索、路线规划、AI 问答、收藏和反馈功能;管理员在 Web 管理端维护楼宇、POI、路线边和校园知识库。
| 模块 | 普通用户 | 管理员 |
|---|---|---|
| 地图浏览 | 查看楼宇与 POI 标记 | 查看、编辑全部数据 |
| POI 检索 | 按名称或关键词搜索 | 新增、修改、上下架 POI |
| 路线规划 | 选择起点终点查看路线 | 维护路线边数据 |
| AI 问答 | 自然语言提问、跳转导航 | 维护知识库内容 |
| 历史记录 | 查看历史导航与 AI 提问 | 查看统计和反馈 |
加分功能通常不是做大而全的管理后台,而是把一两个细节做扎实:比如 AI 回答后能直接跳转到地图路线;路线可分享给好友;用户在 AI 答案下能给“有用/没用”反馈;管理员维护知识库后 AI 回答能立即更新。这些功能实现成本不高,却能在演示和答辩时形成亮点。
2. 技术选型与项目结构设计
2.1 后端技术栈:SpringBoot + MyBatis-Plus + MySQL
后端使用 SpringBoot,这是目前毕业设计中最稳妥的选择,生态资料多、社区问题多、遇到问题容易查到解决方案。持久层使用 MyBatis-Plus,可以少写很多单表 CRUD。数据库使用 MySQL 即可。
| 组件 | 推荐范围 | 说明 |
|---|---|---|
| Java | 8 / 11 / 17 | SpringBoot 2.7.x 用 Java 8 或 11,SpringBoot 3.x 用 Java 17 |
| SpringBoot | 2.7.x 或 3.2.x | 毕业设计建议从 2.7.x 起步,资料多,依赖冲突少 |
| MyBatis-Plus | 3.5.x | 需检查与 SpringBoot 的兼容性 |
| MySQL | 5.7 / 8.0 | 本机开发可用 5.7,生产建议 8.0 |
| Redis | 可选 | 用于缓存 POI 列表或存储登录态,不是必须项 |
这里要特别注意 SpringBoot 版本带来的包名差异。SpringBoot 2.7.x 使用javax.servlet等javax命名空间,SpringBoot 3.x 已经切换到jakarta命名空间。如果网上找的资料用的还是javax,而本地项目是 SpringBoot 3.x,会直接编译报错。建议先固定一个版本,再统一所有依赖版本。
2.2 小程序端:原生框架还是 uni-app
微信小程序端有两种常见选择:原生小程序和 uni-app。
| 对比项 | 原生小程序 | uni-app |
|---|---|---|
| 上手难度 | 低,语法直接 | 中等,要理解 Vue 语法和编译链路 |
| 调试体验 | 微信开发者工具直接调 | 需要区分 H5、微信、其他端差异 |
| 地图组件 | 直接使用map组件 | 通过 uni-app 封装调用 |
| 毕业设计适配 | 好 | 也常见,但不建议为跨端额外增加复杂度 |
如果项目只需要微信端,推荐使用原生小程序。它本身有完整的地图组件、wx.request网络请求和wx.login登录流程,学习成本低,调试时问题定位直接。如果论文里特别强调“一套代码多端运行”,才考虑 uni-app。
2.3 AI 接入的三种方式
AI 大模型接入方式,常见有三类:
| 接入方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 直接调用 HTTP 接口 | 代码直观,模型可切换 | 需要自己处理鉴权、超时、错误 | 毕业设计首选 |
| Spring AI | 抽象统一,支持流式 | 版本演进快,中文资料少 | 熟悉框架后再用 |
| 规则 + 模型混合 | 离线可用,成本低 | 意图识别弱,扩展差 | 演示时网络不稳的兜底 |
毕业设计里最推荐第一种。大模型厂商通常提供兼容的 chat/completions HTTP 接口,只需要配置base-url、api-key、model三项,就能切换不同服务商。论文里也可以写清楚“采用 HTTP 方式接入大模型接口,下面示例以 OpenAI 兼容协议为例”,既清晰又不绑定具体厂商。
2.4 项目目录结构
建议把前后端分成两个工程目录,便于分别启动和打包。
campus-nav-server/ src/main/java/com/example/campusnav/ controller/ # 接口层 service/ # 业务层 mapper/ # MyBatis-Plus Mapper entity/ # 数据库实体 dto/ # 请求和响应对象 config/ # 微信配置、AI 配置、Web 配置 common/ # 统一返回 Result、异常处理 src/main/resources/ mapper/ # XML 文件(如果使用) application.yml pom.xml campus-nav-miniapp/ app.js app.json app.wxss pages/ map/ # 地图页 search/ # 搜索页 chat/ # AI 对话页 mine/ # 个人中心页 utils/ request.js # 请求封装 auth.js # 登录态处理前后端分离后,小程序通过 HTTP 请求后端接口,后端负责业务逻辑和数据持久化,结构清楚,论文画系统架构图也方便。
3. 数据库设计:地图数据先准确,AI 才有意义
3.1 核心表结构
数据库至少要包含楼宇表、POI 表、路线边表、导航记录表和 AI 问答反馈表。先用楼宇表保存建筑主体信息,再用 POI 表保存楼内具体地点,这样导航可以精确到“第三教学楼 201 教室”级别。
CREATE TABLE building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '楼宇名称', code VARCHAR(50) COMMENT '楼宇编码', description VARCHAR(500) COMMENT '楼宇介绍', latitude DECIMAL(10,6) NOT NULL COMMENT '纬度', longitude DECIMAL(10,6) NOT NULL COMMENT '经度', floor_count INT DEFAULT 1 COMMENT '楼层数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE poi ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '地点名称', type VARCHAR(50) COMMENT '类型:教学楼/食堂/图书馆/宿舍/行政楼', building_id BIGINT COMMENT '所属楼宇', floor INT COMMENT '所在楼层', latitude DECIMAL(10,6) NOT NULL, longitude DECIMAL(10,6) NOT NULL, keywords VARCHAR(255) COMMENT '检索关键词,如自习室、复印、还书', description VARCHAR(1000) COMMENT '地点介绍', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );POI 表的keywords字段很重要。用户可能搜索“自习”,也可能搜索“借书”,这些词不一定出现在地点名称里,但可以提前维护在关键词字段中。检索时同时匹配名称和关键词,召回率会高很多。
3.2 路径与经纬度数据怎么组织
路线数据使用边表保存,这也是图算法的基础:
CREATE TABLE route_edge ( id BIGINT PRIMARY KEY AUTO_INCREMENT, start_poi_id BIGINT NOT NULL, end_poi_id BIGINT NOT NULL, distance_meters INT COMMENT '距离,单位米', walk_time_seconds INT COMMENT '步行预计时间,单位秒', path_json TEXT COMMENT '中间经过点坐标 JSON 数组', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_start (start_poi_id), KEY idx_end (end_poi_id) );path_json可以保存一串中间坐标点,例如[{"lat":30.51,"lng":114.41}, {"lat":30.51,"lng":114.42}]。前端画路线时,把两端的 POI 坐标和中间点组合成polyline即可。
路线边数据不需要一开始就完整录入。可以先录入校园主干道路网,保证相邻 POI 之间有边,再逐步补充细节。演示时只需要覆盖校园核心区域就能看出效果。
3.3 导航记录和反馈表
导航记录表用于保存用户请求过的路线,答辩时可以说明“系统支持历史导航查询”。
CREATE TABLE navigation_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, start_poi_id BIGINT, end_poi_id BIGINT, path_json TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE ai_feedback ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, question TEXT, answer TEXT, is_helpful TINYINT DEFAULT -1 COMMENT '-1未反馈 1有用 0无用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );ai_feedback表可以记录每次 AI 问答的内容和用户的反馈结果。这既能作为功能展示,也是后续优化知识库的重要依据。
3.4 校园知识库数据准备
AI 问答要稳定,不能完全依赖模型训练数据,因为模型并不知道你学校的图书馆几点关门。正确做法是准备一张校园知识库表:
CREATE TABLE campus_knowledge ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category VARCHAR(50) COMMENT '分类:开放时间/食堂/图书馆/宿舍/行政', title VARCHAR(200), content TEXT, keywords VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );示例数据:
INSERT INTO campus_knowledge (category, title, content, keywords) VALUES ('开放时间', '图书馆开放时间', '图书馆周一至周日 8:00-22:00,节假日另行通知。', '图书馆,开放时间,几点关门'), ('食堂', '第一食堂', '第一食堂位于图书馆南侧 200 米,共三层,营业时间 6:30-20:30。', '第一食堂,饭,食堂');AI 收到用户问题后,先从这张表检索相关内容,再把检索结果组装进提示词,让模型基于知识库内容回答。这样做的好处是知识更新后,不需要重新训练模型,只需要改数据库内容。
4. SpringBoot 后端核心接口实现
4.1 依赖配置和统一返回结构
pom.xml中核心依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>注意:MyBatis-Plus 的 starter 版本要和 SpringBoot 主版本匹配。SpringBoot 3.x 需要 MyBatis-Plus 3.5.3 以上版本,否则启动会报错。
统一返回结构建议写成Result<T>:
@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }所有接口统一返回这个结构,小程序端只需要判断code是否为 200。
4.2 微信登录 code2session 流程
小程序端调用wx.login()得到临时code,后端拿code调用微信接口换取openid。具体流程:
- 小程序调用
wx.login获取code。 - 小程序把
code传给后端。 - 后端请求
https://api.weixin.qq.com/sns/jscode2session。 - 微信返回
openid和session_key。 - 后端用
openid查询用户,不存在则注册,然后生成自定义token返回小程序。
后端示例:
@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private RestTemplate restTemplate; @Resource private WxConfig wxConfig; @Resource private UserService userService; @PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginRequest request) { String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + wxConfig.getAppid() + "&secret=" + wxConfig.getSecret() + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; String resp = restTemplate.getForObject(url, String.class); // 解析返回 JSON,得到 openid JsonNode node = new ObjectMapper().readTree(resp); if (node.has("errcode") && node.get("errcode").asInt() != 0) { return Result.error("微信登录失败: " + node.get("errmsg").asText()); } String openid = node.get("openid").asText(); // 查询或注册用户 User user = userService.findOrCreateByOpenid(openid); // 生成自定义 token,这里可以用 UUID,生产环境建议用 JWT String token = UUID.randomUUID().toString().replace("-", ""); LoginVO vo = new LoginVO(); vo.setToken(token); vo.setUserId(user.getId()); vo.setNickName(user.getNickName()); return Result.success(vo); } }这里的自定义token是简化做法。毕业设计可以只把 token 存到 Redis 或内存里,但最好说明白:生产环境要用带过期时间的 JWT,不能只靠随机字符串。
4.3 POI 查询与路径规划接口
POI 搜索接口不复杂,关键是要同时匹配名称、类型和关键词:
@RestController @RequestMapping("/api/poi") public class PoiController { @Resource private PoiService poiService; @GetMapping("/search") public Result<List<PoiVO>> search(@RequestParam String keyword) { LambdaQueryWrapper<Poi> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Poi::getName, keyword) .or().like(Poi::getType, keyword) .or().like(Poi::getKeywords, keyword) .eq(Poi::getStatus, 1); List<Poi> list = poiService.list(wrapper); return Result.success(list); } @GetMapping("/list") public Result<List<Poi>> list() { LambdaQueryWrapper<Poi> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Poi::getStatus, 1); return Result.success(poiService.list(wrapper)); } }路线规划接口使用 Dijkstra 算法在route_edge表构造的图上求最短路径:
public RouteVO planRoute(Long startPoiId, Long endPoiId) { List<RouteEdge> edges = routeEdgeMapper.selectList(null); Map<Long, List<RouteEdge>> graph = buildGraph(edges); Map<Long, Long> prev = new HashMap<>(); Map<Long, Integer> dist = new HashMap<>(); PriorityQueue<long[]> queue = new PriorityQueue<>(Comparator.comparingLong(a -> a[1])); dist.put(startPoiId, 0); queue.offer(new long[]{startPoiId, 0}); while (!queue.isEmpty()) { long[] cur = queue.poll(); long curId = cur[0]; int curDist = (int) cur[1]; if (curDist > dist.getOrDefault(curId, Integer.MAX_VALUE)) { continue; } if (curId == endPoiId) { break; } for (RouteEdge edge : graph.getOrDefault(curId, Collections.emptyList())) { int newDist = curDist + edge.getDistanceMeters(); if (newDist < dist.getOrDefault(edge.getEndPoiId(), Integer.MAX_VALUE)) { dist.put(edge.getEndPoiId(), newDist); prev.put(edge.getEndPoiId(), curId); queue.offer(new long[]{edge.getEndPoiId(), newDist}); } } } // 回溯 prev,得到完整 POI 序列,再从 POI 表查坐标组装 polyline return buildRouteVO(prev, startPoiId, endPoiId); }Dijkstra 算法适合 POI 数量在几百到几千级别的校园场景。如果以后扩展到整个城市级别,再考虑改用 A* 算法或接入专业地图引擎。
4.4 AI 对话接口:意图识别、知识检索、结果生成
AI 对话接口是系统的重点。整体处理流程分成四步:
- 接收用户问题。
- 先做轻量级本地检索:从 POI 表查候选地点,从
campus_knowledge表查知识库内容。 - 把候选地点和知识库信息拼进提示词。
- 调用大模型接口,返回文本回复和可选的地点对象。
核心代码如下:
@RestController @RequestMapping("/api/ai") public class AiChatController { @Resource private PoiService poiService; @Resource private CampusKnowledgeService knowledgeService; @Resource private AiService aiService; @PostMapping("/chat") public Result<AiReplyVO> chat(@RequestBody ChatRequest request) { String question = request.getQuestion(); // 1. 本地检索候选 POI List<Poi> poiCandidates = poiService.searchByNameOrKeyword(question); // 2. 检索知识库 List<CampusKnowledge> knowledgeList = knowledgeService.search(question); // 3. 构造提示词 String replyText = aiService.chat(question, poiCandidates, knowledgeList); // 4. 尝试从回复中解析结构化地点 AiReplyVO vo = new AiReplyVO(); vo.setReply(replyText); vo.setPoiList(poiCandidates); return Result.success(vo); } }AiService里真正调用大模型的部分,可以直接用 RestTemplate 请求兼容 chat/completions 协议的接口:
public String chat(String question, List<Poi> poiList, List<CampusKnowledge> knowledgeList) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(aiConfig.getApiKey()); String systemPrompt = buildSystemPrompt(poiList, knowledgeList); Map<String, Object> body = new HashMap<>(); body.put("model", aiConfig.getModel()); body.put("temperature", 0.3); body.put("messages", Arrays.asList( Map.of("role", "system", "content", systemPrompt), Map.of("role", "user", "content", question) )); HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers); ResponseEntity<String> response = restTemplate.postForEntity( aiConfig.getBaseUrl() + "/chat/completions", entity, String.class); return parseContent(response.getBody()); }buildSystemPrompt方法要把检索结果放进提示词:
你是校园智能导航助手。只能基于下列校园知识库和地点信息回答,不要编造不存在的建筑或地点。 校园地点: - 图书馆(坐标:30.5100, 114.4100),类型:图书馆,关键词:自习,借书,还书 - 第一食堂(坐标:30.5080, 114.4120),类型:食堂,关键词:饭,早餐 校园知识库: - 图书馆开放时间:周一至周日 8:00-22:00。 - 第一食堂共三层,营业时间 6:30-20:30。 用户问题: 我想去图书馆还书,怎么走?这样模型就能基于真实数据回答,而不是依靠训练时习得的通用知识。
4.5 AI 请求的关键参数与错误处理
调用大模型接口时,几个参数要特别注意:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| temperature | 0.2 - 0.5 | 导航场景希望答案稳定,温度过高会乱答 |
| max_tokens | 500 - 1000 | 控制回复长度,避免超限报错 |
| connectTimeout | 5 秒 | 连接超时 |
| readTimeout | 30 秒 | 读取超时,大模型生成时间较长 |
| model | 按服务商模型名 | 模型名写错会返回 404 或 model not found |
AI 接口需要处理几类异常:
- API Key 无效:返回 401,需要在日志里记录,不要把 Key 返回给前端。
- 模型名错误:返回 404 或 model not found,检查配置项。
- 限流:部分服务商返回 429,应该提示“AI 服务繁忙,请稍后再试”。
- 网络超时:设置好 RestTemplate 的 connectTimeout 和 readTimeout。
@Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(30000); return new RestTemplate(factory); }实际部署时,api-key 只能放在后端配置里,绝不允许在小程序前端写入。小程序端直接调用大模型接口会泄露密钥,而且微信小程序要求所有请求域名必须备案且配置合法域名,直接把第三方 AI 地址写成请求地址会触发域名校验问题。
5. 微信小程序端实现
5.1 页面结构与 app.json 配置
小程序端页面分四个:地图页、搜索页、AI 对话页、个人中心页。
{ "pages": [ "pages/map/index", "pages/search/index", "pages/chat/index", "pages/mine/index" ], "window": { "navigationBarTitleText": "AI 校园导航", "navigationBarBackgroundColor": "#2196F3", "navigationBarTextStyle": "white" }, "tabBar": { "list": [ { "pagePath": "pages/map/index", "text": "地图" }, { "pagePath": "pages/chat/index", "text": "AI 助手" }, { "pagePath": "pages/mine/index", "text": "我的" } ] }, "permission": { "scope.userLocation": { "desc": "用于展示你在地图上的位置" } } }注意permission字段。小程序申请地理位置权限时,必须配置用途说明,否则在部分平台会被拒绝。
5.2 地图组件:markers + polyline 画路线
地图页的核心逻辑:加载 POI 列表显示标记,点击 POI 后请求后端路线接口,用polyline画出路线。
Page({ data: { markers: [], polyline: [], latitude: 30.5110, longitude: 114.4160, scale: 16 }, onLoad() { this.loadPoiList(); }, loadPoiList() { request.get('/poi/list').then(data => { const markers = data.map(p => ({ id: p.id, latitude: p.latitude, longitude: p.longitude, title: p.name, callout: { content: p.name, display: 'ALWAYS', fontSize: 12, borderRadius: 6, padding: 4, bgColor: '#ffffff' } })); this.setData({ markers }); }); }, planRoute(e) { const { startId, endId } = e.detail; request.get('/route/plan', { startId, endId }).then(data => { this.setData({ polyline: [{ points: data.points, color: '#2196F3', width: 4, arrowLine: true }] }); }); } });地图组件用法:
<map style="width: 100%; height: 100vh;" latitude="{{latitude}}" longitude="{{longitude}}" scale="{{scale}}" markers="{{markers}}" polyline="{{polyline}}" show-location ></map>show-location会在地图上显示用户当前位置,但实测需要用户授权。坐标系如果不统一,marker 会偏移,这是地图页最常见的问题。
5.3 AI 对话页面与流式回复处理
AI 对话页用消息列表展示用户和 AI 的对话。
Page({ data: { messages: [], inputValue: '' }, onInput(e) { this.setData({ inputValue: e.detail.value }); }, sendMessage() { const text = this.data.inputValue.trim(); if (!text) return; const messages = [...this.data.messages, { role: 'user', content: text }]; this.setData({ messages, inputValue: '' }); request.post('/ai/chat', { question: text }).then(data => { this.setData({ messages: [...this.data.messages, { role: 'assistant', content: data.reply }] }); // 如果回复里带了 poiList 或路线,可以提示跳转地图 if (data.poiList && data.poiList.length > 0) { this.setData({ suggestPoi: data.poiList[0] }); } }).catch(() => { wx.showToast({ title: 'AI 服务暂不可用', icon: 'none' }); }); } });这里的对话是请求-响应模式,不是流式。毕业设计用非流式已经足够。如果论文想写“流式输出”,需要后端改用 SSE 或 WebSocket,小程序端也要相应处理,复杂度会明显上升,非必需不推荐。
5.4 登录态和请求封装
小程序所有请求统一走request.js,自动携带 token:
// utils/request.js const BASE_URL = 'http://127.0.0.1:8080/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { reject(err); } }); }); } module.exports = { request, get(path, data) { return request(path, 'GET', data); }, post(path, data) { return request(path, 'POST', data); }, BASE_URL };登录一般在app.js的onLaunch中触发:
App({ onLaunch() { wx.login({ success: (res) => { wx.request({ url: 'http://127.0.0.1:8080/api/auth/login', method: 'POST', data: { code: res.code }, success: (resp) => { wx.setStorageSync('token', resp.data.data.token); } }); } }); } });本地调试时,BASE_URL可以使用http://127.0.0.1:8080,但真机预览时手机不能直接用电脑的 localhost,需要改成电脑在局域网中的 IP,比如http://192.168.1.100:8080。上线发布时,则必须换成备案过的 HTTPS 域名。
6. 联调运行与验证
6.1 后端本地启动
后端启动前先确认 MySQL 建好库,application.yml配置正确:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_nav?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl wx: mini-app: appid: 你的appid secret: 你的secret ai: api-key: 你的APIKey base-url: https://api.example.com/v1 model: your-model-name启动命令:
mvn spring-boot:run启动后用 curl 验证接口:
curl "http://localhost:8080/api/poi/search?keyword=图书馆" curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"code":"test_code_from_devtools"}'注意:wx.login得到的 code 只能使用一次,而且有效期很短。用 curl 测试登录接口时,需要从小程序开发者工具里取真实 code。
6.2 小程序真机调试
小程序在开发者工具里能跑通,不一定在真机上能跑通。主要差异在三个方面:
- 网络地址。真机不能访问电脑 localhost,需要改为局域网 IP 或已备案域名。
- 合法域名。开发者工具可以勾选“不校验合法域名”,真机预览时如果后端不是 HTTPS 域名,必须也开启调试模式,否则请求会被拦截。
- 地理位置。真机定位比模拟器准,但需要在
app.json配置permission,并在真机上授权。
推荐流程:先在开发者工具中调通接口,再开“不校验合法域名”用真机预览调试,最后再配置 HTTPS 域名做发布前验证。
6.3 核心场景自查清单
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 登录 | 打开小程序 | 自动获得 token,个人中心显示用户标识 |
| 地图 | 进入地图页 | 所有启用状态的 POI 显示标记 |
| 搜索 | 输入“图书馆” | 返回图书馆 POI,可选中 |
| 路线 | 选择起点和终点 | 地图画出路线,显示距离或时间 |
| AI 问答 | 输入“图书馆几点关门” | 返回知识库中的开放时间 |
| AI 转导航 | 输入“去第一食堂” | 回答中点名第一食堂,附带 POI,可跳转路线 |
| 异常 | AI 接口关闭 | 显示“AI 服务暂不可用”,不白屏 |
| 反馈 | AI 回答下点“有用” | 数据库中 feedback 表新增记录 |
这个清单既是联调检查项,也是答辩现场演示脚本的雏形。
7. 常见问题与排查路径
7.1 错误现象对照表
| 现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 小程序请求显示 url not in domain list | 合法域名未配置 | 开发者工具 Network 面板 | 本地勾选“不校验合法域名”,上线配置 HTTPS 域名 |
| code2session 返回 40013 | appid 或 secret 错误 | 与小程序后台核对 | 重新复制配置 |
| 登录接口拼接 URL 报错 | 参数顺序或编码问题 | 打印完整 URL | 使用 URIBuilder 或 encode 参数 |
| 地图 marker 位置偏移 | 坐标系不一致 | 对比坐标来源 | 统一入库和展示都为 GCJ-02 |
| 后端返回 500 但看不到日志 | 缺少日志配置 | 查看控制台异常堆栈 | 加全局异常处理和 logback |
| AI 返回 model not found | 模型名错误 | 用 curl 手动调用 AI 接口 | 替换为服务商可用模型名 |
| AI 接口超时 | 超时时间太短或网络慢 | 检查 RestTemplate 配置 | 设置 readTimeout 为 30 秒 |
| 真机请求失败而开发者工具正常 | IP 或域名不可达 | 手机浏览器访问后端地址 | 使用局域网 IP 或 HTTPS 域名 |
排查顺序建议:先确认请求是否发出,再确认后端是否收到,再确认数据库查询结果,最后确认返回数据格式是否被前端正确解析。
7.2 排查链路:请求层、服务层、数据层
一个问题从现象倒推根因,一般按三层查。
请求层:打开微信开发者工具的 Network 面板,看请求是否发出、状态码是多少、响应体是什么。常见结果是 404(路径错)、500(后端异常)、403(域名或权限问题)。
服务层:看后端控制台或日志文件。SpringBoot 默认会打印异常堆栈,先找第一行Caused by,通常是真正的根因。如果接口被拦截,还要查拦截器或过滤器是否放行了/api/auth/login。
数据层:如果接口返回空列表,直接在数据库里执行同样的 SQL,确认数据是否存在、状态字段是否为 1、关键词是否匹配。很多“AI 找不到地点”的问题,实际不是模型问题,而是 POI 表没数据。
7.3 AI 接口接入的典型坑
AI 接入有四个高频坑:
- API Key 泄露。不要把 key 写在小程序前端或 GitHub 仓库里,后端用环境变量或配置文件管理。
- 上下文无限增长。每次请求只传当前问题和系统提示词即可,不要反复累积历史消息,否则 token 会越来越大。
- 知识库检索为空时模型乱答。检索不到内容时,要在提示词里明确写“如果知识库中没有相关信息,请直接告知用户未知,不要编造”。
- 结果不可解析。如果让模型返回 JSON 结构,要设置
temperature偏低,并在代码里做异常捕获,解析失败时降级为普通文本展示。
7.4 SpringBoot 版本和依赖冲突
SpringBoot 3.x 项目使用javax.*包名会直接编译失败。常见错误是import javax.servlet.*无法解析。解决办法是改成jakarta.servlet.*,或者把项目降到 SpringBoot 2.7.x。
另一个典型问题是 MyBatis-Plus 与 SpringBoot 3.x 不兼容。MyBatis-Plus 3.5.3 之前的部分版本对 SpringBoot 3.x 支持不完整,会出现Invalid value type for attribute 'factoryBeanObjectType'这样的报错。遇到时优先升级 MyBatis-Plus 版本。
如果本地创建 SpringBoot 项目时一直卡在下载依赖,可以检查 Maven 镜像源是否配置为国内仓库地址,同时确认 Java 版本和 Maven 版本是否匹配。
8. 毕业设计演示、论文与答辩建议
8.1 演示脚本要覆盖的三条业务线
答辩演示不要只讲“系统能登录”,要按业务线讲,每条线都要有操作和结果。
第一条线是地图导航。登录后进入地图页,展示所有楼宇 POI,搜索“图书馆”,选择目的地,系统画出路线。重点说明路线数据来自route_edge表,路径计算使用 Dijkstra 算法。
第二条线是 AI 问答。输入“图书馆几点关门”,AI 基于知识库回答;再输入“我要去第一食堂吃饭”,AI 识别出导航意图并返回第一食堂 POI,点击后跳转路线。这里要突出 AI 与传统表单搜索的区别。
第三条线是数据管理。在管理端新增一个 POI,比如“咖啡厅”,完善名称、坐标、关键词;回到小程序刷新,新 POI 立即显示;再问 AI“校园里哪里能买咖啡”,AI 能检索到新地点。这条线能证明系统不是静态演示,管理员维护数据后 AI 能力随之更新。
8.2 论文里要写清楚的部分
论文在“系统设计”部分,要重点写清楚三块。
一是数据库设计。给出 E-R 图,说明 building、poi、route_edge 三张表如何支撑导航能力,campus_knowledge 表如何支撑 AI 问答。
二是接口设计。列出核心接口表格,包括登录、POI 搜索、路线规划、AI 对话四个接口,写明请求参数、返回结构和处理流程。
三是 AI 接入设计。说明提示词如何构造,知识库如何检索,模型返回如何解析,以及为什么不能让模型凭空回答未知信息。
测试部分的重点不是“系统能跑”,而是“系统在异常情况下如何处理”。至少写清 AI 接口超时、POI 无数据、数据库连接失败这三种场景下的表现。
8.3 答辩高频问题与回复思路
| 问题 | 回复思路 |
|---|---|
| 为什么使用大模型? | 用户意图表达灵活,大模型负责自然语言理解,路径计算仍由后端算法完成,两者分工明确 |
| 大模型接口收费怎么办? | 演示环境可以使用开发额度,也可以设计成模型可配置切换,论文中说明成本控制方案 |
| 如果模型乱答怎么办? | 系统设置知识库兜底和提示词约束,检索不到内容时模型必须回答“未知”,不编造 |
| 路线规划为什么用 Dijkstra? | 校园 POI 规模小,Dijkstra 简单可解释,能保证最短路径;规模扩大后可扩展为 A* |
| 用户量大了怎么办? | POI 列表加缓存,AI 接口异步化,数据库加索引;论文中给出扩展方向即可 |
8.4 发布前检查清单
写论文前和提交前,最后做一次清单检查。
- 数据库建表脚本完整,POI 和知识库有至少 20 条能演示的数据。
- 微信 appid 和 secret 配置正确,登录接口能返回 token。
- AI 接口的 api-key 放后端,没有提交到前端代码或 GitHub。
- 小程序所有请求都走统一 request.js,token 自动附带。
- 地图 marker 坐标与路线 polyline 坐标系一致。
- AI 接口关闭时,小程序显示友好提示,不白屏。
- 核心接口用 curl 或 Postman 跑通,留存测试截图。
- 论文中的表结构和代码与实际项目一致,避免交错。
- 演示至少准备 5 到 10 分钟脚本,覆盖地图、搜索、路线、AI 问答和数据维护五条操作。
校园导航系统的核心并不在于 AI 调用本身,而在于地图数据是否准确、路线计算是否可靠、AI 是否能把用户问题转换成有效的导航动作。把这三层打通,项目就真正完整了。对于初学者,建议先把地图数据和路线接口做到可演示,再接入 AI 对话,最后再考虑流式输出、路线分享这类扩展功能。这个顺序能保证项目在最简形态下已经跑通,后面每一步都是在已有基础上加分。