1、涂鸦设备api集成(未完成)

2、pad服务指令矩阵、首页相关接口调整
This commit is contained in:
1378012178@qq.com 2026-04-01 16:41:22 +08:00
parent 0d0116476e
commit 6e899f16bf
9 changed files with 505 additions and 8 deletions

View File

@ -0,0 +1,123 @@
package com.nu.modules.commonutils;
import com.fasterxml.jackson.databind.JsonNode;
import com.nu.utils.TuyaApiUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/tuya/aircon")
public class TuyaAirconApi {
@Autowired
private TuyaApiUtil tuyaApiUtil;
private String deviceId = "6c36cb2d2c352683bfnsm1";
/**
* 测试连接
*/
@GetMapping("/test")
public Map<String, Object> test() {
Map<String, Object> result = new HashMap<>();
try {
String token = tuyaApiUtil.getAccessToken();
result.put("success", true);
result.put("message", "连接成功");
result.put("token", token);
} catch (Exception e) {
result.put("success", false);
result.put("message", "连接失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 获取设备状态
*/
@GetMapping("/status")
public Map<String, Object> getDeviceStatus() {
Map<String, Object> result = new HashMap<>();
try {
JsonNode response = tuyaApiUtil.getDeviceStatus(deviceId);
result.put("success", true);
result.put("data", response);
} catch (Exception e) {
result.put("success", false);
result.put("message", "获取设备状态失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 获取设备功能点规格
*/
@GetMapping("/specifications")
public Map<String, Object> getDeviceSpecifications() {
Map<String, Object> result = new HashMap<>();
try {
JsonNode response = tuyaApiUtil.getDeviceSpecifications(deviceId);
result.put("success", true);
result.put("data", response);
} catch (Exception e) {
result.put("success", false);
result.put("message", "获取设备规格失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 发送控制指令单个指令
* @param code 功能点code
* @param value 功能点值
*/
@PostMapping("/control/single")
public Map<String, Object> sendSingleCommand(
@RequestParam String code,
@RequestParam Object value) {
Map<String, Object> result = new HashMap<>();
try {
Map<String, Object> command = new HashMap<>();
command.put("code", code);
command.put("value", value);
List<Map<String, Object>> commands = new ArrayList<>();
commands.add(command);
JsonNode response = tuyaApiUtil.sendControlCommand(deviceId, commands);
result.put("success", true);
result.put("message", "指令发送成功");
result.put("data", response);
} catch (Exception e) {
result.put("success", false);
result.put("message", "指令发送失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 发送批量控制指令多个指令
* @param commands 指令列表格式[{"code":"switch","value":true},{"code":"temp_set","value":26}]
*/
@PostMapping("/control/batch")
public Map<String, Object> sendBatchCommands(@RequestBody List<Map<String, Object>> commands) {
Map<String, Object> result = new HashMap<>();
try {
JsonNode response = tuyaApiUtil.sendControlCommand(deviceId, commands);
result.put("success", true);
result.put("message", "批量指令发送成功");
result.put("data", response);
} catch (Exception e) {
result.put("success", false);
result.put("message", "批量指令发送失败: " + e.getMessage());
e.printStackTrace();
}
return result;
}
}

View File

@ -42,7 +42,12 @@
<scope>compile</scope>
</dependency>
<!-- 涂鸦SDK依赖 -->
<dependency>
<groupId>com.tuya</groupId>
<artifactId>tuya-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,330 @@
package com.nu.utils;
import cn.hutool.core.util.HexUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.*;
@Slf4j
@Component
public class TuyaApiUtil {
@Value("${connector.ak}")
private String clientId;
@Value("${connector.sk}")
private String secret;
@Value("${connector.region:CN}")
private String region;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private volatile String accessToken;
private volatile long tokenExpireTime;
private final Object tokenLock = new Object();
public TuyaApiUtil() {
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
}
private String getApiHost() {
switch (region.toUpperCase()) {
case "CN":
return "https://openapi.tuyacn.com";
case "US":
return "https://openapi.tuyaus.com";
case "EU":
return "https://openapi.tuyaeu.com";
case "IN":
return "https://openapi.tuyain.com";
default:
return "https://openapi.tuyacn.com";
}
}
/**
* 生成签名 - 按照涂鸦官方规范
* 签名字符串格式: METHOD\n CONTENT-SHA256\n HEADERS\n PATH
*/
public String generateSign(String method, String url, Map<String, String> headers, String body) {
try {
// 1. 获取路径和查询参数
String path = getPathAndQuery(url);
// 2. 计算body的SHA256
String contentSha256 = getContentSha256(body);
// 3. 构建header字符串只包含client_id, t, sign_method, nonce等
String headerString = buildHeaderString(headers);
// 4. 构建签名字符串
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase()).append("\n");
sb.append(contentSha256).append("\n");
sb.append(headerString).append("\n");
sb.append(path);
String signStr = sb.toString();
log.debug("签名字符串: \n{}", signStr);
// 5. HMAC-SHA256加密
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] signBytes = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
return HexUtil.encodeHexStr(signBytes).toUpperCase();
} catch (Exception e) {
log.error("签名生成失败", e);
throw new RuntimeException("签名生成失败", e);
}
}
private String getPathAndQuery(String url) {
try {
// 移除协议和域名只保留路径和查询参数
int protocolEnd = url.indexOf("://");
if (protocolEnd != -1) {
int pathStart = url.indexOf("/", protocolEnd + 3);
if (pathStart != -1) {
return url.substring(pathStart);
}
}
return "/";
} catch (Exception e) {
log.error("解析URL失败: {}", url, e);
return url;
}
}
private String getContentSha256(String body) {
try {
if (body == null || body.isEmpty()) {
return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
}
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(body.getBytes(StandardCharsets.UTF_8));
return HexUtil.encodeHexStr(hash).toLowerCase();
} catch (NoSuchAlgorithmException e) {
log.error("计算SHA256失败", e);
return "";
}
}
private String buildHeaderString(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
// 按key排序
List<String> sortedKeys = new ArrayList<>(headers.keySet());
Collections.sort(sortedKeys);
StringBuilder sb = new StringBuilder();
for (String key : sortedKeys) {
sb.append(key).append(":").append(headers.get(key)).append("\n");
}
// 移除最后一个换行符
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
/**
* 获取Access Token
*/
public String getAccessToken() {
if (accessToken != null && System.currentTimeMillis() < tokenExpireTime) {
return accessToken;
}
synchronized (tokenLock) {
if (accessToken != null && System.currentTimeMillis() < tokenExpireTime) {
return accessToken;
}
try {
long timestamp = Instant.now().toEpochMilli();
String nonce = UUID.randomUUID().toString().replace("-", "");
String url = getApiHost() + "/v1.0/token?grant_type=1";
// 构建签名需要的headers
Map<String, String> signHeaders = new LinkedHashMap<>();
signHeaders.put("client_id", clientId);
signHeaders.put("sign_method", "HMAC-SHA256");
signHeaders.put("t", String.valueOf(timestamp));
signHeaders.put("nonce", nonce);
// 生成签名
String sign = generateSign("GET", url, signHeaders, null);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("client_id", clientId);
headers.set("sign", sign);
headers.set("t", String.valueOf(timestamp));
headers.set("sign_method", "HMAC-SHA256");
headers.set("nonce", nonce);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
log.info("Token响应: {}", response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
JsonNode jsonNode = objectMapper.readTree(response.getBody());
if (jsonNode.has("success") && jsonNode.get("success").asBoolean()) {
JsonNode result = jsonNode.get("result");
accessToken = result.get("access_token").asText();
long expireTime = result.get("expire_time").asLong();
tokenExpireTime = System.currentTimeMillis() + (expireTime * 1000) - 300000;
log.info("获取Access Token成功过期时间: {}", new Date(tokenExpireTime));
return accessToken;
} else {
String msg = jsonNode.has("msg") ? jsonNode.get("msg").asText() : "未知错误";
throw new RuntimeException("获取Token失败: " + msg);
}
} else {
throw new RuntimeException("获取Token HTTP错误: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("获取Access Token异常", e);
throw new RuntimeException("获取Access Token异常", e);
}
}
}
/**
* 发送设备控制指令
*/
public JsonNode sendControlCommand(String deviceId, List<Map<String, Object>> commands) throws Exception {
Map<String, Object> body = new HashMap<>();
body.put("commands", commands);
String path = "/v1.0/devices/" + deviceId + "/commands";
return post(path, body);
}
/**
* 获取设备状态
*/
public JsonNode getDeviceStatus(String deviceId) throws Exception {
String path = "/v1.0/devices/" + deviceId;
return get(path, null);
}
/**
* 获取设备功能点规格
*/
public JsonNode getDeviceSpecifications(String deviceId) throws Exception {
String path = "/v1.0/devices/" + deviceId + "/specifications";
return get(path, null);
}
/**
* 获取设备列表
*/
public JsonNode getDeviceList(int pageNo, int pageSize) throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("page_no", pageNo);
params.put("page_size", pageSize);
String path = "/v1.0/devices";
return get(path, params);
}
/**
* POST请求
*/
public JsonNode post(String path, Object body) throws Exception {
String bodyJson = body == null ? null : objectMapper.writeValueAsString(body);
String url = getApiHost() + path;
return executeRequest(HttpMethod.POST, url, bodyJson);
}
/**
* GET请求
*/
public JsonNode get(String path, Map<String, Object> params) throws Exception {
String url = buildUrl(path, params);
return executeRequest(HttpMethod.GET, url, null);
}
private String buildUrl(String path, Map<String, Object> params) {
String baseUrl = getApiHost() + path;
if (params == null || params.isEmpty()) {
return baseUrl;
}
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl);
for (Map.Entry<String, Object> entry : params.entrySet()) {
builder.queryParam(entry.getKey(), entry.getValue());
}
return builder.build().toUriString();
}
private JsonNode executeRequest(HttpMethod method, String url, String body) throws Exception {
long timestamp = Instant.now().toEpochMilli();
String nonce = UUID.randomUUID().toString().replace("-", "");
// 构建签名需要的headers
Map<String, String> signHeaders = new LinkedHashMap<>();
signHeaders.put("client_id", clientId);
signHeaders.put("sign_method", "HMAC-SHA256");
signHeaders.put("t", String.valueOf(timestamp));
signHeaders.put("nonce", nonce);
// 如果需要access_token
String token = getAccessToken();
signHeaders.put("access_token", token);
// 生成签名
String sign = generateSign(method.name(), url, signHeaders, body);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("client_id", clientId);
headers.set("sign", sign);
headers.set("t", String.valueOf(timestamp));
headers.set("sign_method", "HMAC-SHA256");
headers.set("nonce", nonce);
headers.set("access_token", token);
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(url, method, entity, String.class);
log.debug("响应: {}", response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
JsonNode jsonNode = objectMapper.readTree(response.getBody());
if (jsonNode.has("success") && jsonNode.get("success").asBoolean()) {
return jsonNode;
} else {
String errorMsg = jsonNode.has("msg") ? jsonNode.get("msg").asText() : "未知错误";
throw new RuntimeException("API调用失败: " + errorMsg);
}
} else {
throw new RuntimeException("HTTP请求失败: " + response.getStatusCode());
}
}
}

View File

@ -557,7 +557,11 @@
dire.preview_file
FROM nu_biz_directive_order t
LEFT JOIN nu_config_service_directive dire on t.directive_id = dire.id
WHERE t.nu_id = #{entity.nuId} AND t.employee_id = #{entity.employeeId}
WHERE t.nu_id = #{entity.nuId}
AND (
(t.employee_ids IS NOT NULL AND FIND_IN_SET(#{entity.employeeId}, t.employee_ids) > 0)
OR (t.employee_ids IS NULL AND t.employee_id = #{entity.employeeId})
)
<choose>
<when test='entity.workType == "1"'>
AND t.serv_end_time >= NOW()
@ -597,7 +601,7 @@
category.category_name,
directive.type_id,
stype.type_name,
directive.id as directive_id,
directive.id as directive_id,
directive.directive_name,
directive.immediate_file,
directive.immediate_file_focus,
@ -608,7 +612,8 @@
directive.service_attribute,
directive.service_duration,
directive.service_content,
p.sys_org_code
directive.toll_price,
directive.com_price
FROM nu_config_service_directive directive
LEFT JOIN nu_config_service_instruction_tag inst ON directive.instruction_tag_id = inst.id
LEFT JOIN nu_config_service_category category ON directive.category_id = category.id

View File

@ -332,11 +332,15 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl<DirectiveOrderMapp
DirectiveOrder beforeData = baseMapper.selectById(dto.getId());
//如果已到服务结束时间 不允许转单
if (beforeData.getServStartTime().before(new Date())) {
//如果已点击开始 不允许转单
if (beforeData.getEmpStartTime() != null) {
return Result.error("当前服务已开始,不可转单");
}
if (beforeData.getServEndTime().before(new Date())) {
return Result.error("当前服务已超时,不可协助执行");
}
UpdateWrapper<DirectiveOrder> uw = new UpdateWrapper<>();
uw.eq("serv_start_time", beforeData.getServStartTime());
@ -373,11 +377,15 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl<DirectiveOrderMapp
DirectiveOrder beforeData = baseMapper.selectById(dto.getId());
//如果已到服务结束时间 不允许转单
if (beforeData.getServStartTime().before(new Date())) {
//如果已点击开始 不允许协助执行
if (beforeData.getEmpStartTime() != null) {
return Result.error("当前服务已开始,不可协助执行");
}
if (beforeData.getServEndTime().before(new Date())) {
return Result.error("当前服务已超时,不可协助执行");
}
UpdateWrapper<DirectiveOrder> uw = new UpdateWrapper<>();
uw.eq("serv_start_time", beforeData.getServStartTime());
@ -443,6 +451,7 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl<DirectiveOrderMapp
newOrder.setServEndTime(new Date(startTime.getTime() + Integer.parseInt(directiveInfo.getServiceDuration()) * 60 * 1000L));//服务结束时间
newOrder.setOrderStartTime(new Date());//工单开始时间
newOrder.setCreateBy("即时指令派单");
newOrder.setIzMulti("N");
newOrder.setInstructionId(directiveInfo.getInstructionId());
newOrder.setInstructionName(directiveInfo.getInstructionName());
@ -461,6 +470,8 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl<DirectiveOrderMapp
newOrder.setServiceAttribute(directiveInfo.getServiceAttribute());
newOrder.setServiceDuration(directiveInfo.getServiceDuration());
newOrder.setServiceContent(directiveInfo.getServiceContent());
newOrder.setTollPrice(directiveInfo.getTollPrice());
newOrder.setComPrice(directiveInfo.getComPrice());
baseMapper.insert(newOrder);

View File

@ -43,9 +43,14 @@ public class DirectivePlanDateServiceImpl extends ServiceImpl<DirectivePlanDateM
@Override
public void generateOrder(DirectivePlanDateEntity queryParam) {
String empId = "2028395421069524993";
//所有要派发的数据查对应时间点数据 所有护理单元所有分类的数据
List<DirectivePlanDate> diretiveList = baseMapper.queryAllTaskByDateTime(queryParam);
//已派发指令不再重复派发逻辑 首先排除即时指令干扰不差cycleTypeId = 2的) 但是已派发出去的工单不知道是不是即时指令因为没有cycleTypeId
//查nuid+directive完全一致的
List<DirectiveOrder> orders = BeanUtil.copyToList(diretiveList, DirectiveOrder.class);
orders.stream().forEach(item -> {
item.setId(null);

View File

@ -428,3 +428,10 @@ mqtt:
clean-session: true
connection-timeout: 30
keep-alive-interval: 60
# 涂鸦
connector:
ak: 4mafm3qk4sx4w3j5p3jy
sk: 1de9cc0bd2e04624bfde30d4daa0e781
region: CN

View File

@ -440,3 +440,9 @@ mqtt:
clean-session: true
connection-timeout: 30
keep-alive-interval: 60
# 涂鸦
connector:
ak: 4mafm3qk4sx4w3j5p3jy
sk: 1de9cc0bd2e04624bfde30d4daa0e781
region: CN

View File

@ -128,6 +128,11 @@
<enabled>true</enabled>
</snapshots>
</repository>
<!-- 涂鸦仓库 -->
<repository>
<id>tuya-maven</id>
<url>https://maven-other.tuya.com/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>