diff --git a/njzscloud-svr/pom.xml b/njzscloud-svr/pom.xml index 3cfe9fd..2ed0d55 100644 --- a/njzscloud-svr/pom.xml +++ b/njzscloud-svr/pom.xml @@ -125,6 +125,11 @@ aspectjweaver 1.9.7 + + com.volcengine + volcengine-java-sdk-ark-runtime + LATEST + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/API文档.md b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/API文档.md new file mode 100644 index 0000000..34f0251 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/API文档.md @@ -0,0 +1,520 @@ +# 豆包垃圾分析接口文档 + +## 接口概述 + +本接口基于火山引擎豆包AI模型,提供垃圾车辆图片分析功能。通过上传多张垃圾车辆图片,AI会自动识别车棚状态、垃圾类型、轻物质和重物质占比等信息。 + +**接口路径:** `/douBao/analyzeGarbage` +**请求方式:** `POST` +**Content-Type:** `application/json` + +--- + +## 请求参数 + +### 请求体(GarbageAnalysisParam) + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| imageUrls | List<String> | 是 | 图片URL列表,支持多张图片,至少需要一张 | +| orderSn | String | 否 | 订单号,用于关联订单信息 | +| remark | String | 否 | 备注信息,可提供额外的分析上下文 | + +### 参数说明 + +- **imageUrls**:图片URL列表,支持HTTP/HTTPS协议的图片链接。建议使用可公开访问的图片URL。 +- **orderSn**:订单号,用于在分析结果中关联订单信息,便于后续查询和追踪。 +- **remark**:备注信息,可以包含订单相关的额外信息,如装货地点、卸货地点等,有助于AI更准确地进行分析。 + +--- + +## 响应结果 + +### 响应体(GarbageAnalysisResult) + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| isShedOpened | Boolean | 车棚是否完全打开。true表示已打开,false表示未打开 | +| details | String | 明细信息,车厢内包含的物品列表。格式:物品1(数量描述),物品2(数量描述),例如:石块(多),木材(少) | +| lightMaterialPercentage | BigDecimal | 轻物质占比(百分比,0-100) | +| heavyMaterialPercentage | BigDecimal | 重物质占比(百分比,0-100) | +| garbageType | String | 垃圾类型,可能的值:拆除垃圾、装修垃圾、抛货 | +| judgmentBasis | String | 判断依据,说明AI判断垃圾类型的原因 | +| analysisTime | Long | 分析时间戳(毫秒) | +| rawResponse | String | AI原始响应内容,用于调试和问题排查 | +| garbageTypes | List<GarbageTypeInfo> | 垃圾类型分析列表(保留字段,用于兼容) | +| totalWeight | BigDecimal | 总重量(单位:千克)(保留字段,用于兼容) | + +### GarbageTypeInfo 对象结构(保留字段) + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| typeName | String | 垃圾类型名称 | +| category | String | 具体垃圾种类 | +| judgmentBasis | String | 判断依据 | +| percentage | BigDecimal | 占比(百分比,0-100) | +| weight | BigDecimal | 估算重量(单位:千克) | + +--- + +## 请求示例 + +### 场景一:最小参数(仅图片URL) + +**适用场景:** 快速测试,仅提供必要的图片URL + +```json +{ + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ] +} +``` + +**cURL 命令:** +```bash +curl -X POST "http://localhost:8080/douBao/analyzeGarbage" \ + -H "Content-Type: application/json" \ + -d '{ + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ] + }' +``` + +--- + +### 场景二:单张图片 + 订单号 + +**适用场景:** 关联订单信息 + +```json +{ + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ], + "orderSn": "ORD20240101001" +} +``` + +**cURL 命令:** +```bash +curl -X POST "http://localhost:8080/douBao/analyzeGarbage" \ + -H "Content-Type: application/json" \ + -d '{ + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ], + "orderSn": "ORD20240101001" + }' +``` + +--- + +### 场景三:多张图片(推荐) + +**适用场景:** 从不同角度拍摄,提高分析准确性 + +```json +{ + "imageUrls": [ + "https://example.com/garbage-truck-front.jpg", + "https://example.com/garbage-truck-side.jpg", + "https://example.com/garbage-truck-back.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "装货地点:南京市建邺区,卸货地点:南京市江宁区" +} +``` + +**cURL 命令:** +```bash +curl -X POST "http://localhost:8080/douBao/analyzeGarbage" \ + -H "Content-Type: application/json" \ + -d '{ + "imageUrls": [ + "https://example.com/garbage-truck-front.jpg", + "https://example.com/garbage-truck-side.jpg", + "https://example.com/garbage-truck-back.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "装货地点:南京市建邺区,卸货地点:南京市江宁区" + }' +``` + +--- + +### 场景四:完整参数示例 + +**适用场景:** 生产环境,包含所有可选信息 + +```json +{ + "imageUrls": [ + "https://cdn.example.com/orders/2024/01/01/truck-front-001.jpg", + "https://cdn.example.com/orders/2024/01/01/truck-side-001.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "订单编号:ORD20240101001;装货地点:南京市建邺区奥体中心;卸货地点:南京市江宁区垃圾处理厂;装货时间:2024-01-01 10:00:00" +} +``` + +**cURL 命令:** +```bash +curl -X POST "http://localhost:8080/douBao/analyzeGarbage" \ + -H "Content-Type: application/json" \ + -d '{ + "imageUrls": [ + "https://cdn.example.com/orders/2024/01/01/truck-front-001.jpg", + "https://cdn.example.com/orders/2024/01/01/truck-side-001.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "订单编号:ORD20240101001;装货地点:南京市建邺区奥体中心;卸货地点:南京市江宁区垃圾处理厂;装货时间:2024-01-01 10:00:00" + }' +``` + +--- + +### Java 示例 + +#### 方式一:使用链式调用(推荐) + +```java +import com.njzscloud.supervisory.doubao.param.GarbageAnalysisParam; +import com.njzscloud.supervisory.doubao.result.GarbageAnalysisResult; +import com.njzscloud.common.core.utils.R; +import java.util.Arrays; + +// 最小参数 +GarbageAnalysisParam param1 = new GarbageAnalysisParam() + .setImageUrls(Arrays.asList("https://example.com/garbage-truck-1.jpg")); + +// 完整参数 +GarbageAnalysisParam param2 = new GarbageAnalysisParam() + .setImageUrls(Arrays.asList( + "https://example.com/garbage-truck-front.jpg", + "https://example.com/garbage-truck-side.jpg" + )) + .setOrderSn("ORD20240101001") + .setRemark("装货地点:南京市建邺区,卸货地点:南京市江宁区"); + +R result = douBaoController.analyzeGarbage(param2); +if (result.getCode() == 200) { + GarbageAnalysisResult data = result.getData(); + System.out.println("车棚状态:" + (data.getIsShedOpened() ? "已打开" : "未打开")); + System.out.println("垃圾类型:" + data.getGarbageType()); + System.out.println("轻物质占比:" + data.getLightMaterialPercentage() + "%"); + System.out.println("重物质占比:" + data.getHeavyMaterialPercentage() + "%"); +} +``` + +#### 方式二:传统方式 + +```java +GarbageAnalysisParam param = new GarbageAnalysisParam(); +param.setImageUrls(Arrays.asList( + "https://example.com/garbage-truck-1.jpg", + "https://example.com/garbage-truck-2.jpg" +)); +param.setOrderSn("ORD20240101001"); +param.setRemark("装货地点:南京市建邺区,卸货地点:南京市江宁区"); + +R result = douBaoController.analyzeGarbage(param); +``` + +--- + +### JavaScript/Axios 示例 + +```javascript +// 使用 fetch +fetch('http://localhost:8080/douBao/analyzeGarbage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + imageUrls: [ + 'https://example.com/garbage-truck-1.jpg', + 'https://example.com/garbage-truck-2.jpg' + ], + orderSn: 'ORD20240101001', + remark: '装货地点:南京市建邺区,卸货地点:南京市江宁区' + }) +}) +.then(response => response.json()) +.then(data => { + if (data.code === 200) { + console.log('分析成功:', data.data); + console.log('车棚状态:', data.data.isShedOpened ? '已打开' : '未打开'); + console.log('垃圾类型:', data.data.garbageType); + } else { + console.error('分析失败:', data.msg); + } +}) +.catch(error => console.error('请求错误:', error)); + +// 使用 axios +import axios from 'axios'; + +const analyzeGarbage = async () => { + try { + const response = await axios.post('http://localhost:8080/douBao/analyzeGarbage', { + imageUrls: [ + 'https://example.com/garbage-truck-1.jpg', + 'https://example.com/garbage-truck-2.jpg' + ], + orderSn: 'ORD20240101001', + remark: '装货地点:南京市建邺区,卸货地点:南京市江宁区' + }); + + if (response.data.code === 200) { + console.log('分析结果:', response.data.data); + } + } catch (error) { + console.error('请求失败:', error); + } +}; +``` + +--- + +### Postman/Apifox 测试配置 + +#### 请求配置 +- **请求方式:** `POST` +- **请求URL:** `http://localhost:8080/douBao/analyzeGarbage` +- **Headers:** + - `Content-Type: application/json` + +#### Body(raw JSON)示例 + +**示例1:最小参数** +```json +{ + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ] +} +``` + +**示例2:完整参数** +```json +{ + "imageUrls": [ + "https://example.com/garbage-truck-front.jpg", + "https://example.com/garbage-truck-side.jpg", + "https://example.com/garbage-truck-back.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "装货地点:南京市建邺区奥体中心;卸货地点:南京市江宁区垃圾处理厂" +} +``` + +--- + +### Python 示例 + +```python +import requests +import json + +url = "http://localhost:8080/douBao/analyzeGarbage" + +# 最小参数 +payload1 = { + "imageUrls": [ + "https://example.com/garbage-truck-1.jpg" + ] +} + +# 完整参数 +payload2 = { + "imageUrls": [ + "https://example.com/garbage-truck-front.jpg", + "https://example.com/garbage-truck-side.jpg" + ], + "orderSn": "ORD20240101001", + "remark": "装货地点:南京市建邺区,卸货地点:南京市江宁区" +} + +headers = { + "Content-Type": "application/json" +} + +response = requests.post(url, headers=headers, data=json.dumps(payload2)) +result = response.json() + +if result.get("code") == 200: + data = result.get("data") + print(f"车棚状态:{'已打开' if data.get('isShedOpened') else '未打开'}") + print(f"垃圾类型:{data.get('garbageType')}") + print(f"轻物质占比:{data.get('lightMaterialPercentage')}%") + print(f"重物质占比:{data.get('heavyMaterialPercentage')}%") +else: + print(f"分析失败:{result.get('msg')}") +``` + +--- + +### 测试图片URL说明 + +**注意:** 实际测试时,请替换为真实可访问的图片URL。图片URL需要满足以下条件: + +1. **可公开访问**:图片URL必须可以通过HTTP/HTTPS协议直接访问 +2. **支持格式**:建议使用 JPG、PNG 等常见图片格式 +3. **图片质量**:建议图片清晰,能够清楚看到车辆和垃圾内容 +4. **推荐来源**: + - OSS对象存储(如阿里云OSS、腾讯云COS等) + - CDN加速域名 + - 公网可访问的图片服务器 + +**测试用占位图片(仅供参考,实际使用时请替换):** +``` +https://via.placeholder.com/800x600.jpg?text=Garbage+Truck+Front +https://via.placeholder.com/800x600.jpg?text=Garbage+Truck+Side +``` + +--- + +### 本地开发环境测试 + +如果是在本地开发环境测试,请确保: + +1. **服务已启动**:确保Spring Boot应用已启动 +2. **端口正确**:默认端口通常是 `8080`,请根据实际配置调整 +3. **跨域配置**:如果前端调用,可能需要配置CORS +4. **完整URL示例**: + - 本地:`http://localhost:8080/douBao/analyzeGarbage` + - 开发环境:`http://dev.example.com:8080/douBao/analyzeGarbage` + - 生产环境:`https://api.example.com/douBao/analyzeGarbage` + +--- + +## 响应示例 + +### 成功响应(车棚已打开) + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "isShedOpened": true, + "details": "石块(多),木材(少)", + "lightMaterialPercentage": 10.00, + "heavyMaterialPercentage": 90.00, + "garbageType": "拆除垃圾", + "judgmentBasis": "车厢内主要为石块,这类垃圾多来自建筑物拆除等工程", + "analysisTime": 1704067200000, + "rawResponse": "明细:石块(多),木材(少);轻物质:10%;重物质:90%;垃圾类型:拆除垃圾;判断依据:车厢内主要为石块,这类垃圾多来自建筑物拆除等工程", + "garbageTypes": null, + "totalWeight": null + } +} +``` + +### 成功响应(车棚未打开) + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "isShedOpened": false, + "details": "未打开车棚", + "lightMaterialPercentage": null, + "heavyMaterialPercentage": null, + "garbageType": null, + "judgmentBasis": null, + "analysisTime": 1704067200000, + "rawResponse": "未打开车棚", + "garbageTypes": null, + "totalWeight": null + } +} +``` + +### 错误响应 + +```json +{ + "code": 500, + "msg": "垃圾分析失败:垃圾分析参数不能为空,且必须包含至少一张图片URL", + "data": null +} +``` + +--- + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 400 | 请求参数错误(如:图片URL列表为空) | +| 500 | 服务器内部错误(如:AI服务调用失败、解析失败等) | + +--- + +## 业务逻辑说明 + +### 分析流程 + +1. **车棚状态检测**:AI首先判断车棚是否完全打开 + - 如果车棚未完全打开,直接返回"未打开车棚",不进行后续分析 + - 如果车棚已打开,继续执行垃圾分析 + +2. **垃圾识别**(仅当车棚已打开时): + - 识别车厢内包含的物品类型和数量 + - 如果有多辆车,只分析画面中间的车辆 + - 分析轻物质和重物质的占比(百分比,0-100,两者之和应为100%) + - 判断垃圾类型:拆除垃圾、装修垃圾或抛货 + - 提供判断依据说明 + +### 垃圾类型说明 + +- **拆除垃圾**:主要来自建筑物拆除等工程,通常包含大量石块、混凝土块等重物质 +- **装修垃圾**:主要来自室内装修,通常包含白色板材、泡沫、木棍等轻物质 +- **抛货**:其他类型的垃圾 + +--- + +## 注意事项 + +1. **图片要求**: + - 图片URL必须可公开访问 + - 支持HTTP/HTTPS协议 + - 建议图片清晰,能够清楚看到车辆和垃圾内容 + - 支持多张图片,建议从不同角度拍摄 + +2. **性能考虑**: + - AI分析需要一定时间,建议设置合理的超时时间 + - 图片数量越多,分析时间可能越长 + +3. **数据准确性**: + - AI分析结果仅供参考,建议结合人工审核 + - 如果分析结果不准确,可以通过 `rawResponse` 字段查看AI原始响应进行排查 + +4. **字段说明**: + - `garbageTypes` 和 `totalWeight` 为保留字段,当前版本可能为null + - `isShedOpened` 为false时,其他分析字段(如 `garbageType`、`lightMaterialPercentage` 等)可能为null + +5. **格式要求**: + - AI返回的格式为固定格式,解析失败时会记录警告日志 + - 如果解析结果不完整,建议查看 `rawResponse` 字段获取原始响应 + +--- + +## 更新日志 + +| 版本 | 日期 | 更新内容 | +|------|------|----------| +| 1.0.0 | 2024-01-01 | 初始版本,支持垃圾车辆图片分析功能 | + +--- + +## 联系方式 + +如有问题或建议,请联系开发团队。 + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoConfiguration.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoConfiguration.java new file mode 100644 index 0000000..b769859 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoConfiguration.java @@ -0,0 +1,48 @@ +package com.njzscloud.supervisory.doubao.config; + +import com.volcengine.ark.runtime.service.ArkService; +import lombok.RequiredArgsConstructor; +import okhttp3.ConnectionPool; +import okhttp3.Dispatcher; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import java.util.concurrent.TimeUnit; + +/** + * 火山引擎豆包配置类 + */ +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(DouBaoProperties.class) +public class DouBaoConfiguration { + + private final DouBaoProperties properties; + + @Bean(destroyMethod = "shutdownExecutor") + @ConditionalOnMissingBean + public ArkService arkService() { + String apiKey = properties.getApiKey(); + if (!StringUtils.hasText(apiKey)) { + throw new IllegalStateException("豆包API Key未配置,请设置环境变量 ARK_API_KEY 或在配置文件中设置 doubao.api-key"); + } + + ConnectionPool connectionPool = new ConnectionPool( + properties.getConnectionPoolSize(), + properties.getConnectionKeepAliveSeconds(), + TimeUnit.SECONDS + ); + Dispatcher dispatcher = new Dispatcher(); + + return ArkService.builder() + .dispatcher(dispatcher) + .connectionPool(connectionPool) + .baseUrl(properties.getBaseUrl()) + .apiKey(apiKey) + .build(); + } +} + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoProperties.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoProperties.java new file mode 100644 index 0000000..3312ee0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/config/DouBaoProperties.java @@ -0,0 +1,52 @@ +package com.njzscloud.supervisory.doubao.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 火山引擎豆包配置 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +@ConfigurationProperties(prefix = "doubao") +public class DouBaoProperties { + + /** + * API Key,优先从环境变量 ARK_API_KEY 获取,如果环境变量不存在则使用配置值 + */ + private String apiKey; + + /** + * 基础URL,默认为 https://ark.cn-beijing.volces.com/api/v3 + */ + private String baseUrl = "https://ark.cn-beijing.volces.com/api/v3"; + + /** + * 模型ID,默认为 doubao-seed-1-6-vision-250815 + */ + private String model = "doubao-seed-1-6-vision-250815"; + + /** + * 连接池大小,默认5 + */ + private Integer connectionPoolSize = 5; + + /** + * 连接保持时间(秒),默认1 + */ + private Long connectionKeepAliveSeconds = 1L; + + /** + * 获取API Key,优先从环境变量获取 + */ + public String getApiKey() { + String envApiKey = System.getenv("ARK_API_KEY"); + return envApiKey != null ? envApiKey : apiKey; + } +} + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/controller/DouBaoController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/controller/DouBaoController.java new file mode 100644 index 0000000..b654df3 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/controller/DouBaoController.java @@ -0,0 +1,31 @@ +package com.njzscloud.supervisory.doubao.controller; + +import com.njzscloud.common.core.utils.R; +import com.njzscloud.supervisory.doubao.param.GarbageAnalysisParam; +import com.njzscloud.supervisory.doubao.result.GarbageAnalysisResult; +import com.njzscloud.supervisory.doubao.sevice.DouBaoService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 豆包 + * 使用豆包官方SDK的生产级实现 + */ +@Slf4j +@RestController +@RequestMapping("/douBao") +@RequiredArgsConstructor +public class DouBaoController { + + private final DouBaoService douBaoService; + + @PostMapping("/analyzeGarbage") + public R analyzeGarbage(@RequestBody GarbageAnalysisParam param) { + return R.success(douBaoService.analyzeGarbage(param)); + } + +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/param/GarbageAnalysisParam.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/param/GarbageAnalysisParam.java new file mode 100644 index 0000000..d30eec0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/param/GarbageAnalysisParam.java @@ -0,0 +1,34 @@ +package com.njzscloud.supervisory.doubao.param; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 垃圾分析请求参数 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class GarbageAnalysisParam { + + /** + * 图片URL列表(支持多张图片) + */ + private List imageUrls; + + /** + * 订单号(可选) + */ + private String orderSn; + + /** + * 备注信息(可选) + */ + private String remark; +} + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/result/GarbageAnalysisResult.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/result/GarbageAnalysisResult.java new file mode 100644 index 0000000..3c2d3e3 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/result/GarbageAnalysisResult.java @@ -0,0 +1,105 @@ +package com.njzscloud.supervisory.doubao.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 垃圾分析结果 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class GarbageAnalysisResult { + + /** + * 车棚是否完全打开 + */ + private Boolean isShedOpened; + + /** + * 明细:车厢内包含的物品列表,格式:物品1(数量描述),物品2(数量描述) + * 例如:石块(多),木材(少) + */ + private String details; + + /** + * 轻物质占比(百分比,0-100) + */ + private BigDecimal lightMaterialPercentage; + + /** + * 重物质占比(百分比,0-100) + */ + private BigDecimal heavyMaterialPercentage; + + /** + * 垃圾类型:拆除垃圾、装修垃圾、抛货 + */ + private String garbageType; + + /** + * 判断依据:说明判断垃圾类型的原因 + */ + private String judgmentBasis; + + /** + * 分析时间戳 + */ + private Long analysisTime; + + /** + * 原始响应内容 + */ + private String rawResponse; + + /** + * 垃圾类型分析列表(保留用于兼容) + */ + private List garbageTypes; + + /** + * 总重量(单位:千克)(保留用于兼容) + */ + private BigDecimal totalWeight; + + /** + * 垃圾类型信息(保留用于兼容) + */ + @Getter + @Setter + @ToString + @Accessors(chain = true) + public static class GarbageTypeInfo { + /** + * 垃圾类型名称(如:可回收垃圾、有害垃圾、厨余垃圾、其他垃圾等) + */ + private String typeName; + + /** + * 具体垃圾种类(如:塑料瓶、废纸、电池等) + */ + private String category; + + /** + * 判断依据(AI识别该垃圾类型的理由) + */ + private String judgmentBasis; + + /** + * 占比(百分比,0-100) + */ + private BigDecimal percentage; + + /** + * 估算重量(单位:千克) + */ + private BigDecimal weight; + } +} + diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/DouBaoService.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/DouBaoService.java new file mode 100644 index 0000000..2f26a4d --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/DouBaoService.java @@ -0,0 +1,18 @@ +package com.njzscloud.supervisory.doubao.sevice; + +import com.njzscloud.supervisory.doubao.param.GarbageAnalysisParam; +import com.njzscloud.supervisory.doubao.result.GarbageAnalysisResult; + +/** + * 火山引擎豆包服务接口 + */ +public interface DouBaoService { + + /** + * 垃圾分析 - 分析多张图片中的垃圾类型、占比和重量 + * + * @param param 垃圾分析请求参数,包含多张图片URL + * @return 垃圾分析结果,包含垃圾类型、判断依据、占比、重量等信息 + */ + GarbageAnalysisResult analyzeGarbage(GarbageAnalysisParam param); +} diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/impl/DouBaoServiceImpl.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/impl/DouBaoServiceImpl.java new file mode 100644 index 0000000..666cbe0 --- /dev/null +++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/doubao/sevice/impl/DouBaoServiceImpl.java @@ -0,0 +1,242 @@ +package com.njzscloud.supervisory.doubao.sevice.impl; + +import com.njzscloud.supervisory.doubao.config.DouBaoProperties; +import com.njzscloud.supervisory.doubao.param.GarbageAnalysisParam; +import com.njzscloud.supervisory.doubao.result.GarbageAnalysisResult; +import com.njzscloud.supervisory.doubao.sevice.DouBaoService; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionContentPart; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessage; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole; +import com.volcengine.ark.runtime.service.ArkService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 火山引擎豆包服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DouBaoServiceImpl implements DouBaoService { + + private final ArkService arkService; + private final DouBaoProperties douBaoProperties; + + /** + * 垃圾分析 - 分析多张图片中的垃圾类型、占比和重量 + * + * @param param 垃圾分析请求参数,包含多张图片URL + * @return 垃圾分析结果,包含垃圾类型、判断依据、占比、重量等信息 + */ + @Override + public GarbageAnalysisResult analyzeGarbage(GarbageAnalysisParam param) { + try { + log.info("开始调用火山引擎豆包API进行垃圾分析,订单号:{},图片数量:{}", + param != null ? param.getOrderSn() : "未知", + param != null && param.getImageUrls() != null ? param.getImageUrls().size() : 0); + + // 参数校验 + if (param == null || CollectionUtils.isEmpty(param.getImageUrls())) { + throw new IllegalArgumentException("垃圾分析参数不能为空,且必须包含至少一张图片URL"); + } + + // 构建消息列表 + final List messages = new ArrayList<>(); + final List multiParts = new ArrayList<>(); + + // 添加所有图片URL + for (String imageUrl : param.getImageUrls()) { + if (StringUtils.hasText(imageUrl)) { + multiParts.add(ChatCompletionContentPart.builder() + .type("image_url") + .imageUrl(new ChatCompletionContentPart.ChatCompletionContentPartImageURL(imageUrl)) + .build()); + } + } + + // 添加文本提示词,要求返回JSON格式 + String textPrompt = buildGarbageAnalysisPrompt(param); + multiParts.add(ChatCompletionContentPart.builder() + .type("text") + .text(textPrompt) + .build()); + + // 构建用户消息 + final ChatMessage userMessage = ChatMessage.builder() + .role(ChatMessageRole.USER) + .multiContent(multiParts) + .build(); + messages.add(userMessage); + + // 构建请求 + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(douBaoProperties.getModel()) + .messages(messages) + .build(); + + // 调用API并获取响应 + String response = arkService.createChatCompletion(chatCompletionRequest) + .getChoices() + .stream() + .map(choice -> String.valueOf(choice.getMessage().getContent())) + .collect(Collectors.joining("\n")); + + log.info("火山引擎豆包API调用成功,原始响应内容:{}", response); + + // 解析响应并转换为结果对象 + GarbageAnalysisResult result = parseGarbageAnalysisResponse(response); + result.setRawResponse(response); + result.setAnalysisTime(System.currentTimeMillis()); + + log.info("垃圾分析完成,车棚状态:{},垃圾类型:{},轻物质占比:{}%,重物质占比:{}%", + result.getIsShedOpened() != null && result.getIsShedOpened() ? "已打开" : "未打开", + result.getGarbageType(), + result.getLightMaterialPercentage(), + result.getHeavyMaterialPercentage()); + + return result; + + } catch (Exception e) { + log.error("调用火山引擎豆包API进行垃圾分析失败,订单号:{}", + param != null ? param.getOrderSn() : "未知", e); + throw new RuntimeException("垃圾分析失败:" + e.getMessage(), e); + } + } + + /** + * 构建垃圾分析提示词 + * 要求AI返回指定格式的文本 + */ + private String buildGarbageAnalysisPrompt(GarbageAnalysisParam param) { + StringBuilder prompt = new StringBuilder(); + prompt.append("请仔细分析这些图片中的垃圾内容。\n\n"); + + // 添加订单信息(如果有) + if (StringUtils.hasText(param.getOrderSn())) { + prompt.append("订单号:").append(param.getOrderSn()).append("\n"); + } + if (StringUtils.hasText(param.getRemark())) { + prompt.append("备注:").append(param.getRemark()).append("\n"); + } + + prompt.append("\n请按照以下要求进行分析:\n"); + prompt.append("1. 首先判断图片中是否有垃圾运输车辆:\n"); + prompt.append(" - 如果图片中没有车辆(直接是垃圾堆、垃圾场等场景),请直接跳到第3步分析垃圾内容\n"); + prompt.append(" - 如果图片中有车辆,请继续第2步判断车棚状态\n"); + prompt.append("2. 如果图片中有车辆,判断车棚是否完全打开:\n"); + prompt.append(" - 如果车棚没有完全打开,无法看到车厢内的垃圾内容,直接回答:未打开车棚\n"); + prompt.append(" - 如果车棚完全打开,可以看到车厢内的垃圾,请继续第3步分析\n"); + prompt.append("3. 分析垃圾内容(适用于:无车辆的场景,或有车辆且车棚已打开的场景):\n"); + prompt.append(" - 识别垃圾中包含哪几种物品(如果有多辆车,只分析画面中间的车辆或主要垃圾堆)\n"); + prompt.append(" - 说明哪种物品占比最多\n"); + prompt.append(" - 给出轻物质和重物质的大概占比(使用百分比数值,0-100)\n"); + prompt.append(" - 判断垃圾类型:拆除垃圾、装修垃圾或抛货\n"); + prompt.append(" - 说明判断垃圾类型的原因\n\n"); + + prompt.append("请严格按照以下格式返回结果,不要添加任何其他文字说明:\n"); + prompt.append("如果图片中有车辆且车棚未打开,直接回答:未打开车棚\n"); + prompt.append("如果图片中没有车辆,或者有车辆且车棚已打开,按照以下格式回答:\n"); + prompt.append("明细:物品1(数量描述),物品2(数量描述);轻物质:x%;重物质:x%;垃圾类型:x;判断依据:x\n\n"); + prompt.append("格式说明:\n"); + prompt.append("- 明细:列出垃圾中包含的所有物品,用逗号分隔,每个物品后标注数量(多、少、较多、较少等)\n"); + prompt.append("- 轻物质:轻物质的占比百分比(0-100)\n"); + prompt.append("- 重物质:重物质的占比百分比(0-100),轻物质和重物质占比之和应为100%\n"); + prompt.append("- 垃圾类型:拆除垃圾、装修垃圾或抛货\n"); + prompt.append("- 判断依据:详细说明为什么判断为该垃圾类型\n\n"); + prompt.append("示例:\n"); + prompt.append("明细:石块(多);轻物质:0%;重物质:100%;垃圾类型:拆除垃圾;判断依据:主要为石块,这类垃圾多来自建筑物拆除等工程\n"); + prompt.append("明细:白色板材(多),白色泡沫(少),木棍(少);轻物质:90%;重物质:10%;垃圾类型:装修垃圾;判断依据:主要是白色板材、泡沫等装修常见废弃物\n"); + prompt.append("明细:纸箱(多),塑料瓶(少),泡沫箱(少);轻物质:95%;重物质:5%;垃圾类型:抛货;判断依据:主要是轻质包装材料,属于抛货类垃圾\n\n"); + prompt.append("注意:必须严格按照上述格式返回,不要添加任何其他文字说明。"); + + return prompt.toString(); + } + + /** + * 解析垃圾分析响应 + * 从AI响应中提取信息并转换为结果对象 + * 格式:明细:xx(x),xx(x);轻物质:x%;重物质:x%;垃圾类型:x;判断依据:x + * 或者:未打开车棚 + */ + private GarbageAnalysisResult parseGarbageAnalysisResponse(String response) { + if (!StringUtils.hasText(response)) { + throw new RuntimeException("AI响应为空,无法解析"); + } + + try { + GarbageAnalysisResult result = new GarbageAnalysisResult(); + String trimmed = response.trim(); + + // 检查是否是"未打开车棚"的情况 + if (trimmed.contains("未打开车棚")) { + result.setIsShedOpened(false); + result.setDetails("未打开车棚"); + result.setLightMaterialPercentage(BigDecimal.ZERO); + result.setHeavyMaterialPercentage(BigDecimal.ZERO); + result.setGarbageType("无"); + result.setJudgmentBasis("车棚未完全打开,无法进行分析"); + return result; + } + + // 车棚已打开,解析详细格式 + result.setIsShedOpened(true); + + // 提取明细 + Pattern detailsPattern = Pattern.compile("明细[::]([^;]+)"); + Matcher detailsMatcher = detailsPattern.matcher(trimmed); + if (detailsMatcher.find()) { + result.setDetails(detailsMatcher.group(1).trim()); + } + + // 提取轻物质占比 + Pattern lightPattern = Pattern.compile("轻物质[::](\\d+(?:\\.\\d+)?)%"); + Matcher lightMatcher = lightPattern.matcher(trimmed); + if (lightMatcher.find()) { + result.setLightMaterialPercentage(new BigDecimal(lightMatcher.group(1))); + } + + // 提取重物质占比 + Pattern heavyPattern = Pattern.compile("重物质[::](\\d+(?:\\.\\d+)?)%"); + Matcher heavyMatcher = heavyPattern.matcher(trimmed); + if (heavyMatcher.find()) { + result.setHeavyMaterialPercentage(new BigDecimal(heavyMatcher.group(1))); + } + + // 提取垃圾类型 + Pattern typePattern = Pattern.compile("垃圾类型[::]([^;]+)"); + Matcher typeMatcher = typePattern.matcher(trimmed); + if (typeMatcher.find()) { + result.setGarbageType(typeMatcher.group(1).trim()); + } + + // 提取判断依据 + Pattern basisPattern = Pattern.compile("判断依据[::](.+)"); + Matcher basisMatcher = basisPattern.matcher(trimmed); + if (basisMatcher.find()) { + result.setJudgmentBasis(basisMatcher.group(1).trim()); + } + + // 验证必要字段 + if (result.getDetails() == null || result.getGarbageType() == null) { + log.warn("解析结果不完整,响应内容:{}", response); + } + + return result; + + } catch (Exception e) { + log.error("解析垃圾分析响应失败,响应内容:{}", response, e); + throw new RuntimeException("解析AI响应失败:" + e.getMessage(), e); + } + } +} diff --git a/njzscloud-svr/src/main/resources/application-dev.yml b/njzscloud-svr/src/main/resources/application-dev.yml index 078efbf..7bae6aa 100644 --- a/njzscloud-svr/src/main/resources/application-dev.yml +++ b/njzscloud-svr/src/main/resources/application-dev.yml @@ -80,6 +80,9 @@ wechat: notify-url: http://115.29.236.92:8082/payment/wechat/notify # 退款回调地址 refund-notify-url: http://115.29.236.92:8082/payment/wechat/refundNotify +doubao: + api-name: api-key-20250411103646 + api-key: e0fc97bb-2bb6-4525-a79b-1e3555026821 mqtt: enabled: true broker: tcp://139.224.54.144:1883 diff --git a/njzscloud-svr/src/main/resources/application-prod.yml b/njzscloud-svr/src/main/resources/application-prod.yml index 9891674..ad723ff 100644 --- a/njzscloud-svr/src/main/resources/application-prod.yml +++ b/njzscloud-svr/src/main/resources/application-prod.yml @@ -70,7 +70,9 @@ wechat: notify-url: http://139.224.32.69:80/api/payment/wechat/notify # 退款回调地址 refund-notify-url: http://139.224.32.69:80/api/payment/wechat/refundNotify - +doubao: + api-name: api-key-20250411103646 + api-key: e0fc97bb-2bb6-4525-a79b-1e3555026821 mqtt: enabled: true broker: tcp://127.0.0.1:1883