1、增加每天23:59:50定时任务,将当天的工单 - 已开始未结束的结束时间设置为23:59:59
2、生成工单定时任务增加逻辑处理:手动触发定时任务后,如果已经派发过对应工单不再重复派发 3、涂鸦相关接口
This commit is contained in:
parent
9957093846
commit
bc3463cdd3
|
|
@ -1,7 +1,9 @@
|
|||
package com.nu.modules.commonutils;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.nu.utils.TuyaApiUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nu.connector.AirConditionerConnector;
|
||||
import com.nu.connector.DeviceConnector;
|
||||
import com.nu.entity.CommandResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
|
@ -12,112 +14,271 @@ import java.util.*;
|
|||
public class TuyaAirconApi {
|
||||
|
||||
@Autowired
|
||||
private TuyaApiUtil tuyaApiUtil;
|
||||
private DeviceConnector deviceConnector;
|
||||
|
||||
private String deviceId = "6c36cb2d2c352683bfnsm1";
|
||||
@Autowired
|
||||
private AirConditionerConnector airConditionerConnector;
|
||||
|
||||
/**
|
||||
* 测试连接
|
||||
* 获取设备信息
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
@GetMapping("/test")
|
||||
public Map<String, Object> test() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@GetMapping("/getDeviceById")
|
||||
public Map<String, Object> getDeviceById(@RequestParam String deviceId) {
|
||||
try {
|
||||
String token = tuyaApiUtil.getAccessToken();
|
||||
result.put("success", true);
|
||||
result.put("message", "连接成功");
|
||||
result.put("token", token);
|
||||
Map<String, Object> device = deviceConnector.getDeviceById(deviceId);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "连接成功",
|
||||
"data", device
|
||||
);
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "连接失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "连接失败: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态
|
||||
* 获取设备属性
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
@GetMapping("/status")
|
||||
public Map<String, Object> getDeviceStatus() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@GetMapping("/getDeviceProperties")
|
||||
public Map<String, Object> getDeviceProperties(@RequestParam String deviceId) {
|
||||
try {
|
||||
JsonNode response = tuyaApiUtil.getDeviceStatus(deviceId);
|
||||
result.put("success", true);
|
||||
result.put("data", response);
|
||||
Map<String, Object> device = deviceConnector.getDeviceProperties(deviceId);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "查询属性成功",
|
||||
"data", device
|
||||
);
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "获取设备状态失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "查询属性失败: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备功能点规格
|
||||
* 获取设备期望属性
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
@GetMapping("/specifications")
|
||||
public Map<String, Object> getDeviceSpecifications() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@GetMapping("/getDeviceDesiredProperties")
|
||||
public Map<String, Object> getDeviceDesiredProperties(@RequestParam String deviceId) {
|
||||
try {
|
||||
JsonNode response = tuyaApiUtil.getDeviceSpecifications(deviceId);
|
||||
result.put("success", true);
|
||||
result.put("data", response);
|
||||
Map<String, Object> device = deviceConnector.getDeviceDesiredProperties(deviceId);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "查询期望成功",
|
||||
"data", device
|
||||
);
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "获取设备规格失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "查询期望失败: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送控制指令(单个指令)
|
||||
* @param code 功能点code
|
||||
* @param value 功能点值
|
||||
* 获取设备期望属性
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
@PostMapping("/control/single")
|
||||
public Map<String, Object> sendSingleCommand(
|
||||
@GetMapping("/getDeviceFunctions")
|
||||
public Map<String, Object> getDeviceFunctions(@RequestParam String deviceId) {
|
||||
try {
|
||||
Map<String, Object> device = deviceConnector.getDeviceFunctions(deviceId);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "查询设备指令集成功",
|
||||
"data", device
|
||||
);
|
||||
} catch (Exception e) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "查询设备指令集失败: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送控制指令(单个)
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param code 功能点code
|
||||
* @param value 功能点值
|
||||
*/
|
||||
@PostMapping("/control")
|
||||
public Map<String, Object> sendCommand(
|
||||
@RequestParam String deviceId,
|
||||
@RequestParam String code,
|
||||
@RequestParam Object value) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@RequestParam String value) {
|
||||
try {
|
||||
Map<String, Object> command = new HashMap<>();
|
||||
command.put("code", code);
|
||||
command.put("value", value);
|
||||
// 1. 构建 input_params(注意是 JSON 字符串!!)
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put(code, value);
|
||||
|
||||
List<Map<String, Object>> commands = new ArrayList<>();
|
||||
commands.add(command);
|
||||
// 👉 转成 JSON 字符串
|
||||
String inputParams = new com.fasterxml.jackson.databind.ObjectMapper()
|
||||
.writeValueAsString(params);
|
||||
|
||||
// 2. 构建 body
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", code); // 动作 code
|
||||
body.put("input_params", inputParams); // JSON字符串
|
||||
|
||||
// 3. 调用
|
||||
Map<String, Object> result = deviceConnector.sendAction(deviceId, body);
|
||||
|
||||
return result;
|
||||
|
||||
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 Map.of("success", false, "message", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/switch")
|
||||
public Map<String, Object> switchAircon(
|
||||
@RequestParam String deviceId,
|
||||
@RequestParam Boolean on) {
|
||||
try {
|
||||
|
||||
String code = on ? "PowerOn" : "PowerOff";
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put(code, code);
|
||||
|
||||
String inputParams = new ObjectMapper().writeValueAsString(params);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", code);
|
||||
body.put("input_params", inputParams);
|
||||
|
||||
Map<String, Object> result = deviceConnector.sendAction(deviceId, body);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
return Map.of("success", false, "message", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送批量控制指令(多个指令)
|
||||
* @param commands 指令列表,格式:[{"code":"switch","value":true},{"code":"temp_set","value":26}]
|
||||
* 空调组合按键下发
|
||||
* 通过组合命令控制空调
|
||||
*
|
||||
* @param infraredId 设备ID (infrared_id)
|
||||
* @param remoteId 遥控器ID (remote_id)
|
||||
* @param power 电源:0-关闭,1-打开(必填)
|
||||
* @param mode 模式:0-制冷,1-制热,2-自动,3-送风,4-除湿(可选)
|
||||
* @param temp 温度(可选)
|
||||
* @param wind 风速:0-自动,1-低,2-中,3-高(可选)
|
||||
* @param remoteIndex 遥控器索引ID(可选)
|
||||
* @param categoryId 品类ID(可选)
|
||||
*/
|
||||
@PostMapping("/control/batch")
|
||||
public Map<String, Object> sendBatchCommands(@RequestBody List<Map<String, Object>> commands) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@PostMapping("/sendCommand")
|
||||
public Map<String, Object> sendAirconCommand(
|
||||
@RequestParam String infraredId,
|
||||
@RequestParam String remoteId,
|
||||
@RequestParam Integer power,
|
||||
@RequestParam(required = false) Integer mode,
|
||||
@RequestParam(required = false) Integer temp,
|
||||
@RequestParam(required = false) Integer wind,
|
||||
@RequestParam(required = false) Integer remoteIndex,
|
||||
@RequestParam(required = false) Integer categoryId) {
|
||||
try {
|
||||
JsonNode response = tuyaApiUtil.sendControlCommand(deviceId, commands);
|
||||
result.put("success", true);
|
||||
result.put("message", "批量指令发送成功");
|
||||
result.put("data", response);
|
||||
// 构建请求体
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("power", power);
|
||||
|
||||
if (mode != null) {
|
||||
body.put("mode", mode);
|
||||
}
|
||||
if (temp != null) {
|
||||
body.put("temp", temp);
|
||||
}
|
||||
if (wind != null) {
|
||||
body.put("wind", wind);
|
||||
}
|
||||
if (remoteIndex != null) {
|
||||
body.put("remote_index", remoteIndex);
|
||||
}
|
||||
if (categoryId != null) {
|
||||
body.put("category_id", categoryId);
|
||||
}
|
||||
|
||||
// 调用 connector 发送命令
|
||||
Map<String, Object> result = deviceConnector.sendCommandKT(infraredId, remoteId, body);
|
||||
|
||||
// 处理返回结果
|
||||
if (result != null && Boolean.TRUE.equals(result.get("success"))) {
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "空调控制指令发送成功",
|
||||
"data", result
|
||||
);
|
||||
} else {
|
||||
String errorMsg = result != null ? result.getOrDefault("message", "未知错误").toString() : "返回结果为空";
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "空调控制指令发送失败: " + errorMsg
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "批量指令发送失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "空调控制指令发送异常: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空调组合按键下发(使用Body方式传参)
|
||||
*
|
||||
* @param infraredId 设备ID
|
||||
* @param remoteId 遥控器ID
|
||||
* @param body 请求参数体
|
||||
*/
|
||||
@PostMapping("/sendCommandWithBody")
|
||||
public Map<String, Object> sendAirconCommandWithBody(
|
||||
@RequestParam String infraredId,
|
||||
@RequestParam String remoteId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
try {
|
||||
// 验证必填参数 power
|
||||
if (body == null || !body.containsKey("power")) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "缺少必填参数: power"
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object> result = deviceConnector.sendCommandKT(infraredId, remoteId, body);
|
||||
|
||||
if (result != null && Boolean.TRUE.equals(result.get("success"))) {
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "空调控制指令发送成功",
|
||||
"data", result
|
||||
);
|
||||
} else {
|
||||
String errorMsg = result != null ? result.getOrDefault("message", "未知错误").toString() : "返回结果为空";
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "空调控制指令发送失败: " + errorMsg
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", "空调控制指令发送异常: " + e.getMessage()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<dependency>
|
||||
<groupId>com.tuya</groupId>
|
||||
<artifactId>tuya-spring-boot-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.5.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.nu.connector;
|
||||
|
||||
import com.tuya.connector.api.annotations.GET;
|
||||
import com.tuya.connector.api.annotations.POST;
|
||||
import com.tuya.connector.api.annotations.Path;
|
||||
import com.tuya.connector.api.annotations.Body;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface AirConditionerConnector {
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
*/
|
||||
@GET("/v1.0/devices")
|
||||
Map<String, Object> getDeviceList();
|
||||
|
||||
/**
|
||||
* 空调控制
|
||||
*/
|
||||
@POST("/v1.0/devices/{device_id}/commands")
|
||||
Map<String, Object> control(@Path("device_id") String deviceId, @Body Map<String, Object> command);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.nu.connector;
|
||||
|
||||
import com.nu.entity.CommandResponse;
|
||||
import com.tuya.connector.api.annotations.Body;
|
||||
import com.tuya.connector.api.annotations.GET;
|
||||
import com.tuya.connector.api.annotations.POST;
|
||||
import com.tuya.connector.api.annotations.Path;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface DeviceConnector {
|
||||
|
||||
/**
|
||||
* 获取设备详情
|
||||
*/
|
||||
@GET("/v2.0/cloud/thing/{device_id}")
|
||||
Map<String, Object> getDeviceById(@Path("device_id") String deviceId);
|
||||
|
||||
/**
|
||||
* 查询属性
|
||||
*/
|
||||
@GET("/v2.0/cloud/thing/{device_id}/shadow/properties")
|
||||
Map<String, Object> getDeviceProperties(@Path("device_id") String deviceId);
|
||||
|
||||
/**
|
||||
* 查询期望属性
|
||||
*/
|
||||
@GET("/v2.0/cloud/thing/{device_id}/shadow/properties/desired")
|
||||
Map<String, Object> getDeviceDesiredProperties(@Path("device_id") String deviceId);
|
||||
|
||||
/**
|
||||
* 获取设备支持的指令集
|
||||
*/
|
||||
@GET("/v1.0/iot-03/devices/{device_id}/functions")
|
||||
Map<String, Object> getDeviceFunctions(@Path("device_id") String deviceId);
|
||||
|
||||
/**
|
||||
* 发送控制指令
|
||||
*/
|
||||
@POST("/v2.0/cloud/thing/{device_id}/shadow/actions")
|
||||
Map<String, Object> sendAction(@Path("device_id") String deviceId, @Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 发送控制指令
|
||||
*/
|
||||
@POST("/v1.0/iot-03/devices/{device_id}/commands")
|
||||
Map<String, Object> sendCommands2(@Path("device_id") String deviceId, @Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 发送控制指令
|
||||
*/
|
||||
// 在 AirConditionerConnector 中修改为:
|
||||
@POST("/v2.0/infrareds/{infrared_id}/air-conditioners/{remote_id}/scenes/command")
|
||||
Map<String, Object> sendCommandKT(
|
||||
@Path("infrared_id") String infraredId,
|
||||
@Path("remote_id") String remoteId,
|
||||
@Body Map<String, Object> body
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nu.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CommandParams {
|
||||
private String code;
|
||||
private Object value;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.nu.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CommandResponse {
|
||||
private Boolean result;
|
||||
private Long t;
|
||||
private Boolean success;
|
||||
private String tid;
|
||||
}
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.nu.modules.biz.plan.care.job;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.nu.modules.biz.order.entity.DirectiveOrder;
|
||||
import com.nu.modules.biz.order.service.IDirectiveOrderService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 结束工单定时 每天23:59:50把当天已开始未结束的 结束时间设置为23:59:59
|
||||
*/
|
||||
@Slf4j
|
||||
public class DirectiveEndOrderJob implements Job {
|
||||
@Autowired
|
||||
private IDirectiveOrderService directiveOrderService;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
||||
DirectiveOrder upData = new DirectiveOrder();
|
||||
//设置为当天23:59:59
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
upData.setEmpEndTime(new Date(calendar.get(Calendar.YEAR) - 1900,
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH),
|
||||
23, 59, 59));
|
||||
|
||||
//当天任务且已开始的
|
||||
UpdateWrapper<DirectiveOrder> uw = new UpdateWrapper<>();
|
||||
uw.apply("DATE(serv_start_time) = CURDATE()");
|
||||
uw.isNotNull("emp_start_time");
|
||||
uw.isNull("emp_end_time");
|
||||
directiveOrderService.update(upData,uw);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.nu.modules.biz.plan.care.service.impl;
|
|||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.nu.entity.CareDirectiveEntity;
|
||||
import com.nu.entity.DirectivePlanDateEntity;
|
||||
import com.nu.modules.biz.order.entity.DirectiveOrder;
|
||||
|
|
@ -12,13 +13,17 @@ import com.nu.modules.biz.plan.care.service.IDirectivePlanDateService;
|
|||
import com.nu.modules.care.api.IDirectivePlanDateApi;
|
||||
import com.nu.websocket.SdWebsocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.Lists;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Description: 服务指令编排-日期-快照表
|
||||
|
|
@ -46,40 +51,46 @@ public class DirectivePlanDateServiceImpl extends ServiceImpl<DirectivePlanDateM
|
|||
|
||||
//所有要派发的数据(查对应时间点数据 所有护理单元所有分类的数据)
|
||||
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);
|
||||
item.setEmployeeId(empId);
|
||||
item.setEmployeeName("王伟东");
|
||||
item.setOptType("1");//单人执行
|
||||
item.setServStartTime(item.getStartTime());//服务开始时间
|
||||
item.setServEndTime(item.getEndTime());//服务结束时间
|
||||
item.setOrderStartTime(new Date());//工单开始时间
|
||||
item.setCreateBy("工单定时任务");
|
||||
});
|
||||
|
||||
directiveOrderService.saveBatch(orders);
|
||||
//查询这个时间点的已派发指令
|
||||
QueryWrapper<DirectiveOrder> orderQW = new QueryWrapper<>();
|
||||
orderQW.eq("serv_start_time", queryParam.getStartTime());
|
||||
List<DirectiveOrder> havingOrderList = directiveOrderService.list(orderQW);
|
||||
//已派发指令不再重复派发逻辑 不用考虑即时指令 即时指令属于客户主动触发行为
|
||||
//如何判断重复: 查servStartTime+nuid+directive完全一致的 (查询时已经是同一年月日时分秒了 所以不用特意再判断)
|
||||
List<DirectiveOrder> genOrders = orders.stream().filter(item -> havingOrderList.stream().noneMatch(existingOrder -> existingOrder.getNuId().equals(item.getNuId()) && existingOrder.getDirectiveId().equals(item.getDirectiveId()))).collect(Collectors.toList());
|
||||
|
||||
//发送websocket消息
|
||||
try {
|
||||
// 发送数据
|
||||
JSONObject message = new JSONObject();
|
||||
message.put("type", "directiveOrder");//消息类型
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
message.put("from", "system");//发送者
|
||||
message.put("to", empId);//先固定都发给伟东
|
||||
message.put("data", orders);//业务数据
|
||||
String messageJson = message.toJSONString();
|
||||
//发送给单个用户
|
||||
sdWebsocket.sendMessage(empId, messageJson);
|
||||
log.info("【ws消息推送】发送给用户 {}: {}", empId, messageJson);
|
||||
} catch (Exception e) {
|
||||
log.error("ws发送消息失败", e);
|
||||
if (!CollectionUtils.isEmpty(genOrders)) {
|
||||
genOrders.stream().forEach(item -> {
|
||||
item.setId(null);
|
||||
item.setEmployeeId(empId);
|
||||
item.setEmployeeName("王伟东");
|
||||
item.setOptType("1");//单人执行
|
||||
item.setServStartTime(item.getStartTime());//服务开始时间
|
||||
item.setServEndTime(item.getEndTime());//服务结束时间
|
||||
item.setOrderStartTime(new Date());//工单开始时间
|
||||
item.setCreateBy("工单定时任务");
|
||||
});
|
||||
directiveOrderService.saveBatch(genOrders);
|
||||
//发送websocket消息
|
||||
try {
|
||||
// 发送数据
|
||||
JSONObject message = new JSONObject();
|
||||
message.put("type", "directiveOrder");//消息类型
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
message.put("from", "system");//发送者
|
||||
message.put("to", empId);//先固定都发给伟东
|
||||
message.put("data", genOrders);//业务数据
|
||||
String messageJson = message.toJSONString();
|
||||
//发送给单个用户
|
||||
sdWebsocket.sendMessage(empId, messageJson);
|
||||
log.info("【ws消息推送】发送给用户 {}: {}", empId, messageJson);
|
||||
} catch (Exception e) {
|
||||
log.error("ws发送消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.jeecg;
|
||||
|
||||
import com.tuya.connector.spring.annotations.ConnectorScan;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
|
|
@ -24,6 +25,7 @@ import java.util.Map;
|
|||
@EnableAsync
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.nu","org.jeecg"})
|
||||
@ConnectorScan(basePackages = "com.nu.connector")
|
||||
@MapperScan({"com.nu.**.mapper","org.jeecg.**.mapper"})
|
||||
public class NUSystemApplication extends SpringBootServletInitializer {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue