Merge branch 'device-microservice'
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good

# Conflicts:
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java
#	src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java
This commit is contained in:
2025-09-04 12:28:32 +08:00
53 changed files with 3675 additions and 874 deletions

102
CLAUDE.md
View File

@@ -122,6 +122,15 @@ mvn test -DskipTests=false
3. 使用 `@Scheduled` 进行基于 cron 的执行
4. 遵循现有的错误处理和日志记录模式
### 多端API架构
应用程序通过路径前缀区分不同的客户端:
- **移动端**: `/api/mobile/*` - 针对移动应用优化的接口
- **PC管理端**: `/api/*` - Web管理面板接口
- **任务处理**: `/task/*` - 后台任务和渲染服务接口
- **外部集成**: 专用集成接口(打印机、代理、viid、vpt、wvp等)
每个端点都有对应的Controller包结构,确保API的职责分离和维护性。
## 价格查询系统 (Pricing Module)
### 核心架构
@@ -188,3 +197,96 @@ ProductType枚举定义了支持的商品类型:
- 配置验证测试:DefaultConfigValidationTest验证default配置
- JSON序列化测试:验证复杂对象的数据库存储
- 分页功能测试:验证PageHelper集成
## 关键架构模式
### Repository 层模式
项目使用Repository层抽象数据访问逻辑:
- Repository接口定义数据访问契约
- Mapper接口处理MyBatis Plus的数据库映射
- Service层通过Repository访问数据,避免直接依赖Mapper
### 异常处理架构
- **全局异常处理**: `CustomExceptionHandle` 提供统一的异常处理和响应格式
- **业务异常**: 自定义异常类继承RuntimeException,携带业务错误码
- **集成异常**: `IntegrationException` 专门处理外部服务调用异常
### 配置驱动的扩展性
通过配置文件驱动的多供应商支持:
- 存储:本地、AWS S3、阿里云 OSS
- 支付:微信支付、聪明支付
- 人脸识别:阿里云、百度
每个供应商通过统一接口访问,配置切换无需代码修改。
### 业务层架构
- **Service层**: 核心业务逻辑实现
- **Biz层**: 高级业务流程编排,组合多个Service
- **Controller层**: HTTP请求处理和响应转换
- **Repository层**: 数据访问抽象
### 认证和会话管理
- **JWT**: 使用jjwt库进行身份验证
- **Redis**: 存储会话信息和缓存
- **BaseContextHandler**: 提供当前用户上下文访问
## 微服务集成架构 (Integration Package)
### 核心架构
位于 `com.ycwl.basic.integration` 包,使用 Spring Cloud OpenFeign 和 Nacos 实现外部微服务集成。
#### 通用基础设施
- **IntegrationProperties**: 所有集成的集中配置管理
- **FeignErrorDecoder**: 自定义错误解码器,统一错误处理
- **IntegrationException**: 标准化集成异常
- **CommonResponse/PageResponse**: 外部服务响应包装器
#### 已实现的服务集成
- **Scenic Integration** (`integration.scenic`): ZT-Scenic 微服务集成
- **Device Integration** (`integration.device`): ZT-Device 微服务集成
#### 集成模式
每个外部服务按以下结构组织:
```
service/
├── client/ # Feign 客户端
├── config/ # 服务特定配置
├── dto/ # 数据传输对象
├── service/ # 业务逻辑层
└── example/ # 使用示例
```
### 配置管理
```yaml
integration:
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
```
### 使用模式
所有集成服务使用统一的 `handleResponse` 模式进行错误处理,确保一致的异常包装和日志记录。
### 测试集成服务
```bash
# 运行特定集成测试
mvn test -Dtest=ScenicIntegrationServiceTest
mvn test -Dtest=DeviceIntegrationServiceTest
# 运行所有集成测试
mvn test -Dtest="com.ycwl.basic.integration.*Test"
```
### 调试集成问题
启用 Feign 客户端日志:
```yaml
logging:
level:
com.ycwl.basic.integration: DEBUG
```

View File

@@ -11,7 +11,7 @@ import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.util.ScenicConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.utils.ApiResponse;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;

View File

@@ -1,79 +0,0 @@
package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceBatchSortRequest;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.req.DeviceSortRequest;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.model.pc.template.req.TemplateSortRequest;
import com.ycwl.basic.service.pc.DeviceService;
import com.ycwl.basic.utils.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Author:longbinbin
* @Date:2024/12/2 16:13
*/
@RestController
@RequestMapping("/api/device/v1")
// 设备管理
public class DeviceController {
@Autowired
private DeviceService deviceService;
// 设备分页查询
@PostMapping("/page")
public ApiResponse<PageInfo<DeviceRespVO>> pageQuery(@RequestBody DeviceReqQuery deviceReqQuery) {
return deviceService.pageQuery(deviceReqQuery);
}
// 设备列表查询
@PostMapping("/list")
public ApiResponse list(@RequestBody DeviceReqQuery deviceReqQuery) {
return deviceService.list(deviceReqQuery);
}
// 设备详情查询
@GetMapping("/getDetails/{id}")
public ApiResponse<DeviceRespVO> getDetails(@PathVariable("id") Long id) {
return deviceService.getById(id);
}
// 新增或修改设备
@PostMapping("/addOrUpdate")
public ApiResponse addOrUpdate(@RequestBody DeviceAddOrUpdateReq deviceReqQuery) {
return deviceService.addOrUpdate(deviceReqQuery);
}
// 删除设备
@DeleteMapping("/delete/{id}")
public ApiResponse delete(@PathVariable("id") Long id) {
return deviceService.deleteById(id);
}
// 修改设备状态
@PutMapping("/updateStatus/{id}")
public ApiResponse updateStatus(@PathVariable("id") Long id) {
return deviceService.updateStatus(id);
}
// 排序设备
@PostMapping("/sort")
public ApiResponse<Boolean> sortDevice(@RequestBody DeviceSortRequest request) {
return deviceService.sortDevice(request.getDeviceId(), request.getAfterDeviceId());
}
@PostMapping("/scenic/{scenicId}/sortBatch")
public ApiResponse<Boolean> sortDeviceBatch(@PathVariable("scenicId") Long scenicId, @RequestBody DeviceBatchSortRequest request) {
return deviceService.batchSort(scenicId, request);
}
@GetMapping("/config/{id}")
public ApiResponse<DeviceConfigEntity> getConfig(@PathVariable("id") Long id) {
return ApiResponse.success(deviceService.getConfig(id));
}
@PostMapping("/saveConfig/{configId}")
public ApiResponse saveConfig(@PathVariable("configId") Long configId, @RequestBody DeviceConfigEntity deviceConfigEntity) {
deviceService.saveConfig(configId, deviceConfigEntity);
return ApiResponse.success(null);
}
}

View File

@@ -0,0 +1,487 @@
package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 设备管理 V2 版本控制器 - 基于 zt-device 集成服务
*
* @author Claude Code
* @date 2025-09-01
*/
@Slf4j
@RestController
@RequestMapping("/api/device/v2")
@RequiredArgsConstructor
public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
// ========== 设备基础 CRUD 操作 ==========
/**
* 设备V2核心信息分页列表
*/
@GetMapping("/")
public ApiResponse<DeviceV2ListResponse> listDevices(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String no,
@RequestParam(required = false) String type,
@RequestParam(required = false) Integer isActive,
@RequestParam(required = false) Long scenicId) {
log.info("分页查询设备核心信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
DeviceV2ListResponse response = deviceIntegrationService.listDevices(page, pageSize, name, no, type, isActive, scenicId);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询设备核心信息列表失败", e);
return ApiResponse.fail("分页查询设备列表失败: " + e.getMessage());
}
}
/**
* 设备V2带配置信息分页列表
*/
@GetMapping("/with-config")
public ApiResponse<DeviceV2WithConfigListResponse> listDevicesWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String no,
@RequestParam(required = false) String type,
@RequestParam(required = false) Integer isActive,
@RequestParam(required = false) Long scenicId) {
log.info("分页查询设备带配置信息列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
DeviceV2WithConfigListResponse response = deviceIntegrationService.listDevicesWithConfig(page, pageSize, name, no, type, isActive, scenicId);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("分页查询设备带配置信息列表失败", e);
return ApiResponse.fail("分页查询设备带配置信息列表失败: " + e.getMessage());
}
}
/**
* 根据ID获取设备信息
*/
@GetMapping("/{id}")
public ApiResponse<DeviceV2DTO> getDevice(@PathVariable Long id) {
log.info("获取设备信息, id: {}", id);
try {
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("获取设备信息失败, id: {}", id, e);
return ApiResponse.fail("获取设备信息失败: " + e.getMessage());
}
}
/**
* 根据ID获取设备带配置信息
*/
@GetMapping("/{id}/with-config")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable Long id) {
log.info("获取设备配置信息, id: {}", id);
try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfig(id);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("获取设备配置信息失败, id: {}", id, e);
return ApiResponse.fail("获取设备配置信息失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取设备信息
*/
@GetMapping("/no/{no}")
public ApiResponse<DeviceV2DTO> getDeviceByNo(@PathVariable String no) {
log.info("根据设备编号获取设备信息, no: {}", no);
try {
DeviceV2DTO device = deviceIntegrationService.getDeviceByNo(no);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("根据设备编号获取设备信息失败, no: {}", no, e);
return ApiResponse.fail("根据设备编号获取设备信息失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取设备带配置信息
*/
@GetMapping("/no/{no}/with-config")
public ApiResponse<DeviceV2WithConfigDTO> getDeviceWithConfigByNo(@PathVariable String no) {
log.info("根据设备编号获取设备配置信息, no: {}", no);
try {
DeviceV2WithConfigDTO device = deviceIntegrationService.getDeviceWithConfigByNo(no);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("根据设备编号获取设备配置信息失败, no: {}", no, e);
return ApiResponse.fail("根据设备编号获取设备配置信息失败: " + e.getMessage());
}
}
/**
* 创建设备
*/
@PostMapping("/")
public ApiResponse<DeviceV2DTO> createDevice(@Valid @RequestBody CreateDeviceRequest request) {
log.info("创建设备, name: {}, no: {}, type: {}, sort: {}",
request.getName(), request.getNo(), request.getType(), request.getSort());
try {
DeviceV2DTO device = deviceIntegrationService.createDevice(request);
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建设备失败", e);
return ApiResponse.fail("创建设备失败: " + e.getMessage());
}
}
/**
* 创建IPC摄像头设备(快捷方法)
*/
@PostMapping("/ipc")
public ApiResponse<DeviceV2DTO> createIpcDevice(@RequestBody Map<String, Object> request) {
String name = (String) request.get("name");
String deviceNo = (String) request.get("no");
Long scenicId = Long.valueOf(request.get("scenicId").toString());
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
log.info("创建IPC摄像头设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
try {
DeviceV2DTO device;
if (sort != null) {
device = deviceIntegrationService.createIpcDeviceWithSort(name, deviceNo, scenicId, sort);
} else {
device = deviceIntegrationService.createIpcDevice(name, deviceNo, scenicId);
}
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建IPC摄像头设备失败", e);
return ApiResponse.fail("创建IPC摄像头设备失败: " + e.getMessage());
}
}
/**
* 创建自定义设备(快捷方法)
*/
@PostMapping("/custom")
public ApiResponse<DeviceV2DTO> createCustomDevice(@RequestBody Map<String, Object> request) {
String name = (String) request.get("name");
String deviceNo = (String) request.get("no");
Long scenicId = Long.valueOf(request.get("scenicId").toString());
Integer sort = request.get("sort") != null ? Integer.valueOf(request.get("sort").toString()) : null;
log.info("创建自定义设备, name: {}, no: {}, scenicId: {}, sort: {}", name, deviceNo, scenicId, sort);
try {
DeviceV2DTO device;
if (sort != null) {
device = deviceIntegrationService.createCustomDeviceWithSort(name, deviceNo, scenicId, sort);
} else {
device = deviceIntegrationService.createCustomDevice(name, deviceNo, scenicId);
}
return ApiResponse.success(device);
} catch (Exception e) {
log.error("创建自定义设备失败", e);
return ApiResponse.fail("创建自定义设备失败: " + e.getMessage());
}
}
/**
* 更新设备信息
*/
@PutMapping("/{id}")
public ApiResponse<String> updateDevice(@PathVariable Long id, @Valid @RequestBody UpdateDeviceRequest request) {
log.info("更新设备信息, id: {}", id);
try {
deviceIntegrationService.updateDevice(id, request);
return ApiResponse.success("设备信息更新成功");
} catch (Exception e) {
log.error("更新设备信息失败, id: {}", id, e);
return ApiResponse.fail("更新设备信息失败: " + e.getMessage());
}
}
/**
* 更新设备排序
*/
@PutMapping("/{id}/sort")
public ApiResponse<String> updateDeviceSort(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
Integer sort = request.get("sort");
log.info("更新设备排序, id: {}, sort: {}", id, sort);
try {
deviceIntegrationService.updateDeviceSort(id, sort);
return ApiResponse.success("设备排序更新成功");
} catch (Exception e) {
log.error("更新设备排序失败, id: {}, sort: {}", id, sort, e);
return ApiResponse.fail("更新设备排序失败: " + e.getMessage());
}
}
/**
* 启用设备
*/
@PutMapping("/{id}/enable")
public ApiResponse<String> enableDevice(@PathVariable Long id) {
log.info("启用设备, id: {}", id);
try {
deviceIntegrationService.enableDevice(id);
return ApiResponse.success("设备启用成功");
} catch (Exception e) {
log.error("启用设备失败, id: {}", id, e);
return ApiResponse.fail("启用设备失败: " + e.getMessage());
}
}
/**
* 禁用设备
*/
@PutMapping("/{id}/disable")
public ApiResponse<String> disableDevice(@PathVariable Long id) {
log.info("禁用设备, id: {}", id);
try {
deviceIntegrationService.disableDevice(id);
return ApiResponse.success("设备禁用成功");
} catch (Exception e) {
log.error("禁用设备失败, id: {}", id, e);
return ApiResponse.fail("禁用设备失败: " + e.getMessage());
}
}
/**
* 删除设备
*/
@DeleteMapping("/{id}")
public ApiResponse<String> deleteDevice(@PathVariable Long id) {
log.info("删除设备, id: {}", id);
try {
deviceIntegrationService.deleteDevice(id);
return ApiResponse.success("设备删除成功");
} catch (Exception e) {
log.error("删除设备失败, id: {}", id, e);
return ApiResponse.fail("删除设备失败: " + e.getMessage());
}
}
// ========== 设备配置管理操作 ==========
/**
* 获取设备配置列表
*/
@GetMapping("/{id}/config")
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigs(@PathVariable Long id) {
log.info("获取设备配置列表, deviceId: {}", id);
try {
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigs(id);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取设备配置列表失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备配置列表失败: " + e.getMessage());
}
}
/**
* 获取设备扁平化配置
*/
@GetMapping("/{id}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable Long id) {
log.info("获取设备扁平化配置, deviceId: {}", id);
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfig(id);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("获取设备扁平化配置失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备扁平化配置失败: " + e.getMessage());
}
}
/**
* 根据配置键获取配置
*/
@GetMapping("/{id}/config/{configKey}")
public ApiResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable Long id,
@PathVariable String configKey) {
log.info("根据键获取设备配置, deviceId: {}, configKey: {}", id, configKey);
try {
DeviceConfigV2DTO config = deviceConfigIntegrationService.getDeviceConfigByKey(id, configKey);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据键获取设备配置失败, deviceId: {}, configKey: {}", id, configKey, e);
return ApiResponse.fail("根据键获取设备配置失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取配置列表
*/
@GetMapping("/no/{no}/config")
public ApiResponse<List<DeviceConfigV2DTO>> getDeviceConfigsByNo(@PathVariable String no) {
log.info("根据设备编号获取配置列表, deviceNo: {}", no);
try {
List<DeviceConfigV2DTO> configs = deviceConfigIntegrationService.getDeviceConfigsByNo(no);
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("根据设备编号获取配置列表失败, deviceNo: {}", no, e);
return ApiResponse.fail("根据设备编号获取配置列表失败: " + e.getMessage());
}
}
/**
* 根据设备编号获取扁平化配置
*/
@GetMapping("/no/{no}/flat-config")
public ApiResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable String no) {
log.info("根据设备编号获取扁平化配置, deviceNo: {}", no);
try {
Map<String, Object> config = deviceConfigIntegrationService.getDeviceFlatConfigByNo(no);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("根据设备编号获取扁平化配置失败, deviceNo: {}", no, e);
return ApiResponse.fail("根据设备编号获取扁平化配置失败: " + e.getMessage());
}
}
/**
* 创建设备配置
*/
@PostMapping("/{id}/config")
public ApiResponse<DeviceConfigV2DTO> createDeviceConfig(@PathVariable Long id,
@Valid @RequestBody CreateDeviceConfigRequest request) {
log.info("创建设备配置, deviceId: {}, configKey: {}", id, request.getConfigKey());
try {
DeviceConfigV2DTO config = deviceConfigIntegrationService.createDeviceConfig(id, request);
return ApiResponse.success(config);
} catch (Exception e) {
log.error("创建设备配置失败, deviceId: {}, configKey: {}", id, request.getConfigKey(), e);
return ApiResponse.fail("创建设备配置失败: " + e.getMessage());
}
}
/**
* 批量创建/更新设备配置
*/
@PostMapping("/{id}/config/batch")
public ApiResponse<BatchUpdateResponse> batchUpdateDeviceConfig(@PathVariable Long id,
@Valid @RequestBody BatchDeviceConfigRequest request) {
log.info("批量更新设备配置, deviceId: {}, configs count: {}", id, request.getConfigs().size());
try {
BatchUpdateResponse result = deviceConfigIntegrationService.batchUpdateDeviceConfig(id, request);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("批量更新设备配置失败, deviceId: {}", id, e);
return ApiResponse.fail("批量更新设备配置失败: " + e.getMessage());
}
}
/**
* 更新设备配置
*/
@PutMapping("/{id}/config/{configId}")
public ApiResponse<String> updateDeviceConfig(@PathVariable Long id, @PathVariable Long configId,
@Valid @RequestBody UpdateDeviceConfigRequest request) {
log.info("更新设备配置, deviceId: {}, configId: {}", id, configId);
try {
deviceConfigIntegrationService.updateDeviceConfig(id, configId, request);
return ApiResponse.success("设备配置更新成功");
} catch (Exception e) {
log.error("更新设备配置失败, deviceId: {}, configId: {}", id, configId, e);
return ApiResponse.fail("更新设备配置失败: " + e.getMessage());
}
}
/**
* 删除设备配置
*/
@DeleteMapping("/{id}/config/{configId}")
public ApiResponse<String> deleteDeviceConfig(@PathVariable Long id, @PathVariable Long configId) {
log.info("删除设备配置, deviceId: {}, configId: {}", id, configId);
try {
deviceConfigIntegrationService.deleteDeviceConfig(id, configId);
return ApiResponse.success("设备配置删除成功");
} catch (Exception e) {
log.error("删除设备配置失败, deviceId: {}, configId: {}", id, configId, e);
return ApiResponse.fail("删除设备配置失败: " + e.getMessage());
}
}
// ========== 景区设备管理操作 ==========
/**
* 获取景区IPC设备列表
*/
@GetMapping("/scenic/{scenicId}/ipc")
public ApiResponse<DeviceV2ListResponse> getScenicIpcDevices(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区IPC设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
DeviceV2ListResponse response = deviceIntegrationService.getScenicIpcDevices(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区IPC设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区IPC设备列表失败: " + e.getMessage());
}
}
/**
* 获取景区激活设备列表
*/
@GetMapping("/scenic/{scenicId}/active")
public ApiResponse<DeviceV2ListResponse> getScenicActiveDevices(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区激活设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
DeviceV2ListResponse response = deviceIntegrationService.getScenicActiveDevices(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区激活设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区激活设备列表失败: " + e.getMessage());
}
}
/**
* 获取景区所有设备列表
*/
@GetMapping("/scenic/{scenicId}/all")
public ApiResponse<DeviceV2ListResponse> getScenicAllDevices(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("获取景区所有设备列表, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
try {
DeviceV2ListResponse response = deviceIntegrationService.listDevices(page, pageSize, null, null, null, null, scenicId);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("获取景区所有设备列表失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
}
}
}

View File

@@ -4,12 +4,16 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.ObjectUtil;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.annotation.IgnoreLogReq;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.CreateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.UpdateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
@@ -33,7 +37,6 @@ import com.ycwl.basic.model.viid.req.UnRegisterReq;
import com.ycwl.basic.model.viid.resp.SystemTimeResp;
import com.ycwl.basic.model.viid.resp.VIIDBaseResp;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.storage.StorageFactory;
@@ -80,15 +83,13 @@ import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
@Slf4j
public class ViidController {
@Autowired
private DeviceMapper deviceMapper;
private DeviceIntegrationService deviceIntegrationService;
private static final String serverId = "00000000000000000001";
@Autowired
private SourceMapper sourceMapper;
@Autowired
private DeviceRepository deviceRepository;
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private TaskFaceService taskFaceService;
private final Map<Long, ThreadPoolExecutor> executors = new ConcurrentHashMap<>();
@Autowired
@@ -129,12 +130,21 @@ public class ViidController {
}
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
if (device.getId() != null) {
deviceMapper.updateEntity(device);
} else {
device.setId(SnowFlakeUtil.getLongId());
deviceMapper.addEntity(device);
deviceRepository.clearDeviceCache(deviceId);
if (device.getId() == null) {
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
}
return new VIIDBaseResp(
new ResponseStatusObject(serverId, "/VIID/System/Register", "0", "注册成功", sdfTime.format(new Date()))
@@ -166,9 +176,20 @@ public class ViidController {
device.setOnline(1);
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
device.setId(SnowFlakeUtil.getLongId());
deviceMapper.addEntity(device);
deviceRepository.clearDeviceCache(deviceId);
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
} else {
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 1, new Date());
}
@@ -245,17 +266,15 @@ public class ViidController {
if (device == null) {
continue;
}
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(device.getId());
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(device.getId());
DeviceConfigEntity deviceConfigEntity = deviceRepository.getDeviceConfig(device.getId());
if (deviceConfig == null) {
log.warn("设备配置不存在:" + deviceID);
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
int viidMode = 0;
if (deviceConfig.getViidType() != null) {
viidMode = deviceConfig.getViidType();
}
Integer viidMode = deviceConfig.getInteger("viid_mode", 0);
Date shotTime = null;
if (StringUtils.isNotBlank(face.getShotTime())) {
try {
@@ -333,7 +352,7 @@ public class ViidController {
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
}
}
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
});
@@ -352,7 +371,7 @@ public class ViidController {
MultipartFile _file = ImageUtils.base64ToMultipartFile(_subImage.getData());
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
List<DeviceCropConfig> cropConfigs = deviceConfig._getCropConfig();
List<DeviceCropConfig> cropConfigs = deviceConfigEntity._getCropConfig();
for (DeviceCropConfig cropConfig : cropConfigs) {
source.setId(SnowFlakeUtil.getLongId());
String filename = StorageUtil.joinPath(PHOTO_PATH, UUID.randomUUID() + "." + ext);
@@ -425,7 +444,7 @@ public class ViidController {
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
}
}
if (Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
});

View File

@@ -6,7 +6,6 @@ import com.ycwl.basic.constant.StorageConstant;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
import com.ycwl.basic.model.wvp.WvpSyncReqVo;
import com.ycwl.basic.service.pc.DeviceService;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
@@ -30,15 +29,12 @@ import java.util.List;
@RequestMapping("/wvp/v1/")
public class WvpController {
@Autowired
private DeviceService deviceService;
@Autowired
private ScenicService scenicService;
@IgnoreLogReq
@PostMapping("/scenic/{scenicId}/sync")
public ApiResponse<List<WvpPassiveStorageOperator.Task>> sync(@PathVariable("scenicId") Long scenicId, @RequestBody WvpSyncReqVo reqVo) {
deviceService.updateDevices(scenicId, reqVo);
return ApiResponse.success(WvpPassiveStorageOperator.getTaskListByScenicId(scenicId));
}

View File

@@ -11,10 +11,13 @@ import com.ycwl.basic.device.operator.VptPassiveStorageOperator;
import com.ycwl.basic.device.operator.WvpActiveStorageOperator;
import com.ycwl.basic.device.operator.WvpPassiveStorageOperator;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import java.time.ZoneId;
import java.util.Date;
public class DeviceFactory {
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceEntity device, DeviceConfigEntity config) {
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceV2DTO device, DeviceConfigEntity config) {
IDeviceStorageOperator operator = null;
if (config == null) {
return null;
@@ -35,11 +38,34 @@ public class DeviceFactory {
if (operator == null) {
return null;
}
operator.setDevice(device);
operator.setDevice(convertToEntity(device));
operator.setDeviceConfig(config);
return operator;
}
/**
* 将DeviceV2DTO转换为DeviceEntity
*/
private static DeviceEntity convertToEntity(DeviceV2DTO dto) {
if (dto == null) {
return null;
}
DeviceEntity entity = new DeviceEntity();
entity.setId(dto.getId());
entity.setName(dto.getName());
entity.setNo(dto.getNo());
entity.setScenicId(dto.getScenicId());
entity.setStatus(dto.getIsActive());
// 转换时间格式:LocalDateTime -> Date
if (dto.getCreateTime() != null) {
entity.setCreateAt(Date.from(dto.getCreateTime().atZone(ZoneId.systemDefault()).toInstant()));
}
if (dto.getUpdateTime() != null) {
entity.setUpdateAt(Date.from(dto.getUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
}
return entity;
}
public static IDeviceStatusChecker getDeviceStatusChecker(DeviceEntity device, DeviceConfigEntity config) {
IDeviceStatusChecker checker = null;
if (config.getOnlineCheck() <= 0) {

View File

@@ -84,7 +84,8 @@ public class VptPassiveStorageOperator extends ADeviceStorageOperator {
if (StringUtils.isNotBlank(config.getDeviceNo())) {
task.deviceNo = config.getDeviceNo();
} else {
task.deviceNo = device.getNo2();
log.warn("设备未配置deviceNo:{}", device);
return Collections.emptyList();
}
task.startTime = startDate;
task.endTime = endDate;

View File

@@ -81,7 +81,8 @@ public class WvpPassiveStorageOperator extends ADeviceStorageOperator {
if (StringUtils.isNotBlank(config.getDeviceNo())) {
task.deviceNo = config.getDeviceNo();
} else {
task.deviceNo = device.getNo2();
log.warn("设备未配置deviceNo:{}", device);
return Collections.emptyList();
}
task.startTime = startDate;
task.endTime = endDate;

View File

@@ -0,0 +1,662 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Integration Package Overview
The integration package (`com.ycwl.basic.integration`) is responsible for external microservice integrations using Spring Cloud OpenFeign and Nacos service discovery. It provides a standardized approach for calling external services with proper error handling, configuration management, and response processing.
## Architecture
### Core Components
#### Common Infrastructure (`com.ycwl.basic.integration.common`)
- **IntegrationProperties**: Centralized configuration properties for all integrations
- **FeignConfig**: Global Feign configuration with error decoder and request interceptors
- **FeignErrorDecoder**: Custom error decoder that converts Feign errors to IntegrationException
- **IntegrationException**: Standardized exception for integration failures
- **CommonResponse/PageResponse**: Standard response wrappers for external service calls
- **ConfigValueUtil**: Utility for handling configuration values
- **IntegrationFallbackService**: Universal fallback service for handling integration failures
#### Service-Specific Integrations
Currently implemented:
- **Scenic Integration** (`com.ycwl.basic.integration.scenic`): ZT-Scenic microservice integration
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
### Integration Pattern
Each external service integration follows this structure:
```
service/
├── client/ # Feign clients for HTTP calls
├── config/ # Service-specific configuration
├── dto/ # Data transfer objects
├── service/ # Service layer with business logic
└── example/ # Usage examples
```
## Integration Fallback Mechanism
### Overview
The integration package includes a comprehensive fallback mechanism that provides automatic degradation when external microservices are unavailable or return errors. This mechanism ensures system resilience and maintains service availability even when dependencies fail.
### Core Components
#### IntegrationFallbackService
Universal fallback service that handles failure degradation for all microservice integrations:
**Key Features:**
- **Automatic Fallback**: Transparently handles service failures and returns cached results
- **Configurable TTL**: Service-specific cache TTL configuration (default: 7 days)
- **Cache Management**: Comprehensive cache statistics, cleanup, and monitoring
- **Service Isolation**: Per-service cache namespacing and configuration
- **Operation Types**: Supports both query operations (return cached data) and mutation operations (ignore failures with history)
**Core Methods:**
```java
// Query operations with fallback
<T> T executeWithFallback(String serviceName, String cacheKey, Supplier<T> operation, Class<T> resultClass)
// Mutation operations with fallback
void executeWithFallback(String serviceName, String cacheKey, Runnable operation)
// Cache management
boolean hasFallbackCache(String serviceName, String cacheKey)
void clearFallbackCache(String serviceName, String cacheKey)
void clearAllFallbackCache(String serviceName)
FallbackCacheStats getFallbackCacheStats(String serviceName)
```
### Fallback Strategy
#### Query Operations (GET methods) - WITH FALLBACK
1. **Normal Execution**: Execute the primary operation
2. **Success Handling**: Store successful result in fallback cache for future degradation
3. **Failure Handling**: On failure, attempt to retrieve cached result from previous success
4. **Final Fallback**: If no cached result exists, propagate the original exception
#### Mutation Operations (PUT/POST/DELETE methods) - NO FALLBACK
1. **Direct Execution**: Execute the primary operation directly without fallback
2. **Success Handling**: Operation completes successfully
3. **Failure Handling**: Immediately propagate the original exception to caller
4. **Rationale**: Users need to know if their create/update/delete operations truly succeeded or failed
### Configuration
#### Properties Structure
```yaml
integration:
fallback:
enabled: true # Global fallback switch
cachePrefix: "integration:fallback:" # Global cache prefix
defaultTtlDays: 7 # Default cache TTL in days
# Service-specific configurations
scenic:
enabled: true # Service-specific fallback switch
ttlDays: 10 # Custom TTL for scenic service
cachePrefix: "scenic:fallback:" # Custom cache prefix (optional)
device:
enabled: true
ttlDays: 5 # Custom TTL for device service
```
#### Configuration Priority
1. **Service-specific settings**: Override global defaults when specified
2. **Global defaults**: Used when service-specific settings are not provided
3. **Hardcoded defaults**: Final fallback when no configuration is available
### Cache Key Strategy
Cache keys follow a standardized naming convention:
```
{cachePrefix}{serviceName}:{operationType}:{resourceId}
```
**Examples:**
- `integration:fallback:zt-device:device:1001` - Device info cache
- `integration:fallback:zt-device:device:config:1001` - Device config cache
- `integration:fallback:zt-scenic:scenic:2001` - Scenic info cache
- `integration:fallback:zt-scenic:scenic:flat:config:2001` - Scenic flat config cache
## Scenic Integration (ZT-Scenic Microservice)
### Key Components
#### Feign Clients
- **ScenicV2Client**: Main scenic operations (CRUD, filtering, listing)
- **ScenicConfigV2Client**: Scenic configuration management
- **DefaultConfigClient**: Default configuration operations
#### Services
- **ScenicIntegrationService**: High-level scenic operations (with automatic fallback)
- **ScenicConfigIntegrationService**: Configuration management (with automatic fallback)
- **DefaultConfigIntegrationService**: Default configuration handling
#### Configuration
```yaml
integration:
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
retryEnabled: false
maxRetries: 3
```
### Usage Examples
#### Basic Scenic Operations (with Automatic Fallback)
```java
@Autowired
private ScenicIntegrationService scenicService;
// Get scenic with configuration (automatically falls back to cache on failure)
ScenicV2WithConfigDTO scenic = scenicService.getScenicWithConfig(scenicId);
// Get scenic basic info (automatically falls back to cache on failure)
ScenicV2DTO scenicInfo = scenicService.getScenic(scenicId);
// Get flat configuration (automatically falls back to cache on failure)
Map<String, Object> config = scenicService.getScenicFlatConfig(scenicId);
// Create new scenic (direct operation, fails immediately on error)
CreateScenicRequest request = new CreateScenicRequest();
request.setName("Test Scenic");
ScenicV2DTO result = scenicService.createScenic(request);
// Update scenic (direct operation, fails immediately on error)
UpdateScenicRequest updateRequest = new UpdateScenicRequest();
updateRequest.setName("Updated Scenic");
ScenicV2DTO updated = scenicService.updateScenic(scenicId, updateRequest);
// Filter scenics (no fallback for complex queries)
ScenicFilterRequest filterRequest = new ScenicFilterRequest();
// configure filters...
ScenicFilterPageResponse response = scenicService.filterScenics(filterRequest);
```
#### Configuration Management (with Automatic Fallback)
```java
@Autowired
private ScenicConfigIntegrationService configService;
// Get flat configuration (automatically falls back to cache on failure)
Map<String, Object> config = configService.getFlatConfigs(scenicId);
// Batch update configurations (direct operation, fails immediately on error)
BatchConfigRequest batchRequest = new BatchConfigRequest();
// configure batch updates...
BatchUpdateResponse result = configService.batchUpdateConfigs(scenicId, batchRequest);
// Batch flat update configurations (direct operation, fails immediately on error)
Map<String, Object> flatUpdates = new HashMap<>();
flatUpdates.put("max_visitors", "5000");
flatUpdates.put("opening_hours", "08:00-18:00");
BatchUpdateResponse flatResult = configService.batchFlatUpdateConfigs(scenicId, flatUpdates);
```
#### Fallback Cache Management for Scenics
```java
@Autowired
private IntegrationFallbackService fallbackService;
// Check fallback cache status
boolean hasScenicCache = fallbackService.hasFallbackCache("zt-scenic", "scenic:2001");
boolean hasConfigCache = fallbackService.hasFallbackCache("zt-scenic", "scenic:flat:configs:2001");
// Get cache statistics
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats("zt-scenic");
log.info("Scenic fallback cache: {} items, TTL: {} days", stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// Clear specific cache
fallbackService.clearFallbackCache("zt-scenic", "scenic:2001");
// Clear all scenic caches
fallbackService.clearAllFallbackCache("zt-scenic");
```
## Device Integration (ZT-Device Microservice)
### Key Components
#### Feign Clients
- **DeviceV2Client**: Main device operations (CRUD, filtering, listing)
- **DeviceConfigV2Client**: Device configuration management
#### Services
- **DeviceIntegrationService**: High-level device operations (with automatic fallback)
- **DeviceConfigIntegrationService**: Device configuration management (with automatic fallback)
#### Configuration
```yaml
integration:
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
retryEnabled: false
maxRetries: 3
```
### Usage Examples
#### Basic Device Operations (with Automatic Fallback)
```java
@Autowired
private DeviceIntegrationService deviceService;
// Create IPC camera device (operation tracked for future fallback)
DeviceV2DTO ipcDevice = deviceService.createIpcDevice("前门摄像头", "CAM001", scenicId);
// Get device with configuration (automatically falls back to cache on failure)
DeviceV2WithConfigDTO device = deviceService.getDeviceWithConfig(deviceId);
// Get device by number (automatically falls back to cache on failure)
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo("CAM001");
// Get basic device info (automatically falls back to cache on failure)
DeviceV2DTO basicDevice = deviceService.getDevice(deviceId);
// List scenic devices (no fallback for list operations)
DeviceV2ListResponse deviceList = deviceService.getScenicIpcDevices(scenicId, 1, 10);
// Enable/disable device (direct operation, fails immediately on error)
deviceService.enableDevice(deviceId);
deviceService.disableDevice(deviceId);
// Update device (direct operation, fails immediately on error)
UpdateDeviceRequest updateRequest = new UpdateDeviceRequest();
updateRequest.setName("更新后的摄像头");
deviceService.updateDevice(deviceId, updateRequest);
```
#### Device Configuration Management (with Automatic Fallback)
```java
@Autowired
private DeviceConfigIntegrationService configService;
// Get device configurations (with fallback)
List<DeviceConfigV2DTO> configs = configService.getDeviceConfigs(deviceId);
// Get flat configuration (automatically falls back to cache on failure)
Map<String, Object> config = configService.getDeviceFlatConfig(deviceId);
// Get flat configuration by device number (with fallback)
Map<String, Object> configByNo = configService.getDeviceFlatConfigByNo("CAM001");
// Batch update configurations (direct operation, fails immediately on error)
BatchDeviceConfigRequest batchRequest = configService.createBatchConfigBuilder()
.addConfig("brightness", "50")
.addConfig("contrast", "80")
.build();
configService.batchUpdateDeviceConfig(deviceId, batchRequest);
// New batch configuration API with detailed results
BatchDeviceConfigRequest request = configService.createBatchConfigBuilder()
.addVideoConfig("4K", 30, "H265") // Add video configuration
.addNetworkConfig("192.168.1.100", 554, "RTSP") // Add network configuration
.addAuthConfig("admin", "password") // Add authentication configuration
.addConfig("recording_enabled", "true") // Use default configuration
.addConfig("custom_setting", "value", "string", "Custom setting") // Custom configuration
.build();
BatchUpdateResponse result = configService.batchUpdateDeviceConfigWithResult(deviceId, request);
if (result.getFailed() > 0) {
// Handle partial failure
result.getProcessedItems().stream()
.filter(item -> "failed".equals(item.getStatus()))
.forEach(item -> log.warn("Config {} failed: {}", item.getConfigKey(), item.getMessage()));
}
```
#### Device Management Patterns
```java
// Create and configure camera in one operation (both operations direct, no fallback)
DeviceV2DTO camera = deviceService.createIpcDevice("摄像头1", "CAM001", scenicId);
// Use batch configuration to set camera parameters (direct operation)
BatchDeviceConfigRequest cameraConfig = configService.createBatchConfigBuilder()
.addConfig("ip_address", "192.168.1.100")
.addConfig("resolution", "1920x1080")
.addConfig("framerate", "30")
.addConfig("protocol", "RTSP")
.addConfig("username", "admin")
.addConfig("password", "password")
.build();
configService.batchUpdateDeviceConfig(camera.getId(), cameraConfig);
// Get scenic camera status
DeviceV2WithConfigListResponse camerasWithConfig =
deviceService.listDevicesWithConfig(1, 100, null, null, "IPC", 1, scenicId);
// Batch update camera resolution (direct operations, no fallback)
for (DeviceV2WithConfigDTO device : camerasWithConfig.getList()) {
BatchDeviceConfigRequest resolutionUpdate = configService.createBatchConfigBuilder()
.addConfig("resolution", "2560x1440")
.build();
configService.batchUpdateDeviceConfig(device.getId(), resolutionUpdate);
}
// Monitor device configuration completeness (with automatic fallback)
for (DeviceV2DTO device : activeDevices.getList()) {
Map<String, Object> config = configService.getDeviceFlatConfig(device.getId());
boolean hasIpConfig = config.containsKey("ip_address");
// log configuration status...
}
```
#### Fallback Cache Management for Devices
```java
@Autowired
private IntegrationFallbackService fallbackService;
// Check fallback cache status
boolean hasDeviceCache = fallbackService.hasFallbackCache("zt-device", "device:1001");
boolean hasConfigCache = fallbackService.hasFallbackCache("zt-device", "device:flat:config:1001");
// Get cache statistics
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats("zt-device");
log.info("Device fallback cache: {} items, TTL: {} days", stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// Clear specific cache
fallbackService.clearFallbackCache("zt-device", "device:1001");
// Clear all device caches
fallbackService.clearAllFallbackCache("zt-device");
```
### Enhanced Batch Configuration API
Device Integration now supports an enhanced batch configuration API that provides detailed processing results and supports default configuration rules.
#### Default Configuration Rules
The system uses `device_id = 0` configurations as default templates:
- **Configurations with defaults**: System enforces default `config_type` and `description`, only `configValue` is updated
- **Configurations without defaults**: Allows custom `config_type` and `description`
#### Usage Examples
```java
// 1. Using Builder Pattern
BatchDeviceConfigRequest request = configService.createBatchConfigBuilder()
.addVideoConfig("1920x1080", 30, "H264") // Video settings
.addNetworkConfig("192.168.1.100", 554, "RTSP") // Network settings
.addAuthConfig("admin", "password123") // Authentication
.addConfig("recording_enabled", "true") // Use default config rule
.addConfig("custom_path", "/data", "string", "Storage path") // Custom config
.build();
BatchUpdateResponse result = configService.batchUpdateDeviceConfigWithResult(deviceId, request);
// 2. Processing Results
log.info("Batch update: {} success, {} failed", result.getSuccess(), result.getFailed());
for (ProcessedConfigItem item : result.getProcessedItems()) {
if ("success".equals(item.getStatus())) {
log.info("✅ {} updated (action: {}, hasDefault: {})",
item.getConfigKey(), item.getAction(), item.getHasDefault());
} else {
log.warn("❌ {} failed: {}", item.getConfigKey(), item.getMessage());
}
}
// 3. Error Handling
if (result.getFailed() > 0) {
result.getErrors().forEach(error -> log.warn("Error: {}", error));
}
```
#### Response Format
- **200 OK**: All configurations updated successfully
- **202 Accepted**: Partial success (some configurations failed)
- **400 Bad Request**: All configurations failed
Each processed item includes:
- `status`: "success" or "failed"
- `action`: "create" or "update"
- `hasDefault`: Whether default configuration rules were applied
- `finalType`/`finalDescription`: Actually used type and description
### Device Types
- **IPC**: IP Camera devices for video monitoring
- **CUSTOM**: Custom device types for sensors, controllers, etc.
### Common Configuration Keys
- `ip_address`: Device IP address
- `resolution`: Video resolution (e.g., "1920x1080", "3840x2160")
- `framerate`: Video frame rate (integer)
- `protocol`: Communication protocol (e.g., "RTSP", "HTTP")
- `username`: Authentication username
- `password`: Authentication password
- `brightness`: Display brightness (0-100)
- `contrast`: Display contrast (0-100)
- `quality`: Video quality ("low", "medium", "high")
## Adding New Service Integrations
### 1. Create Package Structure
```
com.ycwl.basic.integration.{service-name}/
├── client/
├── config/
├── dto/
├── service/
└── example/
```
### 2. Add Configuration Properties
Update `IntegrationProperties` to include new service configuration:
```java
@Data
public static class NewServiceConfig {
private boolean enabled = true;
private String serviceName = "service-name";
private int connectTimeout = 5000;
private int readTimeout = 10000;
// other configs...
}
```
### 3. Create Feign Client
```java
@FeignClient(name = "service-name", contextId = "service-context", path = "/api/path")
public interface NewServiceClient {
@GetMapping("/endpoint")
CommonResponse<ResponseDTO> getData(@PathVariable Long id);
// other endpoints...
}
```
### 4. Implement Service Layer
```java
@Service
@RequiredArgsConstructor
public class NewServiceIntegrationService {
private final NewServiceClient client;
public ResponseDTO getData(Long id) {
CommonResponse<ResponseDTO> response = client.getData(id);
return handleResponse(response, "Failed to get data");
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null
? response.getMessage()
: errorMessage;
Integer code = response != null ? response.getCode() : 5000;
throw new IntegrationException(code, msg, "service-name");
}
return response.getData();
}
}
```
## Error Handling
### IntegrationException
All integration failures are wrapped in `IntegrationException`:
```java
public class IntegrationException extends RuntimeException {
private final Integer code;
private final String serviceName;
// constructors and getters...
}
```
### FeignErrorDecoder
Automatically converts Feign errors to IntegrationException:
- Parses CommonResponse error format
- Extracts service name from method key
- Provides meaningful error messages
## Configuration Management
### Properties Structure
```yaml
integration:
# Fallback configuration
fallback:
enabled: true # Global fallback switch
cachePrefix: "integration:fallback:" # Global cache prefix
defaultTtlDays: 7 # Default cache TTL in days
# Service-specific fallback configurations
scenic:
enabled: true # Enable fallback for scenic service
ttlDays: 10 # Custom TTL for scenic (longer due to stable data)
# cachePrefix: "scenic:fallback:" # Optional custom prefix
device:
enabled: true # Enable fallback for device service
ttlDays: 5 # Custom TTL for device (shorter due to dynamic data)
# cachePrefix: "device:fallback:" # Optional custom prefix
# Service configurations
scenic:
enabled: true
serviceName: zt-scenic
connectTimeout: 5000
readTimeout: 10000
retryEnabled: false
maxRetries: 3
device:
enabled: true
serviceName: zt-device
connectTimeout: 5000
readTimeout: 10000
retryEnabled: false
maxRetries: 3
```
### Configuration Refresh
Uses `@RefreshScope` to support dynamic configuration updates without restart.
## Testing Integration Services
### Unit Testing
Test service layers by mocking Feign clients:
```java
@ExtendWith(MockitoExtension.class)
class ScenicIntegrationServiceTest {
@Mock
private ScenicV2Client scenicV2Client;
@InjectMocks
private ScenicIntegrationService scenicService;
@Test
void testGetScenic() {
// Mock response
CommonResponse<ScenicV2DTO> response = new CommonResponse<>();
response.setSuccess(true);
response.setData(new ScenicV2DTO());
when(scenicV2Client.getScenic(1L)).thenReturn(response);
// Test
ScenicV2DTO result = scenicService.getScenic(1L);
assertNotNull(result);
}
}
```
### Integration Testing
Use `@SpringBootTest` with test profiles and mock external services.
## Common Development Tasks
### Running Integration Tests
```bash
# Run specific integration test class
mvn test -Dtest=ScenicIntegrationServiceTest
# Run all integration tests
mvn test -Dtest="com.ycwl.basic.integration.*Test"
# Run device integration tests
mvn test -Dtest=DeviceIntegrationServiceTest
mvn test -Dtest="com.ycwl.basic.integration.device.*Test"
```
### Adding New DTOs
1. Create DTO classes in the appropriate `dto` package
2. Follow existing patterns with proper Jackson annotations
3. Use `@JsonProperty` for field mapping when needed
### Debugging Integration Issues
1. Enable Feign client logging in `application-dev.yml`:
```yaml
logging:
level:
com.ycwl.basic.integration: DEBUG
```
2. Check Nacos service discovery in development
3. Verify service configurations and timeouts
4. Review FeignErrorDecoder logs for detailed error information
## Best Practices
### Response Handling
- Always use the `handleResponse` pattern for consistent error handling
- Provide meaningful error messages for business context
- Include service name in IntegrationException for debugging
### Fallback Strategy Best Practices
- **Query operations only**: Only apply fallback to GET operations (data retrieval)
- **No fallback for mutations**: CREATE/UPDATE/DELETE operations should fail immediately to provide clear feedback
- **Cache key design**: Use clear, hierarchical cache keys for easy identification and cleanup
- **TTL management**: Set appropriate TTL based on data freshness requirements
- **Monitoring**: Regularly check fallback cache statistics and cleanup stale entries
- **Service isolation**: Configure service-specific fallback settings based on reliability needs
- **Error transparency**: Let users know exactly when their modification operations fail
### Configuration
- Use environment-specific configuration profiles
- Set appropriate timeouts based on service characteristics
- Enable retries only when safe (idempotent operations)
- Configure fallback TTL based on business requirements:
- **Critical data**: Longer TTL (7-14 days) for essential operations
- **Volatile data**: Shorter TTL (1-3 days) for frequently changing data
- **Configuration data**: Medium TTL (5-7 days) for semi-static settings
### DTOs and Mapping
- Keep DTOs simple and focused on data transfer
- Use proper Jackson annotations for field mapping
- Separate request/response DTOs for clarity
### Service Layer Design
- Keep integration services focused on external service calls
- Handle response transformation and error conversion
- Avoid business logic in integration services
- Use fallback service for resilience without changing business logic
### Cache Management Strategies
- **Proactive cleanup**: Monitor cache size and clean up periodically
- **Service-specific management**: Separate cache management per service
- **Debugging support**: Use cache statistics for troubleshooting
- **Configuration validation**: Ensure fallback configuration matches service requirements

View File

@@ -11,11 +11,21 @@ import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "integration")
public class IntegrationProperties {
/**
* 降级服务配置
*/
private FallbackConfig fallback = new FallbackConfig();
/**
* 景区服务配置
*/
private ScenicConfig scenic = new ScenicConfig();
/**
* 设备服务配置
*/
private DeviceConfig device = new DeviceConfig();
@Data
public static class ScenicConfig {
/**
@@ -40,4 +50,71 @@ public class IntegrationProperties {
private boolean retryEnabled = false;
private int maxRetries = 3;
}
@Data
public static class DeviceConfig {
/**
* 是否启用设备服务集成
*/
private boolean enabled = true;
/**
* 服务名称
*/
private String serviceName = "zt-device";
/**
* 超时配置(毫秒)
*/
private int connectTimeout = 5000;
private int readTimeout = 10000;
/**
* 重试配置
*/
private boolean retryEnabled = false;
private int maxRetries = 3;
}
@Data
public static class FallbackConfig {
/**
* 是否启用降级功能
*/
private boolean enabled = true;
/**
* 降级缓存前缀
*/
private String cachePrefix = "integration:fallback:";
/**
* 默认降级缓存TTL(天)
*/
private long defaultTtlDays = 1;
/**
* 服务特定的降级配置
*/
private ServiceFallbackConfig scenic = new ServiceFallbackConfig();
private ServiceFallbackConfig device = new ServiceFallbackConfig();
}
@Data
public static class ServiceFallbackConfig {
/**
* 是否启用此服务的降级功能
*/
private boolean enabled = true;
/**
* 服务特定的缓存TTL(天),如果为0则使用默认TTL
*/
private long ttlDays = 0;
/**
* 服务特定的缓存前缀,如果为空则使用默认前缀
*/
private String cachePrefix;
}
}

View File

@@ -0,0 +1,406 @@
package com.ycwl.basic.integration.common.manager;
import com.ycwl.basic.utils.JacksonUtil;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 通用配置管理器基类
* 提供配置查找、类型转换等通用功能
*
* @param <T> 配置DTO类型
*/
public abstract class ConfigManager<T> {
protected List<T> configs;
public ConfigManager(List<T> configs) {
this.configs = configs != null ? configs : new ArrayList<>();
}
/**
* 获取配置项的键
* 子类需要实现此方法来提取配置键
*/
protected abstract String getConfigKey(T config);
/**
* 获取配置项的值
* 子类需要实现此方法来提取配置值
*/
protected abstract Object getConfigValue(T config);
/**
* 根据键查找配置项
*/
protected T findConfigByKey(String key) {
if (key == null || configs == null) {
return null;
}
return configs.stream()
.filter(config -> key.equals(getConfigKey(config)))
.findFirst()
.orElse(null);
}
/**
* 获取字符串配置值
*/
public String getString(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
return value != null ? value.toString() : null;
}
/**
* 获取字符串配置值,如果不存在则返回默认值
*/
public String getString(String key, String defaultValue) {
String value = getString(key);
return value != null ? value : defaultValue;
}
/**
* 获取整型配置值
*/
public Integer getInteger(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof Integer) return (Integer) value;
if (value instanceof Number) return ((Number) value).intValue();
if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 获取整型配置值,如果不存在则返回默认值
*/
public Integer getInteger(String key, Integer defaultValue) {
Integer value = getInteger(key);
return value != null ? value : defaultValue;
}
/**
* 获取长整型配置值
*/
public Long getLong(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof Long) return (Long) value;
if (value instanceof Number) return ((Number) value).longValue();
if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 获取长整型配置值,如果不存在则返回默认值
*/
public Long getLong(String key, Long defaultValue) {
Long value = getLong(key);
return value != null ? value : defaultValue;
}
/**
* 获取布尔型配置值
*/
public Boolean getBoolean(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof Boolean) return (Boolean) value;
if (value instanceof String) {
String str = ((String) value).toLowerCase().trim();
return "true".equals(str) || "1".equals(str) || "yes".equals(str);
}
if (value instanceof Number) {
return ((Number) value).intValue() != 0;
}
return null;
}
/**
* 获取布尔型配置值,如果不存在则返回默认值
*/
public Boolean getBoolean(String key, Boolean defaultValue) {
Boolean value = getBoolean(key);
return value != null ? value : defaultValue;
}
/**
* 获取浮点型配置值
*/
public Float getFloat(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof Float) return (Float) value;
if (value instanceof Number) return ((Number) value).floatValue();
if (value instanceof String) {
try {
return Float.parseFloat((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 获取浮点型配置值,如果不存在则返回默认值
*/
public Float getFloat(String key, Float defaultValue) {
Float value = getFloat(key);
return value != null ? value : defaultValue;
}
/**
* 获取BigDecimal配置值
*/
public BigDecimal getBigDecimal(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof BigDecimal) return (BigDecimal) value;
if (value instanceof Number) return new BigDecimal(value.toString());
if (value instanceof String) {
try {
return new BigDecimal((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 获取BigDecimal配置值,如果不存在则返回默认值
*/
public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {
BigDecimal value = getBigDecimal(key);
return value != null ? value : defaultValue;
}
/**
* 获取枚举配置值
*/
public <E extends Enum<E>> E getEnum(String key, Class<E> enumClass) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
return parseEnum(value, enumClass);
}
/**
* 解析枚举值
*/
private <E extends Enum<E>> E parseEnum(Object value, Class<E> enumClass) {
if (value == null) return null;
try {
if (value instanceof String) {
return Enum.valueOf(enumClass, (String) value);
}
return Enum.valueOf(enumClass, value.toString());
} catch (IllegalArgumentException e) {
return null;
}
}
/**
* 获取枚举配置值,如果不存在则返回默认值
*/
public <E extends Enum<E>> E getEnum(String key, Class<E> enumClass, E defaultValue) {
E value = getEnum(key, enumClass);
return value != null ? value : defaultValue;
}
/**
* 检查指定键的配置是否存在
*/
public boolean hasConfig(String key) {
return findConfigByKey(key) != null;
}
/**
* 获取所有配置项的数量
*/
public int size() {
return configs.size();
}
/**
* 获取所有配置项
*/
public List<T> getAllConfigs() {
return new ArrayList<>(configs);
}
/**
* 获取原始对象配置值
*/
public Object getObject(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
return getConfigValue(config);
}
/**
* 获取并转换为指定类型的对象
* 支持JSON字符串自动反序列化
*/
@SuppressWarnings("unchecked")
public <R> R getObject(String key, Class<R> clazz) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
// 如果类型匹配,直接返回
if (clazz.isInstance(value)) {
return (R) value;
}
// 如果是String类型的JSON,尝试反序列化
if (value instanceof String && !clazz.equals(String.class)) {
try {
return JacksonUtil.parseObject((String) value, clazz);
} catch (Exception e) {
return null;
}
}
// 如果目标是String,直接转换
if (clazz.equals(String.class)) {
return (R) value.toString();
}
// 其他情况尝试JSON转换
try {
String json = JacksonUtil.toJSONString(value);
return JacksonUtil.parseObject(json, clazz);
} catch (Exception e) {
return null;
}
}
/**
* 获取Map类型的配置值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getMap(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof Map) {
return (Map<String, Object>) value;
}
if (value instanceof String) {
try {
return JacksonUtil.parseObject((String) value, Map.class);
} catch (Exception e) {
return null;
}
}
return null;
}
/**
* 获取List类型的配置值
*/
@SuppressWarnings("unchecked")
public List<Object> getList(String key) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof List) {
return (List<Object>) value;
}
if (value instanceof String) {
try {
return JacksonUtil.parseObject((String) value, List.class);
} catch (Exception e) {
return null;
}
}
return null;
}
/**
* 获取指定元素类型的List配置值
*/
public <R> List<R> getList(String key, Class<R> elementClass) {
T config = findConfigByKey(key);
if (config == null) {
return null;
}
Object value = getConfigValue(config);
if (value == null) return null;
if (value instanceof String) {
try {
return JacksonUtil.parseArray((String) value, elementClass);
} catch (Exception e) {
return null;
}
}
try {
String json = JacksonUtil.toJSONString(value);
return JacksonUtil.parseArray(json, elementClass);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,51 @@
package com.ycwl.basic.integration.common.manager;
import com.ycwl.basic.integration.device.dto.config.DeviceConfigV2DTO;
import java.util.List;
/**
* 设备配置管理器
* 基于通用ConfigManager实现设备配置的管理功能
*/
public class DeviceConfigManager extends ConfigManager<DeviceConfigV2DTO> {
public DeviceConfigManager(List<DeviceConfigV2DTO> configs) {
super(configs);
}
@Override
protected String getConfigKey(DeviceConfigV2DTO config) {
return config != null ? config.getConfigKey() : null;
}
@Override
protected Object getConfigValue(DeviceConfigV2DTO config) {
return config != null ? config.getConfigValue() : null;
}
/**
* 获取设备配置类型
*/
public String getConfigType(String key) {
DeviceConfigV2DTO config = findConfigByKey(key);
return config != null ? config.getConfigType() : null;
}
/**
* 获取设备配置描述
*/
public String getConfigDescription(String key) {
DeviceConfigV2DTO config = findConfigByKey(key);
return config != null ? config.getDescription() : null;
}
/**
* 获取设备配置ID
*/
public Long getConfigId(String key) {
DeviceConfigV2DTO config = findConfigByKey(key);
return config != null ? config.getId() : null;
}
}

View File

@@ -1,4 +1,4 @@
package com.ycwl.basic.util;
package com.ycwl.basic.integration.common.manager;
import com.ycwl.basic.integration.common.util.ConfigValueUtil;
import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO;
@@ -9,11 +9,12 @@ import java.util.stream.Collectors;
/**
* 景区配置管理器
* 基于通用ConfigManager实现景区配置的管理功能
*
* 提供类型安全的配置值获取功能支持多种数据类型的自动转换
* 当类型不兼容时返回null而不是抛出异常
*/
public class ScenicConfigManager {
public class ScenicConfigManager extends ConfigManager<ScenicConfigV2DTO> {
private final Map<String, Object> configMap;
@@ -23,6 +24,7 @@ public class ScenicConfigManager {
* @param configList 配置项列表
*/
public ScenicConfigManager(List<ScenicConfigV2DTO> configList) {
super(configList);
this.configMap = new HashMap<>();
if (configList != null) {
for (ScenicConfigV2DTO config : configList) {
@@ -39,50 +41,20 @@ public class ScenicConfigManager {
* @param configMap 配置Map
*/
public ScenicConfigManager(Map<String, Object> configMap) {
super(null); // 使用Map构造时父类configs为null
this.configMap = configMap != null ? new HashMap<>(configMap) : new HashMap<>();
}
/**
* 获取字符串值
*
* @param key 配置键
* @return 字符串值如果键不存在或转换失败返回null
*/
public String getString(String key) {
return ConfigValueUtil.getStringValue(configMap, key);
@Override
protected String getConfigKey(ScenicConfigV2DTO config) {
return config != null ? config.getConfigKey() : null;
}
/**
* 获取字符串值如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return 字符串值或默认值
*/
public String getString(String key, String defaultValue) {
return ConfigValueUtil.getStringValue(configMap, key, defaultValue);
@Override
protected Object getConfigValue(ScenicConfigV2DTO config) {
return config != null ? config.getConfigValue() : null;
}
/**
* 获取整数值
*
* @param key 配置键
* @return Integer值如果键不存在或转换失败返回null
*/
public Integer getInteger(String key) {
return ConfigValueUtil.getIntValue(configMap, key);
}
/**
* 获取整数值如果为null则返回默认值
*
* @param key 配置键
* @param defaultValue 默认值
* @return Integer值或默认值
*/
public Integer getInteger(String key, Integer defaultValue) {
return ConfigValueUtil.getIntValue(configMap, key, defaultValue);
}
/**
* 获取长整数值
@@ -193,85 +165,7 @@ public class ScenicConfigManager {
return ConfigValueUtil.getBooleanValue(configMap, key, defaultValue);
}
/**
* 获取枚举值
*
* @param key 配置键
* @param enumClass 枚举类型
* @param <T> 枚举类型泛型
* @return 枚举值如果键不存在或转换失败返回null
*/
public <T extends Enum<T>> T getEnum(String key, Class<T> enumClass) {
return ConfigValueUtil.getEnumValue(configMap, key, enumClass);
}
/**
* 获取枚举值如果为null则返回默认值
*
* @param key 配置键
* @param enumClass 枚举类型
* @param defaultValue 默认值
* @param <T> 枚举类型泛型
* @return 枚举值或默认值
*/
public <T extends Enum<T>> T getEnum(String key, Class<T> enumClass, T defaultValue) {
T value = ConfigValueUtil.getEnumValue(configMap, key, enumClass);
return value != null ? value : defaultValue;
}
/**
* 获取原始对象值
*
* @param key 配置键
* @return 原始Object值
*/
public Object getObject(String key) {
return ConfigValueUtil.getObjectValue(configMap, key);
}
/**
* 获取并转换为指定类型的对象
*
* @param key 配置键
* @param clazz 目标类型
* @param <T> 目标类型泛型
* @return 转换后的对象如果转换失败返回null
*/
public <T> T getObject(String key, Class<T> clazz) {
return ConfigValueUtil.getObjectValue(configMap, key, clazz);
}
/**
* 获取Map类型的值
*
* @param key 配置键
* @return Map值如果转换失败返回null
*/
public Map<String, Object> getMap(String key) {
return ConfigValueUtil.getMapValue(configMap, key);
}
/**
* 获取List类型的值
*
* @param key 配置键
* @return List值如果转换失败返回null
*/
public List<Object> getList(String key) {
return ConfigValueUtil.getListValue(configMap, key);
}
/**
* 获取指定元素类型的List值
*
* @param key 配置键
* @param elementClass List元素类型
* @param <T> List元素类型泛型
* @return 指定类型的List如果转换失败返回null
*/
public <T> List<T> getList(String key, Class<T> elementClass) {
return ConfigValueUtil.getListValue(configMap, key, elementClass);
}
/**
* 检查配置键是否存在
@@ -307,6 +201,7 @@ public class ScenicConfigManager {
*
* @return 配置项数量
*/
@Override
public int size() {
return configMap.size();
}
@@ -325,7 +220,7 @@ public class ScenicConfigManager {
*
* @return 配置Map的拷贝
*/
public Map<String, Object> getAllConfigs() {
public Map<String, Object> getAllConfigsAsMap() {
return new HashMap<>(configMap);
}

View File

@@ -0,0 +1,257 @@
package com.ycwl.basic.integration.common.service;
import com.ycwl.basic.integration.common.config.IntegrationProperties;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* 集成服务通用失败降级处理
* 提供统一的降级策略,支持所有微服务集成
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IntegrationFallbackService {
private final RedisTemplate<String, String> redisTemplate;
private final IntegrationProperties integrationProperties;
// 默认降级缓存配置
private static final String DEFAULT_FALLBACK_PREFIX = "integration:fallback:";
private static final long DEFAULT_FALLBACK_TTL = 7; // 7天
/**
* 执行操作,失败时降级到缓存结果
*
* @param serviceName 服务名称 (如: zt-device, zt-scenic)
* @param cacheKey 缓存键
* @param operation 主要操作
* @param resultClass 结果类型
* @param <T> 结果类型
* @return 操作结果或缓存的结果
*/
public <T> T executeWithFallback(String serviceName, String cacheKey, Supplier<T> operation, Class<T> resultClass) {
try {
T result = operation.get();
if (result != null) {
// 操作成功,保存结果用于将来的降级
storeFallbackCache(serviceName, cacheKey, result);
}
return result;
} catch (Exception e) {
log.warn("[{}] 操作失败,尝试降级到缓存结果, cacheKey: {}", serviceName, cacheKey, e);
T fallbackResult = getFallbackFromCache(serviceName, cacheKey, resultClass);
if (fallbackResult == null) {
log.error("[{}] 操作失败且无缓存数据, cacheKey: {}", serviceName, cacheKey);
throw e;
}
log.info("[{}] 成功从降级缓存获取结果, cacheKey: {}", serviceName, cacheKey);
return fallbackResult;
}
}
/**
* 执行操作,失败时降级到缓存结果,无返回值版本
*
* @param serviceName 服务名称
* @param cacheKey 缓存键
* @param operation 主要操作
*/
public void executeWithFallback(String serviceName, String cacheKey, Runnable operation) {
try {
operation.run();
// 操作成功,记录成功状态
storeFallbackCache(serviceName, cacheKey + ":success", "true");
log.debug("[{}] 操作成功,已记录成功状态, cacheKey: {}", serviceName, cacheKey);
} catch (Exception e) {
log.warn("[{}] 操作失败,检查是否有历史成功记录, cacheKey: {}", serviceName, cacheKey, e);
String successRecord = getFallbackFromCache(serviceName, cacheKey + ":success", String.class);
if (successRecord == null) {
log.error("[{}] 操作失败且无历史成功记录, cacheKey: {}", serviceName, cacheKey);
throw e;
}
log.info("[{}] 操作失败但有历史成功记录,忽略此次失败, cacheKey: {}", serviceName, cacheKey);
}
}
/**
* 存储降级缓存
*/
private void storeFallbackCache(String serviceName, String cacheKey, Object value) {
try {
String fullKey = buildFullCacheKey(serviceName, cacheKey);
String jsonValue = JacksonUtil.toJSONString(value);
long ttl = getFallbackTtl(serviceName);
redisTemplate.opsForValue().set(fullKey, jsonValue, ttl, TimeUnit.DAYS);
log.debug("[{}] 存储降级缓存成功, key: {}", serviceName, fullKey);
} catch (Exception e) {
log.warn("[{}] 存储降级缓存失败, cacheKey: {}", serviceName, cacheKey, e);
}
}
/**
* 从降级缓存获取结果
*/
private <T> T getFallbackFromCache(String serviceName, String cacheKey, Class<T> resultClass) {
try {
String fullKey = buildFullCacheKey(serviceName, cacheKey);
String cachedValue = redisTemplate.opsForValue().get(fullKey);
if (cachedValue != null) {
log.debug("[{}] 从降级缓存获取结果, key: {}", serviceName, fullKey);
if (resultClass == String.class) {
return resultClass.cast(cachedValue);
}
return JacksonUtil.parseObject(cachedValue, resultClass);
}
} catch (Exception e) {
log.warn("[{}] 从降级缓存获取结果失败, cacheKey: {}", serviceName, cacheKey, e);
}
return null;
}
/**
* 清除降级缓存
*
* @param serviceName 服务名称
* @param cacheKey 缓存键
*/
public void clearFallbackCache(String serviceName, String cacheKey) {
String fullKey = buildFullCacheKey(serviceName, cacheKey);
redisTemplate.delete(fullKey);
log.debug("[{}] 清除降级缓存, key: {}", serviceName, fullKey);
}
/**
* 批量清除服务的所有降级缓存
*
* @param serviceName 服务名称
*/
public void clearAllFallbackCache(String serviceName) {
String pattern = buildFullCacheKey(serviceName, "*");
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
log.info("[{}] 批量清除降级缓存,共删除 {} 个缓存项", serviceName, keys.size());
}
}
/**
* 检查是否有降级缓存
*
* @param serviceName 服务名称
* @param cacheKey 缓存键
* @return 是否存在降级缓存
*/
public boolean hasFallbackCache(String serviceName, String cacheKey) {
String fullKey = buildFullCacheKey(serviceName, cacheKey);
return Boolean.TRUE.equals(redisTemplate.hasKey(fullKey));
}
/**
* 获取服务的降级缓存统计信息
*
* @param serviceName 服务名称
* @return 缓存统计信息
*/
public FallbackCacheStats getFallbackCacheStats(String serviceName) {
String pattern = buildFullCacheKey(serviceName, "*");
Set<String> keys = redisTemplate.keys(pattern);
int totalCount = keys != null ? keys.size() : 0;
return FallbackCacheStats.builder()
.serviceName(serviceName)
.totalCacheCount(totalCount)
.cacheKeyPattern(pattern)
.fallbackTtlDays(getFallbackTtl(serviceName))
.build();
}
/**
* 构建完整的缓存键
*/
private String buildFullCacheKey(String serviceName, String cacheKey) {
String prefix = getFallbackPrefix(serviceName);
return prefix + serviceName + ":" + cacheKey;
}
/**
* 获取服务的降级缓存前缀
*/
private String getFallbackPrefix(String serviceName) {
if (!integrationProperties.getFallback().isEnabled()) {
return DEFAULT_FALLBACK_PREFIX;
}
// 获取服务特定的缓存前缀
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
if (serviceConfig != null && serviceConfig.getCachePrefix() != null) {
return serviceConfig.getCachePrefix();
}
// 使用全局配置的前缀
return integrationProperties.getFallback().getCachePrefix();
}
/**
* 获取服务的降级缓存TTL
*/
private long getFallbackTtl(String serviceName) {
if (!integrationProperties.getFallback().isEnabled()) {
return DEFAULT_FALLBACK_TTL;
}
// 获取服务特定的TTL
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
if (serviceConfig != null && serviceConfig.getTtlDays() > 0) {
return serviceConfig.getTtlDays();
}
// 使用全局配置的TTL
return integrationProperties.getFallback().getDefaultTtlDays();
}
/**
* 获取服务特定的降级配置
*/
private IntegrationProperties.ServiceFallbackConfig getServiceFallbackConfig(String serviceName) {
switch (serviceName.toLowerCase()) {
case "zt-scenic":
return integrationProperties.getFallback().getScenic();
case "zt-device":
return integrationProperties.getFallback().getDevice();
default:
return null;
}
}
/**
* 检查服务是否启用降级功能
*/
public boolean isFallbackEnabled(String serviceName) {
if (!integrationProperties.getFallback().isEnabled()) {
return false;
}
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
return serviceConfig == null || serviceConfig.isEnabled();
}
/**
* 降级缓存统计信息
*/
@lombok.Builder
@lombok.Data
public static class FallbackCacheStats {
private String serviceName;
private int totalCacheCount;
private String cacheKeyPattern;
private long fallbackTtlDays;
}
}

View File

@@ -0,0 +1,74 @@
package com.ycwl.basic.integration.device.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.device.dto.config.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient(name = "zt-device", contextId = "device-config-v2", path = "/api/device/config/v2")
public interface DeviceConfigV2Client {
/**
* 获取设备所有配置
*/
@GetMapping("/{deviceId}")
CommonResponse<List<DeviceConfigV2DTO>> getDeviceConfigs(@PathVariable("deviceId") Long deviceId);
/**
* 根据设备编号获取设备所有配置
*/
@GetMapping("/no/{no}")
CommonResponse<List<DeviceConfigV2DTO>> getDeviceConfigsByNo(@PathVariable("no") String no);
/**
* 获取设备特定配置
*/
@GetMapping("/{deviceId}/key/{configKey}")
CommonResponse<DeviceConfigV2DTO> getDeviceConfigByKey(@PathVariable("deviceId") Long deviceId,
@PathVariable("configKey") String configKey);
/**
* 获取设备扁平化配置
*/
@GetMapping("/{deviceId}/flat")
CommonResponse<Map<String, Object>> getDeviceFlatConfig(@PathVariable("deviceId") Long deviceId);
/**
* 根据设备编号获取设备扁平化配置
*/
@GetMapping("/no/{no}/flat")
CommonResponse<Map<String, Object>> getDeviceFlatConfigByNo(@PathVariable("no") String no);
/**
* 创建设备配置
*/
@PostMapping("/{deviceId}")
CommonResponse<DeviceConfigV2DTO> createDeviceConfig(@PathVariable("deviceId") Long deviceId,
@RequestBody CreateDeviceConfigRequest request);
/**
* 更新设备配置
*/
@PutMapping("/{deviceId}/{id}")
CommonResponse<String> updateDeviceConfig(@PathVariable("deviceId") Long deviceId,
@PathVariable("id") Long id,
@RequestBody UpdateDeviceConfigRequest request);
/**
* 删除设备配置
*/
@DeleteMapping("/{deviceId}/{id}")
CommonResponse<String> deleteDeviceConfig(@PathVariable("deviceId") Long deviceId,
@PathVariable("id") Long id);
/**
* 批量更新设备配置
*/
@PostMapping("/{deviceId}/batch")
CommonResponse<BatchUpdateResponse> batchUpdateDeviceConfig(@PathVariable("deviceId") Long deviceId,
@RequestBody BatchDeviceConfigRequest request);
}

View File

@@ -0,0 +1,79 @@
package com.ycwl.basic.integration.device.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.device.dto.device.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "zt-device", contextId = "device-v2", path = "/api/device/v2")
public interface DeviceV2Client {
/**
* 获取设备核心信息
*/
@GetMapping("/{id}")
CommonResponse<DeviceV2DTO> getDevice(@PathVariable("id") Long id);
/**
* 根据设备编号获取设备核心信息
*/
@GetMapping("/no/{no}")
CommonResponse<DeviceV2DTO> getDeviceByNo(@PathVariable("no") String no);
/**
* 获取设备详细信息(含配置)
*/
@GetMapping("/{id}/with-config")
CommonResponse<DeviceV2WithConfigDTO> getDeviceWithConfig(@PathVariable("id") Long id);
/**
* 根据设备编号获取设备详细信息(含配置)
*/
@GetMapping("/no/{no}/with-config")
CommonResponse<DeviceV2WithConfigDTO> getDeviceByNoWithConfig(@PathVariable("no") String no);
/**
* 创建设备
*/
@PostMapping("/")
CommonResponse<DeviceV2DTO> createDevice(@RequestBody CreateDeviceRequest request);
/**
* 更新设备
*/
@PutMapping("/{id}")
CommonResponse<String> updateDevice(@PathVariable("id") Long id,
@RequestBody UpdateDeviceRequest request);
/**
* 删除设备
*/
@DeleteMapping("/{id}")
CommonResponse<String> deleteDevice(@PathVariable("id") Long id);
/**
* 分页获取设备列表(核心信息)
*/
@GetMapping("/")
CommonResponse<DeviceV2ListResponse> listDevices(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "no", required = false) String no,
@RequestParam(value = "type", required = false) String type,
@RequestParam(value = "isActive", required = false) Integer isActive,
@RequestParam(value = "scenicId", required = false) Long scenicId);
/**
* 分页获取设备列表(含配置)
*/
@GetMapping("/with-config")
CommonResponse<DeviceV2WithConfigListResponse> listDevicesWithConfig(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "no", required = false) String no,
@RequestParam(value = "type", required = false) String type,
@RequestParam(value = "isActive", required = false) Integer isActive,
@RequestParam(value = "scenicId", required = false) Long scenicId);
}

View File

@@ -0,0 +1,16 @@
package com.ycwl.basic.integration.device.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "integration.device")
public class DeviceIntegrationConfig {
public DeviceIntegrationConfig() {
log.info("ZT-Device集成配置初始化完成");
}
}

View File

@@ -0,0 +1,18 @@
package com.ycwl.basic.integration.device.dto.config;
import lombok.Data;
import java.util.List;
@Data
public class BatchDeviceConfigRequest {
private List<BatchDeviceConfigItem> configs;
@Data
public static class BatchDeviceConfigItem {
private String configKey;
private String configValue;
private String configType;
private String description;
}
}

View File

@@ -0,0 +1,36 @@
package com.ycwl.basic.integration.device.dto.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 批量配置更新响应
*/
@Data
public class BatchUpdateResponse {
/**
* 成功处理的配置数量
*/
@JsonProperty("success")
private Integer success;
/**
* 失败的配置数量
*/
@JsonProperty("failed")
private Integer failed;
/**
* 处理详情列表
*/
@JsonProperty("processedItems")
private List<ProcessedConfigItem> processedItems;
/**
* 错误信息列表
*/
@JsonProperty("errors")
private List<String> errors;
}

View File

@@ -0,0 +1,12 @@
package com.ycwl.basic.integration.device.dto.config;
import lombok.Data;
@Data
public class CreateDeviceConfigRequest {
private String configKey;
private String configValue;
private String configType;
private String description;
private Integer isActive;
}

View File

@@ -0,0 +1,39 @@
package com.ycwl.basic.integration.device.dto.config;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DeviceConfigV2DTO {
@JsonProperty("id")
private Long id;
@JsonProperty("deviceId")
private Long deviceId;
@JsonProperty("configKey")
private String configKey;
@JsonProperty("configValue")
private String configValue;
@JsonProperty("configType")
private String configType;
@JsonProperty("description")
private String description;
@JsonProperty("isActive")
private Integer isActive;
@JsonProperty("createTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonProperty("updateTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,52 @@
package com.ycwl.basic.integration.device.dto.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 处理的配置项详情
*/
@Data
public class ProcessedConfigItem {
/**
* 配置键名
*/
@JsonProperty("configKey")
private String configKey;
/**
* 处理状态: success/failed
*/
@JsonProperty("status")
private String status;
/**
* 执行动作: create/update
*/
@JsonProperty("action")
private String action;
/**
* 是否有默认配置
*/
@JsonProperty("hasDefault")
private Boolean hasDefault;
/**
* 处理消息
*/
@JsonProperty("message")
private String message;
/**
* 最终使用的类型
*/
@JsonProperty("finalType")
private String finalType;
/**
* 最终使用的描述
*/
@JsonProperty("finalDescription")
private String finalDescription;
}

View File

@@ -0,0 +1,35 @@
package com.ycwl.basic.integration.device.dto.config;
import lombok.Data;
import java.util.Map;
@Data
public class UpdateDeviceConfigRequest {
private String configKey;
private String configValue;
private String configType;
private String description;
private Integer isActive;
// 支持灵活的字段更新
public static UpdateDeviceConfigRequest fromMap(Map<String, Object> updates) {
UpdateDeviceConfigRequest request = new UpdateDeviceConfigRequest();
if (updates.containsKey("configKey")) {
request.setConfigKey((String) updates.get("configKey"));
}
if (updates.containsKey("configValue")) {
request.setConfigValue((String) updates.get("configValue"));
}
if (updates.containsKey("configType")) {
request.setConfigType((String) updates.get("configType"));
}
if (updates.containsKey("description")) {
request.setDescription((String) updates.get("description"));
}
if (updates.containsKey("isActive")) {
request.setIsActive((Integer) updates.get("isActive"));
}
return request;
}
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.integration.device.dto.device;
import lombok.Data;
@Data
public class CreateDeviceRequest {
private String name;
private String no;
private String type;
private Integer isActive;
private Long scenicId;
private Integer sort;
}

View File

@@ -0,0 +1,39 @@
package com.ycwl.basic.integration.device.dto.device;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DeviceV2DTO {
@JsonProperty("id")
private Long id;
@JsonProperty("name")
private String name;
@JsonProperty("no")
private String no;
@JsonProperty("type")
private String type; // IPC, CUSTOM
@JsonProperty("isActive")
private Integer isActive;
@JsonProperty("scenicId")
private Long scenicId;
@JsonProperty("sort")
private Integer sort;
@JsonProperty("createTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonProperty("updateTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.integration.device.dto.device;
import lombok.Data;
import java.util.List;
@Data
public class DeviceV2ListResponse {
private List<DeviceV2DTO> list;
private Long total;
private Integer page;
private Integer pageSize;
}

View File

@@ -0,0 +1,12 @@
package com.ycwl.basic.integration.device.dto.device;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceV2WithConfigDTO extends DeviceV2DTO {
private Map<String, Object> config;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.integration.device.dto.device;
import lombok.Data;
import java.util.List;
@Data
public class DeviceV2WithConfigListResponse {
private List<DeviceV2WithConfigDTO> list;
private Long total;
private Integer page;
private Integer pageSize;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.integration.device.dto.device;
import lombok.Data;
@Data
public class UpdateDeviceRequest {
private String name;
private String no;
private String type;
private Integer isActive;
private Long scenicId;
private Integer sort;
}

View File

@@ -0,0 +1,184 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Device Integration 使用示例
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceIntegrationExample {
private final DeviceIntegrationService deviceService;
private final DeviceConfigIntegrationService deviceConfigService;
/**
* 基本设备操作
*/
public void basicDeviceOperations() {
log.info("=== 基本设备操作 ===");
// 创建IPC摄像头设备(默认排序)
DeviceV2DTO ipcDevice = deviceService.createIpcDevice(
"前门摄像头", "CAM001", 1001L);
log.info("创建IPC设备: {}, 排序值: {}", ipcDevice.getName(), ipcDevice.getSort());
// 根据ID获取设备信息
DeviceV2DTO device = deviceService.getDevice(ipcDevice.getId());
log.info("获取设备信息: {}, 排序值: {}", device.getName(), device.getSort());
// 根据设备编号获取设备信息
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo("CAM001");
log.info("根据编号获取设备: {}", deviceByNo.getName());
// 获取设备详细信息(含配置)
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(ipcDevice.getId());
log.info("获取设备配置: {}", deviceWithConfig.getName());
// 分页查询景区设备列表
DeviceV2ListResponse deviceList = deviceService.getScenicIpcDevices(1001L, 1, 10);
log.info("景区设备列表: 总数={}", deviceList.getTotal());
// 启用设备
deviceService.enableDevice(ipcDevice.getId());
log.info("设备已启用");
}
/**
* 设备排序功能演示
*/
public void deviceSortingOperations() {
log.info("=== 设备排序功能演示 ===");
Long scenicId = 1001L;
// 创建带排序的设备
DeviceV2DTO camera1 = deviceService.createIpcDeviceWithSort(
"大门摄像头", "CAM_GATE", scenicId, 10);
log.info("创建摄像头1: {}, 排序: {}", camera1.getName(), camera1.getSort());
DeviceV2DTO camera2 = deviceService.createIpcDeviceWithSort(
"后门摄像头", "CAM_BACK", scenicId, 20);
log.info("创建摄像头2: {}, 排序: {}", camera2.getName(), camera2.getSort());
DeviceV2DTO sensor1 = deviceService.createCustomDeviceWithSort(
"温度传感器", "TEMP_01", scenicId, 5);
log.info("创建传感器: {}, 排序: {}", sensor1.getName(), sensor1.getSort());
// 更新设备排序
deviceService.updateDeviceSort(camera1.getId(), 1);
log.info("更新摄像头1排序为1(置顶)");
// 获取排序后的设备列表
DeviceV2ListResponse sortedList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
log.info("排序后的设备列表:");
for (DeviceV2DTO device : sortedList.getList()) {
log.info(" - {}: 排序={}, 类型={}", device.getName(), device.getSort(), device.getType());
}
// 批量调整排序演示
log.info("--- 批量调整排序演示 ---");
deviceService.updateDeviceSort(sensor1.getId(), 15); // 传感器排到中间
deviceService.updateDeviceSort(camera2.getId(), 30); // 后门摄像头排到最后
log.info("批量排序调整完成");
}
/**
* 设备配置管理
*/
public void deviceConfigurationOperations() {
log.info("=== 设备配置管理 ===");
Long deviceId = 1L;
// 获取设备所有配置
List<DeviceConfigV2DTO> configs = deviceConfigService.getDeviceConfigs(deviceId);
log.info("设备配置数量: {}", configs.size());
// 获取扁平化配置
Map<String, Object> flatConfig = deviceConfigService.getDeviceFlatConfig(deviceId);
log.info("扁平化配置项数: {}", flatConfig.size());
// 使用批量配置API
BatchDeviceConfigRequest builderRequest = deviceConfigService.createBatchConfigBuilder()
.build();
BatchUpdateResponse result = deviceConfigService.batchUpdateDeviceConfig(deviceId, builderRequest);
log.info("批量配置更新结果: 成功={}, 失败={}", result.getSuccess(), result.getFailed());
}
/**
* 排序最佳实践演示
*/
public void sortingBestPractices() {
log.info("=== 排序最佳实践演示 ===");
Long scenicId = 1001L;
// 推荐使用10的倍数作为排序值
DeviceV2DTO device1 = deviceService.createIpcDeviceWithSort(
"重要摄像头", "CAM_IMPORTANT", scenicId, 10);
DeviceV2DTO device2 = deviceService.createIpcDeviceWithSort(
"普通摄像头", "CAM_NORMAL", scenicId, 20);
DeviceV2DTO device3 = deviceService.createIpcDeviceWithSort(
"备用摄像头", "CAM_BACKUP", scenicId, 30);
log.info("使用10的倍数创建设备排序: 10, 20, 30");
// 在中间插入新设备
DeviceV2DTO insertDevice = deviceService.createIpcDeviceWithSort(
"中间摄像头", "CAM_MIDDLE", scenicId, 25);
log.info("在20和30之间插入设备,排序值: 25");
// 置顶操作
deviceService.updateDeviceSort(device2.getId(), 1);
log.info("将普通摄像头置顶(排序值: 1)");
// 查看最终排序结果
DeviceV2ListResponse finalList = deviceService.listDevices(1, 10, null, null, null, 1, scenicId);
log.info("最终排序结果:");
for (DeviceV2DTO device : finalList.getList()) {
log.info(" - {}: 排序={}", device.getName(), device.getSort());
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
try {
basicDeviceOperations();
deviceSortingOperations();
sortingBestPractices();
deviceConfigurationOperations();
log.info("=== 所有示例执行完成 ===");
} catch (Exception e) {
log.error("示例执行过程中发生错误", e);
}
}
/**
* 运行基础示例(简化版)
*/
public void runBasicExamples() {
try {
basicDeviceOperations();
deviceConfigurationOperations();
log.info("=== 基础示例执行完成 ===");
} catch (Exception e) {
log.error("示例执行过程中发生错误", e);
}
}
}

View File

@@ -0,0 +1,124 @@
package com.ycwl.basic.integration.device.example;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.dto.device.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 设备集成示例(包含降级机制)
* 演示设备集成和失败降级策略的使用
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceIntegrationFallbackExample {
private final DeviceIntegrationService deviceService;
private final DeviceConfigIntegrationService configService;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
/**
* 演示设备信息获取的降级机制
*/
public void deviceInfoFallbackExample() {
log.info("=== 设备信息获取降级示例 ===");
Long deviceId = 1001L;
String deviceNo = "CAM001";
try {
// 获取设备信息 - 自动降级
DeviceV2DTO device = deviceService.getDevice(deviceId);
log.info("获取设备成功: {}", device.getName());
// 根据设备号获取设备 - 自动降级
DeviceV2DTO deviceByNo = deviceService.getDeviceByNo(deviceNo);
log.info("根据设备号获取设备成功: {}", deviceByNo.getName());
// 获取设备配置 - 自动降级
DeviceV2WithConfigDTO deviceWithConfig = deviceService.getDeviceWithConfig(deviceId);
log.info("获取设备配置成功,配置数量: {}", deviceWithConfig.getConfig().size());
} catch (Exception e) {
log.error("所有降级策略失败", e);
}
}
/**
* 演示设备操作(无降级机制)
*/
public void deviceOperationExample() {
log.info("=== 设备操作示例 ===");
Long deviceId = 1001L;
try {
// 设备更新操作 - 直接操作,失败时抛出异常
UpdateDeviceRequest updateRequest = new UpdateDeviceRequest();
updateRequest.setName("更新后的摄像头");
deviceService.updateDevice(deviceId, updateRequest);
log.info("设备更新操作完成");
// 设备排序更新 - 直接操作,失败时抛出异常
deviceService.updateDeviceSort(deviceId, 5);
log.info("设备排序更新完成");
} catch (Exception e) {
log.error("设备操作失败", e);
}
}
/**
* 演示降级缓存管理
*/
public void fallbackCacheManagementExample() {
log.info("=== 降级缓存管理示例 ===");
String deviceCacheKey = "device:1001";
String configCacheKey = "device:flat:config:1001";
// 检查降级缓存状态
boolean hasDeviceCache = fallbackService.hasFallbackCache(SERVICE_NAME, deviceCacheKey);
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
log.info("设备降级缓存存在: {}", hasDeviceCache);
log.info("配置降级缓存存在: {}", hasConfigCache);
// 清理特定的降级缓存
if (hasDeviceCache) {
fallbackService.clearFallbackCache(SERVICE_NAME, deviceCacheKey);
log.info("已清理设备降级缓存");
}
// 获取降级缓存统计信息
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
log.info("设备服务降级缓存统计: {}", stats);
// 批量清理所有设备降级缓存
if (stats.getTotalCacheCount() > 10) {
fallbackService.clearAllFallbackCache(SERVICE_NAME);
log.info("已批量清理所有设备降级缓存");
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
log.info("开始运行设备集成示例...");
deviceInfoFallbackExample();
deviceOperationExample();
fallbackCacheManagementExample();
log.info("设备集成示例运行完成");
}
}

View File

@@ -0,0 +1,167 @@
package com.ycwl.basic.integration.device.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.device.client.DeviceConfigV2Client;
import com.ycwl.basic.integration.device.dto.config.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceConfigIntegrationService {
private final DeviceConfigV2Client deviceConfigV2Client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
public List<DeviceConfigV2DTO> getDeviceConfigs(Long deviceId) {
log.info("获取设备配置列表, deviceId: {}", deviceId);
CommonResponse<List<DeviceConfigV2DTO>> response = deviceConfigV2Client.getDeviceConfigs(deviceId);
return handleResponse(response, "获取设备配置列表失败");
}
public List<DeviceConfigV2DTO> getDeviceConfigsByNo(String deviceNo) {
log.info("根据设备编号获取配置列表, deviceNo: {}", deviceNo);
CommonResponse<List<DeviceConfigV2DTO>> response = deviceConfigV2Client.getDeviceConfigsByNo(deviceNo);
return handleResponse(response, "根据设备编号获取配置列表失败");
}
public DeviceConfigV2DTO getDeviceConfigByKey(Long deviceId, String configKey) {
log.info("根据键获取设备配置, deviceId: {}, configKey: {}", deviceId, configKey);
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.getDeviceConfigByKey(deviceId, configKey);
return handleResponse(response, "根据键获取设备配置失败");
}
public Map<String, Object> getDeviceFlatConfig(Long deviceId) {
log.info("获取设备扁平化配置, deviceId: {}", deviceId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:flat:config:" + deviceId,
() -> {
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfig(deviceId);
return handleResponse(response, "获取设备扁平化配置失败");
},
Map.class
);
}
public Map<String, Object> getDeviceFlatConfigByNo(String deviceNo) {
log.info("根据设备编号获取扁平化配置, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:flat:config:no:" + deviceNo,
() -> {
CommonResponse<Map<String, Object>> response = deviceConfigV2Client.getDeviceFlatConfigByNo(deviceNo);
return handleResponse(response, "根据设备编号获取扁平化配置失败");
},
Map.class
);
}
public DeviceConfigV2DTO createDeviceConfig(Long deviceId, CreateDeviceConfigRequest request) {
log.info("创建设备配置, deviceId: {}, configKey: {}", deviceId, request.getConfigKey());
CommonResponse<DeviceConfigV2DTO> response = deviceConfigV2Client.createDeviceConfig(deviceId, request);
return handleResponse(response, "创建设备配置失败");
}
public void updateDeviceConfig(Long deviceId, Long configId, UpdateDeviceConfigRequest request) {
log.info("更新设备配置, deviceId: {}, configId: {}", deviceId, configId);
CommonResponse<String> response = deviceConfigV2Client.updateDeviceConfig(deviceId, configId, request);
handleResponse(response, "更新设备配置失败");
}
public void deleteDeviceConfig(Long deviceId, Long configId) {
log.info("删除设备配置, deviceId: {}, configId: {}", deviceId, configId);
CommonResponse<String> response = deviceConfigV2Client.deleteDeviceConfig(deviceId, configId);
handleResponse(response, "删除设备配置失败");
}
/**
* 批量更新设备配置
*/
public BatchUpdateResponse batchUpdateDeviceConfig(Long deviceId, BatchDeviceConfigRequest request) {
log.info("批量更新设备配置, deviceId: {}, configs count: {}", deviceId, request.getConfigs().size());
CommonResponse<BatchUpdateResponse> response = deviceConfigV2Client.batchUpdateDeviceConfig(deviceId, request);
return handleResponse(response, "批量更新设备配置失败");
}
/**
* 获取设备特定配置值
*/
public String getDeviceConfigValue(Long deviceId, String configKey) {
DeviceConfigV2DTO config = getDeviceConfigByKey(deviceId, configKey);
return config != null ? config.getConfigValue() : null;
}
/**
* 创建批量配置请求构建器
*/
public BatchDeviceConfigRequestBuilder createBatchConfigBuilder() {
return new BatchDeviceConfigRequestBuilder();
}
/**
* 批量配置请求构建器
*/
public static class BatchDeviceConfigRequestBuilder {
private final BatchDeviceConfigRequest request = new BatchDeviceConfigRequest();
public BatchDeviceConfigRequestBuilder() {
request.setConfigs(new java.util.ArrayList<>());
}
/**
* 添加配置项(有默认配置的情况,只需提供key和value)
*/
public BatchDeviceConfigRequestBuilder addConfig(String configKey, String configValue) {
BatchDeviceConfigRequest.BatchDeviceConfigItem item = new BatchDeviceConfigRequest.BatchDeviceConfigItem();
item.setConfigKey(configKey);
item.setConfigValue(configValue);
request.getConfigs().add(item);
return this;
}
/**
* 添加配置项(完整参数,用于自定义配置)
*/
public BatchDeviceConfigRequestBuilder addConfig(String configKey, String configValue,
String configType, String description) {
BatchDeviceConfigRequest.BatchDeviceConfigItem item = new BatchDeviceConfigRequest.BatchDeviceConfigItem();
item.setConfigKey(configKey);
item.setConfigValue(configValue);
item.setConfigType(configType);
item.setDescription(description);
request.getConfigs().add(item);
return this;
}
/**
* 构建请求对象
*/
public BatchDeviceConfigRequest build() {
return request;
}
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null
? response.getMessage()
: errorMessage;
Integer code = response != null ? response.getCode() : 5000;
throw new IntegrationException(code, msg, "zt-device");
}
return response.getData();
}
}

View File

@@ -0,0 +1,218 @@
package com.ycwl.basic.integration.device.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.device.client.DeviceV2Client;
import com.ycwl.basic.integration.device.dto.device.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceIntegrationService {
private final DeviceV2Client deviceV2Client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
public DeviceV2DTO getDevice(Long deviceId) {
log.info("获取设备信息, deviceId: {}", deviceId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:" + deviceId,
() -> {
CommonResponse<DeviceV2DTO> response = deviceV2Client.getDevice(deviceId);
return handleResponse(response, "获取设备信息失败");
},
DeviceV2DTO.class
);
}
public DeviceV2DTO getDeviceByNo(String deviceNo) {
log.info("根据设备编号获取设备信息, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:no:" + deviceNo,
() -> {
CommonResponse<DeviceV2DTO> response = deviceV2Client.getDeviceByNo(deviceNo);
return handleResponse(response, "根据设备编号获取设备信息失败");
},
DeviceV2DTO.class
);
}
public DeviceV2WithConfigDTO getDeviceWithConfig(Long deviceId) {
log.info("获取设备配置信息, deviceId: {}", deviceId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:config:" + deviceId,
() -> {
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceWithConfig(deviceId);
return handleResponse(response, "获取设备配置信息失败");
},
DeviceV2WithConfigDTO.class
);
}
public DeviceV2WithConfigDTO getDeviceWithConfigByNo(String deviceNo) {
log.info("根据设备编号获取设备配置信息, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:config:no:" + deviceNo,
() -> {
CommonResponse<DeviceV2WithConfigDTO> response = deviceV2Client.getDeviceByNoWithConfig(deviceNo);
return handleResponse(response, "根据设备编号获取设备配置信息失败");
},
DeviceV2WithConfigDTO.class
);
}
public DeviceV2DTO createDevice(CreateDeviceRequest request) {
log.info("创建设备, name: {}, no: {}, type: {}", request.getName(), request.getNo(), request.getType());
CommonResponse<DeviceV2DTO> response = deviceV2Client.createDevice(request);
return handleResponse(response, "创建设备失败");
}
public void updateDevice(Long deviceId, UpdateDeviceRequest request) {
log.info("更新设备信息, deviceId: {}", deviceId);
CommonResponse<String> response = deviceV2Client.updateDevice(deviceId, request);
handleResponse(response, "更新设备信息失败");
}
public void deleteDevice(Long deviceId) {
log.info("删除设备, deviceId: {}", deviceId);
CommonResponse<String> response = deviceV2Client.deleteDevice(deviceId);
handleResponse(response, "删除设备失败");
}
public DeviceV2ListResponse listDevices(Integer page, Integer pageSize, String name, String no,
String type, Integer isActive, Long scenicId) {
log.info("分页查询设备列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
CommonResponse<DeviceV2ListResponse> response = deviceV2Client.listDevices(
page, pageSize, name, no, type, isActive, scenicId);
return handleResponse(response, "分页查询设备列表失败");
}
public DeviceV2WithConfigListResponse listDevicesWithConfig(Integer page, Integer pageSize, String name, String no,
String type, Integer isActive, Long scenicId) {
log.info("分页查询设备带配置列表, page: {}, pageSize: {}, name: {}, no: {}, type: {}, isActive: {}, scenicId: {}",
page, pageSize, name, no, type, isActive, scenicId);
CommonResponse<DeviceV2WithConfigListResponse> response = deviceV2Client.listDevicesWithConfig(
page, pageSize, name, no, type, isActive, scenicId);
return handleResponse(response, "分页查询设备带配置列表失败");
}
/**
* 创建IPC摄像头设备
*/
public DeviceV2DTO createIpcDevice(String name, String deviceNo, Long scenicId) {
CreateDeviceRequest request = new CreateDeviceRequest();
request.setName(name);
request.setNo(deviceNo);
request.setType("IPC");
request.setIsActive(1);
request.setScenicId(scenicId);
request.setSort(0);
return createDevice(request);
}
/**
* 创建IPC摄像头设备(带排序)
*/
public DeviceV2DTO createIpcDeviceWithSort(String name, String deviceNo, Long scenicId, Integer sort) {
CreateDeviceRequest request = new CreateDeviceRequest();
request.setName(name);
request.setNo(deviceNo);
request.setType("IPC");
request.setIsActive(1);
request.setScenicId(scenicId);
request.setSort(sort != null ? sort : 0);
return createDevice(request);
}
/**
* 创建自定义设备
*/
public DeviceV2DTO createCustomDevice(String name, String deviceNo, Long scenicId) {
CreateDeviceRequest request = new CreateDeviceRequest();
request.setName(name);
request.setNo(deviceNo);
request.setType("CUSTOM");
request.setIsActive(1);
request.setScenicId(scenicId);
request.setSort(0);
return createDevice(request);
}
/**
* 创建自定义设备(带排序)
*/
public DeviceV2DTO createCustomDeviceWithSort(String name, String deviceNo, Long scenicId, Integer sort) {
CreateDeviceRequest request = new CreateDeviceRequest();
request.setName(name);
request.setNo(deviceNo);
request.setType("CUSTOM");
request.setIsActive(1);
request.setScenicId(scenicId);
request.setSort(sort != null ? sort : 0);
return createDevice(request);
}
/**
* 启用设备
*/
public void enableDevice(Long deviceId) {
UpdateDeviceRequest request = new UpdateDeviceRequest();
request.setIsActive(1);
updateDevice(deviceId, request);
}
/**
* 禁用设备
*/
public void disableDevice(Long deviceId) {
UpdateDeviceRequest request = new UpdateDeviceRequest();
request.setIsActive(0);
updateDevice(deviceId, request);
}
/**
* 更新设备排序
*/
public void updateDeviceSort(Long deviceId, Integer sort) {
log.info("更新设备排序, deviceId: {}, sort: {}", deviceId, sort);
UpdateDeviceRequest request = new UpdateDeviceRequest();
request.setSort(sort);
updateDevice(deviceId, request);
}
/**
* 获取景区的IPC设备列表
*/
public DeviceV2ListResponse getScenicIpcDevices(Long scenicId, Integer page, Integer pageSize) {
return listDevices(page, pageSize, null, null, "IPC", 1, scenicId);
}
/**
* 获取景区的所有激活设备
*/
public DeviceV2ListResponse getScenicActiveDevices(Long scenicId, Integer page, Integer pageSize) {
return listDevices(page, pageSize, null, null, null, 1, scenicId);
}
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null
? response.getMessage()
: errorMessage;
Integer code = response != null ? response.getCode() : 5000;
throw new IntegrationException(code, msg, "zt-device");
}
return response.getData();
}
}

View File

@@ -13,4 +13,12 @@ public class BatchUpdateResponse {
@JsonProperty("message")
private String message;
public Integer getSuccess() {
return (updatedCount != null ? updatedCount : 0) + (createdCount != null ? createdCount : 0);
}
public Integer getFailed() {
return 0; // 当前响应格式不包含失败计数,返回0作为默认值
}
}

View File

@@ -1,9 +1,9 @@
package com.ycwl.basic.integration.scenic.example;
import com.ycwl.basic.integration.scenic.dto.config.CreateConfigRequest;
import com.ycwl.basic.integration.scenic.dto.filter.FilterCondition;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest;
import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.scenic.dto.config.*;
import com.ycwl.basic.integration.scenic.dto.filter.*;
import com.ycwl.basic.integration.scenic.dto.scenic.*;
import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService;
import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService;
import lombok.RequiredArgsConstructor;
@@ -11,10 +11,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* ZT-Scenic集成服务使用示例
* 仅供参考,实际使用时根据业务需要调用相应的服务方法
* 景区集成示例(包含降级机制)
* 演示景区集成和失败降级策略的使用
*/
@Slf4j
@Component
@@ -23,6 +25,9 @@ public class ScenicIntegrationExample {
private final ScenicIntegrationService scenicIntegrationService;
private final ScenicConfigIntegrationService scenicConfigIntegrationService;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-scenic";
/**
* 示例:创建景区并设置配置
@@ -75,4 +80,105 @@ public class ScenicIntegrationExample {
log.error("筛选景区失败", e);
}
}
/**
* 演示基础景区操作的降级机制
*/
public void basicScenicOperationsExample() {
log.info("=== 基础景区操作示例(含降级机制) ===");
Long scenicId = 2001L;
try {
// 获取景区信息 - 自动降级
ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId);
log.info("获取景区成功: {}", scenic.getName());
// 获取景区配置信息 - 自动降级
ScenicV2WithConfigDTO scenicWithConfig = scenicIntegrationService.getScenicWithConfig(scenicId);
log.info("获取景区配置成功,配置数量: {}", scenicWithConfig.getConfig().size());
// 获取扁平化配置 - 自动降级
Map<String, Object> flatConfig = scenicIntegrationService.getScenicFlatConfig(scenicId);
log.info("获取扁平化配置成功,配置项数量: {}", flatConfig.size());
} catch (Exception e) {
log.error("景区操作降级失败", e);
}
}
/**
* 演示景区配置管理的降级机制
*/
public void scenicConfigManagementFallbackExample() {
log.info("=== 景区配置管理示例(含降级机制) ===");
Long scenicId = 2001L;
try {
// 获取扁平化配置 - 自动降级
Map<String, Object> flatConfigs = scenicConfigIntegrationService.getFlatConfigs(scenicId);
log.info("获取扁平化配置成功,配置项数量: {}", flatConfigs.size());
// 批量更新配置 - 直接操作,失败时抛出异常
Map<String, Object> updates = new HashMap<>();
updates.put("max_visitors", "5000");
updates.put("opening_hours", "08:00-18:00");
BatchUpdateResponse result = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, updates);
log.info("批量更新配置完成: 成功 {}, 失败 {}", result.getSuccess(), result.getFailed());
} catch (Exception e) {
log.error("景区配置管理操作失败", e);
}
}
/**
* 演示降级缓存管理
*/
public void fallbackCacheManagementExample() {
log.info("=== 景区降级缓存管理示例 ===");
String scenicCacheKey = "scenic:2001";
String configCacheKey = "scenic:flat:configs:2001";
// 检查降级缓存状态
boolean hasScenicCache = fallbackService.hasFallbackCache(SERVICE_NAME, scenicCacheKey);
boolean hasConfigCache = fallbackService.hasFallbackCache(SERVICE_NAME, configCacheKey);
log.info("景区降级缓存存在: {}", hasScenicCache);
log.info("配置降级缓存存在: {}", hasConfigCache);
// 获取降级缓存统计信息
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(SERVICE_NAME);
log.info("景区服务降级缓存统计: 缓存数量={}, TTL={}天",
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// 清理特定的降级缓存
if (hasScenicCache) {
fallbackService.clearFallbackCache(SERVICE_NAME, scenicCacheKey);
log.info("已清理景区降级缓存");
}
// 如果缓存过多,批量清理
if (stats.getTotalCacheCount() > 50) {
fallbackService.clearAllFallbackCache(SERVICE_NAME);
log.info("已批量清理所有景区降级缓存");
}
}
/**
* 运行所有示例
*/
public void runAllExamples() {
log.info("开始运行景区集成示例(包含降级机制)...");
createScenicWithConfig();
filterScenics();
basicScenicOperationsExample();
scenicConfigManagementFallbackExample();
fallbackCacheManagementExample();
log.info("景区集成示例运行完成");
}
}

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.integration.scenic.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.scenic.client.ScenicConfigV2Client;
import com.ycwl.basic.integration.scenic.dto.config.*;
import lombok.RequiredArgsConstructor;
@@ -17,43 +18,61 @@ import java.util.Map;
public class ScenicConfigIntegrationService {
private final ScenicConfigV2Client scenicConfigV2Client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-scenic";
public List<ScenicConfigV2DTO> listConfigs(Long scenicId) {
log.info("获取景区配置列表, scenicId: {}", scenicId);
CommonResponse<List<ScenicConfigV2DTO>> response = scenicConfigV2Client.listConfigs(scenicId);
return handleResponse(response, "获取景区配置列表失败");
}
public ScenicConfigV2DTO getConfigByKey(Long scenicId, String configKey) {
log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey);
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.getConfigByKey(scenicId, configKey);
return handleResponse(response, "根据键获取景区配置失败");
}
public Map<String, Object> getFlatConfigs(Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:flat:configs:" + scenicId,
() -> {
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
},
Map.class
);
}
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
return handleResponse(response, "创建景区配置失败");
}
public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) {
log.info("更新景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.updateConfig(scenicId, id, request);
return handleResponse(response, "更新景区配置失败");
}
public void deleteConfig(Long scenicId, String id) {
log.info("删除景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<Void> response = scenicConfigV2Client.deleteConfig(scenicId, id);
handleResponse(response, "删除景区配置失败");
}
public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) {
log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request);
return handleResponse(response, "批量更新景区配置失败");
}
public BatchUpdateResponse batchFlatUpdateConfigs(Long scenicId, Map<String, Object> configs) {
log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchFlatUpdateConfigs(scenicId, configs);
return handleResponse(response, "扁平化批量更新景区配置失败");
}

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.integration.scenic.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.scenic.client.ScenicConfigV2Client;
import com.ycwl.basic.integration.scenic.client.ScenicV2Client;
import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse;
@@ -25,48 +26,81 @@ public class ScenicIntegrationService {
private final ScenicV2Client scenicV2Client;
private final ScenicConfigV2Client scenicConfigV2Client;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-scenic";
public ScenicV2DTO getScenic(Long scenicId) {
log.info("获取景区信息, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:" + scenicId,
() -> {
CommonResponse<ScenicV2DTO> response = scenicV2Client.getScenic(scenicId);
return handleResponse(response, "获取景区信息失败");
},
ScenicV2DTO.class
);
}
public ScenicV2WithConfigDTO getScenicWithConfig(Long scenicId) {
log.info("获取景区配置信息, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:config:" + scenicId,
() -> {
CommonResponse<ScenicV2WithConfigDTO> response = scenicV2Client.getScenicWithConfig(scenicId);
return handleResponse(response, "获取景区配置信息失败");
},
ScenicV2WithConfigDTO.class
);
}
public Map<String, Object> getScenicFlatConfig(Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"scenic:flat:config:" + scenicId,
() -> {
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
},
Map.class
);
}
public ScenicV2DTO createScenic(CreateScenicRequest request) {
log.info("创建景区, name: {}", request.getName());
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
return handleResponse(response, "创建景区失败");
}
public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) {
log.info("更新景区信息, scenicId: {}", scenicId);
CommonResponse<ScenicV2DTO> response = scenicV2Client.updateScenic(scenicId, request);
return handleResponse(response, "更新景区信息失败");
}
public void deleteScenic(Long scenicId) {
log.info("删除景区, scenicId: {}", scenicId);
CommonResponse<Void> response = scenicV2Client.deleteScenic(scenicId);
handleResponse(response, "删除景区失败");
}
public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) {
log.info("筛选景区, filters: {}", request.getFilters().size());
CommonResponse<ScenicFilterPageResponse> response = scenicV2Client.filterScenics(request);
return handleResponse(response, "筛选景区失败");
}
public ScenicV2ListResponse listScenics(Integer page, Integer pageSize, Integer status, String name) {
log.info("分页查询景区列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
CommonResponse<ScenicV2ListResponse> response = scenicV2Client.listScenics(page, pageSize, status, name);
return handleResponse(response, "分页查询景区列表失败");
}
public ScenicV2WithConfigListResponse listScenicsWithConfig(Integer page, Integer pageSize, Integer status, String name) {
log.info("分页查询景区带配置列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name);
CommonResponse<ScenicV2WithConfigListResponse> response = scenicV2Client.listScenicsWithConfig(page, pageSize, status, name);
return handleResponse(response, "分页查询景区带配置列表失败");
}

View File

@@ -1,52 +0,0 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* @Author:longbinbin
* @Date:2024/11/29 14:48
* device(设备管理)
*/
@Mapper
public interface DeviceMapper {
List<DeviceRespVO> list(DeviceReqQuery deviceReqQuery);
List<DeviceEntity> listAll();
DeviceRespVO getById(Long id);
int add(DeviceAddOrUpdateReq deviceReqQuery);
int deleteById(Long id);
int update(DeviceAddOrUpdateReq deviceReqQuery);
int updateStatus(Long id);
DeviceEntity getByDeviceId(Long deviceId);
List<DeviceRespVO> listByScenicIdWithWVP(Long scenicId);
ScenicDeviceCountVO deviceCountByScenicId(@Param("scenicId") Long scenicId);
DeviceConfigEntity getConfigByDeviceId(Long deviceId);
int addConfig(DeviceConfigEntity deviceConfigEntity);
int updateConfig(DeviceConfigEntity deviceConfigEntity);
DeviceEntity getByDeviceNo(String deviceNo);
int updateEntity(DeviceEntity device);
int addEntity(DeviceEntity device);
int updateOnlineStatus(Long id, String ipAddr, int online, Date keepaliveAt);
DeviceEntity getByDeviceNo2(String deviceNo);
List<Long> listAllByScenicId(Long scenicId);
int updateSort(Long id, Integer sort);
}

View File

@@ -28,7 +28,6 @@ public class DeviceEntity {
* 设备编号
*/
private String no;
private String no2;
/**
* 经度
*/

View File

@@ -16,7 +16,7 @@ import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.printer.ticket.FeiETicketPrinter;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.util.ScenicConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.utils.WxMpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;

View File

@@ -1,98 +1,111 @@
package com.ycwl.basic.repository;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.dto.config.DeviceConfigV2DTO;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.utils.SnowFlakeUtil;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Component
public class DeviceRepository {
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private DeviceIntegrationService deviceIntegrationService;
public static final String DEVICE_ONLINE_CACHE_KEY = "device:online_status:%s";
public static final String DEVICE_CACHE_KEY = "device:%s";
public static final String DEVICE_CONFIG_CACHE_KEY = "device:%s:config";
@Autowired
private DeviceConfigIntegrationService deviceConfigIntegrationService;
/**
* 将DeviceV2DTO转换为DeviceEntity
*/
private DeviceEntity convertToEntity(DeviceV2DTO dto) {
if (dto == null) {
return null;
}
DeviceEntity entity = new DeviceEntity();
entity.setId(dto.getId());
entity.setName(dto.getName());
entity.setNo(dto.getNo());
entity.setScenicId(dto.getScenicId());
// DeviceV2DTO中的isActive对应DeviceEntity中的status
entity.setStatus(dto.getIsActive());
// 转换时间格式:LocalDateTime -> Date
if (dto.getCreateTime() != null) {
entity.setCreateAt(Date.from(dto.getCreateTime().atZone(ZoneId.systemDefault()).toInstant()));
}
if (dto.getUpdateTime() != null) {
entity.setUpdateAt(Date.from(dto.getUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
}
return entity;
}
public DeviceEntity getDevice(Long deviceId) {
if (redisTemplate.hasKey(String.format(DEVICE_CACHE_KEY, deviceId))) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_CACHE_KEY, deviceId)), DeviceEntity.class);
}
DeviceEntity device = deviceMapper.getByDeviceId(deviceId);
if (null != device) {
redisTemplate.opsForValue().set(String.format(DEVICE_CACHE_KEY, deviceId), JacksonUtil.toJSONString(device), 3, TimeUnit.DAYS);
}
log.debug("获取设备信息, deviceId: {}", deviceId);
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
DeviceEntity device = convertToEntity(deviceDto);
return device;
}
public DeviceEntity getDeviceByDeviceNo(String deviceNo) {
if (redisTemplate.hasKey(String.format(DEVICE_CACHE_KEY, deviceNo))) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_CACHE_KEY, deviceNo)), DeviceEntity.class);
}
DeviceEntity device = deviceMapper.getByDeviceNo(deviceNo);
if (null == device) {
device = deviceMapper.getByDeviceNo2(deviceNo);
}
if (null != device) {
redisTemplate.opsForValue().set(String.format(DEVICE_CACHE_KEY, deviceNo), JacksonUtil.toJSONString(device), 3, TimeUnit.DAYS);
redisTemplate.opsForValue().set(String.format(DEVICE_CACHE_KEY, device.getId()), JacksonUtil.toJSONString(device), 3, TimeUnit.DAYS);
} else {
redisTemplate.opsForValue().set(String.format(DEVICE_CACHE_KEY, deviceNo), "null", 60L, TimeUnit.SECONDS);
}
log.debug("根据设备编号获取设备信息, deviceNo: {}", deviceNo);
DeviceV2DTO deviceDto = deviceIntegrationService.getDeviceByNo(deviceNo);
DeviceEntity device = convertToEntity(deviceDto);
return device;
}
public DeviceConfigEntity getDeviceConfig(Long deviceId) {
if (redisTemplate.hasKey(String.format(DEVICE_CONFIG_CACHE_KEY, deviceId))) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_CONFIG_CACHE_KEY, deviceId)), DeviceConfigEntity.class);
List<DeviceConfigV2DTO> configList = deviceConfigIntegrationService.getDeviceConfigs(deviceId);
if (configList != null && !configList.isEmpty()) {
return convertToDeviceConfigEntity(configList, deviceId);
}
DeviceConfigEntity deviceConfig = deviceMapper.getConfigByDeviceId(deviceId);
if (null == deviceConfig) {
deviceConfig = new DeviceConfigEntity();
deviceConfig.setId(SnowFlakeUtil.getLongId());
deviceConfig.setDeviceId(deviceId);
deviceMapper.addConfig(deviceConfig);
}
redisTemplate.opsForValue().set(String.format(DEVICE_CONFIG_CACHE_KEY, deviceId), JacksonUtil.toJSONString(deviceConfig), 3, TimeUnit.DAYS);
return deviceConfig;
return null;
}
public boolean clearDeviceCache(String deviceNo) {
if (deviceNo == null) {
return true;
}
if (redisTemplate.hasKey(String.format(DEVICE_CACHE_KEY, deviceNo))) {
DeviceEntity device = getDeviceByDeviceNo(deviceNo);
if (device != null) {
redisTemplate.delete(String.format(DEVICE_CACHE_KEY, device.getNo()));
clearDeviceCache(device.getId());
} else {
redisTemplate.delete(String.format(DEVICE_CACHE_KEY, deviceNo));
/**
* 获取设备配置管理器
*
* @param deviceId 设备ID
* @return DeviceConfigManager实例,如果获取失败返回null
*/
public DeviceConfigManager getDeviceConfigManager(Long deviceId) {
try {
List<DeviceConfigV2DTO> configList = deviceConfigIntegrationService.getDeviceConfigs(deviceId);
return new DeviceConfigManager(configList);
} catch (Exception e) {
log.warn("获取设备配置管理器失败, deviceId: {}", deviceId, e);
return null;
}
}
redisTemplate.delete(String.format(DEVICE_CACHE_KEY, deviceNo));
return true;
}
public boolean clearDeviceCache(Long deviceId) {
if (redisTemplate.hasKey(String.format(DEVICE_CACHE_KEY, deviceId))) {
DeviceEntity device = getDevice(deviceId);
redisTemplate.delete(String.format(DEVICE_CACHE_KEY, device.getNo()));
redisTemplate.delete(String.format(DEVICE_CONFIG_CACHE_KEY, device.getNo()));
}
redisTemplate.delete(String.format(DEVICE_CACHE_KEY, deviceId));
redisTemplate.delete(String.format(DEVICE_CONFIG_CACHE_KEY, deviceId));
return true;
/**
* 将DeviceConfigV2DTO列表转换为DeviceConfigEntity(为了兼容性)
*/
private DeviceConfigEntity convertToDeviceConfigEntity(List<DeviceConfigV2DTO> configList, Long deviceId) {
DeviceConfigEntity entity = new DeviceConfigEntity();
entity.setId(SnowFlakeUtil.getLongId());
entity.setDeviceId(deviceId);
// 由于DeviceConfigEntity没有通用的configJson字段,这里只设置基本信息
// 具体的配置项需要通过DeviceConfigManager来访问
return entity;
}
public void updateOnlineStatus(String deviceNo, int online, Date keepaliveAt) {
@@ -129,8 +142,15 @@ public class DeviceRepository {
redisTemplate.opsForValue().set(String.format(DEVICE_CACHE_KEY, device.getNo()), JacksonUtil.toJSONString(device));
}
public List<DeviceEntity> getAllDeviceByScenicId(Long scenicId) {
List<Long> deviceIdList = deviceMapper.listAllByScenicId(scenicId);
return deviceIdList.stream().map(this::getDevice).collect(Collectors.toList());
public List<DeviceV2DTO> getAllDeviceByScenicId(Long scenicId) {
try {
var deviceListResponse = deviceIntegrationService.getScenicActiveDevices(scenicId, 1, 1000);
if (deviceListResponse != null && deviceListResponse.getList() != null) {
return deviceListResponse.getList();
}
} catch (Exception e) {
log.warn("获取景区设备列表失败,景区ID: {}, 错误: {}", scenicId, e.getMessage());
}
return List.of();
}
}

View File

@@ -19,13 +19,12 @@ import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.pay.enums.PayAdapterType;
import com.ycwl.basic.storage.enums.StorageType;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.util.ScenicConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class ScenicRepository {
@@ -49,51 +48,15 @@ public class ScenicRepository {
public ScenicV2DTO getScenicBasic(Long id) {
String key = String.format(SCENIC_BASIC_CACHE_KEY, id);
try {
ScenicV2DTO scenicDTO = scenicIntegrationService.getScenic(id);
// 请求成功,写入缓存
if (scenicDTO != null) {
redisTemplate.opsForValue().set(
key,
JacksonUtil.toJSONString(scenicDTO)
);
}
return scenicDTO;
} catch (Exception e) {
// 请求失败,尝试从缓存获取历史成功数据
String cacheKey = key;
if (redisTemplate.hasKey(cacheKey)) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), ScenicV2DTO.class);
}
// 缓存也没有,返回null
return null;
}
}
public ScenicEntity getScenic(Long id) {
String key = String.format(SCENIC_CACHE_KEY, id);
try {
ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id);
ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO);
// 请求成功,写入缓存
if (scenicEntity != null) {
redisTemplate.opsForValue().set(
key,
JacksonUtil.toJSONString(scenicEntity)
);
}
return scenicEntity;
} catch (Exception e) {
// 请求失败,尝试从缓存获取历史成功数据
String cacheKey = key;
if (redisTemplate.hasKey(cacheKey)) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), ScenicEntity.class);
}
// 缓存也没有,返回null
return null;
}
}
public ScenicConfigEntity getScenicConfig(Long scenicId) {
@@ -384,38 +347,13 @@ public class ScenicRepository {
public ScenicConfigManager getScenicConfigManagerWithCache(Long scenicId) {
String key = String.format(SCENIC_CONFIG_CACHE_KEY + ":manager", scenicId);
try {
List<com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO> configList =
scenicConfigIntegrationService.listConfigs(scenicId);
if (configList != null) {
ScenicConfigManager manager = new ScenicConfigManager(configList);
// 请求成功,写入缓存(将配置列表序列化存储)
redisTemplate.opsForValue().set(
key,
JacksonUtil.toJSONString(configList)
);
return manager;
}
return null;
} catch (Exception e) {
// 请求失败,尝试从缓存获取历史成功数据
if (redisTemplate.hasKey(key)) {
try {
String cachedConfigJson = redisTemplate.opsForValue().get(key);
@SuppressWarnings("unchecked")
List<ScenicConfigV2DTO> cachedConfigList =
JacksonUtil.parseArray(cachedConfigJson, ScenicConfigV2DTO.class);
return new ScenicConfigManager(cachedConfigList);
} catch (Exception cacheException) {
// 缓存解析失败,返回null
return null;
}
}
// 缓存也没有,返回null
return null;
}
}
}

View File

@@ -1,7 +1,5 @@
package com.ycwl.basic.service.custom;
import com.aliyun.credentials.Client;
import com.aliyun.credentials.models.Config;
import com.aliyun.mts20140618.models.QuerySmarttagJobRequest;
import com.aliyun.mts20140618.models.QuerySmarttagJobResponse;
import com.aliyun.mts20140618.models.QuerySmarttagJobResponseBody;
@@ -10,11 +8,11 @@ import com.aliyun.mts20140618.models.SubmitSmarttagJobResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.CustomUploadTaskMapper;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.custom.entity.CustomUploadTaskEntity;
import com.ycwl.basic.model.custom.entity.FaceData;
import com.ycwl.basic.model.custom.req.CreateUploadTaskReq;
@@ -30,20 +28,15 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
@Slf4j
@Service
public class CustomUploadTaskService {
@@ -52,7 +45,7 @@ public class CustomUploadTaskService {
private CustomUploadTaskMapper customUploadTaskMapper;
@Autowired
private DeviceMapper deviceMapper;
private DeviceIntegrationService deviceIntegrationService;
@Autowired
private ScenicService scenicService;
@@ -80,7 +73,7 @@ public class CustomUploadTaskService {
}
// 验证设备访问权限
DeviceEntity device = validateDeviceAccess(req.getAccessKey(), req.getType());
DeviceV2DTO device = validateDeviceAccess(req.getAccessKey(), req.getType());
long taskId = SnowFlakeUtil.getLongId();
String savePath = generateSavePath(device.getScenicId(), req.getFileName());
@@ -108,7 +101,7 @@ public class CustomUploadTaskService {
public void completeUpload(String accessKey, Long taskId) {
// 验证设备访问权限
DeviceEntity device = validateDeviceAccess(accessKey, null);
DeviceV2DTO device = validateDeviceAccess(accessKey, null);
CustomUploadTaskEntity task = customUploadTaskMapper.selectById(taskId);
if (task == null) {
@@ -146,7 +139,7 @@ public class CustomUploadTaskService {
public void markTaskFailed(String accessKey, Long taskId, String errorMsg) {
// 验证设备访问权限
DeviceEntity device = validateDeviceAccess(accessKey, null);
DeviceV2DTO device = validateDeviceAccess(accessKey, null);
CustomUploadTaskEntity task = customUploadTaskMapper.selectById(taskId);
if (task == null) {
@@ -214,17 +207,18 @@ public class CustomUploadTaskService {
}
}
private DeviceEntity validateDeviceAccess(String accessKey, String type) {
private DeviceV2DTO validateDeviceAccess(String accessKey, String type) {
if (StringUtils.isBlank(accessKey)) {
throw new RuntimeException("设备访问密钥不能为空");
}
DeviceEntity device = deviceMapper.getByDeviceNo(accessKey);
if (device == null || device.getStatus() != 1) {
// 通过zt-device服务获取设备信息
DeviceV2DTO deviceV2DTO = deviceIntegrationService.getDeviceByNo(accessKey);
if (deviceV2DTO == null || deviceV2DTO.getIsActive() != 1) {
throw new RuntimeException("无效的设备访问密钥或设备已被禁用");
}
return device;
return deviceV2DTO;
}
private String createAliyunMtsTask(String inputPath) {

View File

@@ -2,8 +2,10 @@ package com.ycwl.basic.service.mobile.impl;
import cn.hutool.core.bean.BeanUtil;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2ListResponse;
import com.ycwl.basic.mapper.ExtraDeviceMapper;
import com.ycwl.basic.mapper.ScenicAccountMapper;
import com.ycwl.basic.model.jwt.JwtInfo;
@@ -34,8 +36,10 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
@@ -48,7 +52,7 @@ import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
public class AppScenicServiceImpl implements AppScenicService {
@Autowired
private DeviceMapper deviceMapper;
private DeviceIntegrationService deviceIntegrationService;
@Autowired
private ScenicAccountMapper scenicAccountMapper;
@Autowired
@@ -80,7 +84,14 @@ public class AppScenicServiceImpl implements AppScenicService {
@Override
public ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(Long scenicId) {
JwtInfo worker = JwtTokenUtil.getWorker();
ScenicDeviceCountVO scenicDeviceCountVO = deviceMapper.deviceCountByScenicId(scenicId);
// 通过zt-device服务获取设备统计
DeviceV2ListResponse deviceListResponse = deviceIntegrationService.getScenicActiveDevices(scenicId, 1, 1000);
ScenicDeviceCountVO scenicDeviceCountVO = new ScenicDeviceCountVO();
if (deviceListResponse != null && deviceListResponse.getList() != null) {
scenicDeviceCountVO.setTotalDeviceCount(deviceListResponse.getList().size());
} else {
scenicDeviceCountVO.setTotalDeviceCount(0);
}
return ApiResponse.success(scenicDeviceCountVO);
}
@@ -107,7 +118,14 @@ public class AppScenicServiceImpl implements AppScenicService {
scenicRespVO.setKfCodeUrl(scenic.getKfCodeUrl());
}
ScenicDeviceCountVO scenicDeviceCountVO = deviceMapper.deviceCountByScenicId(id);
// 通过zt-device服务获取设备统计
DeviceV2ListResponse deviceListResponse = deviceIntegrationService.getScenicActiveDevices(id, 1, 1000);
ScenicDeviceCountVO scenicDeviceCountVO = new ScenicDeviceCountVO();
if (deviceListResponse != null && deviceListResponse.getList() != null) {
scenicDeviceCountVO.setTotalDeviceCount(deviceListResponse.getList().size());
} else {
scenicDeviceCountVO.setTotalDeviceCount(0);
}
scenicRespVO.setLensNum(scenicDeviceCountVO.getTotalDeviceCount());
return ApiResponse.success(scenicRespVO);
}
@@ -257,7 +275,18 @@ public class AppScenicServiceImpl implements AppScenicService {
@Override
public ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId) {
List<DeviceRespVO> deviceRespVOList = deviceMapper.listByScenicIdWithWVP(scenicId);
DeviceV2ListResponse deviceV2ListResponse = deviceIntegrationService.listDevices(1, 1000, null, null, null, 1, scenicId);
List<DeviceRespVO> deviceRespVOList = deviceV2ListResponse.getList().stream().map(device -> {
DeviceRespVO deviceRespVO = new DeviceRespVO();
deviceRespVO.setId(device.getId());
deviceRespVO.setName(device.getName());
deviceRespVO.setNo(device.getNo());
DeviceConfigManager config = deviceRepository.getDeviceConfigManager(device.getId());
deviceRespVO.setDeviceNo(device.getNo());
deviceRespVO.setChannelNo(config.getString("channel_no"));
return deviceRespVO;
}).collect(Collectors.toList());
for (DeviceRespVO deviceRespVO : deviceRespVOList) {
DeviceEntity onlineStatus = deviceRepository.getOnlineStatus(deviceRespVO.getId());
if (onlineStatus != null) {
@@ -282,7 +311,7 @@ public class AppScenicServiceImpl implements AppScenicService {
deviceRespVO.setOnline(0);
continue;
}
Long ts = Long.parseLong(onlineTs);
long ts = Long.parseLong(onlineTs);
Date keepaliveAt = new Date(ts*1000);
deviceRespVO.setUpdateAt(keepaliveAt);
deviceRespVO.setKeepaliveAt(keepaliveAt);

View File

@@ -1,35 +0,0 @@
package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceBatchSortRequest;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.model.wvp.WvpSyncReqVo;
import com.ycwl.basic.utils.ApiResponse;
import java.util.List;
/**
* @Author:longbinbin
* @Date:2024/12/2 16:14
* 设备管理
*/
public interface DeviceService {
ApiResponse<PageInfo<DeviceRespVO>> pageQuery(DeviceReqQuery deviceReqQuery);
ApiResponse<List<DeviceRespVO>> list(DeviceReqQuery deviceReqQuery);
ApiResponse<DeviceRespVO> getById(Long id);
ApiResponse addOrUpdate(DeviceAddOrUpdateReq deviceReqQuery);
ApiResponse deleteById(Long id);
ApiResponse updateStatus(Long id);
DeviceConfigEntity getConfig(Long id);
void saveConfig(Long configId, DeviceConfigEntity config);
void updateDevices(Long scenicId, WvpSyncReqVo reqVo);
ApiResponse<Boolean> sortDevice(Long deviceId, Long afterDeviceId);
ApiResponse<Boolean> batchSort(Long scenicId, DeviceBatchSortRequest request);
}

View File

@@ -1,176 +0,0 @@
package com.ycwl.basic.service.pc.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.req.DeviceBatchSortRequest;
import com.ycwl.basic.model.wvp.WvpSyncReqVo;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.service.pc.DeviceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author:longbinbin
* @Date:2024/12/2 16:18
*/
@Service
public class DeviceServiceImpl implements DeviceService {
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private DeviceRepository deviceRepository;
@Override
public ApiResponse<PageInfo<DeviceRespVO>> pageQuery(DeviceReqQuery deviceReqQuery) {
PageHelper.startPage(deviceReqQuery.getPageNum(), deviceReqQuery.getPageSize());
List<DeviceRespVO> list = deviceMapper.list(deviceReqQuery);
for (DeviceRespVO deviceRespVO : list) {
DeviceEntity onlineStatus = deviceRepository.getOnlineStatus(deviceRespVO.getId());
if (onlineStatus != null) {
deviceRespVO.setKeepaliveAt(onlineStatus.getKeepaliveAt());
if (new Date().getTime() - onlineStatus.getKeepaliveAt().getTime() > 300000) {
deviceRespVO.setOnline(0);
} else {
deviceRespVO.setOnline(onlineStatus.getOnline());
}
} else {
deviceRespVO.setOnline(0);
deviceRespVO.setKeepaliveAt(null);
}
}
PageInfo<DeviceRespVO> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo);
}
@Override
public ApiResponse<List<DeviceRespVO>> list(DeviceReqQuery deviceReqQuery) {
return ApiResponse.success(deviceMapper.list(deviceReqQuery));
}
@Override
public ApiResponse<DeviceRespVO> getById(Long id) {
return ApiResponse.success(deviceMapper.getById(id));
}
@Override
public ApiResponse addOrUpdate(DeviceAddOrUpdateReq deviceReqQuery) {
Long id = deviceReqQuery.getId();
if (id == null) {
deviceReqQuery.setId(SnowFlakeUtil.getLongId());
if (StringUtils.isBlank(deviceReqQuery.getNo())) {
deviceReqQuery.setNo(deviceReqQuery.getId().toString());
}
deviceReqQuery.setStatus(0);
return ApiResponse.success(deviceMapper.add(deviceReqQuery));
} else {
deviceRepository.clearDeviceCache(deviceReqQuery.getId());
deviceMapper.update(deviceReqQuery);
deviceRepository.clearDeviceCache(deviceReqQuery.getId());
return ApiResponse.success(0);
}
}
@Override
public ApiResponse deleteById(Long id) {
return ApiResponse.success(deviceMapper.deleteById(id));
}
@Override
public ApiResponse updateStatus(Long id) {
deviceRepository.clearDeviceCache(id);
deviceMapper.updateStatus(id);
deviceRepository.clearDeviceCache(id);
return ApiResponse.success(1);
}
@Override
public DeviceConfigEntity getConfig(Long id) {
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(id);
if (config == null) {
config = new DeviceConfigEntity();
config.setId(SnowFlakeUtil.getLongId());
config.setDeviceId(id);
deviceMapper.addConfig(config);
}
return config;
}
@Override
public void saveConfig(Long configId, DeviceConfigEntity config) {
config.setId(configId);
deviceMapper.updateConfig(config);
deviceRepository.clearDeviceCache(config.getDeviceId());
}
@Override
public void updateDevices(Long scenicId, WvpSyncReqVo reqVo) {
if (reqVo == null) {
return;
}
if (reqVo.getDevices() != null && !reqVo.getDevices().isEmpty()) {
for (WvpSyncReqVo.DeviceItem deviceItem : reqVo.getDevices()) {
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceItem.getDeviceNo());
if (device != null) {
device.setOnline(deviceItem.getOnline());
device.setKeepaliveAt(deviceItem.getKeepaliveAt());
deviceRepository.updateOnlineStatus(device.getId(), deviceItem.getIp(), 1, deviceItem.getKeepaliveAt());
}
}
}
}
@Override
public ApiResponse<Boolean> sortDevice(Long deviceId, Long afterDeviceId) {
DeviceEntity device = deviceRepository.getDevice(deviceId);
if (device == null) {
return ApiResponse.fail("设备不存在");
}
List<DeviceEntity> scenicDeviceList = deviceRepository.getAllDeviceByScenicId(device.getScenicId());
AtomicInteger sortNum = new AtomicInteger(0);
for (DeviceEntity item : scenicDeviceList) {
item.setSort(sortNum.addAndGet(1));
}
Optional<DeviceEntity> templateOptional = scenicDeviceList.stream().filter(item -> item.getId().equals(deviceId)).findAny();
if (templateOptional.isEmpty()) {
return ApiResponse.fail("设备不存在");
}
Optional<DeviceEntity> afterTemplateOptional = scenicDeviceList.stream().filter(item -> item.getId().equals(afterDeviceId)).findAny();
if (afterTemplateOptional.isPresent()) {
DeviceEntity afterTemplate = afterTemplateOptional.get();
Integer newSort = afterTemplate.getSort();
DeviceEntity oldTemplate = templateOptional.get();
Integer oldSort = oldTemplate.getSort();
afterTemplate.setSort(oldSort);
oldTemplate.setSort(newSort);
}
scenicDeviceList.forEach(item -> {
deviceMapper.updateSort(item.getId(), item.getSort());
deviceRepository.clearDeviceCache(item.getId());
deviceRepository.clearDeviceCache(item.getNo());
deviceRepository.clearDeviceCache(item.getNo2());
});
return ApiResponse.success(true);
}
@Override
public ApiResponse<Boolean> batchSort(Long scenicId, DeviceBatchSortRequest request) {
for (DeviceBatchSortRequest.SortItem item : request.getList()) {
deviceMapper.updateSort(item.getId(), item.getSort());
deviceRepository.clearDeviceCache(item.getId());
}
return ApiResponse.success(true);
}
}

View File

@@ -9,6 +9,7 @@ import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.StatisticEnum;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.FaceMapper;
@@ -290,7 +291,7 @@ public class FaceServiceImpl implements FaceService {
if (sampleListIds != null && !sampleListIds.isEmpty()) {// 匹配原片:照片
List<SourceEntity> sourceEntities = sourceMapper.listBySampleIds(sampleListIds);
List<MemberSourceEntity> memberSourceEntityList = sourceEntities.stream().map(sourceEntity -> {
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(sourceEntity.getDeviceId());
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
memberSourceEntity.setScenicId(face.getScenicId());
memberSourceEntity.setFaceId(face.getId());
@@ -300,11 +301,11 @@ public class FaceServiceImpl implements FaceService {
memberSourceEntity.setIsFree(0);
if (deviceConfig != null) {
if (sourceEntity.getType() == 1) {
if (Integer.valueOf(1).equals(deviceConfig.getVideoFree())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) {
memberSourceEntity.setIsFree(1);
}
} else if (sourceEntity.getType() == 2) {
if (Integer.valueOf(1).equals(deviceConfig.getImageFree())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) {
memberSourceEntity.setIsFree(1);
}
}

View File

@@ -3,7 +3,6 @@ package com.ycwl.basic.service.pc.impl;
import com.ycwl.basic.facebody.FaceBodyFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.pay.PayFactory;
import com.ycwl.basic.pay.adapter.IPayAdapter;
@@ -12,10 +11,9 @@ import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.exceptions.StorageUnsupportedException;
import com.ycwl.basic.util.ScenicConfigManager;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.util.TtlCacheMap;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.service.task.impl;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.utils.JacksonUtil;
import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest;
import com.ycwl.basic.biz.OrderBiz;
@@ -115,7 +116,7 @@ public class TaskFaceServiceImpl implements TaskFaceService {
if (sampleListIds != null && !sampleListIds.isEmpty()) {// 匹配原片:照片
List<SourceEntity> sourceEntities = sourceMapper.listBySampleIds(sampleListIds);
List<MemberSourceEntity> memberSourceEntityList = sourceEntities.stream().map(sourceEntity -> {
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(sourceEntity.getDeviceId());
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(sourceEntity.getDeviceId());
MemberSourceEntity memberSourceEntity = new MemberSourceEntity();
memberSourceEntity.setScenicId(scenicId);
memberSourceEntity.setFaceId(faceEntity.getId());
@@ -125,11 +126,11 @@ public class TaskFaceServiceImpl implements TaskFaceService {
memberSourceEntity.setIsFree(0);
if (deviceConfig != null) {
if (sourceEntity.getType() == 1) {
if (Integer.valueOf(1).equals(deviceConfig.getVideoFree())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("video_free"))) {
memberSourceEntity.setIsFree(1);
}
} else if (sourceEntity.getType() == 2) {
if (Integer.valueOf(1).equals(deviceConfig.getImageFree())) {
if (Integer.valueOf(1).equals(deviceConfig.getInteger("image_free"))) {
memberSourceEntity.setIsFree(1);
}
}

View File

@@ -3,11 +3,16 @@ package com.ycwl.basic.task;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2ListResponse;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import java.util.stream.Collectors;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.repository.DeviceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
@@ -24,29 +29,33 @@ import java.util.List;
@Profile("prod")
public class VideoPieceCleaner {
@Autowired
private DeviceMapper deviceMapper;
private DeviceIntegrationService deviceIntegrationService;
@Autowired
private DeviceRepository deviceRepository;
@Scheduled(cron = "0 0 0 * * ?")
public void clean() {
log.info("开始删除视频文件");
List<DeviceEntity> deviceList = deviceMapper.listAll();
for (DeviceEntity device : deviceList) {
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(device.getId());
if (config == null) {
// 通过zt-device服务获取所有激活设备
DeviceV2ListResponse deviceListResponse = deviceIntegrationService.listDevices(1, 10000, null, null, null, 1, null);
List<DeviceEntity> deviceList;
if (deviceListResponse == null) {
return;
}
for (DeviceV2DTO device : deviceListResponse.getList()) {
DeviceConfigManager config = deviceRepository.getDeviceConfigManager(device.getId());
DeviceConfigEntity dConfig = deviceRepository.getDeviceConfig(device.getId());
Integer storeExpireDay = config.getInteger("store_expire_day");
if (storeExpireDay == null || storeExpireDay <= 0) {
continue;
}
if (config.getStoreExpireDay() == null) {
continue;
}
if (config.getStoreExpireDay() <= 0) {
continue;
}
IDeviceStorageOperator storageOperator = DeviceFactory.getDeviceStorageOperator(device, config);
IDeviceStorageOperator storageOperator = DeviceFactory.getDeviceStorageOperator(device, dConfig);
if (storageOperator == null) {
continue;
}
storageOperator.removeFilesBeforeDate(new Date(System.currentTimeMillis() - config.getStoreExpireDay() * 24 * 60 * 60 * 1000));
storageOperator.removeFilesBeforeDate(new Date(System.currentTimeMillis() - storeExpireDay * 24 * 60 * 60 * 1000));
log.info("删除视频文件完成");
}
}
}

View File

@@ -7,6 +7,7 @@ import com.ycwl.basic.constant.StorageConstant;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
@@ -15,6 +16,8 @@ import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
@@ -76,6 +79,8 @@ public class VideoPieceGetter {
private VideoReUploader videoReUploader;
@Autowired
private ScenicRepository scenicRepository;
@Autowired
private DeviceIntegrationService deviceIntegrationService;
@Data
public static class Task {
@@ -144,12 +149,13 @@ public class VideoPieceGetter {
Map<Long, Long> pairDeviceMap = new ConcurrentHashMap<>();
if (!list.isEmpty()) {
Long scenicId = list.getFirst().getScenicId();
List<DeviceEntity> allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId);
List<DeviceV2DTO> allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId);
allDeviceByScenicId.forEach(device -> {
Long deviceId = device.getId();
DeviceConfigEntity deviceConfig = deviceRepository.getDeviceConfig(deviceId);
if (deviceConfig != null && deviceConfig.getPairDevice() != null) {
pairDeviceMap.putIfAbsent(deviceId, deviceConfig.getPairDevice());
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
Long pairDevice = deviceConfig.getLong("pair_device");
if (pairDevice != null) {
pairDeviceMap.putIfAbsent(deviceId, pairDevice);
}
});
}
@@ -254,24 +260,21 @@ public class VideoPieceGetter {
}
private boolean doCut(Long deviceId, Long faceSampleId, Date baseTime, Task task) {
DeviceEntity device = deviceRepository.getDevice(deviceId);
DeviceConfigEntity config = deviceRepository.getDeviceConfig(deviceId);
// 通过zt-device服务获取设备信息
DeviceV2DTO deviceV2 = deviceIntegrationService.getDevice(deviceId);
if (deviceV2 == null) {
log.warn("设备不存在,设备ID: {}", deviceId);
return false;
}
DeviceConfigManager config = deviceRepository.getDeviceConfigManager(deviceId);
DeviceConfigEntity dConfig = deviceRepository.getDeviceConfig(deviceId);
SourceEntity source = sourceMapper.querySameVideo(faceSampleId, device.getId());
SourceEntity source = sourceMapper.querySameVideo(faceSampleId, deviceV2.getId());
if (source == null || task.force) {
BigDecimal cutPre = BigDecimal.valueOf(5L);
BigDecimal cutPost = BigDecimal.valueOf(4L);
if (config != null) {
// 有配置
if (config.getCutPre() != null) {
cutPre = config.getCutPre();
}
if (config.getCutPost() != null) {
cutPost = config.getCutPost();
}
}
IDeviceStorageOperator pieceGetter = DeviceFactory.getDeviceStorageOperator(device, config);
BigDecimal cutPre = config.getBigDecimal("cut_pre", BigDecimal.valueOf(5L));
BigDecimal cutPost = config.getBigDecimal("cut_post", BigDecimal.valueOf(5L));
IDeviceStorageOperator pieceGetter = DeviceFactory.getDeviceStorageOperator(deviceV2, dConfig);
if (pieceGetter == null) {
return false;
}
@@ -317,12 +320,12 @@ public class VideoPieceGetter {
sourceEntity.setUrl(imgSource.getUrl());
sourceEntity.setPosJson(imgSource.getPosJson());
}
if (StringUtils.isNotBlank(config.getVideoCrop())) {
sourceEntity.setPosJson(config.getVideoCrop());
if (StringUtils.isNotBlank(config.getString("video_crop"))) {
sourceEntity.setPosJson(config.getString("video_crop"));
}
sourceEntity.setVideoUrl(url);
sourceEntity.setFaceSampleId(faceSampleId);
sourceEntity.setScenicId(device.getScenicId());
sourceEntity.setScenicId(deviceV2.getScenicId());
sourceEntity.setDeviceId(deviceId);
sourceEntity.setType(1);
if (task.memberId != null && task.faceId != null) {
@@ -330,9 +333,9 @@ public class VideoPieceGetter {
videoSource.setMemberId(task.getMemberId());
videoSource.setType(1);
videoSource.setFaceId(task.getFaceId());
videoSource.setScenicId(device.getScenicId());
videoSource.setScenicId(deviceV2.getScenicId());
videoSource.setSourceId(sourceEntity.getId());
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), device.getScenicId(), 1, task.getFaceId());
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId());
if (isBuy.isBuy()) { // 如果用户买过
videoSource.setIsBuy(1);
} else if (isBuy.isFree()) { // 全免费逻辑
@@ -346,8 +349,8 @@ public class VideoPieceGetter {
videoReUploader.addTask(sourceEntity.getId());
} else {
source.setVideoUrl(url);
if (StringUtils.isNotBlank(config.getVideoCrop())) {
source.setPosJson(config.getVideoCrop());
if (StringUtils.isNotBlank(config.getString("video_crop"))) {
source.setPosJson(config.getString("video_crop"));
}
sourceMapper.update(source);
videoReUploader.addTask(source.getId());
@@ -358,10 +361,10 @@ public class VideoPieceGetter {
int count = sourceMapper.hasRelationTo(task.getMemberId(), source.getId(), 1);
if (count <= 0) {
// 没有关联
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), device.getScenicId(), 1, task.getFaceId());
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId());
MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setId(SnowFlakeUtil.getLongId());
videoSource.setScenicId(device.getScenicId());
videoSource.setScenicId(deviceV2.getScenicId());
videoSource.setFaceId(task.getFaceId());
videoSource.setMemberId(task.getMemberId());
videoSource.setType(1);

View File

@@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.DeviceMapper">
<insert id="add">
insert into device(id, scenic_id, name, no, latitude, longitude) values (#{id}, #{scenicId}, #{name}, #{no}, #{latitude}, #{longitude})
</insert>
<insert id="addConfig">
insert into device_config(id, device_id, create_time)
values (#{id}, #{deviceId}, now())
</insert>
<insert id="addEntity">
insert into device(id, name, no, status, online, ip_addr, keepalive_at, create_at, update_at)
values (#{id}, #{name}, #{no}, 0, #{online}, #{ipAddr}, #{keepaliveAt}, now(), now())
</insert>
<update id="update">
update device set scenic_id = #{scenicId}, name = #{name}, no = #{no}, longitude = #{longitude}, latitude = #{latitude}, update_at = now() where id = #{id}
</update>
<update id="updateStatus">
update device
set status = (CASE
status
WHEN 1 THEN
0
WHEN 0 THEN
1
ELSE null
END)
where id = #{id}
</update>
<update id="updateConfig">
update device_config
set viid_type = #{viidType},
store_type = #{storeType},
store_config_json = #{storeConfigJson},
store_expire_day = #{storeExpireDay},
online_check = #{onlineCheck},
online_max_interval = #{onlineMaxInterval},
cut_pre = #{cutPre},
cut_post = #{cutPost},
enable_pre_book = #{enablePreBook},
image_free = #{imageFree},
video_free = #{videoFree},
pair_device = #{pairDevice},
video_crop = #{videoCrop}
where id = #{id}
</update>
<update id="updateEntity">
update device
set name = #{name},
no = #{no},
online = #{online},
ip_addr = #{ipAddr},
keepalive_at = #{keepaliveAt},
update_at = now()
where id = #{id}
</update>
<update id="updateOnlineStatus">
update device
set online = #{online},
<if test="ipAddr != null">ip_addr = #{ipAddr},</if>
keepalive_at = #{keepaliveAt},
update_at = now()
where id = #{id}
</update>
<update id="updateSort">
update device
set sort = #{sort}
where id = #{id}
</update>
<delete id="deleteById">
delete from device where id = #{id}
</delete>
<select id="list" resultType="com.ycwl.basic.model.pc.device.resp.DeviceRespVO">
select d.id, scenic_id, d.name, no, d.longitude, d.latitude, d.status, create_at, d.update_at, s.name scenic_name, d.keepalive_at, d.online, p.wvp_device_no as device_no, p.wvp_channel_no channel_no,
(select s.url from source s where s.device_id=d.id order by id desc limit 1) coverUrl
from device d
left join scenic s on d.scenic_id = s.id
left join device_preview_config p on d.id = p.device_id and p.status = 1
<where>
<if test="name!= null and name!= ''">
and d.`name` like concat('%', #{name}, '%')
</if>
<if test="no!= null and no!= ''">
and `no` like concat('%', #{no}, '%')
</if>
<if test="status!= null">
and d.`status` = #{status}
</if>
<if test="scenicId!= null and scenicId!= ''">
and scenic_id = #{scenicId}
</if>
<if test="startTime!=null">
and d.create_at >= #{startTime}
</if>
<if test="endTime!=null">
and d.create_at &lt;= #{endTime}
</if>
</where>
order by d.scenic_id desc, sort asc
</select>
<select id="getById" resultType="com.ycwl.basic.model.pc.device.resp.DeviceRespVO">
select d.id, scenic_id, d.name, no, d.longitude, d.latitude, d.status, create_at, d.update_at, s.name scenic_name
from device d
left join scenic s on d.scenic_id = s.id
where d.id = #{id}
</select>
<select id="listByScenicIdWithWVP" resultType="com.ycwl.basic.model.pc.device.resp.DeviceRespVO">
select d.id, d.name, no, d.status, create_at, d.update_at, p.wvp_device_no as device_no, p.wvp_channel_no channel_no
from device d
left join device_preview_config p on d.id = p.device_id and p.status = 1
where d.scenic_id = #{scenicId}
order by d.sort
</select>
<select id="deviceCountByScenicId" resultType="com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO">
select count(1) totalDeviceCount
from device
where scenic_id = #{scenicId} and status = 1
</select>
<select id="getConfigByDeviceId" resultType="com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity">
select *
from device_config
where device_id = #{deviceId}
limit 1
</select>
<select id="getByDeviceId" resultType="com.ycwl.basic.model.pc.device.entity.DeviceEntity">
select *
from device
where id = #{deviceId}
</select>
<select id="getByDeviceNo" resultType="com.ycwl.basic.model.pc.device.entity.DeviceEntity">
select *
from device
where no = #{deviceNo}
</select>
<select id="listAll" resultType="com.ycwl.basic.model.pc.device.entity.DeviceEntity">
select *
from device
where status = 1
order by sort
</select>
<select id="getByDeviceNo2" resultType="com.ycwl.basic.model.pc.device.entity.DeviceEntity">
select *
from device
where no_2 = #{deviceNo}
limit 1
</select>
<select id="listAllByScenicId" resultType="java.lang.Long">
select id
from device
where scenic_id = #{scenicId}
order by sort
</select>
</mapper>