From 6e899f16bf5af577aa2a76185c6ac2dbba5f711d Mon Sep 17 00:00:00 2001 From: "1378012178@qq.com" <1378012178@qq.com> Date: Wed, 1 Apr 2026 16:41:22 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=B6=82=E9=B8=A6=E8=AE=BE=E5=A4=87a?= =?UTF-8?q?pi=E9=9B=86=E6=88=90=EF=BC=88=E6=9C=AA=E5=AE=8C=E6=88=90)=202?= =?UTF-8?q?=E3=80=81pad=E6=9C=8D=E5=8A=A1=E6=8C=87=E4=BB=A4=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=E3=80=81=E9=A6=96=E9=A1=B5=E7=9B=B8=E5=85=B3=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nu/modules/commonutils/TuyaAirconApi.java | 123 +++++++ nursing-unit-common/pom.xml | 7 +- .../main/java/com/nu/utils/TuyaApiUtil.java | 330 ++++++++++++++++++ .../order/mapper/xml/DirectiveOrderMapper.xml | 11 +- .../impl/DirectiveOrderPadServiceImpl.java | 19 +- .../impl/DirectivePlanDateServiceImpl.java | 5 + .../main/resources/application-dev-nu002.yml | 7 + .../src/main/resources/application-dev.yml | 6 + pom.xml | 5 + 9 files changed, 505 insertions(+), 8 deletions(-) create mode 100644 nursing-unit-api/src/main/java/com/nu/modules/commonutils/TuyaAirconApi.java create mode 100644 nursing-unit-common/src/main/java/com/nu/utils/TuyaApiUtil.java diff --git a/nursing-unit-api/src/main/java/com/nu/modules/commonutils/TuyaAirconApi.java b/nursing-unit-api/src/main/java/com/nu/modules/commonutils/TuyaAirconApi.java new file mode 100644 index 00000000..ac8ccc82 --- /dev/null +++ b/nursing-unit-api/src/main/java/com/nu/modules/commonutils/TuyaAirconApi.java @@ -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 test() { + Map 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 getDeviceStatus() { + Map 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 getDeviceSpecifications() { + Map 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 sendSingleCommand( + @RequestParam String code, + @RequestParam Object value) { + Map result = new HashMap<>(); + try { + Map command = new HashMap<>(); + command.put("code", code); + command.put("value", value); + + List> 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 sendBatchCommands(@RequestBody List> commands) { + Map 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; + } +} diff --git a/nursing-unit-common/pom.xml b/nursing-unit-common/pom.xml index 6376793a..447ab926 100644 --- a/nursing-unit-common/pom.xml +++ b/nursing-unit-common/pom.xml @@ -42,7 +42,12 @@ compile - + + + com.tuya + tuya-spring-boot-starter + 1.0.0 + diff --git a/nursing-unit-common/src/main/java/com/nu/utils/TuyaApiUtil.java b/nursing-unit-common/src/main/java/com/nu/utils/TuyaApiUtil.java new file mode 100644 index 00000000..42a071b4 --- /dev/null +++ b/nursing-unit-common/src/main/java/com/nu/utils/TuyaApiUtil.java @@ -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 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 headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + // 按key排序 + List 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 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 entity = new HttpEntity<>(headers); + ResponseEntity 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> commands) throws Exception { + Map 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 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 params) throws Exception { + String url = buildUrl(path, params); + return executeRequest(HttpMethod.GET, url, null); + } + + private String buildUrl(String path, Map params) { + String baseUrl = getApiHost() + path; + if (params == null || params.isEmpty()) { + return baseUrl; + } + + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl); + for (Map.Entry 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 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 entity = new HttpEntity<>(body, headers); + ResponseEntity 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()); + } + } +} diff --git a/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/mapper/xml/DirectiveOrderMapper.xml b/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/mapper/xml/DirectiveOrderMapper.xml index 7df4e711..e3978c0b 100644 --- a/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/mapper/xml/DirectiveOrderMapper.xml +++ b/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/mapper/xml/DirectiveOrderMapper.xml @@ -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}) + ) 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 diff --git a/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/service/impl/DirectiveOrderPadServiceImpl.java b/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/service/impl/DirectiveOrderPadServiceImpl.java index 724fcb98..e61f31bc 100644 --- a/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/service/impl/DirectiveOrderPadServiceImpl.java +++ b/nursing-unit-services/nu-services-biz/src/main/java/com/nu/modules/biz/order/service/impl/DirectiveOrderPadServiceImpl.java @@ -332,11 +332,15 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl uw = new UpdateWrapper<>(); uw.eq("serv_start_time", beforeData.getServStartTime()); @@ -373,11 +377,15 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl uw = new UpdateWrapper<>(); uw.eq("serv_start_time", beforeData.getServStartTime()); @@ -443,6 +451,7 @@ public class DirectiveOrderPadServiceImpl extends ServiceImpl diretiveList = baseMapper.queryAllTaskByDateTime(queryParam); + //已派发指令不再重复派发逻辑 首先排除即时指令干扰(不差cycleTypeId = 2的) 但是已派发出去的工单不知道是不是即时指令(因为没有cycleTypeId) + //查nuid+directive完全一致的 + + List orders = BeanUtil.copyToList(diretiveList, DirectiveOrder.class); orders.stream().forEach(item -> { item.setId(null); diff --git a/nursing-unit-system/nu-system-start/src/main/resources/application-dev-nu002.yml b/nursing-unit-system/nu-system-start/src/main/resources/application-dev-nu002.yml index 14ec79ee..f4c6de8b 100644 --- a/nursing-unit-system/nu-system-start/src/main/resources/application-dev-nu002.yml +++ b/nursing-unit-system/nu-system-start/src/main/resources/application-dev-nu002.yml @@ -428,3 +428,10 @@ mqtt: clean-session: true connection-timeout: 30 keep-alive-interval: 60 + + +# 涂鸦 +connector: + ak: 4mafm3qk4sx4w3j5p3jy + sk: 1de9cc0bd2e04624bfde30d4daa0e781 + region: CN diff --git a/nursing-unit-system/nu-system-start/src/main/resources/application-dev.yml b/nursing-unit-system/nu-system-start/src/main/resources/application-dev.yml index 5c291ab9..a9e7f83d 100644 --- a/nursing-unit-system/nu-system-start/src/main/resources/application-dev.yml +++ b/nursing-unit-system/nu-system-start/src/main/resources/application-dev.yml @@ -440,3 +440,9 @@ mqtt: clean-session: true connection-timeout: 30 keep-alive-interval: 60 + +# 涂鸦 +connector: + ak: 4mafm3qk4sx4w3j5p3jy + sk: 1de9cc0bd2e04624bfde30d4daa0e781 + region: CN diff --git a/pom.xml b/pom.xml index 502050fe..7f285c30 100644 --- a/pom.xml +++ b/pom.xml @@ -128,6 +128,11 @@ true + + + tuya-maven + https://maven-other.tuya.com/repository/maven-public/ +