feat: 新功能开发,监测历史记录。微信和物联网平台,模块搭建。

This commit is contained in:
tianyongbao
2025-11-05 00:35:23 +08:00
parent 1d2e2e2513
commit 05fb822744
38 changed files with 2099 additions and 49 deletions

View File

@@ -22,6 +22,11 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
<version>2.0.38</version>
</dependency>
<!-- &lt;!&ndash; mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 &ndash;&gt;-->
<!-- &lt;!&ndash; Oracle &ndash;&gt;-->

View File

@@ -54,14 +54,20 @@ spring:
url: jdbc:postgresql://154.8.147.51:15432/fishery_dev?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres
password: intc@123987
# # 从库数据源
# slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.taosdata.jdbc.rs.RestfulDriver
# url: jdbc:TAOS-RS://117.72.197.29:6041/log?timezone=Shanghai&charset=UTF-8&locale=en_US.UTF-8
# username: root
# password: taosdata
# 从库数据源 - TDengine
taos:
lazy: false
type: ${spring.datasource.type}
driverClassName: com.taosdata.jdbc.rs.RestfulDriver
# 不指定数据库名,在 SQL 中使用完整路径 fishery.table_name
url: jdbc:TAOS-RS://154.8.147.51:6041?timezone=Shanghai&charset=UTF-8&locale=en_US.UTF-8
username: root
password: intc@123456
hikari:
connection-timeout: 60000
validation-timeout: 10000
max-pool-size: 5
min-idle: 2
# oracle:
# type: ${spring.datasource.type}
# driverClassName: oracle.jdbc.OracleDriver

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.intc</groupId>
<artifactId>intc-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>intc-iot</artifactId>
<description>
阿里云生活物联网平台(飞燕平台)对接模块
</description>
<dependencies>
<!-- 阿里云生活物联网平台 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.4</version>
</dependency>
<!-- 阿里云 IoT SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-iot</artifactId>
<version>7.46.0</version>
</dependency>
<!-- MQTT 客户端 -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- HTTP 客户端 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-doc</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-log</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-security</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-tenant</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,41 @@
package com.intc.iot.config;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.profile.DefaultProfile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云生活物联网平台(飞燕平台)配置
*
* @author intc
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "aliyun.living-iot", name = "access-key-id")
public class AliyunIotConfiguration {
private final AliyunIotProperties iotProperties;
/**
* 创建阿里云飞燕平台客户端
*/
@Bean
public IAcsClient livingIotClient() {
DefaultProfile profile = DefaultProfile.getProfile(
iotProperties.getRegionId(),
iotProperties.getAccessKeyId(),
iotProperties.getAccessKeySecret()
);
IAcsClient client = new DefaultAcsClient(profile);
log.info("阿里云生活物联网平台客户端初始化成功RegionId: {}", iotProperties.getRegionId());
return client;
}
}

View File

@@ -0,0 +1,100 @@
package com.intc.iot.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 阿里云生活物联网平台(飞燕平台)配置属性
*
* @author intc
*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.living-iot")
public class AliyunIotProperties {
/**
* 阿里云AccessKey ID
*/
private String accessKeyId;
/**
* 阿里云AccessKey Secret
*/
private String accessKeySecret;
/**
* 地域节点cn-shanghai
*/
private String regionId;
/**
* 飞燕平台项目IDProject ID
*/
private String projectId;
/**
* App Key
*/
private String appKey;
/**
* App Secret
*/
private String appSecret;
/**
* 品类Key
*/
private String categoryKey;
/**
* MQTT 配置
*/
private MqttConfig mqtt = new MqttConfig();
@Data
public static class MqttConfig {
/**
* MQTT Broker 地址
*/
private String brokerUrl;
/**
* 客户端ID
*/
private String clientId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 连接超时时间(秒)
*/
private Integer connectionTimeout = 30;
/**
* 保活时间(秒)
*/
private Integer keepAliveInterval = 60;
/**
* 自动重连
*/
private Boolean autoReconnect = true;
/**
* 清除会话
*/
private Boolean cleanSession = true;
}
}

View File

@@ -0,0 +1,49 @@
package com.intc.iot.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MQTT 客户端配置
*
* @author intc
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "aliyun.living-iot.mqtt", name = "broker-url")
public class MqttConfiguration {
private final AliyunIotProperties iotProperties;
/**
* 创建 MQTT 客户端
*/
@Bean
public MqttClient mqttClient() throws Exception {
AliyunIotProperties.MqttConfig mqtt = iotProperties.getMqtt();
MemoryPersistence persistence = new MemoryPersistence();
MqttClient client = new MqttClient(mqtt.getBrokerUrl(), mqtt.getClientId(), persistence);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(mqtt.getUsername());
options.setPassword(mqtt.getPassword().toCharArray());
options.setConnectionTimeout(mqtt.getConnectionTimeout());
options.setKeepAliveInterval(mqtt.getKeepAliveInterval());
options.setAutomaticReconnect(mqtt.getAutoReconnect());
options.setCleanSession(mqtt.getCleanSession());
client.connect(options);
log.info("MQTT 客户端连接成功Broker: {}", mqtt.getBrokerUrl());
return client;
}
}

View File

@@ -0,0 +1,212 @@
package com.intc.iot.controller;
import com.intc.common.core.domain.R;
import com.intc.common.web.core.BaseController;
import com.intc.iot.service.DeviceDataService;
import com.intc.iot.service.IotDeviceService;
import com.intc.iot.service.MqttService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 阿里云生活物联网平台(飞燕平台)控制器
*
* @author intc
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/iot")
@Tag(name = "生活物联网平台管理", description = "阿里云飞燕平台对接接口")
public class IotController extends BaseController {
@Autowired(required = false)
private IotDeviceService iotDeviceService;
@Autowired(required = false)
private MqttService mqttService;
@Autowired(required = false)
private DeviceDataService deviceDataService;
@Operation(summary = "测试接口")
@GetMapping("/test")
public R<String> test() {
return R.ok("飞燕平台模块测试成功!");
}
@Operation(summary = "查询设备列表")
@GetMapping("/device/list")
public R<Map<String, Object>> queryDeviceList(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer pageSize) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.queryDeviceList(pageNo, pageSize);
return R.ok(response);
} catch (Exception e) {
log.error("查询设备列表失败", e);
return R.fail("查询设备列表失败: " + e.getMessage());
}
}
@Operation(summary = "查询设备详情")
@GetMapping("/device/info")
public R<Map<String, Object>> queryDeviceInfo(
@Parameter(description = "设备ID") @RequestParam String iotId) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.queryDeviceInfo(iotId);
return R.ok(response);
} catch (Exception e) {
log.error("查询设备详情失败", e);
return R.fail("查询设备详情失败: " + e.getMessage());
}
}
@Operation(summary = "查询设备属性")
@GetMapping("/device/properties")
public R<Map<String, Object>> queryDeviceProperties(
@Parameter(description = "设备ID") @RequestParam String iotId) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.queryDeviceProperties(iotId);
return R.ok(response);
} catch (Exception e) {
log.error("查询设备属性失败", e);
return R.fail("查询设备属性失败: " + e.getMessage());
}
}
@Operation(summary = "设置设备属性")
@PostMapping("/device/property/set")
public R<Map<String, Object>> setDeviceProperty(
@Parameter(description = "设备ID") @RequestParam String iotId,
@Parameter(description = "属性JSON") @RequestParam String properties) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.setDeviceProperty(iotId, properties);
return R.ok(response);
} catch (Exception e) {
log.error("设置设备属性失败", e);
return R.fail("设置设备属性失败: " + e.getMessage());
}
}
@Operation(summary = "调用设备服务")
@PostMapping("/device/invoke")
public R<Map<String, Object>> invokeService(
@Parameter(description = "设备ID") @RequestParam String iotId,
@Parameter(description = "服务标识符") @RequestParam String identifier,
@Parameter(description = "参数JSON") @RequestParam String args) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.invokeService(iotId, identifier, args);
return R.ok(response);
} catch (Exception e) {
log.error("调用设备服务失败", e);
return R.fail("调用设备服务失败: " + e.getMessage());
}
}
@Operation(summary = "解绑设备")
@DeleteMapping("/device/unbind")
public R<Map<String, Object>> unbindDevice(
@Parameter(description = "设备ID") @RequestParam String iotId) {
try {
if (iotDeviceService == null) {
return R.fail("飞燕平台配置未启用");
}
Map<String, Object> response = iotDeviceService.unbindDevice(iotId);
return R.ok(response);
} catch (Exception e) {
log.error("解绑设备失败", e);
return R.fail("解绑设备失败: " + e.getMessage());
}
}
@Operation(summary = "发布MQTT消息")
@PostMapping("/mqtt/publish")
public R<String> publishMqtt(
@Parameter(description = "主题") @RequestParam String topic,
@Parameter(description = "消息内容") @RequestParam String payload,
@Parameter(description = "QoS等级") @RequestParam(defaultValue = "1") int qos) {
try {
if (mqttService == null) {
return R.fail("MQTT配置未启用");
}
mqttService.publish(topic, payload, qos);
return R.ok("消息发布成功");
} catch (Exception e) {
log.error("发布MQTT消息失败", e);
return R.fail("发布MQTT消息失败: " + e.getMessage());
}
}
@Operation(summary = "订阅MQTT主题")
@PostMapping("/mqtt/subscribe")
public R<String> subscribeMqtt(
@Parameter(description = "主题") @RequestParam String topic,
@Parameter(description = "QoS等级") @RequestParam(defaultValue = "1") int qos) {
try {
if (mqttService == null) {
return R.fail("MQTT配置未启用");
}
mqttService.subscribe(topic, qos);
return R.ok("订阅成功");
} catch (Exception e) {
log.error("订阅MQTT主题失败", e);
return R.fail("订阅MQTT主题失败: " + e.getMessage());
}
}
@Operation(summary = "取消订阅MQTT主题")
@PostMapping("/mqtt/unsubscribe")
public R<String> unsubscribeMqtt(
@Parameter(description = "主题") @RequestParam String topic) {
try {
if (mqttService == null) {
return R.fail("MQTT配置未启用");
}
mqttService.unsubscribe(topic);
return R.ok("取消订阅成功");
} catch (Exception e) {
log.error("取消订阅MQTT主题失败", e);
return R.fail("取消订阅MQTT主题失败: " + e.getMessage());
}
}
@Operation(summary = "订阅设备实时数据(按产品)")
@PostMapping("/device/data/subscribe")
public R<String> subscribeDeviceData(
@Parameter(description = "产品Key") @RequestParam String productKey) {
try {
if (deviceDataService == null) {
return R.fail("设备数据服务未启用");
}
deviceDataService.subscribeAllDevices(productKey);
return R.ok("订阅成功,设备数据将实时推送");
} catch (Exception e) {
log.error("订阅设备数据失败", e);
return R.fail("订阅设备数据失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,69 @@
package com.intc.iot.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 物联网设备信息
*
* @author intc
*/
@Data
@TableName("iot_device")
public class IotDevice implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 设备名称
*/
private String deviceName;
/**
* 产品Key
*/
private String productKey;
/**
* 设备密钥
*/
private String deviceSecret;
/**
* 设备状态0-未激活 1-在线 2-离线 3-已禁用
*/
private Integer status;
/**
* 设备备注
*/
private String remark;
/**
* 最后上线时间
*/
private LocalDateTime lastOnlineTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,134 @@
package com.intc.iot.handler;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 设备数据处理器
*
* @author intc
*/
@Slf4j
@Component
public class DeviceDataHandler {
/**
* 处理设备属性上报数据
*
* @param topic 主题
* @param payload 消息内容
*/
public void handlePropertyPost(String topic, String payload) {
log.info("收到设备属性上报Topic: {}", topic);
try {
JSONObject data = JSONUtil.parseObj(payload);
// 解析飞燕平台消息格式
String method = data.getStr("method");
String id = data.getStr("id");
JSONObject params = data.getJSONObject("params");
log.info("设备属性数据 - Method: {}, ID: {}, Params: {}", method, id, params);
// TODO: 这里添加您的业务逻辑
// 1. 存储到数据库
// 2. 触发告警
// 3. 推送到前端(通过 WebSocket/SSE
// 4. 数据分析处理
if (params != null) {
params.forEach((key, value) -> {
log.info("属性: {} = {}", key, value);
// 根据不同属性进行不同处理
handleProperty(key, value);
});
}
} catch (Exception e) {
log.error("处理设备属性数据失败", e);
}
}
/**
* 处理设备事件上报数据
*
* @param topic 主题
* @param payload 消息内容
*/
public void handleEventPost(String topic, String payload) {
log.info("收到设备事件上报Topic: {}", topic);
try {
JSONObject data = JSONUtil.parseObj(payload);
String method = data.getStr("method");
String id = data.getStr("id");
JSONObject params = data.getJSONObject("params");
log.info("设备事件数据 - Method: {}, ID: {}, Params: {}", method, id, params);
// TODO: 处理设备事件
// 例如:故障告警、状态变化等
} catch (Exception e) {
log.error("处理设备事件数据失败", e);
}
}
/**
* 处理单个属性
*
* @param propertyName 属性名称
* @param propertyValue 属性值
*/
private void handleProperty(String propertyName, Object propertyValue) {
// 根据属性名称进行不同的业务处理
switch (propertyName) {
case "temperature":
handleTemperature(propertyValue);
break;
case "humidity":
handleHumidity(propertyValue);
break;
case "status":
handleStatus(propertyValue);
break;
default:
log.debug("未处理的属性: {} = {}", propertyName, propertyValue);
}
}
/**
* 处理温度数据
*/
private void handleTemperature(Object value) {
// 示例:温度告警
if (value instanceof Number) {
double temp = ((Number) value).doubleValue();
if (temp > 80) {
log.warn("温度过高告警: {}°C", temp);
// TODO: 发送告警通知
}
}
}
/**
* 处理湿度数据
*/
private void handleHumidity(Object value) {
// 示例:湿度处理逻辑
log.debug("湿度数据: {}", value);
}
/**
* 处理状态数据
*/
private void handleStatus(Object value) {
// 示例:设备状态变化
log.info("设备状态变化: {}", value);
}
}

View File

@@ -0,0 +1,42 @@
package com.intc.iot.service;
/**
* 设备数据服务接口
*
* @author intc
*/
public interface DeviceDataService {
/**
* 订阅设备属性上报
*
* @param iotId 设备ID
* @throws Exception 异常
*/
void subscribeDeviceProperties(String iotId) throws Exception;
/**
* 订阅设备事件上报
*
* @param iotId 设备ID
* @throws Exception 异常
*/
void subscribeDeviceEvents(String iotId) throws Exception;
/**
* 订阅所有设备数据
*
* @param productKey 产品Key
* @throws Exception 异常
*/
void subscribeAllDevices(String productKey) throws Exception;
/**
* 取消订阅设备数据
*
* @param iotId 设备ID
* @throws Exception 异常
*/
void unsubscribeDevice(String iotId) throws Exception;
}

View File

@@ -0,0 +1,70 @@
package com.intc.iot.service;
import java.util.Map;
/**
* 阿里云生活物联网平台(飞燕平台)设备管理服务
*
* @author intc
*/
public interface IotDeviceService {
/**
* 查询设备列表
*
* @param pageNo 页码
* @param pageSize 每页大小
* @return 设备列表
* @throws Exception 异常
*/
Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception;
/**
* 查询设备详情
*
* @param iotId 设备ID
* @return 设备详情
* @throws Exception 异常
*/
Map<String, Object> queryDeviceInfo(String iotId) throws Exception;
/**
* 查询设备属性
*
* @param iotId 设备ID
* @return 设备属性
* @throws Exception 异常
*/
Map<String, Object> queryDeviceProperties(String iotId) throws Exception;
/**
* 设置设备属性
*
* @param iotId 设备ID
* @param properties 属性JSON
* @return 设置结果
* @throws Exception 异常
*/
Map<String, Object> setDeviceProperty(String iotId, String properties) throws Exception;
/**
* 调用设备服务
*
* @param iotId 设备ID
* @param identifier 服务标识符
* @param args 参数
* @return 调用结果
* @throws Exception 异常
*/
Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception;
/**
* 解绑设备
*
* @param iotId 设备ID
* @return 解绑结果
* @throws Exception 异常
*/
Map<String, Object> unbindDevice(String iotId) throws Exception;
}

View File

@@ -0,0 +1,37 @@
package com.intc.iot.service;
/**
* MQTT 消息服务
*
* @author intc
*/
public interface MqttService {
/**
* 发布消息
*
* @param topic 主题
* @param payload 消息内容
* @param qos 服务质量等级0/1/2
* @throws Exception 异常
*/
void publish(String topic, String payload, int qos) throws Exception;
/**
* 订阅主题
*
* @param topic 主题
* @param qos 服务质量等级0/1/2
* @throws Exception 异常
*/
void subscribe(String topic, int qos) throws Exception;
/**
* 取消订阅
*
* @param topic 主题
* @throws Exception 异常
*/
void unsubscribe(String topic) throws Exception;
}

View File

@@ -0,0 +1,85 @@
package com.intc.iot.service.impl;
import cn.hutool.json.JSONUtil;
import com.intc.iot.service.DeviceDataService;
import com.intc.iot.service.MqttService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
/**
* 设备数据服务实现
*
* @author intc
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(MqttClient.class)
public class DeviceDataServiceImpl implements DeviceDataService {
private final MqttService mqttService;
/**
* 飞燕平台设备属性上报 Topic 格式
* /sys/{productKey}/{deviceName}/thing/event/property/post
*/
private static final String PROPERTY_POST_TOPIC = "/sys/%s/%s/thing/event/property/post";
/**
* 飞燕平台设备事件上报 Topic 格式
* /sys/{productKey}/{deviceName}/thing/event/{eventIdentifier}/post
*/
private static final String EVENT_POST_TOPIC = "/sys/%s/%s/thing/event/+/post";
/**
* 所有设备属性上报 Topic使用通配符
* /sys/{productKey}/+/thing/event/property/post
*/
private static final String ALL_DEVICES_PROPERTY_TOPIC = "/sys/%s/+/thing/event/property/post";
@Override
public void subscribeDeviceProperties(String iotId) throws Exception {
// 注意:需要根据 iotId 获取 productKey 和 deviceName
// 这里简化处理,实际应该从数据库或缓存中查询
log.info("订阅设备属性上报IotId: {}", iotId);
// 示例:假设从设备信息中获取
// String productKey = getProductKeyByIotId(iotId);
// String deviceName = getDeviceNameByIotId(iotId);
// String topic = String.format(PROPERTY_POST_TOPIC, productKey, deviceName);
// mqttService.subscribe(topic, 1);
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
}
@Override
public void subscribeDeviceEvents(String iotId) throws Exception {
log.info("订阅设备事件上报IotId: {}", iotId);
// 类似属性订阅,需要映射关系
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
}
@Override
public void subscribeAllDevices(String productKey) throws Exception {
log.info("订阅产品下所有设备属性上报ProductKey: {}", productKey);
String topic = String.format(ALL_DEVICES_PROPERTY_TOPIC, productKey);
mqttService.subscribe(topic, 1);
log.info("成功订阅 Topic: {}", topic);
}
@Override
public void unsubscribeDevice(String iotId) throws Exception {
log.info("取消订阅设备数据IotId: {}", iotId);
// 需要取消对应的 Topic 订阅
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
}
}

View File

@@ -0,0 +1,125 @@
package com.intc.iot.service.impl;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.iot.model.v20180120.*;
import com.intc.iot.config.AliyunIotProperties;
import com.intc.iot.service.IotDeviceService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 阿里云生活物联网平台(飞燕平台)设备管理服务实现
*
* @author intc
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(IAcsClient.class)
public class IotDeviceServiceImpl implements IotDeviceService {
private final IAcsClient acsClient;
private final AliyunIotProperties iotProperties;
@Override
public Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception {
log.info("查询设备列表,页码: {}, 每页大小: {}", pageNo, pageSize);
QueryDeviceRequest request = new QueryDeviceRequest();
request.setCurrentPage(pageNo);
request.setPageSize(pageSize);
QueryDeviceResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("data", response.getData());
result.put("total", response.getTotal());
return result;
}
@Override
public Map<String, Object> queryDeviceInfo(String iotId) throws Exception {
log.info("查询设备详情IotId: {}", iotId);
QueryDeviceDetailRequest request = new QueryDeviceDetailRequest();
request.setIotId(iotId);
QueryDeviceDetailResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("data", response.getData());
return result;
}
@Override
public Map<String, Object> queryDeviceProperties(String iotId) throws Exception {
log.info("查询设备属性IotId: {}", iotId);
QueryDevicePropertyStatusRequest request = new QueryDevicePropertyStatusRequest();
request.setIotId(iotId);
QueryDevicePropertyStatusResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("data", response.getData());
return result;
}
@Override
public Map<String, Object> setDeviceProperty(String iotId, String properties) throws Exception {
log.info("设置设备属性IotId: {}, Properties: {}", iotId, properties);
SetDevicePropertyRequest request = new SetDevicePropertyRequest();
request.setIotId(iotId);
request.setItems(properties);
SetDevicePropertyResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("data", response.getData());
return result;
}
@Override
public Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception {
log.info("调用设备服务IotId: {}, Identifier: {}", iotId, identifier);
InvokeThingServiceRequest request = new InvokeThingServiceRequest();
request.setIotId(iotId);
request.setIdentifier(identifier);
request.setArgs(args);
InvokeThingServiceResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("data", response.getData());
return result;
}
@Override
public Map<String, Object> unbindDevice(String iotId) throws Exception {
log.info("解绑设备IotId: {}", iotId);
// 注: 通用IoT SDK没有直接的解绑接口这里使用删除设备
DeleteDeviceRequest request = new DeleteDeviceRequest();
request.setIotId(iotId);
DeleteDeviceResponse response = acsClient.getAcsResponse(request);
Map<String, Object> result = new HashMap<>();
result.put("success", response.getSuccess());
result.put("errorMessage", response.getErrorMessage());
return result;
}
}

View File

@@ -0,0 +1,87 @@
package com.intc.iot.service.impl;
import com.intc.iot.handler.DeviceDataHandler;
import com.intc.iot.service.MqttService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
/**
* MQTT 消息服务实现
*
* @author intc
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(MqttClient.class)
public class MqttServiceImpl implements MqttService {
private final MqttClient mqttClient;
@Autowired(required = false)
private DeviceDataHandler deviceDataHandler;
@Override
public void publish(String topic, String payload, int qos) throws Exception {
if (!mqttClient.isConnected()) {
log.warn("MQTT 客户端未连接,尝试重连...");
mqttClient.reconnect();
}
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(qos);
message.setRetained(false);
mqttClient.publish(topic, message);
log.info("MQTT 消息发布成功Topic: {}, Payload: {}", topic, payload);
}
@Override
public void subscribe(String topic, int qos) throws Exception {
if (!mqttClient.isConnected()) {
log.warn("MQTT 客户端未连接,尝试重连...");
mqttClient.reconnect();
}
mqttClient.subscribe(topic, qos, new IMqttMessageListener() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload = new String(message.getPayload());
log.info("收到 MQTT 消息Topic: {}, Payload: {}", topic, payload);
// 根据 Topic 类型分发处理
if (deviceDataHandler != null) {
if (topic.contains("/thing/event/property/post")) {
// 设备属性上报
deviceDataHandler.handlePropertyPost(topic, payload);
} else if (topic.contains("/thing/event/") && topic.endsWith("/post")) {
// 设备事件上报
deviceDataHandler.handleEventPost(topic, payload);
} else {
log.debug("未匹配的 Topic 类型: {}", topic);
}
}
}
});
log.info("MQTT 主题订阅成功Topic: {}", topic);
}
@Override
public void unsubscribe(String topic) throws Exception {
if (!mqttClient.isConnected()) {
log.warn("MQTT 客户端未连接");
return;
}
mqttClient.unsubscribe(topic);
log.info("MQTT 主题取消订阅成功Topic: {}", topic);
}
}

View File

@@ -0,0 +1,36 @@
# 阿里云生活物联网平台(飞燕平台)配置
aliyun:
living-iot:
# 阿里云 AccessKey ID必填
access-key-id: LTAI5txxxxxxxxxxxxxxxxxx
# 阿里云 AccessKey Secret必填
access-key-secret: your_access_key_secret_here_32_chars
# 地域节点必填cn-shanghai
region-id: cn-shanghai
# 飞燕平台项目IDProject ID必填
project-id: a1xxxxxx
# App Key必填
app-key: your_app_key_here
# App Secret必填
app-secret: your_app_secret_here_32_characters
# 品类Key选填
category-key:
# MQTT 配置(可选)
mqtt:
# MQTT Broker 地址格式ssl://实例ID.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883
broker-url: ssl://a1xxxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883
# 客户端ID格式{ClientID}|securemode=2,signmethod=hmacsha1|
client-id: your_client_id|securemode=2,signmethod=hmacsha1|
# 用户名(设备名称&产品Key
username: DeviceName&ProductKey
# 密码通过MQTT密码工具生成
password: your_mqtt_password_here
# 连接超时时间(秒)
connection-timeout: 30
# 保活时间(秒)
keep-alive-interval: 60
# 自动重连
auto-reconnect: true
# 清除会话
clean-session: true

View File

@@ -102,11 +102,6 @@
<groupId>com.intc</groupId>
<artifactId>intc-common-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
<version>2.0.40</version>
</dependency>
</dependencies>
</project>

View File

@@ -27,8 +27,11 @@ public class DeviceSensorDataController extends BaseController
private IDeviceSensorDataService deviceSensorDataService;
@GetMapping("/getHistoryData")
public List<DeviceSensorData> getHistoryData(@RequestParam("serialNum") String serialNum, @RequestParam("deviceId") Long deviceId, @RequestParam("mobilePhone") String mobilePhone, @RequestParam("deviceType") int deviceType, @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime)
public List<DeviceSensorData> getHistoryData(@RequestParam("serialNum") String serialNum,
@RequestParam("startTime") String startTime,
@RequestParam("endTime") String endTime,
@RequestParam(value = "intervalType", required = false, defaultValue = "1") Integer intervalType)
{
return deviceSensorDataService.getHistoryDataList(serialNum,deviceId,mobilePhone,deviceType,startTime,endTime);
return deviceSensorDataService.getHistoryDataList(serialNum, startTime, endTime, intervalType);
}
}

View File

@@ -1,8 +1,12 @@
package com.intc.tdengine.domain;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor // 生成无参构造方法
@@ -10,17 +14,64 @@ import java.time.LocalDateTime;
public class DeviceSensorData {
// 测量字段(随时间变化的数值)
private LocalDateTime time; // 时序主键时间戳
private LocalDateTime createTime; // 数据创建时间
private double dissolvedOxygen; // 溶解氧
private double temperature; // 温度
private double saturability; // 饱和度
private double ph; // pH值
private double salinity; // 盐度
private double treference; // 参考值(具体含义需结合业务)
private double tfluorescence; // 荧光值
private double phaseDifference; // 相位差
private double battery; // 电池电量
private String time; // 时序主键时间戳
private String createTime; // 数据创建时间
private Double dissolvedOxygen; // 溶解氧
private Double temperature; // 温度
private Double saturability; // 饱和度
private Double ph; // pH值
private Double salinity; // 盐度
private Double treference; // 参考值(具体含义需结合业务)
private Double tfluorescence; // 荧光值
private Double phaseDifference; // 相位差
private Double battery; // 电池电量
// Getter 方法,返回保留两位小数的值
public Double getDissolvedOxygen() {
return roundToTwoDecimals(dissolvedOxygen);
}
public Double getTemperature() {
return roundToTwoDecimals(temperature);
}
public Double getSaturability() {
return roundToTwoDecimals(saturability);
}
public Double getPh() {
return roundToTwoDecimals(ph);
}
public Double getSalinity() {
return roundToTwoDecimals(salinity);
}
public Double getTreference() {
return roundToTwoDecimals(treference);
}
public Double getTfluorescence() {
return roundToTwoDecimals(tfluorescence);
}
public Double getPhaseDifference() {
return roundToTwoDecimals(phaseDifference);
}
public Double getBattery() {
return roundToTwoDecimals(battery);
}
// 工具方法:保留两位小数
private Double roundToTwoDecimals(Double value) {
if (value == null) {
return null;
}
return BigDecimal.valueOf(value)
.setScale(2, RoundingMode.HALF_UP)
.doubleValue();
}
// 标签字段(元数据,不随时间频繁变化)
private String serialNum; // 设备序列号

View File

@@ -1,5 +1,7 @@
package com.intc.tdengine.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.intc.tdengine.domain.DeviceSensorData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -8,7 +10,6 @@ import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@Mapper
public interface DeviceSensorDataMapper {
/**
@@ -46,13 +47,24 @@ public interface DeviceSensorDataMapper {
* @param dataList 数据列表
* @return 影响行数
*/
@DS("taos")
@InterceptorIgnore(tenantLine = "true")
int batchInsertDeviceSensorData(@Param("dataList") List<DeviceSensorData> dataList);
/**
* 查询数据
*
* @return 影响行数
* @param serialNum 设备序列号
* @param startTime 开始时间
* @param endTime 结束时间
* @param intervalType 间隔类型1-原始数据2-10分钟3-30分钟4-1小时5-3小时6-6小时
* @return 设备传感器数据列表
*/
List<DeviceSensorData> getHistoryDataList(@Param("serialNum") String serialNum,@Param("deviceId") Long deviceId,@Param("mobilePhone") String mobilePhone,@Param("deviceType") int deviceType,@Param("startTime") String startTime,@Param("endTime") String endTime);
@DS("taos")
@InterceptorIgnore(tenantLine = "true")
List<DeviceSensorData> getHistoryDataList(@Param("serialNum") String serialNum,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("intervalType") Integer intervalType);
}

View File

@@ -29,9 +29,13 @@ public interface IDeviceSensorDataService {
public void batchInsertDeviceSensorData(List<DeviceSensorData> dataList);
/**
* 查询数据
* 查询历史数据列表
*
* @return 影响行数
* @param serialNum 设备序列号
* @param startTime 开始时间
* @param endTime 结束时间
* @param intervalType 间隔类型1-原始数据2-10分钟3-30分钟4-1小时5-3小时6-6小时
* @return 设备传感器数据列表
*/
public List<DeviceSensorData> getHistoryDataList(String serialNum, Long deviceId, String mobilePhone, int deviceType, String startTime, String endTime);
public List<DeviceSensorData> getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType);
}

View File

@@ -1,14 +1,19 @@
package com.intc.tdengine.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.intc.tdengine.domain.DeviceSensorData;
import com.intc.tdengine.mapper.DeviceSensorDataMapper;
import com.intc.tdengine.service.IDeviceSensorDataService;
import jakarta.annotation.Resource;
import org.apache.ibatis.annotations.Param;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@DS("taos") // 指定使用 taos 数据源TDengine
public class DeviceSensorDataService implements IDeviceSensorDataService {
@Resource
private DeviceSensorDataMapper deviceSensorDataMapper;
@@ -37,18 +42,69 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
}
/**
* 查询数据
* 查询历史数据列表
*
* @return 影响行数
* @param serialNum 设备序列号
* @param startTime 开始时间
* @param endTime 结束时间
* @param intervalType 间隔类型1-原始数据2-10分钟3-30分钟4-1小时5-3小时6-6小时
* @return 设备传感器数据列表
*/
@Override
public List<DeviceSensorData> getHistoryDataList(String serialNum,Long deviceId, String mobilePhone, int deviceType,String startTime, String endTime) {
List<DeviceSensorData> list=new ArrayList<>();
public List<DeviceSensorData> getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType) {
List<DeviceSensorData> list = new ArrayList<>();
try {
list=deviceSensorDataMapper.getHistoryDataList(serialNum,deviceId,mobilePhone,deviceType,startTime,endTime);
}catch (Exception e){
// 默认为原始数据
if (intervalType == null) {
intervalType = 1;
}
// 验证intervalType参数
if (intervalType < 1 || intervalType > 6) {
log.error("无效的intervalType参数: {}, 使用默认值1", intervalType);
intervalType = 1;
}
// 处理时间格式
if (startTime != null && !startTime.contains(" ")) {
startTime = startTime + " 00:00:00";
}
if (endTime != null && !endTime.contains(" ")) {
endTime = endTime + " 23:59:59";
}
String intervalDesc = getIntervalDesc(intervalType);
log.info("查询TDengine历史数据: serialNum={}, startTime={}, endTime={}, intervalType={} ({})",
serialNum, startTime, endTime, intervalType, intervalDesc);
list = deviceSensorDataMapper.getHistoryDataList(serialNum, startTime, endTime, intervalType);
log.info("查询到 {} 条历史数据 ({})", list.size(), intervalDesc);
} catch (Exception e) {
log.error("查询TDengine历史数据失败", e);
// 打印完整异常链
Throwable cause = e;
int level = 0;
while (cause != null && level < 10) {
log.error("异常层级 {}: {}", level++, cause.getClass().getName() + ": " + cause.getMessage());
cause = cause.getCause();
}
}
return list;
}
/**
* 获取间隔类型描述
*/
private String getIntervalDesc(Integer intervalType) {
switch (intervalType) {
case 1: return "原始数据";
case 2: return "10分钟间隔";
case 3: return "30分钟间隔";
case 4: return "1小时间隔";
case 5: return "3小时间隔";
case 6: return "6小时间隔";
default: return "未知";
}
}
}

View File

@@ -52,14 +52,57 @@
</foreach>
</insert>
<select id="getHistoryDataList" parameterType="String" resultType="com.intc.tdengine.domain.DeviceSensorData">
select time,createTime,dissolvedOxygen,temperature,saturability,ph,salinity,treference,tfluorescence,phaseDifference,battery,serialNum,deviceId,data.mobilePhone,deviceType from fishery.t_#{serialNum}
<where>
time >= #{startTime} and time &lt;= #{endTime}
<if test="serialNum != null and serialNum != ''"> AND serialNum=#{serialNum}</if>
<if test="deviceId != null "> AND deviceId=#{deviceId}</if>
<if test="mobilePhone != null and mobilePhone != ''"> AND mobilePhone=#{mobilePhone}</if>
<if test="deviceType != null"> AND deviceType=#{deviceType}</if>
</where>
<select id="getHistoryDataList" resultType="com.intc.tdengine.domain.DeviceSensorData">
<if test="intervalType == null or intervalType == 1">
<!-- 原始数据,不聚合 -->
SELECT
`time`,
createTime,
dissolvedOxygen,
temperature,
saturability,
ph,
salinity,
treference,
tfluorescence,
phaseDifference,
battery,
serialNum,
deviceId,
mobilePhone,
deviceType
FROM `fishery`.`t_${serialNum}`
<where>
<if test="startTime != null and startTime != ''">AND `time` >= #{startTime}</if>
<if test="endTime != null and endTime != ''">AND `time` &lt;= #{endTime}</if>
</where>
</if>
<if test="intervalType != null and intervalType != 1">
<!-- 按时间间隔聚合 -->
SELECT
_wstart as `time`,
_wstart as createTime,
AVG(dissolvedOxygen) as dissolvedOxygen,
AVG(temperature) as temperature,
AVG(saturability) as saturability,
AVG(ph) as ph,
AVG(salinity) as salinity,
AVG(treference) as treference,
AVG(tfluorescence) as tfluorescence,
AVG(phaseDifference) as phaseDifference,
AVG(battery) as battery
FROM `fishery`.`t_${serialNum}`
<where>
<if test="startTime != null and startTime != ''">AND `time` >= #{startTime}</if>
<if test="endTime != null and endTime != ''">AND `time` &lt;= #{endTime}</if>
</where>
<choose>
<when test="intervalType == 2">INTERVAL(10m)</when>
<when test="intervalType == 3">INTERVAL(30m)</when>
<when test="intervalType == 4">INTERVAL(1h)</when>
<when test="intervalType == 5">INTERVAL(3h)</when>
<when test="intervalType == 6">INTERVAL(6h)</when>
</choose>
</if>
</select>
</mapper>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.intc</groupId>
<artifactId>intc-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>intc-weixin</artifactId>
<description>
微信对接模块
</description>
<dependencies>
<!-- 微信开发 Java SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-open</artifactId>
<version>4.6.0</version>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-doc</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-log</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-security</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-common-tenant</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,37 @@
package com.intc.weixin.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 微信小程序配置
*
* @author intc
*/
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "wx.miniapp", name = "app-id")
public class WxMaConfiguration {
private final WxMaProperties wxMaProperties;
@Bean
public WxMaService wxMaService() {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(wxMaProperties.getAppId());
config.setSecret(wxMaProperties.getSecret());
config.setToken(wxMaProperties.getToken());
config.setAesKey(wxMaProperties.getAesKey());
config.setMsgDataFormat(wxMaProperties.getMsgDataFormat());
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(config);
return service;
}
}

View File

@@ -0,0 +1,42 @@
package com.intc.weixin.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 微信小程序配置属性
*
* @author intc
*/
@Data
@Component
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
/**
* 小程序appId
*/
private String appId;
/**
* 小程序Secret
*/
private String secret;
/**
* 小程序token
*/
private String token;
/**
* 小程序EncodingAESKey
*/
private String aesKey;
/**
* 消息格式XML或者JSON
*/
private String msgDataFormat;
}

View File

@@ -0,0 +1,36 @@
package com.intc.weixin.config;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 微信公众号配置
*
* @author intc
*/
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "wx.mp", name = "app-id")
public class WxMpConfiguration {
private final WxMpProperties wxMpProperties;
@Bean
public WxMpService wxMpService() {
WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
config.setAppId(wxMpProperties.getAppId());
config.setSecret(wxMpProperties.getSecret());
config.setToken(wxMpProperties.getToken());
config.setAesKey(wxMpProperties.getAesKey());
WxMpService service = new WxMpServiceImpl();
service.setWxMpConfigStorage(config);
return service;
}
}

View File

@@ -0,0 +1,37 @@
package com.intc.weixin.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 微信公众号配置属性
*
* @author intc
*/
@Data
@Component
@ConfigurationProperties(prefix = "wx.mp")
public class WxMpProperties {
/**
* 公众号appId
*/
private String appId;
/**
* 公众号Secret
*/
private String secret;
/**
* 公众号token
*/
private String token;
/**
* 公众号EncodingAESKey
*/
private String aesKey;
}

View File

@@ -0,0 +1,39 @@
package com.intc.weixin.config;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 微信支付配置
*
* @author intc
*/
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "wx.pay", name = "mch-id")
public class WxPayConfiguration {
private final WxPayProperties wxPayProperties;
@Bean
public WxPayService wxPayService() {
WxPayConfig config = new WxPayConfig();
config.setMchId(wxPayProperties.getMchId());
config.setMchKey(wxPayProperties.getMchKey());
config.setKeyPath(wxPayProperties.getKeyPath());
config.setApiV3Key(wxPayProperties.getApiV3Key());
config.setCertSerialNo(wxPayProperties.getCertSerialNo());
config.setPrivateKeyPath(wxPayProperties.getPrivateKeyPath());
config.setPrivateCertPath(wxPayProperties.getPrivateContent());
WxPayService service = new WxPayServiceImpl();
service.setConfig(config);
return service;
}
}

View File

@@ -0,0 +1,52 @@
package com.intc.weixin.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 微信支付配置属性
*
* @author intc
*/
@Data
@Component
@ConfigurationProperties(prefix = "wx.pay")
public class WxPayProperties {
/**
* 商户号
*/
private String mchId;
/**
* 商户密钥
*/
private String mchKey;
/**
* apiclient_cert.p12文件的绝对路径或者以classpath:开头的类路径
*/
private String keyPath;
/**
* apiV3秘钥
*/
private String apiV3Key;
/**
* 证书序列号
*/
private String certSerialNo;
/**
* 私钥路径
*/
private String privateKeyPath;
/**
* 私钥内容
*/
private String privateContent;
}

View File

@@ -0,0 +1,109 @@
package com.intc.weixin.controller;
import com.intc.common.core.domain.R;
import com.intc.common.web.core.BaseController;
import com.intc.weixin.service.WxMaService;
import com.intc.weixin.service.WxMpService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
/**
* 微信对接控制器
*
* @author intc
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/weixin")
@Tag(name = "微信对接管理", description = "微信对接相关接口")
public class WeixinController extends BaseController {
@Autowired(required = false)
private WxMpService wxMpService;
@Autowired(required = false)
private WxMaService wxMaService;
@Operation(summary = "测试接口")
@GetMapping("/test")
public R<String> test() {
return R.ok("微信模块测试成功!");
}
@Operation(summary = "获取公众号用户信息")
@GetMapping("/mp/user/{openId}")
public R<WxMpUser> getMpUserInfo(
@Parameter(description = "用户openId") @PathVariable String openId) {
try {
if (wxMpService == null) {
return R.fail("公众号配置未启用");
}
WxMpUser userInfo = wxMpService.getUserInfo(openId);
return R.ok(userInfo);
} catch (WxErrorException e) {
log.error("获取用户信息失败", e);
return R.fail("获取用户信息失败: " + e.getMessage());
}
}
@Operation(summary = "生成公众号二维码")
@GetMapping("/mp/qrcode")
public R<String> createMpQrCode(
@Parameter(description = "场景值") @RequestParam String scene) {
try {
if (wxMpService == null) {
return R.fail("公众号配置未启用");
}
String qrCodeUrl = wxMpService.createQrCode(scene);
return R.ok(qrCodeUrl);
} catch (WxErrorException e) {
log.error("生成二维码失败", e);
return R.fail("生成二维码失败: " + e.getMessage());
}
}
@Operation(summary = "小程序登录")
@GetMapping("/ma/login")
public R<String> maLogin(
@Parameter(description = "登录code") @RequestParam String code) {
try {
if (wxMaService == null) {
return R.fail("小程序配置未启用");
}
String openId = wxMaService.code2Session(code);
return R.ok(openId);
} catch (WxErrorException e) {
log.error("小程序登录失败", e);
return R.fail("小程序登录失败: " + e.getMessage());
}
}
@Operation(summary = "生成小程序码")
@GetMapping("/ma/qrcode")
public R<String> createMaQrCode(
@Parameter(description = "场景值") @RequestParam String scene,
@Parameter(description = "页面路径") @RequestParam(required = false) String page) {
try {
if (wxMaService == null) {
return R.fail("小程序配置未启用");
}
byte[] qrCodeBytes = wxMaService.getUnlimitedQrCode(scene, page);
// 这里可以将字节数组上传到OSS或者直接返回给前端
return R.ok("二维码生成成功,大小: " + qrCodeBytes.length + " bytes");
} catch (WxErrorException e) {
log.error("生成小程序码失败", e);
return R.fail("生成小程序码失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,74 @@
package com.intc.weixin.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 微信用户信息
*
* @author intc
*/
@Data
@TableName("wx_user")
public class WxUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 微信openId
*/
private String openId;
/**
* 微信unionId
*/
private String unionId;
/**
* 昵称
*/
private String nickname;
/**
* 头像
*/
private String avatar;
/**
* 性别 0-未知 1-男 2-女
*/
private Integer gender;
/**
* 手机号
*/
private String mobile;
/**
* 来源类型mp-公众号 ma-小程序
*/
private String sourceType;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,31 @@
package com.intc.weixin.service;
import me.chanjar.weixin.common.error.WxErrorException;
/**
* 微信小程序服务接口
*
* @author intc
*/
public interface WxMaService {
/**
* 登录凭证校验
*
* @param code 登录时获取的 code
* @return sessionKey和openId
* @throws WxErrorException 微信异常
*/
String code2Session(String code) throws WxErrorException;
/**
* 获取小程序码
*
* @param scene 场景值
* @param page 页面路径
* @return 二进制图片数据
* @throws WxErrorException 微信异常
*/
byte[] getUnlimitedQrCode(String scene, String page) throws WxErrorException;
}

View File

@@ -0,0 +1,31 @@
package com.intc.weixin.service;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
/**
* 微信公众号服务接口
*
* @author intc
*/
public interface WxMpService {
/**
* 获取用户信息
*
* @param openId 用户openId
* @return 用户信息
* @throws WxErrorException 微信异常
*/
WxMpUser getUserInfo(String openId) throws WxErrorException;
/**
* 生成二维码ticket
*
* @param sceneStr 场景值
* @return ticket
* @throws WxErrorException 微信异常
*/
String createQrCode(String sceneStr) throws WxErrorException;
}

View File

@@ -0,0 +1,38 @@
package com.intc.weixin.service.impl;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import com.intc.weixin.service.WxMaService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
/**
* 微信小程序服务实现
*
* @author intc
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(cn.binarywang.wx.miniapp.api.WxMaService.class)
public class WxMaServiceImpl implements WxMaService {
private final cn.binarywang.wx.miniapp.api.WxMaService wxMaService;
@Override
public String code2Session(String code) throws WxErrorException {
log.info("小程序登录, code: {}", code);
WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
return session.getOpenid();
}
@Override
public byte[] getUnlimitedQrCode(String scene, String page) throws WxErrorException {
log.info("生成小程序码, scene: {}, page: {}", scene, page);
return wxMaService.getQrcodeService().createWxaCodeUnlimitBytes(scene, page, false, null, 430, true, null, false);
}
}

View File

@@ -0,0 +1,39 @@
package com.intc.weixin.service.impl;
import com.intc.weixin.service.WxMpService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
/**
* 微信公众号服务实现
*
* @author intc
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(me.chanjar.weixin.mp.api.WxMpService.class)
public class WxMpServiceImpl implements WxMpService {
private final me.chanjar.weixin.mp.api.WxMpService wxMpService;
@Override
public WxMpUser getUserInfo(String openId) throws WxErrorException {
log.info("获取微信用户信息, openId: {}", openId);
return wxMpService.getUserService().userInfo(openId);
}
@Override
public String createQrCode(String sceneStr) throws WxErrorException {
log.info("创建微信二维码, sceneStr: {}", sceneStr);
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService()
.qrCodeCreateTmpTicket(sceneStr, 2592000);
return ticket.getUrl();
}
}

View File

@@ -0,0 +1,42 @@
# 微信对接模块配置
wx:
# 微信公众号配置
mp:
# 公众号appId必填
app-id: wx1234567890abcdef
# 公众号Secret必填
secret: your_mp_secret_here_32_characters
# 公众号token选填用于消息加解密
token: your_token_here
# 公众号EncodingAESKey选填用于消息加解密
aes-key: your_aes_key_here_43_characters_base64
# 微信小程序配置
miniapp:
# 小程序appId必填
app-id: wx0987654321fedcba
# 小程序Secret必填
secret: your_miniapp_secret_here_32_chars
# 小程序token选填用于消息加解密
token: your_miniapp_token
# 小程序EncodingAESKey选填用于消息加解密
aes-key: your_miniapp_aes_key_43_characters_b64
# 消息格式XML或者JSON
msg-data-format: JSON
# 微信支付配置
pay:
# 商户号(必填)
mch-id: 1234567890
# 商户密钥V2版本必填
mch-key: your_mch_key_here_32_characters_md5
# 证书路径(退款等操作需要,选填)
key-path: classpath:cert/apiclient_cert.p12
# apiV3秘钥V3版本必填
api-v3-key: your_api_v3_key_here_32_characters
# 证书序列号V3版本必填
cert-serial-no: 1234567890ABCDEF1234567890ABCDEF12345678
# 私钥路径V3版本选填
private-key-path: classpath:cert/apiclient_key.pem
# 私钥内容V3版本选填与private-key-path二选一
private-content:

View File

@@ -17,6 +17,8 @@
<module>intc-workflow</module>
<module>intc-fishery</module>
<module>intc-tdengine</module>
<module>intc-weixin</module>
<module>intc-iot</module>
</modules>
<artifactId>intc-modules</artifactId>