feat(unisoftiot): 新增设备和产品相关功能
- 添加 Account、Device、Product 等实体类
- 实现设备控制、产品列表等 API 接口- 集成 UniSoftAccount 配置和请求处理
- 新增 JSON 解析和 OkHttp 工具类
- 更新数据库配置和 Mapper 接口
 
	
	
	
	
	
	
		
		2 files deleted
	
		
		3 files modified
	
		
		24 files added
	
	
 
	
	
	
	
	
	
	
	
 |  |  | 
 |  |  |     </properties> | 
 |  |  |  | 
 |  |  |     <dependencies> | 
 |  |  |  | 
 |  |  |         <dependency> | 
 |  |  |             <groupId>org.json</groupId> | 
 |  |  |             <artifactId>json</artifactId> | 
 |  |  |             <version>20230618</version> | 
 |  |  |         </dependency> | 
 |  |  |         <dependency> | 
 |  |  |             <groupId>com.github.wechatpay-apiv3</groupId> | 
 |  |  |             <artifactId>wechatpay-apache-httpclient</artifactId> | 
 
 |  |  | 
 |  |  | @EnableScheduling | 
 |  |  | @SpringBootApplication | 
 |  |  | @EnableTransactionManagement | 
 |  |  | @MapperScan("cc.mrbird.febs.*.mapper") | 
 |  |  | @MapperScan({"cc.mrbird.febs.*.mapper", "cc.mrbird.febs.unisoftiot.*.mapper"}) | 
 |  |  | public class FebsShiroApplication { | 
 |  |  |  | 
 |  |  |     public static void main(String[] args) { | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.entity; | 
 |  |  |  | 
 |  |  | import lombok.Data; | 
 |  |  |  | 
 |  |  | @Data | 
 |  |  | public class Account { | 
 |  |  |  | 
 |  |  |     private String appId; | 
 |  |  |  | 
 |  |  |     private String appSecret; | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.entity; | 
 |  |  |  | 
 |  |  | import lombok.Data; | 
 |  |  |  | 
 |  |  | @Data | 
 |  |  | public class Device { | 
 |  |  |  | 
 |  |  |     private String deviceId; | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.entity; | 
 |  |  |  | 
 |  |  | import lombok.Data; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * Product类代表一个产品实体,用于在平台中存储和管理产品相关信息 | 
 |  |  |  * 它包含了产品的基本属性,如产品ID、型号、名称、图标、生产商、设备数量和备注 | 
 |  |  |  */ | 
 |  |  | @Data | 
 |  |  | public class Product { | 
 |  |  |     /** | 
 |  |  |      * 产品ID,平台唯一,固定不变 | 
 |  |  |      */ | 
 |  |  |     private int id; | 
 |  |  |     /** | 
 |  |  |      * 产品型号 | 
 |  |  |      */ | 
 |  |  |     private String model; | 
 |  |  |     /** | 
 |  |  |      * 产品名称 | 
 |  |  |      */ | 
 |  |  |     private String title; | 
 |  |  |     /** | 
 |  |  |      * 产品图标 | 
 |  |  |      */ | 
 |  |  |     private String icon; | 
 |  |  |     /** | 
 |  |  |      * 生产商,Unisoft或自定义 | 
 |  |  |      */ | 
 |  |  |     private String producer; | 
 |  |  |     /** | 
 |  |  |      * 工作台(控制台)下此产品类型的设备数量 | 
 |  |  |      */ | 
 |  |  |     private Integer device; | 
 |  |  |     /** | 
 |  |  |      * 备注 | 
 |  |  |      */ | 
 |  |  |     private String remark; | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.mapper; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Account; | 
 |  |  | import com.baomidou.mybatisplus.core.mapper.BaseMapper; | 
 |  |  |  | 
 |  |  | public interface AccountMapper extends BaseMapper<Account> { | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.mapper; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Device; | 
 |  |  | import com.baomidou.mybatisplus.core.mapper.BaseMapper; | 
 |  |  |  | 
 |  |  | public interface DeviceMapper extends BaseMapper<Device> { | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.mapper; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Product; | 
 |  |  | import com.baomidou.mybatisplus.core.mapper.BaseMapper; | 
 |  |  |  | 
 |  |  | public interface ProductMapper extends BaseMapper<Product> { | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Device; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.IService; | 
 |  |  |  | 
 |  |  | public interface DeviceService extends IService<Device> { | 
 |  |  |  | 
 |  |  |     //控制设备,向设备下发指令 | 
 |  |  |     String controlDevice(Device device); | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Account; | 
 |  |  | import cc.mrbird.febs.unisoftiot.config.UniSoftAccount; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.IService; | 
 |  |  |  | 
 |  |  | public interface InitAccountService  extends IService<Account> { | 
 |  |  |  | 
 |  |  |     UniSoftAccount initAccount(Account account); | 
 |  |  |  | 
 |  |  |     UniSoftAccount initAccountByAppInfo(String appId, String appSecret); | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Product; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.vo.ApiProductVo; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.IService; | 
 |  |  |  | 
 |  |  | import java.util.List; | 
 |  |  |  | 
 |  |  | public interface ProductService  extends IService<Product> { | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 获取产品列表 | 
 |  |  |      */ | 
 |  |  |     List<ApiProductVo> getProductList(); | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service.impl; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Account; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Device; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.mapper.DeviceMapper; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.service.DeviceService; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.service.InitAccountService; | 
 |  |  | import cc.mrbird.febs.unisoftiot.config.UniSoftAccount; | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.DeviceRequestUrlEnum; | 
 |  |  | import cc.mrbird.febs.unisoftiot.utils.UrlUtils; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | 
 |  |  | import lombok.RequiredArgsConstructor; | 
 |  |  | import lombok.extern.slf4j.Slf4j; | 
 |  |  | import org.springframework.stereotype.Service; | 
 |  |  |  | 
 |  |  | import java.util.LinkedHashMap; | 
 |  |  |  | 
 |  |  | @Slf4j | 
 |  |  | @Service | 
 |  |  | @RequiredArgsConstructor | 
 |  |  | public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> implements DeviceService { | 
 |  |  |  | 
 |  |  |     private final InitAccountService initAccountService; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 控制设备的方法 | 
 |  |  |      * 该方法用于向设备发送控制指令,例如开关设备等 | 
 |  |  |      * | 
 |  |  |      * @param device 设备对象,包含设备的相关信息 | 
 |  |  |      * @return 返回控制设备的结果,以字符串形式表示 | 
 |  |  |      */ | 
 |  |  |     @Override | 
 |  |  |     public String controlDevice(Device device) { | 
 |  |  |         // 初始化账户服务,为设备控制做准备 | 
 |  |  |         UniSoftAccount uniSoftAccount = initAccountService.initAccountByAppInfo(UrlUtils.APP_ID,UrlUtils.APP_SECRET); | 
 |  |  |  | 
 |  |  |         // 创建参数集合,用于设备控制 | 
 |  |  |         LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); | 
 |  |  |         // 添加控制设备的参数,这里的例子是开启设备的某个功能 | 
 |  |  |         parameters.put("device", "1438"); | 
 |  |  |         parameters.put("power3", "1"); | 
 |  |  |  | 
 |  |  |         log.info("parameters",parameters.toString()); | 
 |  |  |  | 
 |  |  |         // 发送签名请求,控制设备 | 
 |  |  |         // 这里使用了签名请求来确保设备控制指令的安全性 | 
 |  |  |         return uniSoftAccount.requestHandler.sendSignedRequest( | 
 |  |  |                 UrlUtils.BASE_URL, | 
 |  |  |                 DeviceRequestUrlEnum.DEVICE_CONTROL.getRequestUrl(), | 
 |  |  |                 parameters, | 
 |  |  |                 DeviceRequestUrlEnum.DEVICE_CONTROL.getRequestMethod()); | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service.impl; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.service.InitAccountService; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Account; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.mapper.AccountMapper; | 
 |  |  | import cc.mrbird.febs.unisoftiot.config.UniSoftAccount; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | 
 |  |  | import lombok.RequiredArgsConstructor; | 
 |  |  | import lombok.extern.slf4j.Slf4j; | 
 |  |  | import org.springframework.stereotype.Service; | 
 |  |  |  | 
 |  |  | @Slf4j | 
 |  |  | @Service | 
 |  |  | @RequiredArgsConstructor | 
 |  |  | public class InitAccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements InitAccountService { | 
 |  |  |     @Override | 
 |  |  |     public UniSoftAccount initAccount(Account account) { | 
 |  |  |         return new UniSoftAccount(account.getAppId(),account.getAppSecret()); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     @Override | 
 |  |  |     public UniSoftAccount initAccountByAppInfo(String appId, String appSecret) { | 
 |  |  |         Account account = new Account(); | 
 |  |  |         account.setAppId(appId); | 
 |  |  |         account.setAppSecret(appSecret); | 
 |  |  |         return this.initAccount(account); | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.service.impl; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.entity.Product; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.mapper.ProductMapper; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.service.InitAccountService; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.service.ProductService; | 
 |  |  | import cc.mrbird.febs.unisoftiot.api.vo.ApiProductVo; | 
 |  |  | import cc.mrbird.febs.unisoftiot.config.UniSoftAccount; | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.ProductRequestUrlEnum; | 
 |  |  | import cc.mrbird.febs.unisoftiot.utils.UrlUtils; | 
 |  |  | import cn.hutool.json.JSONObject; | 
 |  |  | import cn.hutool.json.JSONUtil; | 
 |  |  | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | 
 |  |  | import lombok.RequiredArgsConstructor; | 
 |  |  | import lombok.extern.slf4j.Slf4j; | 
 |  |  | import org.springframework.stereotype.Service; | 
 |  |  |  | 
 |  |  | import java.util.LinkedHashMap; | 
 |  |  | import java.util.List; | 
 |  |  |  | 
 |  |  | @Slf4j | 
 |  |  | @Service | 
 |  |  | @RequiredArgsConstructor | 
 |  |  | public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { | 
 |  |  |  | 
 |  |  |     private final InitAccountService initAccountService; | 
 |  |  |     @Override | 
 |  |  |     public List<ApiProductVo> getProductList() { | 
 |  |  |         UniSoftAccount uniSoftAccount = initAccountService.initAccountByAppInfo(UrlUtils.APP_ID, UrlUtils.APP_SECRET); | 
 |  |  |  | 
 |  |  |         // 创建参数集合 | 
 |  |  |         LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); | 
 |  |  |         parameters.put("q", 1); | 
 |  |  |         String bodyStr = uniSoftAccount.requestHandler.sendSignedRequest( | 
 |  |  |                 UrlUtils.BASE_URL, ProductRequestUrlEnum.PRODUCT_LIST.getRequestUrl(), | 
 |  |  |                 parameters, ProductRequestUrlEnum.PRODUCT_LIST.getRequestMethod()); | 
 |  |  |  | 
 |  |  |         JSONObject jsonObject = JSONUtil.parseObj(bodyStr); | 
 |  |  |         return jsonObject.getJSONArray("data").toList(ApiProductVo.class); | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.api.vo; | 
 |  |  |  | 
 |  |  | import lombok.Data; | 
 |  |  |  | 
 |  |  | @Data | 
 |  |  | public class ApiProductVo { | 
 |  |  |     /** | 
 |  |  |      * 产品ID,平台唯一,固定不变 | 
 |  |  |      */ | 
 |  |  |     private int id; | 
 |  |  |     /** | 
 |  |  |      * 产品型号 | 
 |  |  |      */ | 
 |  |  |     private String model; | 
 |  |  |     /** | 
 |  |  |      * 产品名称 | 
 |  |  |      */ | 
 |  |  |     private String title; | 
 |  |  |     /** | 
 |  |  |      * 产品图标 | 
 |  |  |      */ | 
 |  |  |     private String icon; | 
 |  |  |     /** | 
 |  |  |      * 生产商,Unisoft或自定义 | 
 |  |  |      */ | 
 |  |  |     private String producer; | 
 |  |  |     /** | 
 |  |  |      * 工作台(控制台)下此产品类型的设备数量 | 
 |  |  |      */ | 
 |  |  |     private Integer device; | 
 |  |  |     /** | 
 |  |  |      * 备注 | 
 |  |  |      */ | 
 |  |  |     private String remark; | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.config; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.common.exception.FebsException; | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.HttpMethod; | 
 |  |  | import okhttp3.MediaType; | 
 |  |  | import okhttp3.Request; | 
 |  |  | import okhttp3.RequestBody; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 请求构建器类,用于构建HTTP请求 | 
 |  |  |  */ | 
 |  |  | public final class RequestBuilder { | 
 |  |  |  | 
 |  |  |     // 定义JSON类型的媒体类型常量 | 
 |  |  |     private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8"); | 
 |  |  |  | 
 |  |  |     // 私有构造方法,防止实例化 | 
 |  |  |     private RequestBuilder() { | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 根据提供的参数构建HTTP请求 | 
 |  |  |      * | 
 |  |  |      * @param fullUrl    完整的请求URL | 
 |  |  |      * @param body       请求体内容 | 
 |  |  |      * @param httpMethod HTTP方法枚举类型 | 
 |  |  |      * @return 构建好的Request对象 | 
 |  |  |      * @throws FebsException 如果URL无效或HTTP方法不支持,则抛出此异常 | 
 |  |  |      */ | 
 |  |  |     public static Request buildRequest(String fullUrl, String body, HttpMethod httpMethod) { | 
 |  |  |         // 校验 URL | 
 |  |  |         if (fullUrl == null || fullUrl.isEmpty()) { | 
 |  |  |             throw new FebsException("Invalid URL: URL cannot be null or empty"); | 
 |  |  |         } | 
 |  |  |  | 
 |  |  |         // 创建Request.Builder对象用于配置请求 | 
 |  |  |         Request.Builder builder = new Request.Builder(); | 
 |  |  |         try { | 
 |  |  |             // 根据HTTP方法构建不同的请求 | 
 |  |  |             final Request request; | 
 |  |  |             switch (httpMethod) { | 
 |  |  |                 case POST: | 
 |  |  |                     // 构建POST请求,并设置请求体为JSON类型 | 
 |  |  |                     request = builder | 
 |  |  |                             .url(fullUrl) | 
 |  |  |                             .post(RequestBody.create(JSON_TYPE, body)) | 
 |  |  |                             .addHeader("X-APISpace-Token","") | 
 |  |  |                             .addHeader("Content-Type","") | 
 |  |  |                             .build(); | 
 |  |  |                     break; | 
 |  |  |                 case GET: | 
 |  |  |                     // 构建GET请求 | 
 |  |  |                     request = builder | 
 |  |  |                             .url(fullUrl) | 
 |  |  |                             .get() | 
 |  |  |                             .addHeader("Content-Type", "application/x-www-form-urlencoded") | 
 |  |  |                             .build(); | 
 |  |  |                     break; | 
 |  |  |                 default: | 
 |  |  |                     // 如果HTTP方法不支持,抛出异常 | 
 |  |  |                     throw new FebsException("Invalid HTTP method: " + httpMethod); | 
 |  |  |             } | 
 |  |  |             // 返回构建好的请求对象 | 
 |  |  |             return request; | 
 |  |  |         } catch (IllegalArgumentException e) { | 
 |  |  |             // 如果URL格式错误,抛出异常 | 
 |  |  |             throw new FebsException("Invalid URL: " + e.getMessage()); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |  | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.config; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.common.exception.FebsException; | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.HttpMethod; | 
 |  |  | import cc.mrbird.febs.unisoftiot.utils.UrlUtils; | 
 |  |  | import cn.hutool.json.JSONUtil; | 
 |  |  | import com.alibaba.fastjson.JSON; | 
 |  |  | import lombok.extern.slf4j.Slf4j; | 
 |  |  | import okhttp3.Request; | 
 |  |  |  | 
 |  |  | import java.util.LinkedHashMap; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 请求处理器类,用于构建和发送API请求 | 
 |  |  |  */ | 
 |  |  | @Slf4j | 
 |  |  | public class RequestHandler { | 
 |  |  |  | 
 |  |  |     private String appId; | 
 |  |  |     private String appSecret; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 构造函数,初始化appId和appSecret | 
 |  |  |      * | 
 |  |  |      * @param appId     应用ID | 
 |  |  |      * @param appSecret 应用密钥 | 
 |  |  |      */ | 
 |  |  |     public RequestHandler(String appId,String appSecret){ | 
 |  |  |         this.appId = appId; | 
 |  |  |         this.appSecret = appSecret; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 发送API请求 | 
 |  |  |      * | 
 |  |  |      * @param baseUrl       基础URL | 
 |  |  |      * @param urlPath       URL路径 | 
 |  |  |      * @param parameters    请求参数 | 
 |  |  |      * @param httpMethod    HTTP方法 | 
 |  |  |      * @return 请求结果字符串 | 
 |  |  |      * @throws FebsException 当HTTP方法不支持时抛出异常 | 
 |  |  |      */ | 
 |  |  |     private String sendApiRequest(String baseUrl, | 
 |  |  |                                   String urlPath, | 
 |  |  |                                   LinkedHashMap<String, Object> parametersw, | 
 |  |  |                                   HttpMethod httpMethod) { | 
 |  |  |  | 
 |  |  |         // 创建参数集合,用于设备控制 | 
 |  |  |         LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); | 
 |  |  |         // 添加控制设备的参数,这里的例子是开启设备的某个功能 | 
 |  |  |         parameters.put("device", "1438"); | 
 |  |  |         parameters.put("power3", "1"); | 
 |  |  |         log.info("parameters",parameters.toString()); | 
 |  |  |         String fullUrl = ""; | 
 |  |  |         Request request; | 
 |  |  |         // 根据HTTP方法构建请求 | 
 |  |  |         switch (httpMethod) { | 
 |  |  |             case POST: | 
 |  |  |                 // 对于POST请求,将参数序列化为JSON体 | 
 |  |  |                 fullUrl = UrlUtils.getUrl(baseUrl,appId,appSecret,urlPath,null); | 
 |  |  |                 String body = JSON.toJSONString(parameters); | 
 |  |  |                 log.info("parametersJson:", JSONUtil.parseObj(parameters)); | 
 |  |  |                 log.info("body:",body); | 
 |  |  |                 request = RequestBuilder.buildRequest(fullUrl, body,httpMethod); | 
 |  |  |                 break; | 
 |  |  |             case GET: | 
 |  |  |                 // 对于GET请求,将参数直接拼接到URL中 | 
 |  |  |                 fullUrl = UrlUtils.getUrl(baseUrl,appId,appSecret,urlPath,parameters); | 
 |  |  |                 request = RequestBuilder.buildRequest(fullUrl, null,httpMethod); | 
 |  |  |                 break; | 
 |  |  |             default: | 
 |  |  |                 // 不支持的HTTP方法抛出异常 | 
 |  |  |                 throw new FebsException("[RequestHandler] HttpMethod 不支持: " + httpMethod); | 
 |  |  |         } | 
 |  |  |  | 
 |  |  |         // 记录请求信息 | 
 |  |  |         log.info("{} {}", httpMethod, fullUrl); | 
 |  |  |  | 
 |  |  |         // 发送请求并处理响应 | 
 |  |  |         return ResponseHandler.handleResponse(request); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 发送签名请求,确保appId和appSecret不为空 | 
 |  |  |      * | 
 |  |  |      * @param baseUrl       基础URL | 
 |  |  |      * @param urlPath       URL路径 | 
 |  |  |      * @param parameters    请求参数 | 
 |  |  |      * @param httpMethod    HTTP方法 | 
 |  |  |      * @return 请求结果字符串 | 
 |  |  |      * @throws FebsException 当appId或appSecret为空时抛出异常 | 
 |  |  |      */ | 
 |  |  |     public String sendSignedRequest(String baseUrl, | 
 |  |  |                                     String urlPath, | 
 |  |  |                                     LinkedHashMap<String, Object> parameters, | 
 |  |  |                                     HttpMethod httpMethod) { | 
 |  |  |         // 校验appId和appSecret | 
 |  |  |         if (null == appId || appId.isEmpty() || null == appSecret || appSecret.isEmpty()) { | 
 |  |  |             throw new FebsException("[RequestHandler] appId/appSecret 不能为空!"); | 
 |  |  |         } | 
 |  |  |         // 调用sendApiRequest方法发送请求 | 
 |  |  |         return sendApiRequest(baseUrl, urlPath, parameters, httpMethod); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.config; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.common.exception.FebsException; | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.ResponseCodeEnum; | 
 |  |  | import cc.mrbird.febs.unisoftiot.utils.OkHttpUtils; | 
 |  |  | import cn.hutool.core.util.StrUtil; | 
 |  |  | import cn.hutool.json.JSONObject; | 
 |  |  | import okhttp3.Request; | 
 |  |  | import okhttp3.Response; | 
 |  |  | import okhttp3.ResponseBody; | 
 |  |  | import org.json.JSONException; | 
 |  |  |  | 
 |  |  | import java.io.IOException; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 响应处理器类,用于处理OKHTTP请求的响应 | 
 |  |  |  */ | 
 |  |  | public final class ResponseHandler { | 
 |  |  |  | 
 |  |  |     // 错误信息前缀 | 
 |  |  |     private static final String ERROR_PREFIX = "[ResponseHandler] OKHTTP Error: "; | 
 |  |  |  | 
 |  |  |     // 私有构造方法,防止实例化 | 
 |  |  |     private ResponseHandler() { | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 处理HTTP响应 | 
 |  |  |      * @param request 请求对象 | 
 |  |  |      * @return 响应体字符串 | 
 |  |  |      * @throws FebsException 当响应处理失败时抛出 | 
 |  |  |      */ | 
 |  |  |     public static String handleResponse(Request request) { | 
 |  |  |         try ( | 
 |  |  |                 // 执行HTTP请求并获取响应 | 
 |  |  |                 Response response = OkHttpUtils.builder().okHttpClient.newCall(request).execute() | 
 |  |  |         ) { | 
 |  |  |             // 获取响应体内容 | 
 |  |  |             String responseBodyStr = getResponseBodyAsString(response.body()); | 
 |  |  |             if (ResponseCodeEnum.isFail(response.code())) { | 
 |  |  |                 throw new FebsException(ERROR_PREFIX + responseBodyStr); | 
 |  |  |             } | 
 |  |  |             // 如果响应内容为空,则抛出异常 | 
 |  |  |             if(StrUtil.isEmpty(responseBodyStr)){ | 
 |  |  |                 throw new FebsException(ERROR_PREFIX + "请求返回参数为空"); | 
 |  |  |             } | 
 |  |  |             // 解析响应内容为JSON对象 | 
 |  |  |             JSONObject jsonObject = new JSONObject(responseBodyStr); | 
 |  |  |             // 获取响应码 | 
 |  |  |             int code = jsonObject.getInt("code"); | 
 |  |  |             // 如果响应码表示失败,则处理错误响应 | 
 |  |  |             if(ResponseCodeEnum.isFail(code)){ | 
 |  |  |                 throw handleErrorResponse(code, responseBodyStr); | 
 |  |  |             } | 
 |  |  |             // 获取响应消息 | 
 |  |  |             String msg = jsonObject.getStr("msg"); | 
 |  |  |             // 如果响应消息不正确,则抛出异常 | 
 |  |  |             if(!ResponseCodeEnum.SUCCESS.getMsg().equals(msg)){ | 
 |  |  |                 throw new FebsException(ERROR_PREFIX + "请求返回参数msg不正确"); | 
 |  |  |             } | 
 |  |  |             // 返回响应内容 | 
 |  |  |             return responseBodyStr; | 
 |  |  |         } catch (IOException | IllegalStateException | JSONException e) { | 
 |  |  |             // 捕获异常并抛出自定义异常 | 
 |  |  |             throw new FebsException(ERROR_PREFIX + e.getMessage()); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 处理错误响应 | 
 |  |  |      * @param code 响应码 | 
 |  |  |      * @param responseBodyStr 响应体字符串 | 
 |  |  |      * @return 自定义异常 | 
 |  |  |      * @throws JSONException 当JSON解析失败时抛出 | 
 |  |  |      */ | 
 |  |  |     private static FebsException handleErrorResponse(int code, String responseBodyStr) { | 
 |  |  |         try { | 
 |  |  |             // 获取错误描述和消息 | 
 |  |  |             String desc = ResponseCodeEnum.getDesc(code); | 
 |  |  |             String msg = ResponseCodeEnum.getMsg(code); | 
 |  |  |             // 构造并返回自定义异常 | 
 |  |  |             return new FebsException(ERROR_PREFIX + "{" + msg + "},{" + desc + "}"); | 
 |  |  |         } catch (JSONException e) { | 
 |  |  |             // 如果JSON解析失败,构造并返回自定义异常 | 
 |  |  |             throw new FebsException(ERROR_PREFIX + "{" + responseBodyStr + "}"); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 获取响应体内容作为字符串 | 
 |  |  |      * @param body 响应体 | 
 |  |  |      * @return 响应体字符串 | 
 |  |  |      * @throws IOException 当读取响应体失败时抛出 | 
 |  |  |      */ | 
 |  |  |     private static String getResponseBodyAsString(ResponseBody body) throws IOException { | 
 |  |  |         if (body != null) { | 
 |  |  |             // 如果响应体不为空,读取并返回内容 | 
 |  |  |             return body.string(); | 
 |  |  |         } else { | 
 |  |  |             // 如果响应体为空,返回空字符串 | 
 |  |  |             return ""; | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.config; | 
 |  |  |  | 
 |  |  | import lombok.Data; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 类说明:统一软账号配置类 | 
 |  |  |  * 字段说明: | 
 |  |  |  *  - appId: 应用ID,用于标识一个应用 | 
 |  |  |  *  - appSecret: 应用密钥,用于验证应用的身份 | 
 |  |  |  *  - requestHandler: 处理HTTP请求的处理器,用于封装HTTP请求的处理逻辑 | 
 |  |  |  */ | 
 |  |  | @Data | 
 |  |  | public class UniSoftAccount { | 
 |  |  |  | 
 |  |  |     private String appId; | 
 |  |  |  | 
 |  |  |     private String appSecret; | 
 |  |  |  | 
 |  |  |     // 处理HTTP请求的处理器 | 
 |  |  |     public RequestHandler requestHandler; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 构造方法说明:初始化UniSoftAccount实例 | 
 |  |  |      * 参数说明: | 
 |  |  |      *  - appId: 应用ID,用于标识一个应用 | 
 |  |  |      *  - appSecret: 应用密钥,用于验证应用的身份 | 
 |  |  |      *  - requestHandler: 处理HTTP请求的处理器,用于封装HTTP请求的处理逻辑 | 
 |  |  |      */ | 
 |  |  |     public UniSoftAccount(String appId, String appSecret){ | 
 |  |  |         this.appId = appId; | 
 |  |  |         this.appSecret = appSecret; | 
 |  |  |         this.requestHandler = new RequestHandler(appId, appSecret); | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.enums; | 
 |  |  |  | 
 |  |  | import lombok.Getter; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 设备请求URL枚举类 | 
 |  |  |  * 用于定义与设备相关的API请求的URL和方法 | 
 |  |  |  */ | 
 |  |  | @Getter | 
 |  |  | public enum DeviceRequestUrlEnum { | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 设备控制请求配置 | 
 |  |  |      * 使用POST方法发送设备控制指令 | 
 |  |  |      */ | 
 |  |  |     DEVICE_CONTROL(HttpMethod.POST,"/device/control"), | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 设备分组请求配置 | 
 |  |  |      * 使用POST方法对设备进行分组操作 | 
 |  |  |      */ | 
 |  |  |     DEVICE_GROUP(HttpMethod.POST,"/device/group"), | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 设备标签请求配置 | 
 |  |  |      * 使用POST方法添加或修改设备标签 | 
 |  |  |      */ | 
 |  |  |     DEVICE_TAG(HttpMethod.POST,"/device/tag"), | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 设备列表请求配置 | 
 |  |  |      * 使用POST方法获取设备列表 | 
 |  |  |      */ | 
 |  |  |     DEVICE_LIST(HttpMethod.POST,"/device/list"), | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 设备信息请求配置 | 
 |  |  |      * 使用POST方法获取特定设备的信息 | 
 |  |  |      */ | 
 |  |  |     DEVICE_INFO(HttpMethod.POST,"/device/info"); | 
 |  |  |  | 
 |  |  |     // 请求方法,如POST、GET等 | 
 |  |  |     private HttpMethod requestMethod; | 
 |  |  |  | 
 |  |  |     // 请求的URL路径 | 
 |  |  |     private String requestUrl; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 构造函数,初始化设备请求的URL和方法 | 
 |  |  |      * | 
 |  |  |      * @param requestMethod HTTP请求方法,例如POST | 
 |  |  |      * @param requestUrl HTTP请求的URL路径 | 
 |  |  |      */ | 
 |  |  |     DeviceRequestUrlEnum(HttpMethod requestMethod, String requestUrl) { | 
 |  |  |         this.requestMethod = requestMethod; | 
 |  |  |         this.requestUrl = requestUrl; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.enums; | 
 |  |  |  | 
 |  |  | public enum HttpMethod { | 
 |  |  |     POST, | 
 |  |  |     GET, | 
 |  |  |     PUT, | 
 |  |  |     DELETE, | 
 |  |  |     INVALID | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.enums; | 
 |  |  |  | 
 |  |  | import lombok.Getter; | 
 |  |  |  | 
 |  |  | @Getter | 
 |  |  | public enum ProductRequestUrlEnum { | 
 |  |  |  | 
 |  |  |  | 
 |  |  |     PRODUCT_LIST(HttpMethod.POST,"/product/list"); | 
 |  |  |  | 
 |  |  |     // 请求方法,如POST、GET等 | 
 |  |  |     private HttpMethod requestMethod; | 
 |  |  |  | 
 |  |  |     // 请求的URL路径 | 
 |  |  |     private String requestUrl; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 构造函数,初始化设备请求的URL和方法 | 
 |  |  |      * | 
 |  |  |      * @param requestMethod HTTP请求方法,例如POST | 
 |  |  |      * @param requestUrl HTTP请求的URL路径 | 
 |  |  |      */ | 
 |  |  |     ProductRequestUrlEnum(HttpMethod requestMethod, String requestUrl) { | 
 |  |  |         this.requestMethod = requestMethod; | 
 |  |  |         this.requestUrl = requestUrl; | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.enums; | 
 |  |  |  | 
 |  |  | import lombok.Getter; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * 响应码枚举类,用于定义API响应的各种状态码及其含义 | 
 |  |  |  */ | 
 |  |  | @Getter | 
 |  |  | public enum ResponseCodeEnum { | 
 |  |  |  | 
 |  |  |     // 定义各种错误响应码和成功响应码 | 
 |  |  |     FAIL_5009(5009, "too many request", "单个设备访问最高限制1次/秒,请勿超过此限制"), | 
 |  |  |     FAIL_5008(5008, "ip:*.*.*.* is not in white list", "开启了IP检查,但接口访问IP不在白名单内,请在控制台的开发设置中,将IP加入白名单"), | 
 |  |  |     FAIL_5007(5007, "bad debug key", "指定了debug参数,但未开启调试模式,或者是debug参数不正确或已过期"), | 
 |  |  |     FAIL_5006(5006, "bad sign", "签名错误,正确签名为 md5(md5(开发者密码) + (url中的ts参数))"), | 
 |  |  |     FAIL_5005(5005, "please set app secret", "未设置开发者密码,请在控制台的开发设置中进行设置"), | 
 |  |  |     FAIL_5004(5004, "miss param sign", "请在请求接口中指定sign(签名),这是一个计算值"), | 
 |  |  |     FAIL_5003(5003, "bad ts", "时间戳错误,ts为请求发生时的时间,是一个动态的值,必须为中国时间"), | 
 |  |  |     FAIL_5002(5002, "miss param ts", "请在请求接口中指定ts(时间戳),取值为请求时的时间戳,10位数字"), | 
 |  |  |     FAIL_5001(5001, "miss app id", "请求地址为 https://iot-api.unisoft.cn/{APPID}/,缺少{APPID},请在控制台查看您的appid"), | 
 |  |  |     SUCCESS(200, "ok", ""); | 
 |  |  |  | 
 |  |  |     // 响应码 | 
 |  |  |     private int code; | 
 |  |  |     // 简短消息 | 
 |  |  |     private String msg; | 
 |  |  |     // 描述信息 | 
 |  |  |     private String desc; | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 构造函数,初始化响应码、消息和描述 | 
 |  |  |      * | 
 |  |  |      * @param code 响应码 | 
 |  |  |      * @param msg  简短消息 | 
 |  |  |      * @param desc 描述信息 | 
 |  |  |      */ | 
 |  |  |     ResponseCodeEnum(int code, String msg, String desc) { | 
 |  |  |         this.code = code; | 
 |  |  |         this.msg = msg; | 
 |  |  |         this.desc = desc; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 根据响应代码获取描述信息 | 
 |  |  |      * 该方法用于将系统内部的响应代码转换为人类可读的描述信息 | 
 |  |  |      * 主要通过遍历枚举类ResponseCodeEnum来匹配传入的代码,并返回对应的描述 | 
 |  |  |      * 如果没有找到匹配的代码,则返回空字符串 | 
 |  |  |      * | 
 |  |  |      * @param code 响应代码,用于查找对应的描述信息 | 
 |  |  |      * @return 描述信息如果找到匹配的代码,否则返回空字符串 | 
 |  |  |      */ | 
 |  |  |     public static String getDesc(int code) { | 
 |  |  |         for (ResponseCodeEnum item : ResponseCodeEnum.values()) { | 
 |  |  |             if (item.getCode() == code) { | 
 |  |  |                 return item.getDesc(); | 
 |  |  |             } | 
 |  |  |         } | 
 |  |  |         return ""; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 根据响应代码获取消息信息 | 
 |  |  |      * 该方法用于将系统内部的响应代码转换为消息信息 | 
 |  |  |      * 主要通过遍历枚举类ResponseCodeEnum来匹配传入的代码,并返回对应的消息 | 
 |  |  |      * 如果没有找到匹配的代码,则返回空字符串 | 
 |  |  |      * | 
 |  |  |      * @param code 响应代码,用于查找对应的消息信息 | 
 |  |  |      * @return 消息信息如果找到匹配的代码,否则返回空字符串 | 
 |  |  |      */ | 
 |  |  |     public static String getMsg(int code) { | 
 |  |  |         for (ResponseCodeEnum item : ResponseCodeEnum.values()) { | 
 |  |  |             if (item.getCode() == code) { | 
 |  |  |                 return item.getMsg(); | 
 |  |  |             } | 
 |  |  |         } | 
 |  |  |         return ""; | 
 |  |  |     } | 
 |  |  |     /** | 
 |  |  |      * 验证响应码是200 | 
 |  |  |      */ | 
 |  |  |     public static boolean isSuccess(int code) { | 
 |  |  |         return SUCCESS.getCode() == code; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 验证响应码不是200 | 
 |  |  |      */ | 
 |  |  |     public static boolean isFail(int code) { | 
 |  |  |         return !isSuccess(code); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.utils; | 
 |  |  |  | 
 |  |  | import org.json.JSONArray; | 
 |  |  | import org.json.JSONException; | 
 |  |  | import org.json.JSONObject; | 
 |  |  | import java.util.ArrayList; | 
 |  |  |  | 
 |  |  | public final class JSONParser { | 
 |  |  |  | 
 |  |  |     private JSONParser() { | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     public static String getJSONStringValue(String json, String key) { | 
 |  |  |         try { | 
 |  |  |             JSONObject obj = new JSONObject(json); | 
 |  |  |             return obj.getString(key); | 
 |  |  |         } catch (JSONException e) { | 
 |  |  |             throw new JSONException(String.format("[JSONParser] Failed to get \"%s\"  from JSON object", key)); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     public static int getJSONIntValue(String json, String key) { | 
 |  |  |         try { | 
 |  |  |             JSONObject obj = new JSONObject(json); | 
 |  |  |             return obj.getInt(key); | 
 |  |  |         } catch (JSONException e) { | 
 |  |  |             throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key)); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     public static String getJSONArray(ArrayList<?> symbols, String key) { | 
 |  |  |         try { | 
 |  |  |             JSONArray arr = new JSONArray(symbols); | 
 |  |  |             return arr.toString(); | 
 |  |  |         } catch (JSONException e) { | 
 |  |  |             throw new JSONException(String.format("[JSONParser] Failed to convert \"%s\" to JSON array", key)); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  | } | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.utils; | 
 |  |  |  | 
 |  |  | import lombok.extern.slf4j.Slf4j; | 
 |  |  | import okhttp3.*; | 
 |  |  | import org.json.JSONObject; | 
 |  |  |  | 
 |  |  | import javax.net.ssl.SSLContext; | 
 |  |  | import javax.net.ssl.SSLSocketFactory; | 
 |  |  | import javax.net.ssl.TrustManager; | 
 |  |  | import javax.net.ssl.X509TrustManager; | 
 |  |  | import java.io.IOException; | 
 |  |  | import java.net.URLEncoder; | 
 |  |  | import java.security.SecureRandom; | 
 |  |  | import java.security.cert.X509Certificate; | 
 |  |  | import java.util.LinkedHashMap; | 
 |  |  | import java.util.Map; | 
 |  |  | import java.util.concurrent.Semaphore; | 
 |  |  | import java.util.concurrent.TimeUnit; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * OkHttp请求工具封装 | 
 |  |  |  * 参考:https://blog.csdn.net/DwZ735660836/article/details/119976068 | 
 |  |  |  */ | 
 |  |  | @Slf4j | 
 |  |  | public class OkHttpUtils { | 
 |  |  |     public static volatile OkHttpClient okHttpClient = null; | 
 |  |  |     private static volatile Semaphore semaphore = null; | 
 |  |  |     private Map<String, String> headerMap; | 
 |  |  |     private Map<String, String> paramMap; | 
 |  |  |     private String url; | 
 |  |  |     private Request.Builder request; | 
 |  |  |     // 开发环境用的 ShadowsocksR-dotnet4.0 免费版本 正式环境得使用外网服务器 | 
 |  |  |     // 安易代理  http://127.0.0.1:10809/ http://127.0.0.1:10808/ | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 初始化okHttpClient,并且允许https访问 | 
 |  |  |      */ | 
 |  |  |     private OkHttpUtils() { | 
 |  |  |         if (okHttpClient == null) { | 
 |  |  |             synchronized (OkHttpUtils.class) { | 
 |  |  |                 if (okHttpClient == null) { | 
 |  |  |                     TrustManager[] trustManagers = buildTrustManagers(); | 
 |  |  |                     okHttpClient = new OkHttpClient.Builder() | 
 |  |  |                             .connectTimeout(30, TimeUnit.SECONDS) | 
 |  |  |                             .writeTimeout(20, TimeUnit.SECONDS) | 
 |  |  |                             .readTimeout(60, TimeUnit.SECONDS) | 
 |  |  |                             .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0]) | 
 |  |  |                             //.hostnameVerifier((hostName, session) -> true) | 
 |  |  |                             //配置自定义连接池参数 | 
 |  |  |                             .connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS)) | 
 |  |  |                             .retryOnConnectionFailure(true) | 
 |  |  |                             .build(); | 
 |  |  |                     addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"); | 
 |  |  |                     addHeader("Connection", "close"); | 
 |  |  |                     addHeader("Accept-Encoding", "identity"); | 
 |  |  |                 } | 
 |  |  |             } | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 用于异步请求时,控制访问线程数,返回结果 | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     private static Semaphore getSemaphoreInstance() { | 
 |  |  |         //只能1个线程同时访问 | 
 |  |  |         synchronized (OkHttpUtils.class) { | 
 |  |  |             if (semaphore == null) { | 
 |  |  |                 semaphore = new Semaphore(0); | 
 |  |  |             } | 
 |  |  |         } | 
 |  |  |         return semaphore; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 创建OkHttpUtils | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public static OkHttpUtils builder() { | 
 |  |  |         return new OkHttpUtils(); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 添加url | 
 |  |  |      * | 
 |  |  |      * @param url | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public OkHttpUtils url(String url) { | 
 |  |  |         this.url = url; | 
 |  |  |         return this; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 添加参数 | 
 |  |  |      * | 
 |  |  |      * @param key   参数名 | 
 |  |  |      * @param value 参数值 | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public OkHttpUtils addParam(String key, String value) { | 
 |  |  |         if (paramMap == null) { | 
 |  |  |             paramMap = new LinkedHashMap<>(16); | 
 |  |  |         } | 
 |  |  |         paramMap.put(key, value); | 
 |  |  |         return this; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 添加请求头 | 
 |  |  |      * | 
 |  |  |      * @param key   参数名 | 
 |  |  |      * @param value 参数值 | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public OkHttpUtils addHeader(String key, String value) { | 
 |  |  |         if (headerMap == null) { | 
 |  |  |             headerMap = new LinkedHashMap<>(16); | 
 |  |  |         } | 
 |  |  |         headerMap.put(key, value); | 
 |  |  |         return this; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 初始化get方法 | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public OkHttpUtils get() { | 
 |  |  |         request = new Request.Builder().get(); | 
 |  |  |         StringBuilder urlBuilder = new StringBuilder(url); | 
 |  |  |         if (paramMap != null) { | 
 |  |  |             urlBuilder.append("?"); | 
 |  |  |             try { | 
 |  |  |                 for (Map.Entry<String, String> entry : paramMap.entrySet()) { | 
 |  |  |                     urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")). | 
 |  |  |                             append("="). | 
 |  |  |                             append(URLEncoder.encode(entry.getValue(), "utf-8")). | 
 |  |  |                             append("&"); | 
 |  |  |                 } | 
 |  |  |             } catch (Exception e) { | 
 |  |  |                 e.printStackTrace(); | 
 |  |  |             } | 
 |  |  |             urlBuilder.deleteCharAt(urlBuilder.length() - 1); | 
 |  |  |         } | 
 |  |  |         request.url(urlBuilder.toString()); | 
 |  |  |         return this; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 初始化post方法 | 
 |  |  |      * | 
 |  |  |      * @param isJsonPost true等于json的方式提交数据,类似postman里post方法的raw | 
 |  |  |      *                   false等于普通的表单提交 | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public OkHttpUtils post(boolean isJsonPost) { | 
 |  |  |         RequestBody requestBody; | 
 |  |  |         if (isJsonPost) { | 
 |  |  |             String json = ""; | 
 |  |  |             if (paramMap != null) { | 
 |  |  |                 json = JSONObject.valueToString(paramMap); | 
 |  |  |             } | 
 |  |  |             requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); | 
 |  |  |         } else { | 
 |  |  |             FormBody.Builder formBody = new FormBody.Builder(); | 
 |  |  |             if (paramMap != null) { | 
 |  |  |                 paramMap.forEach(formBody::add); | 
 |  |  |             } | 
 |  |  |             requestBody = formBody.build(); | 
 |  |  |         } | 
 |  |  |         request = new Request.Builder().post(requestBody).url(url); | 
 |  |  |         return this; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 同步请求 | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public Request.Builder sync() { | 
 |  |  |         return setHeader(request); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 同步请求 | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     public String syncStr() { | 
 |  |  |         setHeader(request); | 
 |  |  |         try { | 
 |  |  |             Response response = okHttpClient.newCall(request.build()).execute(); | 
 |  |  |             assert response.body() != null; | 
 |  |  |             return response.body().string(); | 
 |  |  |         } catch (IOException e) { | 
 |  |  |             e.printStackTrace(); | 
 |  |  |             return "请求失败:" + e.getMessage(); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 异步请求,有返回值 | 
 |  |  |      */ | 
 |  |  |     public String async() { | 
 |  |  |         StringBuilder buffer = new StringBuilder(""); | 
 |  |  |         setHeader(request); | 
 |  |  |         okHttpClient.newCall(request.build()).enqueue(new Callback() { | 
 |  |  |             @Override | 
 |  |  |             public void onFailure(Call call, IOException e) { | 
 |  |  |                 buffer.append("请求出错:").append(e.getMessage()); | 
 |  |  |             } | 
 |  |  |  | 
 |  |  |             @Override | 
 |  |  |             public void onResponse(Call call, Response response) throws IOException { | 
 |  |  |                 assert response.body() != null; | 
 |  |  |                 buffer.append(response.body().string()); | 
 |  |  |                 getSemaphoreInstance().release(); | 
 |  |  |             } | 
 |  |  |         }); | 
 |  |  |         try { | 
 |  |  |             getSemaphoreInstance().acquire(); | 
 |  |  |         } catch (InterruptedException e) { | 
 |  |  |             e.printStackTrace(); | 
 |  |  |         } | 
 |  |  |         return buffer.toString(); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 异步请求,带有接口回调 | 
 |  |  |      * | 
 |  |  |      * @param callBack | 
 |  |  |      */ | 
 |  |  |     public void async(ICallBack callBack) { | 
 |  |  |         setHeader(request); | 
 |  |  |         okHttpClient.newCall(request.build()).enqueue(new Callback() { | 
 |  |  |             @Override | 
 |  |  |             public void onFailure(Call call, IOException e) { | 
 |  |  |                 callBack.onFailure(call, e.getMessage()); | 
 |  |  |             } | 
 |  |  |  | 
 |  |  |             @Override | 
 |  |  |             public void onResponse(Call call, Response response) throws IOException { | 
 |  |  |                 assert response.body() != null; | 
 |  |  |                 callBack.onSuccessful(call, response.body().string()); | 
 |  |  |             } | 
 |  |  |         }); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 为request添加请求头 | 
 |  |  |      * | 
 |  |  |      * @param request | 
 |  |  |      */ | 
 |  |  |     private Request.Builder setHeader(Request.Builder request) { | 
 |  |  |         if (headerMap != null) { | 
 |  |  |             try { | 
 |  |  |                 for (Map.Entry<String, String> entry : headerMap.entrySet()) { | 
 |  |  |                     request.addHeader(entry.getKey(), entry.getValue()); | 
 |  |  |                 } | 
 |  |  |             } catch (Exception e) { | 
 |  |  |                 e.printStackTrace(); | 
 |  |  |             } | 
 |  |  |         } | 
 |  |  |         return request; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 生成安全套接字工厂,用于https请求的证书跳过 | 
 |  |  |      * | 
 |  |  |      * @return | 
 |  |  |      */ | 
 |  |  |     private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) { | 
 |  |  |         SSLSocketFactory ssfFactory = null; | 
 |  |  |         try { | 
 |  |  |             SSLContext sc = SSLContext.getInstance("SSL"); | 
 |  |  |             sc.init(null, trustAllCerts, new SecureRandom()); | 
 |  |  |             ssfFactory = sc.getSocketFactory(); | 
 |  |  |         } catch (Exception e) { | 
 |  |  |             e.printStackTrace(); | 
 |  |  |         } | 
 |  |  |         return ssfFactory; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     private static TrustManager[] buildTrustManagers() { | 
 |  |  |         return new TrustManager[]{ | 
 |  |  |                 new X509TrustManager() { | 
 |  |  |                     @Override | 
 |  |  |                     public void checkClientTrusted(X509Certificate[] chain, String authType) { | 
 |  |  |                     } | 
 |  |  |  | 
 |  |  |                     @Override | 
 |  |  |                     public void checkServerTrusted(X509Certificate[] chain, String authType) { | 
 |  |  |                     } | 
 |  |  |  | 
 |  |  |                     @Override | 
 |  |  |                     public X509Certificate[] getAcceptedIssuers() { | 
 |  |  |                         return new X509Certificate[]{}; | 
 |  |  |                     } | 
 |  |  |                 } | 
 |  |  |         }; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 自定义一个接口回调 | 
 |  |  |      */ | 
 |  |  |     public interface ICallBack { | 
 |  |  |  | 
 |  |  |         void onSuccessful(Call call, String data); | 
 |  |  |  | 
 |  |  |         void onFailure(Call call, String errorMsg); | 
 |  |  |  | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     public static void main(String[] args) { | 
 |  |  |         String url = "https://api2.binance.com/api/v3/ticker/24hr?symbol=BNBUSDT&type=MINI"; | 
 |  |  |         String result = OkHttpUtils.builder() | 
 |  |  |                 .url(url) | 
 |  |  |                 .addHeader("Content-Type", "application/x-www-form-urlencoded") | 
 |  |  |                 .get() | 
 |  |  |                 .syncStr(); | 
 |  |  |         System.out.println(result); | 
 |  |  |     } | 
 |  |  | } | 
 |  |  |  | 
 |  |  |  | 
 
| New file | 
 |  |  | 
 |  |  | package cc.mrbird.febs.unisoftiot.utils; | 
 |  |  |  | 
 |  |  | import cc.mrbird.febs.unisoftiot.enums.DeviceRequestUrlEnum; | 
 |  |  | import cn.hutool.core.collection.CollUtil; | 
 |  |  | import cn.hutool.core.date.DateUtil; | 
 |  |  | import cn.hutool.crypto.SecureUtil; | 
 |  |  |  | 
 |  |  | import java.io.UnsupportedEncodingException; | 
 |  |  | import java.net.URLEncoder; | 
 |  |  | import java.nio.charset.StandardCharsets; | 
 |  |  | import java.util.LinkedHashMap; | 
 |  |  | import java.util.Map; | 
 |  |  |  | 
 |  |  | /** | 
 |  |  |  * URL工具类,用于构造请求物联网控制台接口的URL | 
 |  |  |  */ | 
 |  |  | public class UrlUtils { | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * http(s)://iot-api.unisoft.cn/{AppID}/{接口列表中的path}/?{其他参数}&sign={sign}&ts={ts} | 
 |  |  |      * | 
 |  |  |      * sign    签名    所有请求物联网控制台接口,均需在url中携带此参数sign={sign} | 
 |  |  |      * 取值方法:md5(md5(开发者密码) + 上面的ts参数),32位字符串 | 
 |  |  |      * ts    时间戳    所有请求物联网控制台接口,均需在url中携带此参数ts={timestap} | 
 |  |  |      * 取值方法:请求时间(timezone,东八区),10位数字 | 
 |  |  |      */ | 
 |  |  |     public static void main(String[] args) { | 
 |  |  |         System.out.println(getUrl(BASE_URL,APP_ID,APP_SECRET, DeviceRequestUrlEnum.DEVICE_CONTROL.getRequestUrl(), null)); | 
 |  |  |         LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); | 
 |  |  |         parameters.put("group", "123456789"); | 
 |  |  |         parameters.put("product", "test"); | 
 |  |  |         System.out.println(getUrl(BASE_URL,APP_ID,APP_SECRET,"device/info",parameters)); | 
 |  |  |     } | 
 |  |  |     // todo 优化到后台配置中 | 
 |  |  |     public static final String BASE_URL = " http://iot-api.unisoft.cn/rtyVWcgmlr"; | 
 |  |  |     public static final String APP_ID = "rtyVWcgmlr"; | 
 |  |  |     public static final String APP_SECRET = "rtyVWcgmlr"; | 
 |  |  |     /** | 
 |  |  |      * 构造请求物联网控制台接口的URL | 
 |  |  |      * | 
 |  |  |      * @param baseUrl       基础URL地址 | 
 |  |  |      * @param appId         应用ID,用于标识应用 | 
 |  |  |      * @param appSecret     应用密钥,用于生成签名 | 
 |  |  |      * @param requestPath   请求路径,标识具体的接口 | 
 |  |  |      * @param parameters    请求参数,包含除sign和ts之外的其他参数 | 
 |  |  |      * @return              返回构造完成的URL字符串 | 
 |  |  |      * | 
 |  |  |      * 该方法首先会构建基础URL,然后添加appId和请求路径,接着添加其他参数, | 
 |  |  |      * 最后通过appSecret和时间戳生成签名(sign),以及添加时间戳(ts)到URL中。 | 
 |  |  |      */ | 
 |  |  |     public static String getUrl(String baseUrl, | 
 |  |  |                                 String appId, | 
 |  |  |                                 String appSecret, | 
 |  |  |                                 String requestPath, | 
 |  |  |                                 LinkedHashMap<String, Object> parameters){ | 
 |  |  |         StringBuilder fullUrl = new StringBuilder(baseUrl); | 
 |  |  | //        fullUrl.append("/"+appId); | 
 |  |  |         fullUrl.append(requestPath); | 
 |  |  |         fullUrl.append("?"); | 
 |  |  |         //判断parameters参数是否为空,不为空则拼接在fullUrl | 
 |  |  |         parametersSetToUrl(fullUrl,parameters); | 
 |  |  |         getSignAndTs(appSecret,fullUrl,parameters); | 
 |  |  |         return fullUrl.toString(); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 将查询参数附加到URL路径上 | 
 |  |  |      * 此方法接受一个URL路径和一个查询参数的映射,然后将这些参数附加到URL路径上 | 
 |  |  |      * 如果没有查询参数,URL路径保持不变 | 
 |  |  |      * | 
 |  |  |      * @param urlPath URL路径的StringBuilder对象,用于构建最终的URL | 
 |  |  |      * @param parameters 查询参数的LinkedHashMap,键为参数名,值为参数值 | 
 |  |  |      * @return 返回包含查询参数的URL路径的StringBuilder对象 | 
 |  |  |      */ | 
 |  |  |     public static StringBuilder parametersSetToUrl(StringBuilder urlPath, LinkedHashMap<String, Object> parameters) { | 
 |  |  |         // 检查参数是否为空,如果为空则直接返回URL路径 | 
 |  |  |         if (parameters == null || parameters.isEmpty()) { | 
 |  |  |             return urlPath; | 
 |  |  |         } | 
 |  |  |         joinQueryParameters(urlPath, parameters); | 
 |  |  |         // 返回包含查询参数的URL路径 | 
 |  |  |         return urlPath; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 将查询参数拼接到URL路径后 | 
 |  |  |      * | 
 |  |  |      * @param urlPath   URL路径 | 
 |  |  |      * @param parameters 查询参数 | 
 |  |  |      * @return 拼接后的URL路径 | 
 |  |  |      */ | 
 |  |  |     public static StringBuilder joinQueryParameters(StringBuilder urlPath, LinkedHashMap<String, Object> parameters) { | 
 |  |  |         if (parameters == null || parameters.isEmpty()) { | 
 |  |  |             return urlPath; | 
 |  |  |         } | 
 |  |  |  | 
 |  |  |         boolean isFirst = true; | 
 |  |  |         for (Map.Entry<String, Object> mapElement : parameters.entrySet()) { | 
 |  |  |             Object value = mapElement.getValue(); | 
 |  |  |             // 根据是否是第一个参数来决定是否添加& | 
 |  |  |             if (isFirst) { | 
 |  |  |                 isFirst = false; | 
 |  |  |             } else { | 
 |  |  |                 urlPath.append('&'); | 
 |  |  |             } | 
 |  |  |  | 
 |  |  |             // 拼接参数到URL路径 | 
 |  |  |             urlPath.append(mapElement.getKey()) | 
 |  |  |                     .append('=') | 
 |  |  |                     .append(urlEncode(value.toString())); | 
 |  |  |         } | 
 |  |  |         return urlPath; | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 对URL参数进行编码 | 
 |  |  |      * | 
 |  |  |      * @param s 参数字符串 | 
 |  |  |      * @return 编码后的参数字符串 | 
 |  |  |      */ | 
 |  |  |     public static String urlEncode(String s) { | 
 |  |  |         try { | 
 |  |  |             return URLEncoder.encode(s, StandardCharsets.UTF_8.name()); | 
 |  |  |         } catch (UnsupportedEncodingException e) { | 
 |  |  |             throw new RuntimeException(StandardCharsets.UTF_8.name() + " is unsupported", e); | 
 |  |  |         } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |  | 
 |  |  |  | 
 |  |  |     /** | 
 |  |  |      * 生成签名和时间戳参数 | 
 |  |  |      * 该方法用于给定的URL路径添加签名(sign)和时间戳(ts)参数,以确保请求的安全性和有效性 | 
 |  |  |      * 签名是通过将appSecret进行MD5加密,然后与当前时间戳拼接后再进行MD5加密生成 | 
 |  |  |      * 这种方法确保了每个请求都有一个唯一的、基于时间的签名,从而防止了重放攻击 | 
 |  |  |      * | 
 |  |  |      * @param appSecret 应用密钥,用于生成签名 | 
 |  |  |      * @param urlPath StringBuilder对象,用于拼接URL路径和查询参数 | 
 |  |  |      * @param parameters 请求参数,用于判断是否需要添加额外的连接符 | 
 |  |  |      * @return 返回包含签名和时间戳参数的URL路径字符串 | 
 |  |  |      */ | 
 |  |  |     public static String getSignAndTs(String appSecret,StringBuilder urlPath, LinkedHashMap<String, Object> parameters) { | 
 |  |  |         //使用MD5加密appSecret | 
 |  |  |         String md5AppSecret = SecureUtil.md5(appSecret); | 
 |  |  |         //获取当前时间戳 | 
 |  |  |         String timestamp = String.valueOf(DateUtil.currentSeconds()); | 
 |  |  |         //生成sign参数,通过md5加密(appSecret的md5值 + 时间戳) | 
 |  |  |         String md5Sign = SecureUtil.md5(md5AppSecret + timestamp); | 
 |  |  |         //返回包含sign和ts参数的字符串 | 
 |  |  |         if (CollUtil.isNotEmpty(parameters)) { | 
 |  |  |             urlPath.append("&"); | 
 |  |  |         } | 
 |  |  |         urlPath.append("sign="); | 
 |  |  |         urlPath.append(md5Sign); | 
 |  |  |         urlPath.append("&ts="); | 
 |  |  |         urlPath.append(timestamp); | 
 |  |  |         return urlPath.toString(); | 
 |  |  |     } | 
 |  |  |  | 
 |  |  | } | 
 
 |  |  | 
 |  |  |       datasource: | 
 |  |  |         # 数据源-1,名称为 base | 
 |  |  |         base: | 
 |  |  |           username: blnka | 
 |  |  |           password: blnka!@#123 | 
 |  |  |           username: db_e2 | 
 |  |  |           password: db_e20806123!@# | 
 |  |  |           driver-class-name: com.mysql.cj.jdbc.Driver | 
 |  |  |           url: jdbc:mysql://127.0.0.1:3306/db_blnka?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8 | 
 |  |  |           url: jdbc:mysql://127.0.0.1:3306/db_e2?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8 | 
 |  |  |  | 
 |  |  |   redis: | 
 |  |  |     # Redis数据库索引(默认为 0) | 
 |  |  | 
 |  |  |     # Redis服务器地址 | 
 |  |  |     host: 127.0.0.1 | 
 |  |  |     # Redis服务器连接端口 | 
 |  |  |     port: 6279 | 
 |  |  |     port: 6379 | 
 |  |  |     # Redis 密码 | 
 |  |  |     password: blnka!@#123 | 
 |  |  |     password: lianghua1!qaz2@WSX | 
 |  |  |     lettuce: | 
 |  |  |       pool: | 
 |  |  |         # 连接池中的最小空闲连接 | 
 |  |  | 
 |  |  |   rabbitmq: | 
 |  |  |     host: 127.0.0.1 | 
 |  |  |     port: 5672 | 
 |  |  |     username: blnka | 
 |  |  |     password: blnka123 | 
 |  |  |     username: e20210816 | 
 |  |  |     password: e20210816 | 
 |  |  |     publisher-confirm-type: correlated | 
 |  |  |  | 
 |  |  | pay: |