简介:PetMatch 是一份面向全栈开发学习者的 React + Spring Boot 宠物配对应用实战项目,适合希望打通前端交互与后端服务、积累完整 Web 开发经验的中级开发者。项目以 JavaScript(React)构建组件化界面,结合 Spring Java 后端提供 RESTful 接口,覆盖 JSX 语法、组件状态与属性、生命周期方法、React Router 路由,以及 Spring Boot 自动配置、JPA 与 Hibernate 数据持久化、Spring Security 安全控制等核心知识点,帮助读者理解前后端分离架构下的协作方式。资源包共 69 个文件,约 2.72MB,包含 13 个 js、11 个 java、11 个 css 源码文件,以及 json 配置、html 页面、properties 配置、图片素材与 Maven 构建脚本等,目录结构清晰,便于按模块查阅。目前已有 111 人学习,可作为课程设计或毕业项目的参考方案,帮助读者掌握从界面组件到数据库操作的完整开发链路。
1. 从 petmatch 说起:一个 TTS Capstone 到底在做什么
第一次看到「petmatch:使用React和SpringJava的TTS Capstone」这个标题,很多人会愣一下:petmatch 听起来像个宠物社交或领养匹配的应用,TTS 又是语音合成,SpringJava 这个写法本身就不太标准(通常写作 Spring Boot / Java),三者凑在一起到底想干嘛?我先把结论摆出来:这是一个典型的全栈毕业设计型项目,前端用 React 做交互界面,后端用 Java 生态(大概率是 Spring Boot)提供接口,核心功能是把文本转成语音(Text-to-Speech)播放出来,业务外壳套在「宠物匹配」这个场景上。它解决的不是算法难题,而是「一个能跑通、能演示、能讲清楚数据怎么从输入流到扬声器」的完整链路问题。适合谁?适合正在做课程设计、Capstone 或想补齐全栈 + 第三方 AI 能力接入经验的同学。下面我按真实落地顺序拆开讲,不玩虚的。
2. 技术选型:为什么是 React + Spring Boot + TTS,而不是别的组合
2.1 前端为什么锁定 React 而不是模板引擎
宠物匹配这类界面天然是「列表 + 卡片 + 实时状态」的形态,用户点一只宠物、看详情、触发语音介绍,状态变化频繁。用 Thymeleaf 或 JSP 这类服务端渲染,每次交互都要整页刷新,体验割裂,而且语音播放是浏览器端行为,服务端模板很难优雅控制音频生命周期。React 的组件状态和useEffect能很自然地把「请求文本 → 拿到音频 → 播放 → 清理」这条链管起来。另一个现实原因是生态:音频播放、加载态、错误提示都有现成模式,Capstone 周期短,不该把时间耗在造轮子上。
2.2 后端为什么用 Spring Boot 承载 TTS 调用
TTS 的密钥绝不能放前端,这是硬性安全底线。Spring Boot 在这里的角色是「代理 + 编排」:接收前端文本,调用 TTS 服务,把音频流或音频地址返回。选 Java 生态的另一个理由是类型稳定,Capstone 答辩时接口契约清晰,@RestController配合 DTO 能让评审一眼看懂数据流。如果换成 Node,其实也能做,但标题既然写了 SpringJava,就按 Java 路线走,别中途换栈给自己找麻烦。
2.3 TTS 能力从哪来:三条常见路线对比
| 路线 | 接入方式 | 优点 | 坑点 |
|---|---|---|---|
| 云厂商 TTS API | HTTP 调用,返回音频二进制或 URL | 音质好、多音色 | 有额度限制、需处理鉴权 |
| 本地开源引擎 | 部署服务,内网调用 | 无外部依赖 | 资源占用高、部署重 |
| 浏览器 Web Speech API | 前端直接调用 | 零后端成本 | 音色受浏览器限制、不可控 |
Capstone 场景我一般推荐云 API 路线,因为演示效果稳定,且能体现「后端代理」这一层设计。下面所有代码都按这条路线写,但把调用层抽象出来,换引擎只改一个类。
2.4 最小可运行骨架的搭建步骤
先建后端。用 Spring Initializr 生成项目,依赖勾选 Spring Web 和 Lombok 即可,TTS 的 HTTP 调用用 Java 自带的HttpClient,避免额外引入重型客户端。
# 生成后端骨架,groupId 和 artifactId 按自己项目改 curl https://start.spring.io/starter.zip \ -d dependencies=web,lombok \ -d type=maven-project \ -d language=java \ -d bootVersion=3.2.0 \ -d groupId=com.example \ -d artifactId=petmatch \ -o petmatch.zip unzip petmatch.zip -d petmatch-backend这段命令的作用是拉取一个标准 Spring Boot 工程,省去手写pom.xml的功夫。bootVersion按你本地 JDK 版本选,JDK 17 配 3.x 系列最稳。生成后先跑一次mvn spring-boot:run,确认能起来再动业务代码,这是血泪经验——很多人一上来就写业务,结果启动失败分不清是环境还是代码问题。
前端用 Vite 起 React 工程,比 CRA 快得多:
npm create vite@latest petmatch-frontend -- --template react cd petmatch-frontend npm install npm install axiosaxios用来调后端接口。到这里前后端骨架都有了,下一章开始接 TTS。
3. 把 TTS 接进 Spring Boot:接口设计与音频返回方式
3.1 接口该返回音频流还是音频 URL
这是第一个必须想清楚的设计点。返回二进制流(byte[]或StreamingResponseBody)的好处是前端拿到就能播,不依赖外部存储;坏处是每次请求都重新合成,重复文本浪费额度。返回 URL 则需要先把音频存到对象存储或本地磁盘,多一步落盘,但可缓存。Capstone 演示场景我建议先用二进制流,简单直接,等要优化再加缓存层。下面按二进制流实现。
3.2 后端 TTS 服务的核心代码
@Service public class TtsService { // 从环境变量读取,绝不硬编码在代码里 private final String apiKey = System.getenv("TTS_API_KEY"); private final String endpoint = System.getenv("TTS_ENDPOINT"); private final HttpClient httpClient = HttpClient.newHttpClient(); /** * 把文本合成音频,返回字节数组 * @param text 待合成文本,长度建议不超过 200 字 * @param voice 音色标识,不同厂商取值不同 */ public byte[] synthesize(String text, String voice) throws Exception { if (text == null || text.isBlank()) { throw new IllegalArgumentException("文本不能为空"); } // 构造请求体,字段名按你选用的 TTS 厂商文档调整 String body = """ { "text": "%s", "voice": "%s", "format": "mp3" } """.formatted(text.replace("\"", "\\\""), voice); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<byte[]> response = httpClient.send( request, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) { // 把状态码带出来,方便排查是鉴权还是额度问题 throw new RuntimeException("TTS 调用失败,状态码:" + response.statusCode()); } return response.body(); } }逻辑说明:synthesize做了三件事——校验输入、拼装请求、检查响应。参数上,text做了引号转义,防止 JSON 拼接被特殊字符破坏,这是手写 JSON 的常见翻车点;voice交给调用方传,方便前端做音色切换。apiKey和endpoint走环境变量,答辩演示时也不会把密钥暴露在代码里。
3.3 暴露给前端的 Controller
@RestController @RequestMapping("/api/tts") public class TtsController { private final TtsService ttsService; public TtsController(TtsService ttsService) { this.ttsService = ttsService; } @PostMapping(value = "/speak", produces = "audio/mpeg") public ResponseEntity<byte[]> speak(@RequestBody SpeakRequest req) { try { byte[] audio = ttsService.synthesize(req.getText(), req.getVoice()); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("audio/mpeg")) .body(audio); } catch (Exception e) { // 出错时返回 500,前端据此提示 return ResponseEntity.status(500).build(); } } }produces = "audio/mpeg"是关键,告诉浏览器这是音频而不是 JSON,否则前端blob处理会出玄学问题。SpeakRequest是个简单 DTO,含text和voice两个字段,用 Lombok 的@Data即可。
3.4 前端如何播放返回的音频
import axios from 'axios'; // 触发语音播放,text 来自宠物详情 async function playPetVoice(text, voice = 'default') { try { const res = await axios.post( '/api/tts/speak', { text, voice }, { responseType: 'blob' } // 必须声明,否则音频被当文本解析 ); const url = URL.createObjectURL(res.data); const audio = new Audio(url); audio.play(); // 播放结束后释放内存,避免多次播放累积 audio.onended = () => URL.revokeObjectURL(url); } catch (e) { console.error('语音播放失败', e); } }responseType: 'blob'是必须的,漏了它 axios 会按 JSON 解析二进制,直接报错。URL.createObjectURL生成临时地址,onended里释放,这是防止内存泄漏的标准做法。到这一步,一条「点击宠物 → 后端合成 → 前端播放」的链路就通了。
4. 避坑与排查:TTS 全栈项目最容易翻车的五个点
4.1 音频能拿到但播放没声音
现象:接口返回 200,前端也拿到 blob,但扬声器没动静。原因通常是Content-Type不对,浏览器把音频当未知类型拒绝播放。解决:后端produces和ResponseEntity的contentType都要设成audio/mpeg,两处一致。排查时打开浏览器开发者工具的 Network 面板,看响应头里的Content-Type到底是什么。
4.2 中文文本合成出来是乱码或报错
现象:英文正常,中文一调就 400 或返回空。原因多半是请求体编码问题,手写 JSON 拼接时没指定 UTF-8。解决:HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)显式指定编码,别依赖默认值。另外确认 TTS 厂商是否支持中文音色,有些默认音色只支持英文。
4.3 密钥泄露到前端或 Git 仓库
现象:代码提交后密钥出现在仓库历史里。原因:图省事把 key 写进application.yml或前端.env。解决:后端一律走环境变量,.gitignore里加上本地配置文件。已经提交的,立刻去厂商后台轮换密钥,别想着删 commit 就完事,历史里还在。
4.4 长文本合成超时
现象:短句正常,超过几百字就超时或返回 500。原因:TTS 接口对单次文本长度有限制,且合成耗时随长度线性增长。解决:后端做分句,按标点切成不超过 200 字的片段,逐段合成后拼接音频,或者前端分次请求。分句逻辑放在TtsService里,别让前端操心。
4.5 跨域请求被浏览器拦截
现象:前端本地 5173 端口调后端 8080,控制台报 CORS 错误。原因:前后端不同源。解决:后端加全局 CORS 配置,开发阶段允许本地来源,上线时收紧到具体域名。别用*一把梭,带鉴权的接口配*会出问题。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") // 按实际前端地址改 .allowedMethods("GET", "POST"); } }5. 让 petmatch 更像一个完整作品:缓存、音色切换与演示技巧
5.1 加一层文本哈希缓存,省额度也提速
演示时同一只宠物的介绍会被反复播放,每次都调 TTS 既慢又费额度。做法是在TtsService里加一个ConcurrentHashMap,key 用「文本 + 音色」的哈希,value 存音频字节。命中直接返回,未命中再调接口。Capstone 阶段不用上 Redis,内存缓存足够,但要在答辩时说明「生产环境应换成带过期策略的分布式缓存」,这句话能体现你的工程判断。
private final Map<String, byte[]> cache = new ConcurrentHashMap<>(); public byte[] synthesizeWithCache(String text, String voice) throws Exception { String key = Integer.toHexString((text + "|" + voice).hashCode()); byte[] cached = cache.get(key); if (cached != null) { return cached; // 命中缓存,直接返回 } byte[] audio = synthesize(text, voice); cache.put(key, audio); return audio; }参数说明:用hashCode做 key 简单但有碰撞概率,Capstone 够用;要严谨就换 SHA-256。缓存没有过期策略,长时间运行会吃内存,演示场景可接受,但心里要有数。
5.2 音色切换的前后端配合
前端加一个下拉框,选项对应后端支持的音色标识,选中后作为voice参数传给/api/tts/speak。后端不用改,因为voice本来就是透传的。这里有个细节:不同音色对同一段文本的合成时长不同,前端播放时要禁用重复点击,否则会叠音。用一个isPlaying状态控制按钮的disabled属性即可。
5.3 答辩演示的三个实用技巧
第一,提前把网络请求的响应时间测出来,演示时留足等待,别在台上干等尴尬。第二,准备一段离线兜底音频,万一现场网络抖动,直接播本地文件,保证演示不中断。第三,把「文本 → 后端 → TTS → 音频 → 播放」这条链路画在一页 PPT 上,评审最想看的就是数据流,而不是你用了多少花哨库。
5.4 验证链路是否真的通了
别只看页面能响,要做分层验证。后端层:用curl直接打接口,确认返回的是二进制且状态码 200。
curl -X POST http://localhost:8080/api/tts/speak \ -H "Content-Type: application/json" \ -d '{"text":"这是一只温顺的猫","voice":"default"}' \ --output test.mp3 -w "状态码:%{http_code} 大小:%{size_download}\n"如果test.mp3能正常播放,说明后端链路没问题,剩下的问题一定在前端。这种「先隔离后端再查前端」的习惯,能帮你省下大量瞎猜的时间。我自己做这类项目时,习惯先把后端用 curl 跑通再写前端,翻车次数少了一大半。希望帮到你。
本文还有配套的精品资源,点击获取