Compare commits

...

8 Commits

Author SHA1 Message Date
3c700a42f9 feat(device): 添加设备在线状态查询功能- 在DeviceV2Controller中新增getDeviceOnlineStatus接口,用于根据设备ID查询设备在线状态
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 引入DeviceStatusDTO和DeviceStatusIntegrationService以支持设备状态查询- 修改DeviceStatusDTO中的时间字段类型为Date,并调整JSON序列化格式- 在DeviceRepository中增加convertToEntityWithStatus方法,用于合并设备信息与状态信息
- 优化DeviceRepository中的getOnlineStatus方法,增加异常处理和降级机制- 完善设备在线状态查询的日志记录和错误处理逻辑
2025-09-25 15:32:09 +08:00
47c6b2ca67 feat(device): 新增设备状态管理集成服务
- 添加设备状态客户端接口,支持设备在线状态查询与设置
- 创建设备状态相关 DTO,包括设备状态、在线状态和状态动作枚举
- 实现设备状态集成服务,封装设备状态操作与异常处理逻辑
- 支持单个及批量设备在线状态检查与设置功能
- 提供
2025-09-25 14:18:06 +08:00
59baf8811b feat(pricing): 添加商品一口价优惠支持检查
- 在 PriceProductConfig 实体中新增 canUseOnePrice 字段
- 更新数据库插入和更新语句,支持 canUseOnePrice 字段持久化- 在 OnePricePurchaseDiscountProvider 中实现商品一口价优惠支持检查逻辑
- 新增 areAllProductsSupportOnePrice 方法,验证购物车商品是否支持一口价优惠
- 支持查询具体商品配置和默认配置的一口价优惠设置
- 添加日志记录和异常处理,确保检查过程不影响主流程
2025-09-25 10:40:10 +08:00
019b9ffca6 refactor(video):优化视频关联关系处理逻辑
- 调整source记录插入时机,确保关联关系处理前数据已存在
- 移除冗余的source存在性检查逻辑- 统一关联关系处理流程,避免重复代码
- 添加日志记录以便追踪处理过程- 优化代码结构,提高可读性和维护性
2025-09-24 18:04:47 +08:00
30805f3e30 refactor(mapper):优化查询逻辑并处理空列表情况
- 将 filterExistingRelations 查询中的 if 判断替换为 choose-when 结构
- 在 otherwise 分支中添加空结果集查询,避免空列表时 SQL 异常- 统一 filterValidSourceRelations 查询结构,增强代码一致性
-修正 foreach 标签中 UNION ALL 前后的空格问题,确保 SQL 语法正确- 提升 XML 映射文件的可读性和健壮性
2025-09-24 17:50:53 +08:00
94d6b2f443 feat(source): 增强source关联关系的数据一致性校验
- 在SourceMapper中新增sourceExists方法,用于校验source是否存在
- 新增filterValidSourceRelations方法,过滤无效的source引用
- 在FaceServiceImpl中增强关联关系创建逻辑,防止重复和无效数据
- 在VideoPieceGetter任务中增加source存在性校验,避免创建孤立关联- 添加详细的日志记录,便于追踪关联关系创建过程
-优化XML映射文件,支持新的校验和过滤查询逻辑
2025-09-24 17:39:05 +08:00
b34f994298 feat(source): 添加过滤已存在关联关系功能
- 在SourceMapper中新增filterExistingRelations方法
- 修改FaceServiceImpl中的关联关系保存逻辑
- 修改TaskFaceServiceImpl中的关联关系保存逻辑
- 修改VideoPieceGetter中的关联关系检查逻辑
- 在SourceMapper.xml中添加filterExistingRelations的SQL实现
2025-09-24 17:16:12 +08:00
7728f4424f status 2025-09-24 13:45:48 +08:00
18 changed files with 713 additions and 43 deletions

View File

@@ -3,8 +3,10 @@ package com.ycwl.basic.controller.pc;
import com.ycwl.basic.integration.device.dto.config.*;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.*;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@@ -29,6 +31,7 @@ public class DeviceV2Controller {
private final DeviceIntegrationService deviceIntegrationService;
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
// ========== 设备基础 CRUD 操作 ==========
@@ -144,6 +147,28 @@ public class DeviceV2Controller {
}
}
/**
* 根据设备ID获取设备在线状态
*/
@GetMapping("/{id}/status")
public ApiResponse<DeviceStatusDTO> getDeviceOnlineStatus(@PathVariable Long id) {
log.info("获取设备在线状态, deviceId: {}", id);
try {
// 首先获取设备信息以获得设备编号
DeviceV2DTO device = deviceIntegrationService.getDevice(id);
if (device == null) {
return ApiResponse.fail("设备不存在");
}
// 使用设备编号查询在线状态
DeviceStatusDTO onlineStatus = deviceStatusIntegrationService.getDeviceStatus(device.getNo());
return ApiResponse.success(onlineStatus);
} catch (Exception e) {
log.error("获取设备在线状态失败, deviceId: {}", id, e);
return ApiResponse.fail("获取设备在线状态失败: " + e.getMessage());
}
}
/**
* 创建设备
*/

View File

@@ -0,0 +1,51 @@
package com.ycwl.basic.integration.device.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@FeignClient(name = "zt-device", contextId = "deviceStatusClient", path = "/api/device/status")
public interface DeviceStatusClient {
/**
* 获取设备状态
*/
@GetMapping("/{deviceNo}")
CommonResponse<DeviceStatusDTO> getDeviceStatus(@PathVariable("deviceNo") String deviceNo);
/**
* 检查设备是否在线
*/
@GetMapping("/{deviceNo}/online")
CommonResponse<OnlineStatusResponseDTO> isDeviceOnline(@PathVariable("deviceNo") String deviceNo);
/**
* 获取所有在线设备
*/
@GetMapping("/online")
CommonResponse<List<DeviceStatusDTO>> getAllOnlineDevices();
/**
* 设置设备离线
*/
@PostMapping("/{deviceNo}/offline")
CommonResponse<String> setDeviceOffline(@PathVariable("deviceNo") String deviceNo);
/**
* 设置设备在线
*/
@PostMapping("/{deviceNo}/online")
CommonResponse<String> setDeviceOnline(@PathVariable("deviceNo") String deviceNo);
/**
* 清理过期设备状态
*/
@PostMapping("/clean")
CommonResponse<String> cleanExpiredDevices();
}

View File

@@ -0,0 +1,41 @@
package com.ycwl.basic.integration.device.dto.status;
/**
* 设备状态动作枚举
*/
public enum DeviceStatusActionEnum {
/**
* 设备注册
*/
REGISTER("register"),
/**
* 设备保活
*/
KEEPALIVE("keepalive"),
/**
* 设备注销
*/
UNREGISTER("unregister");
private final String action;
DeviceStatusActionEnum(String action) {
this.action = action;
}
public String getAction() {
return action;
}
public static DeviceStatusActionEnum fromString(String action) {
for (DeviceStatusActionEnum statusAction : values()) {
if (statusAction.action.equalsIgnoreCase(action)) {
return statusAction;
}
}
throw new IllegalArgumentException("Unknown device status action: " + action);
}
}

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.integration.device.dto.status;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 设备状态信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeviceStatusDTO {
/**
* 设备编号
*/
private String deviceNo;
/**
* 是否在线
*/
private Boolean isOnline;
/**
* 最后活动时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastActiveTime;
/**
* 最后动作(register/keepalive/unregister)
*/
private String lastAction;
/**
* 客户端IP
*/
private String clientIP;
/**
* 状态更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}

View File

@@ -0,0 +1,24 @@
package com.ycwl.basic.integration.device.dto.status;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 在线状态响应
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OnlineStatusResponseDTO {
/**
* 设备编号
*/
private String deviceNo;
/**
* 是否在线
*/
private Boolean isOnline;
}

View File

@@ -0,0 +1,211 @@
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.DeviceStatusClient;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.device.dto.status.OnlineStatusResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceStatusIntegrationService {
private final DeviceStatusClient deviceStatusClient;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-device";
public DeviceStatusDTO getDeviceStatus(String deviceNo) {
log.debug("获取设备状态, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:status:" + deviceNo,
() -> {
CommonResponse<DeviceStatusDTO> response = deviceStatusClient.getDeviceStatus(deviceNo);
return handleResponse(response, "获取设备状态失败");
},
DeviceStatusDTO.class
);
}
public OnlineStatusResponseDTO isDeviceOnline(String deviceNo) {
log.debug("检查设备是否在线, deviceNo: {}", deviceNo);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"device:online:" + deviceNo,
() -> {
CommonResponse<OnlineStatusResponseDTO> response = deviceStatusClient.isDeviceOnline(deviceNo);
return handleResponse(response, "检查设备在线状态失败");
},
OnlineStatusResponseDTO.class
);
}
public List<DeviceStatusDTO> getAllOnlineDevices() {
log.debug("获取所有在线设备");
return fallbackService.executeWithFallback(
SERVICE_NAME,
"devices:online:all",
() -> {
CommonResponse<List<DeviceStatusDTO>> response = deviceStatusClient.getAllOnlineDevices();
return handleResponse(response, "获取所有在线设备失败");
},
List.class
);
}
public void setDeviceOffline(String deviceNo) {
log.debug("设置设备离线, deviceNo: {}", deviceNo);
CommonResponse<String> response = deviceStatusClient.setDeviceOffline(deviceNo);
handleResponse(response, "设置设备离线失败");
}
public void setDeviceOnline(String deviceNo) {
log.debug("设置设备在线, deviceNo: {}", deviceNo);
CommonResponse<String> response = deviceStatusClient.setDeviceOnline(deviceNo);
handleResponse(response, "设置设备在线失败");
}
public void cleanExpiredDevices() {
log.debug("清理过期设备状态");
CommonResponse<String> response = deviceStatusClient.cleanExpiredDevices();
handleResponse(response, "清理过期设备状态失败");
}
/**
* 安全地获取设备状态
*/
public Optional<DeviceStatusDTO> getDeviceStatusSafely(String deviceNo) {
try {
DeviceStatusDTO status = getDeviceStatus(deviceNo);
return Optional.ofNullable(status);
} catch (Exception e) {
log.warn("获取设备状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
return Optional.empty();
}
}
/**
* 安全地检查设备是否在线
*/
public boolean isDeviceOnlineSafely(String deviceNo) {
try {
OnlineStatusResponseDTO response = isDeviceOnline(deviceNo);
return response != null && Boolean.TRUE.equals(response.getIsOnline());
} catch (Exception e) {
log.warn("检查设备在线状态异常: deviceNo={}, error={}", deviceNo, e.getMessage());
return false;
}
}
/**
* 批量检查设备是否在线
*/
public boolean areAllDevicesOnline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return true;
}
log.debug("批量检查设备在线状态, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
if (!isDeviceOnlineSafely(deviceNo)) {
return false;
}
}
return true;
}
/**
* 批量设置设备离线
*/
public void setDevicesOffline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return;
}
log.debug("批量设置设备离线, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
try {
setDeviceOffline(deviceNo);
} catch (Exception e) {
log.error("设置设备离线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
}
}
}
/**
* 批量设置设备在线
*/
public void setDevicesOnline(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return;
}
log.debug("批量设置设备在线, deviceNos: {}", deviceNos);
for (String deviceNo : deviceNos) {
try {
setDeviceOnline(deviceNo);
} catch (Exception e) {
log.error("设置设备在线失败: deviceNo={}, error={}", deviceNo, e.getMessage());
}
}
}
/**
* 获取在线设备数量
*/
public int getOnlineDeviceCount() {
try {
List<DeviceStatusDTO> onlineDevices = getAllOnlineDevices();
return onlineDevices != null ? onlineDevices.size() : 0;
} catch (Exception e) {
log.warn("获取在线设备数量失败: {}", e.getMessage());
return 0;
}
}
/**
* 获取指定设备编号列表中的在线设备
*/
public List<String> getOnlineDeviceNos(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return List.of();
}
return deviceNos.stream()
.filter(this::isDeviceOnlineSafely)
.toList();
}
/**
* 获取指定设备编号列表中的离线设备
*/
public List<String> getOfflineDeviceNos(List<String> deviceNos) {
if (deviceNos == null || deviceNos.isEmpty()) {
return List.of();
}
return deviceNos.stream()
.filter(deviceNo -> !isDeviceOnlineSafely(deviceNo))
.toList();
}
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();
}
}

View File

@@ -12,6 +12,7 @@ import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.task.DynamicTaskGenerator;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
// 不再需要SnowFlakeUtil,使用外部传入的ID
import com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -33,7 +34,6 @@ public class FaceProcessingKafkaService {
private static final String ZT_FACE_TOPIC = "zt-face";
private final ObjectMapper objectMapper;
private final FaceSampleMapper faceSampleMapper;
private final TaskFaceService taskFaceService;
private final ScenicService scenicService;
@@ -43,10 +43,10 @@ public class FaceProcessingKafkaService {
* 消费外部系统发送的人脸处理消息
* 先保存人脸样本数据,再进行异步人脸识别处理
*/
@KafkaListener(topics = ZT_FACE_TOPIC, groupId = "face-processing-group")
@KafkaListener(topics = ZT_FACE_TOPIC)
public void processFaceMessage(String message) {
try {
FaceProcessingMessage faceMessage = objectMapper.readValue(message, FaceProcessingMessage.class);
FaceProcessingMessage faceMessage = JacksonUtil.parseObject(message, FaceProcessingMessage.class);
log.info("接收到外部人脸处理消息, scenicId: {}, deviceId: {}, faceUrl: {}",
faceMessage.getScenicId(), faceMessage.getDeviceId(), faceMessage.getFaceUrl());
@@ -145,7 +145,7 @@ public class FaceProcessingKafkaService {
if (addFaceResp != null) {
// 更新人脸样本得分和状态
faceSampleMapper.updateScore(faceSampleId, addFaceResp.getScore());
updateFaceSampleStatus(faceSampleId, 2); // 成功状态
updateFaceSampleStatus(faceSampleId, 2);
log.info("人脸识别处理完成, faceSampleId: {}, score: {}",
faceSampleId, addFaceResp.getScore());
@@ -176,8 +176,7 @@ public class FaceProcessingKafkaService {
*/
private void updateFaceSampleStatus(Long faceSampleId, Integer status) {
try {
// TODO: 需要在FaceSampleMapper中添加updateStatus方法
// faceSampleMapper.updateStatus(faceSampleId, status);
faceSampleMapper.updateStatus(faceSampleId, status);
log.info("人脸样本状态已更新, faceSampleId: {}, status: {} (0:初始,1:处理中,2:成功,-1:失败)", faceSampleId, status);
} catch (Exception e) {
log.error("更新人脸样本状态失败, faceSampleId: {}", faceSampleId, e);

View File

@@ -29,4 +29,6 @@ public interface FaceSampleMapper {
List<FaceSampleEntity> listEntityBeforeDate(Long scenicId, Date endDate);
void updateScore(Long id, Float score);
void updateStatus(Long id, Integer status);
}

View File

@@ -56,6 +56,12 @@ public interface SourceMapper {
int addRelations(List<MemberSourceEntity> list);
List<MemberSourceEntity> filterExistingRelations(List<MemberSourceEntity> list);
boolean sourceExists(Long sourceId);
List<MemberSourceEntity> filterValidSourceRelations(List<MemberSourceEntity> list);
int updateRelation(MemberSourceEntity memberSourceEntity);
int freeRelations(List<Long> ids, int type);

View File

@@ -68,7 +68,12 @@ public class PriceProductConfig {
* 是否可使用券码
*/
private Boolean canUseVoucher;
/**
* 是否可使用一口价优惠
*/
private Boolean canUseOnePrice;
@TableField("create_time")
private Date createTime;

View File

@@ -57,15 +57,15 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
/**
* 插入商品价格配置
*/
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, create_time, update_time) " +
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, NOW(), NOW())")
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, can_use_one_price, create_time, update_time) " +
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, #{canUseOnePrice}, NOW(), NOW())")
int insertProductConfig(PriceProductConfig config);
/**
* 更新商品价格配置
*/
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, update_time = NOW() WHERE id = #{id}")
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, can_use_one_price = #{canUseOnePrice}, update_time = NOW() WHERE id = #{id}")
int updateProductConfig(PriceProductConfig config);
/**

View File

@@ -4,8 +4,11 @@ import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.DiscountInfo;
import com.ycwl.basic.pricing.dto.DiscountResult;
import com.ycwl.basic.pricing.dto.OnePriceInfo;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.entity.PriceProductConfig;
import com.ycwl.basic.pricing.service.IDiscountProvider;
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
import com.ycwl.basic.pricing.service.IProductConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -21,8 +24,9 @@ import java.util.List;
@Component
@RequiredArgsConstructor
public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
private final IOnePricePurchaseService onePricePurchaseService;
private final IProductConfigService productConfigService;
@Override
public String getProviderType() {
@@ -49,6 +53,12 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
log.debug("景区 {} 不适用一口价", context.getScenicId());
return discounts;
}
// 检查商品是否支持一口价优惠
if (!areAllProductsSupportOnePrice(context.getProducts())) {
log.debug("存在不支持一口价优惠的商品,跳过一口价检测");
return discounts;
}
// 获取一口价信息
OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo(
@@ -170,6 +180,54 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
return BigDecimal.ZERO;
}
/**
* 检查购物车中的所有商品是否都支持一口价优惠
*/
private boolean areAllProductsSupportOnePrice(List<ProductItem> products) {
if (products == null || products.isEmpty()) {
return true; // 空购物车时默认支持
}
for (ProductItem product : products) {
try {
// 查询商品配置
PriceProductConfig productConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), product.getProductId());
if (productConfig != null) {
// 检查商品是否支持一口价优惠
if (Boolean.FALSE.equals(productConfig.getCanUseOnePrice())) {
log.debug("商品 {}({}) 不支持一口价优惠",
product.getProductType().getCode(), product.getProductId());
return false;
}
} else {
// 如果找不到具体商品配置,尝试查询 default 配置
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), "default");
if (defaultConfig != null) {
if (Boolean.FALSE.equals(defaultConfig.getCanUseOnePrice())) {
log.debug("商品类型 {} 的默认配置不支持一口价优惠",
product.getProductType().getCode());
return false;
}
} else {
// 如果既没有具体配置也没有默认配置,默认支持一口价优惠
log.debug("商品 {}({}) 未找到价格配置,默认支持一口价优惠",
product.getProductType().getCode(), product.getProductId());
}
}
} catch (Exception e) {
log.warn("检查商品 {}({}) 一口价优惠支持情况时发生异常,默认支持",
product.getProductType().getCode(), product.getProductId(), e);
// 异常情况下默认支持,避免影响正常业务流程
}
}
return true;
}
/**
* 检查优惠叠加规则
*/

View File

@@ -7,7 +7,9 @@ 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.service.DeviceStatusIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
@@ -33,6 +35,8 @@ public class DeviceRepository {
public static final String DEVICE_CACHE_KEY = "device:%s";
@Autowired
private DeviceConfigIntegrationService deviceConfigIntegrationService;
@Autowired
private DeviceStatusIntegrationService deviceStatusIntegrationService;
/**
* 将DeviceV2DTO转换为DeviceEntity
@@ -57,6 +61,30 @@ public class DeviceRepository {
return entity;
}
/**
* 将DeviceV2DTO和DeviceStatusDTO合并转换为DeviceEntity
*/
private DeviceEntity convertToEntityWithStatus(DeviceV2DTO deviceDto, DeviceStatusDTO statusDto) {
if (deviceDto == null) {
return null;
}
DeviceEntity entity = convertToEntity(deviceDto);
// 合并状态信息
if (statusDto != null) {
// Boolean转Integer: true→1, false→0
entity.setOnline(statusDto.getIsOnline() != null && statusDto.getIsOnline() ? 1 : 0);
entity.setKeepaliveAt(statusDto.getLastActiveTime());
entity.setIpAddr(statusDto.getClientIP());
} else {
// 默认离线状态
entity.setOnline(0);
}
return entity;
}
public DeviceEntity getDevice(Long deviceId) {
log.debug("获取设备信息, deviceId: {}", deviceId);
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
@@ -144,10 +172,31 @@ public class DeviceRepository {
}
public DeviceEntity getOnlineStatus(Long deviceId) {
if (redisTemplate.hasKey(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId))) {
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(DEVICE_ONLINE_CACHE_KEY, deviceId)), DeviceEntity.class);
} else {
return null;
log.debug("获取设备在线状态, deviceId: {}", deviceId);
try {
// 首先获取设备基本信息
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
if (deviceDto == null) {
log.warn("设备不存在, deviceId: {}", deviceId);
return null;
}
// 通过设备编号获取设备状态
DeviceStatusDTO statusDto = deviceStatusIntegrationService.getDeviceStatus(deviceDto.getNo());
// 合并设备信息和状态信息
return convertToEntityWithStatus(deviceDto, statusDto);
} catch (Exception e) {
log.error("获取设备在线状态异常, deviceId: {}", deviceId, e);
// 降级处理:尝试仅返回设备基本信息
try {
DeviceV2DTO deviceDto = deviceIntegrationService.getDevice(deviceId);
return convertToEntityWithStatus(deviceDto, null);
} catch (Exception fallbackException) {
log.error("降级获取设备信息也失败, deviceId: {}", deviceId, fallbackException);
return null;
}
}
}

View File

@@ -343,8 +343,16 @@ public class FaceServiceImpl implements FaceService {
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
face.getMemberId(), sampleListIds, isNew);
// 保存关联关系并创建任务
sourceMapper.addRelations(memberSourceEntityList);
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId);
taskTaskService.autoCreateTaskByFaceId(faceId);
@@ -1115,7 +1123,16 @@ public class FaceServiceImpl implements FaceService {
handleVideoRecreation(scenicConfig, memberSourceEntityList, faceId,
face.getMemberId(), sampleListIds, false);
sourceMapper.addRelations(memberSourceEntityList);
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId);
taskTaskService.autoCreateTaskByFaceId(faceId);

View File

@@ -153,7 +153,16 @@ public class TaskFaceServiceImpl implements TaskFaceService {
memberSourceEntity.setIsBuy(0);
}
}
sourceMapper.addRelations(memberSourceEntityList);
// 过滤已存在的关联关系和无效的source引用,防止数据不一致
List<MemberSourceEntity> existingFiltered = sourceMapper.filterExistingRelations(memberSourceEntityList);
List<MemberSourceEntity> validFiltered = sourceMapper.filterValidSourceRelations(existingFiltered);
if (!validFiltered.isEmpty()) {
sourceMapper.addRelations(validFiltered);
log.debug("创建关联关系: faceId={}, 原始数量={}, 过滤后数量={}",
faceId, memberSourceEntityList.size(), validFiltered.size());
} else {
log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size());
}
memberRelationRepository.clearSCacheByFace(faceId);
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
task.faceId = faceEntity.getId();

View File

@@ -333,7 +333,14 @@ public class VideoPieceGetter {
sourceEntity.setScenicId(deviceV2.getScenicId());
sourceEntity.setDeviceId(deviceId);
sourceEntity.setType(1);
// 先插入source记录
sourceMapper.add(sourceEntity);
videoReUploader.addTask(sourceEntity.getId());
// 然后处理关联关系
if (task.memberId != null && task.faceId != null) {
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setMemberId(task.getMemberId());
videoSource.setType(1);
@@ -348,11 +355,17 @@ public class VideoPieceGetter {
} else {
videoSource.setIsBuy(0);
}
sourceMapper.addRelation(videoSource);
boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
&& memberSourceEntity.getType().equals(videoSource.getType())
&& memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
});
if (!anyMatch) {
// source已插入,可以直接添加关联关系
sourceMapper.addRelation(videoSource);
}
memberRelationRepository.clearSCacheByFace(task.faceId);
}
sourceMapper.add(sourceEntity);
videoReUploader.addTask(sourceEntity.getId());
} else {
source.setVideoUrl(url);
if (StringUtils.isNotBlank(config.getString("video_crop"))) {
@@ -362,29 +375,35 @@ public class VideoPieceGetter {
videoReUploader.addTask(source.getId());
}
} else {
// 有原视频
// 有原视频,source已存在,可以直接添加关联关系
if (task.memberId != null && task.faceId != null) {
int count = sourceMapper.hasRelationTo(task.getMemberId(), source.getId(), 1);
if (count <= 0) {
// 没有关联
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId());
MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setId(SnowFlakeUtil.getLongId());
videoSource.setScenicId(deviceV2.getScenicId());
videoSource.setFaceId(task.getFaceId());
videoSource.setMemberId(task.getMemberId());
videoSource.setType(1);
if (isBuy.isBuy()) { // 如果用户买过
videoSource.setIsBuy(1);
} else if (isBuy.isFree()) { // 全免费逻辑
videoSource.setIsBuy(1);
} else {
videoSource.setIsBuy(0);
}
videoSource.setSourceId(source.getId());
sourceMapper.addRelation(videoSource);
memberRelationRepository.clearSCacheByFace(task.faceId);
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
IsBuyRespVO isBuy = orderBiz.isBuy(task.getMemberId(), deviceV2.getScenicId(), 1, task.getFaceId());
MemberSourceEntity videoSource = new MemberSourceEntity();
videoSource.setId(SnowFlakeUtil.getLongId());
videoSource.setScenicId(deviceV2.getScenicId());
videoSource.setFaceId(task.getFaceId());
videoSource.setMemberId(task.getMemberId());
videoSource.setType(1);
if (isBuy.isBuy()) { // 如果用户买过
videoSource.setIsBuy(1);
} else if (isBuy.isFree()) { // 全免费逻辑
videoSource.setIsBuy(1);
} else {
videoSource.setIsBuy(0);
}
videoSource.setSourceId(source.getId());
// 没有关联
boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
&& memberSourceEntity.getType().equals(videoSource.getType())
&& memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
});
if (!anyMatch) {
// source已存在,可以直接添加关联关系
sourceMapper.addRelation(videoSource);
}
memberRelationRepository.clearSCacheByFace(task.faceId);
}
}
return true;

View File

@@ -41,6 +41,11 @@
set score = #{score}
where id = #{id}
</update>
<update id="updateStatus">
update face_sample
set `status` = #{status}
where id = #{id}
</update>
<delete id="deleteById">
delete from face_sample where id = #{id}
</delete>

View File

@@ -16,6 +16,105 @@
(#{item.scenicId}, #{item.faceId}, #{item.memberId}, #{item.sourceId}, #{item.isBuy}, #{item.type}, #{item.orderId}, #{item.isFree})
</foreach>
</insert>
<select id="filterExistingRelations" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
<choose>
<when test="list != null and list.size() > 0">
SELECT
r.memberId as memberId,
r.sourceId as sourceId,
r.type as type,
r.faceId as faceId,
r.scenicId as scenicId,
r.isBuy as isBuy,
r.orderId as orderId,
r.isFree as isFree,
r.id as id
FROM (
<foreach collection="list" item="item" separator=" UNION ALL ">
SELECT
#{item.memberId} as memberId,
#{item.sourceId} as sourceId,
#{item.type} as type,
#{item.faceId} as faceId,
#{item.scenicId} as scenicId,
#{item.isBuy} as isBuy,
#{item.orderId} as orderId,
#{item.isFree} as isFree,
#{item.id} as id
</foreach>
) r
WHERE NOT EXISTS (
SELECT 1 FROM member_source ms
WHERE ms.member_id = r.memberId
AND ms.source_id = r.sourceId
AND ms.type = r.type
AND ms.face_id = r.faceId
)
</when>
<otherwise>
SELECT
NULL as memberId,
NULL as sourceId,
NULL as type,
NULL as faceId,
NULL as scenicId,
NULL as isBuy,
NULL as orderId,
NULL as isFree,
NULL as id
WHERE 1 = 0
</otherwise>
</choose>
</select>
<select id="sourceExists" resultType="boolean">
SELECT COUNT(1) > 0 FROM source WHERE id = #{sourceId}
</select>
<select id="filterValidSourceRelations" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
<choose>
<when test="list != null and list.size() > 0">
SELECT
r.memberId as memberId,
r.sourceId as sourceId,
r.type as type,
r.faceId as faceId,
r.scenicId as scenicId,
r.isBuy as isBuy,
r.orderId as orderId,
r.isFree as isFree,
r.id as id
FROM (
<foreach collection="list" item="item" separator=" UNION ALL ">
SELECT
#{item.memberId} as memberId,
#{item.sourceId} as sourceId,
#{item.type} as type,
#{item.faceId} as faceId,
#{item.scenicId} as scenicId,
#{item.isBuy} as isBuy,
#{item.orderId} as orderId,
#{item.isFree} as isFree,
#{item.id} as id
</foreach>
) r
WHERE EXISTS (
SELECT 1 FROM source s WHERE s.id = r.sourceId
)
</when>
<otherwise>
SELECT
NULL as memberId,
NULL as sourceId,
NULL as type,
NULL as faceId,
NULL as scenicId,
NULL as isBuy,
NULL as orderId,
NULL as isFree,
NULL as id
WHERE 1 = 0
</otherwise>
</choose>
</select>
<insert id="addSourceWatermark">
insert source_watermark(source_id, face_id, watermark_type, watermark_url)
values (#{sourceId}, #{faceId}, #{type}, #{url})