基于 Spring Boot + OkHttp 实现的百度小度 DuerOS API 集成项目,支持OAuth认证和7种推送模板类型。采用DTO设计模式,提供清晰、类型安全的API接口。
- ✅ OAuth 2.0 认证(获取/刷新Access Token)
- ✅ 7种推送模板类型完整实现
- ✅ DTO设计模式 - 请求响应全部对象化
- ✅ 统一响应格式 - Result统一封装
- ✅ 参数校验 - JSR-303注解验证
- ✅ 完整的异常处理和日志记录
- ✅ 代码复用性高,易于维护
- ✅ 完整测试套件 - JUnit 单元测试 + 集成测试
- ✅ 便捷测试工具 - Shell/Batch 脚本 + Postman 集合
编辑 src/main/resources/application.yml:
xiaodu:
api:
base-url: https://dueros.baidu.com/business
app-id: YOUR_APP_ID # 替换为你的 APP_ID
app-secret: YOUR_APP_SECRET # 替换为你的 APP_SECRET# 构建项目
mvn clean package
# 运行项目
mvn spring-boot:run项目提供了完整的测试用例,包括 OAuth 认证和所有 7 种推送模板的测试。
注意: 运行测试前,请修改测试类中的 TEST_CUID 为您的真实设备 ID:
// 位置:src/test/java/com/xiaodu/demo/service/XiaoduPushServiceTest.java
private static final String TEST_CUID = "your-device-cuid-here";运行所有测试:
mvn test运行单个测试:
# 测试 OAuth 服务
mvn test -Dtest=XiaoduOAuthServiceTest
# 测试推送服务
mvn test -Dtest=XiaoduPushServiceTest测试覆盖:
- ✅ OAuth 认证测试(获取/刷新 Token)
- ✅ 纯文本推送测试
- ✅ 全图片推送测试
- ✅ 左图右文推送测试
- ✅ 上图下文推送测试
- ✅ 音频推送测试
- ✅ 视频推送测试
- ✅ SSML 播报推送测试
项目提供了便捷的测试脚本,可一键测试所有接口:
Linux/Mac:
# 赋予执行权限
chmod +x test-api.sh
# 运行测试(替换为您的设备 CUID)
./test-api.sh your-device-cuidWindows:
# 运行测试(替换为您的设备 CUID)
test-api.bat your-device-cuid项目提供了 Postman 集合文件 Xiaodu-API.postman_collection.json,包含所有接口:
- 导入 Postman 集合
- 设置环境变量:
base_url: http://localhost:8080device_cuid: 您的设备 CUID
- 执行 "获取 Access Token" 请求
- 将返回的
accessToken设置到环境变量access_token - 测试其他推送接口
所有接口统一返回Result<T>对象:
{
"success": true,
"errno": null,
"message": "操作成功",
"data": {
// 具体业务数据
}
}curl -X GET http://localhost:8080/api/xiaodu/token响应示例:
{
"success": true,
"message": "操作成功",
"data": {
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"expiresIn": 2592000,
"deadline": 1543393183,
"refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}
}所有推送接口都使用 POST + JSON Body 方式,并在Header中携带Authorization。
接口: POST /api/xiaodu/push/text
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/text \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"title": "托尔斯泰格言",
"content": "托尔斯泰 - 理想的书籍,是智慧的钥匙"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| title | String | 是 | 推送标题 |
| content | String | 是 | 推送内容 |
响应示例:
{
"success": true,
"message": "推送成功",
"data": {
"result": "success",
"msgId": "1234567890"
}
}接口: POST /api/xiaodu/push/allImage
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/allImage \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"imageSrc": "https://example.com/image.jpg",
"imageThumb": "https://example.com/thumb.jpg"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| imageSrc | String | 是 | 图片地址 |
| imageThumb | String | 是 | 图片缩略图地址 |
接口: POST /api/xiaodu/push/textRight
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/textRight \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"title": "托尔斯泰格言",
"content": "托尔斯泰 - 理想的书籍,是智慧的钥匙",
"image": "https://example.com/image.jpg"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| title | String | 是 | 推送标题 |
| content | String | 是 | 推送内容 |
| image | String | 是 | 图片地址 |
接口: POST /api/xiaodu/push/textBottom
请求参数同左图右文,使用相同的请求格式。
接口: POST /api/xiaodu/push/audio
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/audio \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"streamUrl": "https://example.com/audio.mp3",
"title": "告白气球",
"titleSubtext1": "周杰伦",
"titleSubtext2": "周杰伦的床边故事",
"artSrc": "https://example.com/cover.jpg"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| streamUrl | String | 是 | 音频流地址 |
| title | String | 否 | 音频标题 |
| titleSubtext1 | String | 否 | 子标题1(歌手名) |
| titleSubtext2 | String | 否 | 子标题2(专辑名) |
| artSrc | String | 否 | 封面图片 |
接口: POST /api/xiaodu/push/video
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/video \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"streamUrl": "https://example.com/video.mp4"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| streamUrl | String | 是 | 视频流地址 |
接口: POST /api/xiaodu/push/ssml
请求示例:
curl -X POST \
http://localhost:8080/api/xiaodu/push/ssml \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"cuid": "MA718CNBFCL010122",
"title": "托尔斯泰格言",
"content": "托尔斯泰 - 理想的书籍,是智慧的钥匙",
"outputSpeech": "<speak>这句话之后会有3秒钟的停顿<silence time=\"3s\"></silence>停顿结束</speak>"
}'请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| cuid | String | 是 | 设备唯一标识 |
| title | String | 是 | 推送标题 |
| content | String | 是 | 显示内容 |
| outputSpeech | String | 是 | SSML播报内容 |
@Autowired
private XiaoduOAuthService oauthService;
@Autowired
private XiaoduPushService pushService;
public void example() {
// 1. 获取Access Token
TokenData tokenData = oauthService.getAccessToken();
String accessToken = tokenData.getAccessToken();
// 2. 发送纯文本推送(使用DTO)
TextPushRequest textRequest = TextPushRequest.builder()
.cuid("MA718CNBFCL010122")
.title("托尔斯泰格言")
.content("托尔斯泰 - 理想的书籍,是智慧的钥匙")
.build();
PushResponse response = pushService.sendTextPush(accessToken, textRequest);
System.out.println("推送成功,消息ID:" + response.getMsgId());
// 3. 发送音频推送(使用DTO)
AudioPushRequest audioRequest = AudioPushRequest.builder()
.cuid("MA718CNBFCL010122")
.streamUrl("https://example.com/audio.mp3")
.title("告白气球")
.titleSubtext1("周杰伦")
.build();
PushResponse audioResponse = pushService.sendAudioPush(accessToken, audioRequest);
}xiaodu-demo/
├── src/
│ ├── main/java/com/xiaodu/demo/
│ │ ├── config/ # 配置类
│ │ │ ├── OkHttpConfig.java
│ │ │ └── XiaoduApiProperties.java
│ │ ├── constant/ # 常量定义
│ │ │ └── PushTemplateType.java
│ │ ├── controller/ # REST控制器(使用DTO)
│ │ │ ├── XiaoduOAuthController.java
│ │ │ └── XiaoduPushController.java
│ │ ├── service/ # 业务服务
│ │ │ ├── XiaoduOAuthService.java
│ │ │ └── XiaoduPushService.java
│ │ ├── dto/ # 数据传输对象
│ │ │ ├── request/ # 请求DTO
│ │ │ │ ├── BasePushRequest.java
│ │ │ │ ├── TextPushRequest.java
│ │ │ │ ├── AllImagePushRequest.java
│ │ │ │ ├── ImageTextPushRequest.java
│ │ │ │ ├── AudioPushRequest.java
│ │ │ │ ├── VideoPushRequest.java
│ │ │ │ └── SsmlPushRequest.java
│ │ │ └── response/ # 响应DTO
│ │ │ ├── Result.java
│ │ │ ├── TokenResponse.java
│ │ │ └── PushResponse.java
│ │ ├── model/ # 实体类(百度API原始数据结构)
│ │ │ ├── PushParam.java
│ │ │ ├── TokenData.java
│ │ │ └── XiaoduResponse.java
│ │ ├── util/ # 工具类
│ │ │ └── OkHttpUtil.java
│ │ ├── exception/ # 异常类
│ │ │ ├── XiaoduApiException.java
│ │ │ └── GlobalExceptionHandler.java
│ │ └── XiaoduDemoApplication.java
│ ├── main/resources/
│ │ └── application.yml # 应用配置
│ └── test/java/com/xiaodu/demo/service/ # 测试用例
│ ├── XiaoduOAuthServiceTest.java # OAuth服务测试
│ └── XiaoduPushServiceTest.java # 推送服务测试
├── test-api.sh # Linux/Mac 测试脚本
├── test-api.bat # Windows 测试脚本
├── Xiaodu-API.postman_collection.json # Postman 集合
├── pom.xml
└── README.md
- ✅ 请求参数对象化,类型安全
- ✅ 参数校验自动化(JSR-303注解)
- ✅ 代码可读性强,易于维护
Result<T> {
success: Boolean // 是否成功
errno: Integer // 错误码
message: String // 提示信息
data: T // 业务数据
}BasePushRequest- 所有推送请求的基类- 各推送类型继承基类,复用cuid字段
- 提高代码复用性
@NotBlank(message = "设备ID不能为空")
private String cuid;
@NotBlank(message = "标题不能为空")
private String title;| 模板类型 | 接口路径 | 请求DTO | 响应DTO | 说明 |
|---|---|---|---|---|
| AllText | /push/text |
TextPushRequest | PushResponse | 纯文本 |
| AllImage | /push/allImage |
AllImagePushRequest | PushResponse | 纯图片 |
| TextRight | /push/textRight |
ImageTextPushRequest | PushResponse | 左图右文 |
| TextBottom | /push/textBottom |
ImageTextPushRequest | PushResponse | 上图下文 |
| AudioPlayer | /push/audio |
AudioPushRequest | PushResponse | 音频 |
| VideoPlayer | /push/video |
VideoPushRequest | PushResponse | 视频 |
| SsmlText | /push/ssml |
SsmlPushRequest | PushResponse | SSML播报 |
- Spring Boot 2.7.18
- OkHttp 4.9.3
- Gson(JSON序列化)
- Lombok(简化代码)
- JSR-303(参数校验)
- Java 8+
- Access Token有效期:默认30天,建议提前5分钟刷新
- 请求格式:所有推送接口使用POST + JSON Body
- 参数校验:必填字段会自动校验,返回清晰的错误提示
- 设备ID:cuid需要从小度设备或API中获取
- 媒体资源:图片、音频、视频URL必须是公网可访问的HTTPS地址
# 1. 获取 Access Token
curl -X GET "http://localhost:8080/api/xiaodu/token"
# 响应示例:
# {
# "success": true,
# "message": "操作成功",
# "data": {
# "accessToken": "24.abcdef123456...",
# "expiresIn": 2592000,
# "deadline": 1738108800
# }
# }
# 2. 使用获取的 Token 发送推送(以纯文本推送为例)
ACCESS_TOKEN="24.abcdef123456..." # 替换为实际获取的 token
curl -X POST "http://localhost:8080/api/xiaodu/push/text" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cuid": "your-device-cuid",
"title": "测试推送",
"content": "这是一条测试消息"
}'
# 响应示例:
# {
# "success": true,
# "message": "推送成功",
# "data": {
# "result": "success",
# "msgId": "1234567890"
# }
# }# 设置变量
ACCESS_TOKEN="your-access-token"
DEVICE_CUID="your-device-cuid"
BASE_URL="http://localhost:8080/api/xiaodu"
# 1. 纯文本推送
curl -X POST "$BASE_URL/push/text" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"cuid\":\"$DEVICE_CUID\",\"title\":\"天气提醒\",\"content\":\"今天北京晴转多云\"}"
# 2. 全图片推送
curl -X POST "$BASE_URL/push/allImage" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"cuid\":\"$DEVICE_CUID\",\"imageSrc\":\"https://example.com/image.jpg\",\"imageThumb\":\"https://example.com/thumb.jpg\"}"
# 3. 左图右文推送
curl -X POST "$BASE_URL/push/textRight" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"cuid\":\"$DEVICE_CUID\",\"title\":\"新闻标题\",\"content\":\"新闻内容\",\"image\":\"https://example.com/news.jpg\"}"
# 4. 音频推送
curl -X POST "$BASE_URL/push/audio" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"cuid\":\"$DEVICE_CUID\",\"streamUrl\":\"https://example.com/music.mp3\",\"title\":\"歌曲名\",\"titleSubtext1\":\"歌手名\"}"
# 5. 视频推送
curl -X POST "$BASE_URL/push/video" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"cuid\":\"$DEVICE_CUID\",\"streamUrl\":\"https://example.com/video.mp4\"}"推荐实现 Token 缓存和自动刷新机制:
@Service
public class TokenManagerService {
@Autowired
private XiaoduOAuthService oauthService;
private String cachedToken;
private long tokenExpireTime;
/**
* 获取有效的 Access Token(带缓存)
*/
public synchronized String getValidToken() {
// 检查 token 是否即将过期(提前 5 分钟刷新)
if (cachedToken == null || System.currentTimeMillis() > tokenExpireTime - 300000) {
refreshToken();
}
return cachedToken;
}
private void refreshToken() {
TokenData tokenData = oauthService.getAccessToken();
this.cachedToken = tokenData.getAccessToken();
this.tokenExpireTime = tokenData.getDeadline() * 1000L;
log.info("Token已更新,过期时间:{}", new Date(tokenExpireTime));
}
}对于高并发场景,建议使用异步推送:
@Service
public class AsyncPushService {
@Autowired
private XiaoduPushService pushService;
@Autowired
private TokenManagerService tokenManager;
@Async
public CompletableFuture<PushResponse> sendTextPushAsync(TextPushRequest request) {
String token = tokenManager.getValidToken();
PushResponse response = pushService.sendTextPush(token, request);
return CompletableFuture.completedFuture(response);
}
}实现自动重试机制:
@Service
public class RetryablePushService {
private static final int MAX_RETRIES = 3;
@Retryable(
value = {XiaoduApiException.class},
maxAttempts = MAX_RETRIES,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PushResponse sendWithRetry(String token, TextPushRequest request) {
return pushService.sendTextPush(token, request);
}
}生产环境建议对敏感信息脱敏:
private String maskToken(String token) {
if (token == null || token.length() < 10) {
return "***";
}
return token.substring(0, 10) + "..." + token.substring(token.length() - 10);
}
log.info("使用 Token: {}", maskToken(accessToken));application.yml:
xiaodu:
api:
base-url: ${XIAODU_BASE_URL:https://dueros.baidu.com/business}
app-id: ${XIAODU_APP_ID}
app-secret: ${XIAODU_APP_SECRET}设置环境变量:
# Linux/Mac
export XIAODU_APP_ID="your-app-id"
export XIAODU_APP_SECRET="your-app-secret"
# Windows
set XIAODU_APP_ID=your-app-id
set XIAODU_APP_SECRET=your-app-secretapplication-dev.yml (开发环境):
xiaodu:
api:
base-url: https://dueros-test.baidu.com/business
app-id: dev-app-id
app-secret: dev-app-secret
logging:
level:
okhttp3.logging.HttpLoggingInterceptor: DEBUG
com.xiaodu.demo: DEBUGapplication-prod.yml (生产环境):
xiaodu:
api:
base-url: https://dueros.baidu.com/business
app-id: ${XIAODU_APP_ID}
app-secret: ${XIAODU_APP_SECRET}
logging:
level:
okhttp3.logging.HttpLoggingInterceptor: INFO
com.xiaodu.demo: INFO启动时指定环境:
# 开发环境
java -jar xiaodu-demo.jar --spring.profiles.active=dev
# 生产环境
java -jar xiaodu-demo.jar --spring.profiles.active=prodA: CUID(Client Unique ID)是设备的唯一标识,需要通过以下方式获取:
- 从百度小度开放平台的设备管理页面查看
- 通过设备绑定时的回调接口获取
- 联系百度小度技术支持获取测试设备 CUID
A: 401 错误通常表示 Access Token 无效或过期,请检查:
- Token 是否正确获取
- Token 是否已过期(有效期 30 天)
- 请求头格式是否正确:
Authorization: Bearer {token} - App ID 和 App Secret 是否正确
A: 项目已配置 OkHttp 日志拦截器,在 application.yml 中设置日志级别:
logging:
level:
okhttp3.logging.HttpLoggingInterceptor: DEBUG # 显示完整请求响应
com.xiaodu.demo: DEBUGA: 请检查:
- CUID 是否正确
- 设备是否在线
- 设备是否已授权接收推送
- 推送内容格式是否符合要求
- 媒体资源 URL 是否可访问(需要 HTTPS)
A: 建议:
- 使用缓存机制存储 Token(Redis/内存)
- 实现自动刷新逻辑(提前 5-10 分钟)
- 使用分布式锁避免并发刷新
- 记录 Token 刷新日志便于追踪
A: 项目使用 JSR-303 校验,失败时会返回:
{
"success": false,
"errno": 400,
"message": "设备ID不能为空",
"data": null
}请根据错误信息检查请求参数。
A: 有两种方式:
- 作为独立服务:启动本项目,其他项目通过 REST API 调用
- 集成到现有项目:复制 service、dto、config 等包到您的项目中
A: 当前实现是单个设备推送,如需批量推送,可以:
public List<PushResponse> batchPush(String token, List<TextPushRequest> requests) {
return requests.stream()
.map(req -> pushService.sendTextPush(token, req))
.collect(Collectors.toList());
}| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 0 | 成功 | - |
| 401 | Token 无效 | 重新获取 Access Token |
| 403 | 权限不足 | 检查 App ID 和 Secret |
| 404 | 接口不存在 | 检查 API 路径 |
| 400 | 参数错误 | 检查请求参数格式 |
| 500 | 服务器错误 | 联系技术支持 |
- 连接池配置:OkHttp 默认连接池已优化,如需调整:
@Bean
public OkHttpClient okHttpClient() {
ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
return new OkHttpClient.Builder()
.connectionPool(connectionPool)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build();
}- 异步处理:使用
@Async注解实现异步推送 - 批量处理:合并多个推送请求减少网络开销
- Token 缓存:避免频繁请求 Token 接口
@RestController
@RequestMapping("/actuator")
public class HealthController {
@GetMapping("/health")
public Map<String, String> health() {
Map<String, String> health = new HashMap<>();
health.put("status", "UP");
health.put("service", "xiaodu-push-service");
return health;
}
}建议监控以下日志关键字:
推送失败- 推送失败次数HTTP请求异常- 网络问题Token已更新- Token 刷新记录
可集成 Spring Boot Actuator 和 Prometheus:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>欢迎提交 Issue 和 Pull Request!
- Fork 本仓库
- 创建特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 开启 Pull Request
- ✅ 实现 OAuth 2.0 认证
- ✅ 支持 7 种推送模板
- ✅ DTO 设计模式
- ✅ 统一异常处理
- ✅ HTTP 请求日志
- ✅ 完整测试用例
MIT License