智能语音控制系统部分架构图

我今天主要总结业务中最核心的接入识别部分,第一次做肯定会遇到一些问题总结给大家参考
新手必看|微信小程序与Spring Boot后端交互全流程(踩坑+解决+完整代码)
作为编程新手,第一次做「小程序+后端」联调时,很容易被各种网络问题、字段匹配问题、环境配置问题卡住。本文以「智能设备语音控制小程序」为例,完整记录从0到1实现小程序与Spring Boot后端交互的全过程——包括环境配置、代码编写、遇到的每一个问题及解决方案,全程迭代式讲解,新手也能跟着一步步复刻。
最新效果如图




真机调试需要关闭防火墙

一、前置准备:明确需求与环境
1. 核心需求
实现一个简单的语音控制小程序:
- 小程序端:录音→调用百度语音转文字→将指令(如“打开客厅灯”)传给后端;
- 后端:解析指令→下发指令到远程设备→后端异步返回指令处理结果→小程序同步设备状态。
2. 必备工具/环境
| 类型 | 工具/版本 | 新手备注 |
|---|---|---|
| 后端 | JDK 8+、Spring Boot 2.7+、IDEA | 确保JDK环境变量配置正确 |
| 前端 | 微信开发者工具、小程序账号 | 无需认证,测试阶段用测试号即可 |
| 网络 | 同一局域网(电脑+手机) | 真机测试必须满足此条件 |
| 第三方 | 百度语音识别API Key/Secret | 免费申请,用于语音转文字 |
二、第一步:后端(Spring Boot)开发(新手友好版)
1. 创建Spring Boot项目
打开IDEA,新建Spring Boot项目,勾选核心依赖:Spring Web(web开发)、Spring AI大模型集成、Lombok(简化代码)。
2. 核心代码编写(迭代1:基础结构)
(1)定义枚举(指令动作/状态)
新手容易忽略枚举的序列化问题,先定义核心枚举:
// 指令动作枚举(打开/关闭)
public enum CommandType {
OPEN, // 打开
CLOSE // 关闭
}
// 指令状态枚举(执行中/成功/失败)
public enum CommandStatus {
EXECUTING, // 执行中
SUCCESS, // 执行成功
FAIL // 执行失败
}
(2)定义交互DTO(前后端数据传输对象)
// 后端接收小程序请求的DTO
@Data
public class CommandProcessRequest {
private String voiceText; // 小程序传的语音指令文本
private String userId; // 用户ID(测试用固定值)
private String aiModelType;// AI模型类型(测试用baidu)
}
// 后端返回给小程序的DTO
@Data
public class CommandProcessResponse {
private String commandId; // 指令ID
private String deviceCode; // 设备编码(如客厅灯=LT001)
private String deviceName; // 设备名称(如客厅主灯)
private CommandType actionType; // 动作类型(OPEN/CLOSE)
private LocalDateTime executedAt; // 执行时间
private CommandStatus status; // 指令状态
}
(3)定义统一返回结果(核心!新手必写)
前后端交互必须有统一的返回格式,否则小程序解析会混乱:
// 统一返回结果类
@Data
public class Result<T> {
private int code; // 状态码(200=成功,500=失败)
private String msg; // 提示信息(success/fail)
private T data; // 业务数据
// 成功返回方法
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success"); // 注意:小写,后续小程序要匹配
result.setData(data);
return result;
}
// 失败返回方法
public static <T> Result<T> fail(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
result.setData(null);
return result;
}
}
(4)编写Controller(接口入口)
@RestController
@RequestMapping("/api/commands")
@RequiredArgsConstructor
public class CommandController {
// 模拟业务服务(新手可先注释,后续替换为真实逻辑)
// private final CommandProcessApplicationService commandProcessApplicationService;
/**
* 小程序调用的核心接口:处理语音指令
*/
@PostMapping("/process")
public Result<CommandProcessResponse> processCommand(@RequestBody CommandProcessRequest request) {
// 1. 参数校验(新手必做,避免空指针)
if (request.getVoiceText() == null || request.getUserId() == null) {
return Result.fail("语音文本和用户ID不能为空");
}
// 2. 模拟后端处理(真实场景替换为解析指令、调用设备逻辑)
CommandProcessResponse response = new CommandProcessResponse();
response.setCommandId(UUID.randomUUID().toString().replace("-", "")); // 随机指令ID
response.setDeviceCode("LT001"); // 客厅灯编码
response.setDeviceName("客厅主灯");
response.setActionType(CommandType.OPEN); // 模拟解析为“打开”
response.setExecutedAt(LocalDateTime.now());
response.setStatus(CommandStatus.SUCCESS); // 模拟执行成功
// 3. 返回统一结果
return Result.success(response);
}
}
(5)跨域配置(新手最容易忘的坑!)
小程序调用后端会触发跨域拦截,必须添加跨域配置:
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 开发阶段允许所有来源(上线后改为指定域名)
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有请求方法(GET/POST等)
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // 所有接口生效
return new CorsFilter(source);
}
}
(6)配置枚举序列化(避免返回对象)
Spring Boot默认会把枚举序列化为{name:"OPEN", ordinal:0},小程序无法解析,需配置返回字符串:
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return new Jackson2ObjectMapperBuilder()
.modules(new SimpleModule()
// 所有枚举序列化时返回字符串(如OPEN/CLOSE)
.addSerializer(Enum.class, new ToStringSerializer()));
}
}
3. 启动后端并测试
启动Spring Boot项目,默认端口8080(本文用8889,需在application.yml修改):
server:
port: 8889 # 后端端口
启动后,在电脑浏览器访问http://localhost:8889,若显示“Whitelabel Error Page”(无404/无法访问),说明后端启动成功。
三、第二步:小程序前端开发(迭代1:基础结构)
1. 创建小程序项目
打开微信开发者工具,新建“不使用云服务”的小程序项目,选择“JavaScript-基础模板”。
2. 核心配置与代码
(1)页面结构(pages/voiceControl/voiceControl.wxml)
<view class="container">
<!-- 录音按钮 -->
<button bindtap="startVoiceControl" wx:if="{{!isRecording}}">按住录音</button>
<button bindtap="startVoiceControl" wx:else>停止录音</button>
<!-- 指令输入框(备用,测试用) -->
<input placeholder="手动输入指令(如打开客厅灯)" bindinput="inputVoiceText" value="{{voiceText}}"></input>
<button bindtap="executeVoiceCommand">执行指令</button>
<!-- 结果提示 -->
<view class="result" wx:if="{{showResult}}">{{resultText}}</view>
<!-- 设备状态展示 -->
<view class="device">
<text>客厅主灯:</text>
<text wx:if="{{deviceList.livingRoomLight.switch}}">打开</text>
<text wx:else>关闭</text>
</view>
</view>
(2)核心逻辑(pages/voiceControl/voiceControl.js)
Page({
data: {
voiceText: '',
// 设备列表:补充code字段匹配后端设备编码(LT001/KT001)
deviceList: {
livingRoomLight: {
status: true,
switch: false,
code: 'LT001', // 匹配后端客厅灯编码
name: '客厅主灯'
},
bedroomAir: {
status: true,
switch: true,
code: 'KT001', // 匹配后端空调编码
name: '卧室空调'
}
},
showResult: false,
resultText: '',
isRecording: false,
recorderManager: null,
tempFilePath: '',
userId: 'wx_user_001', // 测试用固定用户ID
baseApiUrl: 'http://192.168.1.3:8889', // 替换为你的电脑局域网IP
commandId: ''
},
onLoad() {
// 初始化录音管理器
this.setData({
recorderManager: wx.getRecorderManager()
});
this.initRecorder();
// 启动时检测后端连接(方便排查)
this.checkBackendConnection();
},
// 检测后端连接是否可用
checkBackendConnection() {
const that = this;
wx.request({
url: `${that.data.baseApiUrl}/api/commands/process`,
method: 'POST',
header: {
'Content-Type': 'application/json; charset=utf-8'
},
data: { voiceText: '测试', userId: 'test', aiModelType: 'baidu' },
timeout: 3000,
success() {
that.showResultToast('后端服务连接正常');
},
fail(err) {
console.error('后端连接检测失败:', err);
that.showResultToast('⚠️ 后端连接失败,请检查:\n1.后端是否启动\n2.IP/端口是否正确\n3.跨域配置是否生效');
}
});
},
// 初始化录音
initRecorder() {
const recorderManager = this.data.recorderManager;
const that = this;
// 录音开始回调
recorderManager.onStart(() => {
that.setData({ isRecording: true, resultText: '正在录音...' });
});
// 录音停止回调(获取临时文件路径)
recorderManager.onStop((res) => {
that.setData({
isRecording: false,
tempFilePath: res.tempFilePath
});
// 录音停止后,调用百度语音转文字
that.voiceToText(res.tempFilePath);
});
// 录音错误回调
recorderManager.onError((err) => {
that.setData({ isRecording: false });
that.showResultToast(`录音失败:${err.errMsg}`);
});
},
// 开始/停止录音
startVoiceControl() {
const isRecording = this.data.isRecording;
const recorderManager = this.data.recorderManager;
if (isRecording) {
recorderManager.stop(); // 停止录音
} else {
// 申请录音权限
wx.authorize({
scope: 'scope.record',
success: () => {
// 配置录音参数(百度语音识别要求wav格式)
recorderManager.start({
format: 'wav',
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 64000,
duration: 6000 // 最长录音6秒
});
},
fail: () => {
wx.showModal({
title: '需要录音权限',
content: '请前往设置开启麦克风权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) wx.openSetting();
}
});
}
});
}
},
// 手动输入指令
inputVoiceText(e) {
this.setData({
voiceText: e.detail.value
});
},
// 百度语音转文字
voiceToText(tempFilePath) {
const that = this;
// 替换为你的百度API Key/Secret
const apiKey = '你的百度API Key';
const secretKey = '你的百度Secret Key';
// 第一步:获取百度Access Token
wx.request({
url: 'https://aip.baidubce.com/oauth/2.0/token',
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
grant_type: 'client_credentials',
client_id: apiKey,
client_secret: secretKey
},
success: (tokenRes) => {
if (tokenRes.statusCode !== 200 || tokenRes.data.error) {
const errMsg = tokenRes.data.error_description || 'Token获取失败';
that.showResultToast(errMsg);
return;
}
const accessToken = tokenRes.data.access_token;
// 第二步:读取录音文件并转base64
const fs = wx.getFileSystemManager();
fs.readFile({
filePath: tempFilePath,
encoding: 'binary',
success: (binaryRes) => {
const fileBinary = binaryRes.data;
const fileLen = fileBinary.length;
// 二进制转base64(百度语音识别要求)
const speechBase64 = wx.arrayBufferToBase64(new Uint8Array(fileBinary.split('').map(char => char.charCodeAt(0))));
// 第三步:调用百度语音识别接口
wx.request({
url: 'https://vop.baidu.com/server_api',
method: 'POST',
header: {
'Content-Type': 'application/json; charset=utf-8'
},
data: JSON.stringify({
"format": "wav",
"rate": 16000,
"channel": 1,
"cuid": "wx-miniprogram-123456789",
"token": accessToken,
"dev_pid": 1537, // 普通话识别
"len": fileLen,
"speech": speechBase64
}),
success: (transRes) => {
console.log('百度语音识别结果:', transRes.data);
if (transRes.data.err_no === 0 && transRes.data.result) {
const text = transRes.data.result[0];
that.setData({ voiceText: text });
that.showResultToast(`识别成功:${text}`);
// 识别成功后调用后端接口
that.callBackendCommandApi(text);
} else {
const errMsg = transRes.data.err_msg || '识别失败';
that.showResultToast(`识别失败:${errMsg}(错误码:${transRes.data.err_no})`);
}
},
fail: (err) => {
console.error('语音识别请求失败:', err);
that.showResultToast(`识别请求失败:${err.errMsg}`);
}
});
},
fail: (fileErr) => {
console.error('读取录音文件失败:', fileErr);
that.showResultToast(`读取录音失败:${fileErr.errMsg}`);
}
});
},
fail: (tokenErr) => {
that.showResultToast(`获取Token失败:${tokenErr.errMsg}`);
}
});
},
// 调用后端接口处理指令
callBackendCommandApi(voiceText) {
const that = this;
const { userId, baseApiUrl } = this.data;
// 参数校验
if (!userId) {
that.showResultToast('用户未登录,请先登录');
return;
}
if (!voiceText.trim()) {
that.showResultToast('指令内容不能为空');
return;
}
wx.showLoading({ title: '指令处理中...' });
// 调用后端接口
wx.request({
url: `${baseApiUrl}/api/commands/process`,
method: 'POST',
header: {
'Content-Type': 'application/json; charset=utf-8'
},
data: {
voiceText: voiceText,
userId: userId,
aiModelType: 'baidu'
},
timeout: 5000,
success: (res) => {
wx.hideLoading();
console.log('后端返回结果:', JSON.stringify(res.data));
// 匹配后端统一返回格式(code=200且msg=success为成功)
if (res.data && res.data.code === 200 && res.data.msg === 'success') {
const commandResponse = res.data.data;
that.setData({ commandId: commandResponse.commandId });
// 同步设备状态
that.syncDeviceStatusFromBackend(commandResponse);
} else {
const errMsg = res.data?.msg || '指令处理失败';
that.showResultToast(`指令处理失败:${errMsg}`);
}
},
fail: (err) => {
wx.hideLoading();
console.error('调用后端接口失败:', err);
// 精准提示错误原因(新手友好)
let errTips = '连接后端失败:';
if (err.errMsg.includes('ERR_CONNECTION_REFUSED')) {
errTips += '\n1. 后端服务未启动(8889端口)';
errTips += '\n2. 确认IP是电脑局域网IP(192.168.1.3)';
errTips += '\n3. 关闭电脑防火墙/检查跨域配置';
} else if (err.errMsg.includes('timeout')) {
errTips += '请求超时,请检查网络';
} else {
errTips += err.errMsg;
}
that.showResultToast(errTips);
}
});
},
// 同步后端返回的设备状态
syncDeviceStatusFromBackend(commandResponse) {
const that = this;
const { deviceCode, actionType, status, deviceName } = commandResponse;
// 匹配后端设备编码与本地设备(LT001 → livingRoomLight)
let deviceKey = '';
const deviceList = that.data.deviceList;
for (const key in deviceList) {
if (deviceList[key].code === deviceCode) {
deviceKey = key;
break;
}
}
// 未匹配到设备
if (!deviceKey) {
that.showResultToast(`未识别设备:${deviceName || deviceCode}`);
return;
}
// 解析动作(OPEN=打开,CLOSE=关闭)
const isOpen = actionType === 'OPEN';
const actionText = isOpen ? '打开' : '关闭';
// 解析状态
let statusText = '';
switch (status) {
case 'EXECUTING':
statusText = '(执行中)';
break;
case 'SUCCESS':
statusText = '(执行成功)';
// 执行成功才更新本地设备状态
const newDeviceList = { ...deviceList };
newDeviceList[deviceKey].switch = isOpen;
that.setData({ deviceList: newDeviceList });
break;
case 'FAIL':
statusText = '(执行失败)';
break;
default:
statusText = '';
}
// 提示结果
that.showResultToast(`${deviceName || deviceList[deviceKey].name}${actionText}${statusText}`);
},
// 手动执行指令(测试用)
executeVoiceCommand() {
const { voiceText } = this.data;
if (!voiceText.trim()) {
this.showResultToast('请输入/说出控制指令');
return;
}
this.callBackendCommandApi(voiceText);
},
// 结果提示框
showResultToast(text) {
this.setData({
showResult: true,
resultText: text
});
// 3秒后隐藏提示
setTimeout(() => {
this.setData({
showResult: false
});
}, 3000);
}
});
(3)关闭小程序域名校验(开发阶段)
新手容易被“合法域名”拦截,开发阶段临时关闭:
- 打开微信开发者工具 → 点击右上角「详情」;
- 在「本地设置」中,勾选「不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书」。
四、迭代2:联调踩坑与解决(核心!新手必看)
写完基础代码后,联调时会遇到各种问题,以下是我遇到的所有问题及解决方案,按“出现频率”排序:
坑1:小程序提示“连接后端失败:ERR_CONNECTION_REFUSED”
现象
小程序控制台打印:errMsg: "request:fail errcode:-102 cronet_error_code:-102 error_msg:net::ERR_CONNECTION_REFUSED"
原因
- 后端服务未启动;
- 后端端口被占用(8889);
- 小程序
baseApiUrl用了localhost(小程序模拟器/真机的localhost指向自身,而非电脑); - 电脑防火墙拦截了8889端口。
解决步骤
- 确认后端已启动:IDEA中启动Spring Boot项目,确保无报错;
- 检查端口占用:
- Windows:CMD输入
netstat -ano | findstr :8889,若有结果,关闭占用进程; - Mac/Linux:终端输入
lsof -i :8889,kill占用进程;
- Windows:CMD输入
- 替换
baseApiUrl:将localhost改为电脑局域网IP(如192.168.1.3); - 关闭电脑防火墙:
- Windows:设置→更新和安全→Windows安全中心→防火墙和网络保护→关闭所有网络的防火墙;
- Mac:系统设置→网络→防火墙→关闭。
坑2:小程序解析后端结果时,始终提示“指令处理失败”
现象
后端日志显示处理成功,但小程序提示“指令处理失败”。
原因
- 小程序解析逻辑匹配
success布尔值,但后端返回的是code=200+msg=success; msg大小写不匹配(后端返回success,小程序判断SUCCESS)。
解决步骤
- 修正小程序解析逻辑:将
if (res.data.success)改为if (res.data && res.data.code === 200 && res.data.msg === 'success'); - 统一
msg大小写:确保后端返回msg=success(小写),小程序也匹配小写。
坑3:真机测试提示“请求超时/无法连接”
现象
模拟器中能正常访问后端,但真机扫码后提示“连接后端失败:请求超时”。
原因
- 真机和电脑未连同一Wi-Fi;
- 电脑用了有线网络,小程序
baseApiUrl填的是有线IP(真机连无线,无法访问); - 路由器开启了“设备隔离”,阻止设备间通信。
解决步骤
- 确保真机和电脑连同一个无线Wi-Fi(电脑切换为无线,不要用有线);
- 重新获取电脑的无线局域网IP(而非有线IP),替换
baseApiUrl; - 关闭电脑防火墙(尤其是“公用网络防火墙”);
- 用手机浏览器测试:输入
http://电脑无线IP:8889,能访问则网络正常。
坑4:设备状态无法同步(客厅灯开关不变)
现象
后端返回deviceCode=LT001,但小程序的客厅灯开关始终为“关闭”。
原因
小程序本地设备用livingRoomLight标识,后端用LT001,无映射关系。
解决步骤
在小程序deviceList中补充code字段,匹配后端设备编码:
deviceList: {
livingRoomLight: {
status: true,
switch: false,
code: 'LT001', // 匹配后端LT001
name: '客厅主灯'
}
}
坑5:后端返回的枚举是对象(而非字符串)
现象
小程序控制台打印actionType: {name:"OPEN", ordinal:0},无法解析。
原因
Spring Boot默认将枚举序列化为对象,小程序需要字符串。
解决步骤
添加JacksonConfig配置类(见后端开发部分),让枚举序列化返回字符串。
坑6:百度语音转文字失败(err_no=500)
现象
小程序提示“识别失败:server error(错误码:500)”。
原因
- API Key/Secret错误;
- 录音格式不匹配(百度要求wav/16000采样率);
- 录音文件转base64错误。
解决步骤
- 核对百度API Key/Secret(确保未填错);
- 确认录音参数:
format: 'wav'、sampleRate: 16000; - 检查base64转换逻辑(代码中已封装,直接复用即可)。
五、迭代3:最终验证
完成所有坑的修复后,验证流程:
- 启动后端服务(IDEA);
- 小程序模拟器中输入“打开客厅灯”→ 点击“执行指令”;
- 控制台打印后端返回:
{"code":200,"msg":"success","data":{"commandId":"xxx","deviceCode":"LT001","deviceName":"客厅主灯","actionType":"OPEN","executedAt":"2025-12-21T12:41:14.352356900","status":"SUCCESS"}} - 小程序提示“客厅主灯打开(执行成功)”,客厅灯开关变为“打开”;
- 真机扫码测试:重复步骤2-4,功能正常。
六、新手建议(避坑总结)
- 网络优先:联调前先确保“电脑+真机同Wi-Fi”“关闭防火墙”“替换局域网IP”,80%的问题是网络导致;
- 日志为王:遇到问题先看控制台日志(小程序→Network/Console,后端→IDEA控制台),日志会明确提示错误原因;
- 统一格式:前后端约定好统一的返回格式(如
code+msg+data),避免字段匹配混乱; - 分步测试:先测试后端接口(Postman),再测试小程序调用后端,最后测试完整流程,分步定位问题;
- 测试完成后恢复配置:开启电脑防火墙、取消小程序域名校验勾选,避免安全风险。
七、完整代码仓库(可选)
为方便新手复用,我将完整的后端+前端代码整理到了Gitee(新手友好,带注释):
(注:“代码已在文中完整贴出,直接复制即可”)
总结
小程序与后端交互的核心是“网络连通+数据格式匹配”:网络问题靠“同Wi-Fi+局域网IP+关闭防火墙”解决,数据问题靠“统一返回格式+字段映射”解决。作为新手,不用怕踩坑——每一个坑都是对“网络通信”“数据交互”的理解加深。跟着本文的迭代步骤,从基础代码到踩坑解决,你能完整掌握小程序与后端交互的核心逻辑。
1947

被折叠的 条评论
为什么被折叠?



