feat(device): 新增阿里云语音通知功能并扩展设备查询接口

- 集成阿里云语音服务,实现TTS语音拨号功能
- 添加异步报警拨号机制,支持SOS紧急呼叫
- 新增按设备名称查询设备的接口和SQL
- 扩展设备类型统计和批量查询功能
- 添加租户ID字段支持多租户设备管理
- 配置阿里云语音API依赖和客户端单例模式
This commit is contained in:
2026-01-30 16:03:13 +08:00
parent b280038502
commit cb87871982
7 changed files with 210 additions and 1 deletions

View File

@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* HBY670设备控制类 * HBY670设备控制类
@ -144,4 +145,26 @@ public class AppDeviceXinghanController extends BaseController {
return R.ok(); return R.ok();
} }
@PostMapping(value = "/GetDeviceByName")
@Operation(summary = "通过蓝牙名/设备名称查询设备")
public R<Object> GetDeviceByName(@RequestBody DeviceForm deviceForm) {
Object device = appDeviceService.GetDeviceByName(deviceForm);
return R.ok(device);
}
@PostMapping(value = "/getEquipCountByType")
@Operation(summary = "查询某个类型下的设备总数量")
public R<Object> getEquipCountByType(@RequestBody DeviceForm deviceForm) {
Object device = appDeviceService.getEquipCountByType(deviceForm);
return R.ok(device);
}
@PostMapping(value = "/getEquipAllByType")
@Operation(summary = "查询某个类型下的设备")
public R<List<Map<String,Object>>> getEquipAllByType(@RequestBody DeviceForm deviceForm){
List<Map<String,Object>> list=appDeviceService.getEquipAllByType(deviceForm);
return R.ok(list);
}
} }

View File

@ -57,9 +57,11 @@ import com.fuyuanshen.web.domain.Dto.DeviceDebugLogoUploadDto;
import com.fuyuanshen.web.domain.Dto.DeviceXinghanInstructDto; import com.fuyuanshen.web.domain.Dto.DeviceXinghanInstructDto;
import com.fuyuanshen.web.domain.vo.DeviceXinghanDetailVo; import com.fuyuanshen.web.domain.vo.DeviceXinghanDetailVo;
import com.fuyuanshen.web.enums.AlarmTypeEnum; import com.fuyuanshen.web.enums.AlarmTypeEnum;
import com.fuyuanshen.web.util.AliyunVoiceUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -93,6 +95,7 @@ public class DeviceXinghanBizService {
private final DeviceAssignmentsService deviceAssignmentsService; private final DeviceAssignmentsService deviceAssignmentsService;
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
private final AliyunVoiceUtil voiceUtil;
/** /**
* 所有档位的描述表 * 所有档位的描述表
@ -135,7 +138,6 @@ public class DeviceXinghanBizService {
public void upSOSGradeSettings(DeviceXinghanInstructDto dto) { public void upSOSGradeSettings(DeviceXinghanInstructDto dto) {
if(dto.getIsBluetooth()){ if(dto.getIsBluetooth()){
long deviceId = dto.getDeviceId(); long deviceId = dto.getDeviceId();
// 1. 使用Optional简化空值检查使代码更简洁 // 1. 使用Optional简化空值检查使代码更简洁
Device device = Optional.ofNullable(deviceMapper.selectById(deviceId)) Device device = Optional.ofNullable(deviceMapper.selectById(deviceId))
.orElseThrow(() -> new ServiceException("设备不存在")); .orElseThrow(() -> new ServiceException("设备不存在"));
@ -147,6 +149,24 @@ public class DeviceXinghanBizService {
} }
} }
/**
* 触发异步报警
* Spring 会自动调用 AsyncConfig.getAsyncExecutor() 来执行此方法
*/
@Async
public void executeSosCall(String phone) {
log.info("[SOS业务] 准备发起语音拨号 -> 目标: {}", phone);
Map<String, String> params = Map.of("device", "670");
String callId = voiceUtil.sendTtsSync(phone, "TTS_328730104", params);
if (callId != null) {
log.info("[SOS业务] 拨号指令下发成功, callId: {}", callId);
// 这里可以记录拨打日志到数据库
} else {
log.error("[SOS业务] 拨号指令下发失败,请检查配置或余额");
}
}
/** /**
* 设置强制报警 * 设置强制报警
*/ */
@ -768,4 +788,23 @@ public class DeviceXinghanBizService {
return uuidStr.replaceAll("-", ""); return uuidStr.replaceAll("-", "");
} }
public Map<String, Object> GetDeviceByName(DeviceForm deviceForm){
List<Map<String, Object>> list= deviceMapper.GetDeviceByName(deviceForm);
Map<String, Object> device=null;
if(list!=null && list.size()>0){
device=list.get(0);
}
return device;
}
public int getEquipCountByType(DeviceForm form){
var res=deviceMapper.getEquipCountByType(form);
return res;
}
public List<Map<String,Object>> getEquipAllByType(DeviceForm deviceForm){
List<Map<String, Object>> list= deviceMapper.getEquipAllByType(deviceForm);
return list;
}
} }

View File

@ -0,0 +1,87 @@
package com.fuyuanshen.web.util;
import com.aliyun.dyvmsapi20170525.Client;
import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsRequest;
import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Map;
@Slf4j
@Component
public class AliyunVoiceUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Value("${alibaba.tts.akId}")
private String akId;
@Value("${alibaba.tts.akSecret}")
private String akSecret;
// @Value("${alibaba.tts.calledShowNumber:}")
private String calledShowNumber;
// ========== 核心:单例客户端(类似 OkHttpClient ==========
private volatile Client client;
/**
* 获取客户端(双重检查锁实现单例)
* 只有在第一次调用时才会根据配置实例化,后续直接返回复用
*/
private Client getClient() throws Exception {
if (client == null) {
synchronized (this) {
if (client == null) {
log.info("[AliyunVoice] 正在初始化阿里云语音客户端...");
Config config = new Config()
.setAccessKeyId(akId)
.setAccessKeySecret(akSecret)
.setEndpoint("dyvmsapi.aliyuncs.com");
this.client = new Client(config);
}
}
}
return client;
}
/**
* 同步发送方法:由异步架构调用
*/
public String sendTtsSync(String phone, String templateCode, Map<String, String> params) {
try {
// 1. 获取(或初始化)单例客户端
Client voiceClient = getClient();
SingleCallByTtsRequest request = new SingleCallByTtsRequest()
.setCalledNumber(phone)
.setTtsCode(templateCode)
.setTtsParam(objectMapper.writeValueAsString(params));
if (StringUtils.hasText(calledShowNumber)) {
request.setCalledShowNumber(calledShowNumber);
}
// 生产级超时配置
RuntimeOptions runtime = new RuntimeOptions();
runtime.setConnectTimeout(5000);
runtime.setReadTimeout(10000);
SingleCallByTtsResponse response = voiceClient.singleCallByTtsWithOptions(request, runtime);
if ("OK".equalsIgnoreCase(response.getBody().getCode())) {
return response.getBody().getCallId();
} else {
log.error("[AliyunVoice] 拨号失败: {}", response.getBody().getMessage());
}
} catch (Exception e) {
log.error("[AliyunVoice] 接口异常", e);
}
return null;
}
}

View File

@ -140,6 +140,18 @@
<version>3.3.1</version> <version>3.3.1</version>
</dependency> </dependency>
<!-- 电话语音通知 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dyvmsapi20170525</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.2</version>
</dependency>
<!-- fastjson2 --> <!-- fastjson2 -->
<dependency> <dependency>
<groupId>com.alibaba.fastjson2</groupId> <groupId>com.alibaba.fastjson2</groupId>

View File

@ -54,6 +54,9 @@ public class DeviceForm {
@Schema(title = "备注") @Schema(title = "备注")
private String remark; private String remark;
@Schema(title = "商户号")
private Long tenant_id;
// 设备类型相关字段 // 设备类型相关字段
@Schema(title = "设备类型名称") @Schema(title = "设备类型名称")

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fuyuanshen.equipment.domain.Device; import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.dto.InstructionRecordDto; import com.fuyuanshen.equipment.domain.dto.InstructionRecordDto;
import com.fuyuanshen.equipment.domain.form.DeviceForm;
import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria; import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria;
import com.fuyuanshen.equipment.domain.vo.*; import com.fuyuanshen.equipment.domain.vo.*;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -148,4 +149,10 @@ public interface DeviceMapper extends BaseMapper<Device> {
*/ */
int countByDeviceTypeId(@Param("deviceTypeId") Long deviceTypeId); int countByDeviceTypeId(@Param("deviceTypeId") Long deviceTypeId);
List<Map<String, Object>> GetDeviceByName(DeviceForm deviceForm);
int getEquipCountByType(DeviceForm deviceForm);
List<Map<String,Object>> getEquipAllByType(DeviceForm deviceForm);
} }

View File

@ -264,6 +264,44 @@
</if> </if>
</select> </select>
<select id="GetDeviceByName" resultType="map" >
select a.id,
a.device_type,
a.device_name,
a.device_mac,
a.type_name,
a.bluetooth_name,
a.device_imei
from device a
<where>
a.tenant_id = #{tenant_id} and
a.device_type = #{deviceType}
AND (
a.device_name=#{deviceName} or
a.bluetooth_name=#{deviceName}
)
</where>
</select>
<select id="getEquipCountByType" resultType="int">
select count(1) cnt
from device a
<where>
a.tenant_id = #{tenant_id} and
a.device_type = #{deviceType}
</where>
</select>
<select id="getEquipAllByType" resultType="map">
select device_mac,bluetooth_name,device_name
from device a
<where>
a.tenant_id = #{tenant_id} and
a.device_type = #{deviceType}
</where>
</select>
<!-- 获取分配设备的客户 --> <!-- 获取分配设备的客户 -->
<select id="getAssignCustomer" resultType="com.fuyuanshen.equipment.domain.Device"> <select id="getAssignCustomer" resultType="com.fuyuanshen.equipment.domain.Device">
SELECT * SELECT *