You've already forked FrameTour-BE
Compare commits
58 Commits
5cc32ddf61
...
2a3b4ca19f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a3b4ca19f | |||
| 85599aa84a | |||
| fbd4cfa83c | |||
| 4596a61ba8 | |||
| d6780ccb7a | |||
| 58e8189b13 | |||
| 84cb5ad8f9 | |||
| 71bfa00c25 | |||
| 1916dd96a2 | |||
| c583d4b007 | |||
| 50ee14cf8f | |||
| 3f4d3cb7ac | |||
| f2ba5ed65b | |||
| 677893272a | |||
| 956ace77a8 | |||
| 08e2a4ebec | |||
| 3c8b3b0ace | |||
| 94b37d47ec | |||
| cd4422eb23 | |||
| 8dc0e993e1 | |||
| 2432cf496f | |||
| 95a5977ae2 | |||
| 7e157eaba9 | |||
| 00dd6a16a3 | |||
| 9e6b623b0e | |||
| 10b39ec4c1 | |||
| 3e938ad171 | |||
| 6c404e210e | |||
| 25681806c3 | |||
| 99d0b9c340 | |||
| 8e0990832b | |||
| 144c338972 | |||
| 2dcb736df5 | |||
| c8560e3aca | |||
| 171932c05c | |||
| 6cd47649fc | |||
| 00890c764e | |||
| a9c33352f7 | |||
| a9555d612a | |||
| c1f35e1f3a | |||
| a5903a9831 | |||
| b207b5805a | |||
| 5d7fe1638e | |||
| c0f07ee9f4 | |||
| 832f6a2339 | |||
| 7348994427 | |||
| 0665eef37d | |||
| adabe88648 | |||
| 3c838ec36e | |||
| 5bef712b1c | |||
| f08d590a3d | |||
| 844bc318ae | |||
| c9c4d9454a | |||
| 398a3750f8 | |||
| aceea9af18 | |||
| 54088f46d9 | |||
| 8064c68b8b | |||
| bdeb41bead |
6
pom.xml
6
pom.xml
@@ -286,6 +286,12 @@
|
|||||||
<artifactId>spring-kafka</artifactId>
|
<artifactId>spring-kafka</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Caffeine Cache -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Apache POI - 处理Excel文件 -->
|
<!-- Apache POI - 处理Excel文件 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.ycwl.basic.mapper.StatisticsMapper;
|
|||||||
import com.ycwl.basic.model.pc.broker.entity.BrokerRecord;
|
import com.ycwl.basic.model.pc.broker.entity.BrokerRecord;
|
||||||
import com.ycwl.basic.model.pc.broker.resp.BrokerRespVO;
|
import com.ycwl.basic.model.pc.broker.resp.BrokerRespVO;
|
||||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.repository.OrderRepository;
|
import com.ycwl.basic.repository.OrderRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -43,14 +43,14 @@ public class BrokerBiz {
|
|||||||
log.info("订单不存在,订单ID:{}", orderId);
|
log.info("订单不存在,订单ID:{}", orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(order.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId());
|
||||||
if (scenicConfig == null) {
|
if (scenicConfig == null) {
|
||||||
log.info("景区不存在,订单ID:{}", orderId);
|
log.info("景区不存在,订单ID:{}", orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int expireDay = 3;
|
int expireDay = 3;
|
||||||
if (scenicConfig.getSampleStoreDay() != null) {
|
if (scenicConfig.getInteger("sample_store_day") != null) {
|
||||||
expireDay = scenicConfig.getSampleStoreDay();
|
expireDay = scenicConfig.getInteger("sample_store_day");
|
||||||
}
|
}
|
||||||
List<Long> brokerIdList = statisticsMapper.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt());
|
List<Long> brokerIdList = statisticsMapper.getBrokerIdListForUser(order.getMemberId(), DateUtil.offsetDay(DateUtil.beginOfDay(order.getCreateAt()), -expireDay), order.getCreateAt());
|
||||||
if (brokerIdList == null || brokerIdList.isEmpty()) {
|
if (brokerIdList == null || brokerIdList.isEmpty()) {
|
||||||
@@ -103,7 +103,7 @@ public class BrokerBiz {
|
|||||||
BigDecimal realRate = broker.getBrokerRate();
|
BigDecimal realRate = broker.getBrokerRate();
|
||||||
BigDecimal brokerPrice = order.getPayPrice().multiply(realRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
|
BigDecimal brokerPrice = order.getPayPrice().multiply(realRate).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
|
||||||
// todo 需要计算实际提成比例
|
// todo 需要计算实际提成比例
|
||||||
BigDecimal firstRate = scenicConfig.getBrokerDirectRate();
|
BigDecimal firstRate = scenicConfig.getBigDecimal("broker_direct_rate");
|
||||||
if (firstRate == null) {
|
if (firstRate == null) {
|
||||||
firstRate = BigDecimal.ZERO;
|
firstRate = BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
package com.ycwl.basic.biz;
|
|
||||||
|
|
||||||
import com.ycwl.basic.mapper.CouponMapper;
|
|
||||||
import com.ycwl.basic.mapper.CouponRecordMapper;
|
|
||||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
|
||||||
import com.ycwl.basic.model.pc.couponRecord.entity.CouponRecordEntity;
|
|
||||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class CouponBiz {
|
|
||||||
@Autowired
|
|
||||||
private CouponMapper couponMapper;
|
|
||||||
@Autowired
|
|
||||||
private CouponRecordMapper couponRecordMapper;
|
|
||||||
|
|
||||||
public CouponRecordQueryResp queryUserCouponRecord(Long scenicId, Long memberId, Long faceId, String goodsId) {
|
|
||||||
CouponRecordQueryResp resp = new CouponRecordQueryResp();
|
|
||||||
List<CouponRecordEntity> recordList = couponRecordMapper.queryByUserWithGoodsId(scenicId, memberId, goodsId);
|
|
||||||
if (recordList != null && !recordList.isEmpty()) {
|
|
||||||
Optional<CouponRecordEntity> record = recordList.stream().filter(item -> item.getStatus() == 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
|
|
||||||
if (record.isPresent()) {
|
|
||||||
CouponRecordEntity recordEntity = record.get();
|
|
||||||
resp.setExist(true);
|
|
||||||
resp.setId(recordEntity.getId());
|
|
||||||
resp.setCouponId(recordEntity.getCouponId());
|
|
||||||
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
|
|
||||||
if (coupon != null) {
|
|
||||||
resp.setMemberId(recordEntity.getMemberId());
|
|
||||||
resp.setFaceId(recordEntity.getFaceId());
|
|
||||||
resp.setStatus(recordEntity.getStatus());
|
|
||||||
resp.setCreateTime(recordEntity.getCreateTime());
|
|
||||||
resp.setUsedTime(recordEntity.getUsedTime());
|
|
||||||
resp.setUsedOrderId(recordEntity.getUsedOrderId());
|
|
||||||
resp.setCoupon(coupon);
|
|
||||||
} else {
|
|
||||||
resp.setExist(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Optional<CouponRecordEntity> usedRecord = recordList.stream().filter(item -> item.getStatus() != 0).filter(item -> item.getFaceId() == null || item.getFaceId().equals(faceId)).findAny();
|
|
||||||
if (usedRecord.isPresent()) {
|
|
||||||
CouponRecordEntity recordEntity = usedRecord.get();
|
|
||||||
resp.setExist(true);
|
|
||||||
resp.setId(recordEntity.getId());
|
|
||||||
resp.setCouponId(recordEntity.getCouponId());
|
|
||||||
CouponEntity coupon = couponMapper.getById(recordEntity.getCouponId());
|
|
||||||
if (coupon != null) {
|
|
||||||
resp.setMemberId(recordEntity.getMemberId());
|
|
||||||
resp.setFaceId(recordEntity.getFaceId());
|
|
||||||
resp.setStatus(recordEntity.getStatus());
|
|
||||||
resp.setCreateTime(recordEntity.getCreateTime());
|
|
||||||
resp.setUsedTime(recordEntity.getUsedTime());
|
|
||||||
resp.setUsedOrderId(recordEntity.getUsedOrderId());
|
|
||||||
resp.setCoupon(coupon);
|
|
||||||
} else {
|
|
||||||
resp.setExist(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean userGetCoupon(Long memberId, Long faceId, Integer couponId) {
|
|
||||||
CouponEntity coupon = couponMapper.getById(couponId);
|
|
||||||
if (coupon == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CouponRecordEntity entity = new CouponRecordEntity();
|
|
||||||
entity.setCouponId(couponId);
|
|
||||||
entity.setFaceId(faceId);
|
|
||||||
entity.setMemberId(memberId);
|
|
||||||
entity.setStatus(0);
|
|
||||||
entity.setCreateTime(new Date());
|
|
||||||
return couponRecordMapper.insert(entity) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean userUseCoupon(Long memberId, Long faceId, Integer couponRecordId, Long orderId) {
|
|
||||||
CouponRecordEntity entity = new CouponRecordEntity();
|
|
||||||
entity.setId(couponRecordId);
|
|
||||||
entity.setStatus(1);
|
|
||||||
entity.setUsedTime(new Date());
|
|
||||||
entity.setUsedOrderId(orderId);
|
|
||||||
return couponRecordMapper.updateById(entity) > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
296
src/main/java/com/ycwl/basic/biz/FaceStatusManager.java
Normal file
296
src/main/java/com/ycwl/basic/biz/FaceStatusManager.java
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package com.ycwl.basic.biz;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import com.ycwl.basic.enums.FaceCutStatus;
|
||||||
|
import com.ycwl.basic.enums.FacePieceUpdateStatus;
|
||||||
|
import com.ycwl.basic.enums.TemplateRenderStatus;
|
||||||
|
import com.ycwl.basic.mapper.TaskMapper;
|
||||||
|
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸状态缓存管理器
|
||||||
|
* 统一管理人脸相关的内存缓存状态(使用Caffeine)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FaceStatusManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认过期时间:1小时
|
||||||
|
*/
|
||||||
|
private static final long DEFAULT_EXPIRE_SECONDS = 3600L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸切片状态缓存
|
||||||
|
*/
|
||||||
|
private final Cache<String, Integer> faceCutStatusCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸片段更新状态缓存(全局和模板级)
|
||||||
|
* 键存在=无新片段,键不存在=有新片段
|
||||||
|
*/
|
||||||
|
private final Cache<String, Boolean> faceNoPieceUpdateCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸模板渲染状态缓存
|
||||||
|
*/
|
||||||
|
private final Cache<String, Integer> templateRenderCache;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskMapper taskMapper;
|
||||||
|
|
||||||
|
public FaceStatusManager() {
|
||||||
|
// 初始化三个独立的缓存实例
|
||||||
|
this.faceCutStatusCache = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.maximumSize(10000)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.faceNoPieceUpdateCache = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.maximumSize(10000)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.templateRenderCache = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.maximumSize(10000)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 切片状态相关方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置人脸切片状态
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param status 切片状态
|
||||||
|
*/
|
||||||
|
public void setFaceCutStatus(Long faceId, FaceCutStatus status) {
|
||||||
|
if (faceId == null || status == null) {
|
||||||
|
log.warn("设置切片状态参数为空: faceId={}, status={}", faceId, status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
faceCutStatusCache.put(String.valueOf(faceId), status.getCode());
|
||||||
|
log.debug("设置切片状态: faceId={}, status={}", faceId, status.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸切片状态
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 切片状态,缓存不存在时返回 COMPLETED(已完成)
|
||||||
|
*/
|
||||||
|
public FaceCutStatus getFaceCutStatus(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
log.warn("获取切片状态参数为空: faceId={}", faceId);
|
||||||
|
return FaceCutStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
Integer code = faceCutStatusCache.getIfPresent(String.valueOf(faceId));
|
||||||
|
if (code == null) {
|
||||||
|
log.debug("切片状态缓存不存在,返回默认值COMPLETED: faceId={}", faceId);
|
||||||
|
return FaceCutStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
return FaceCutStatus.fromCodeOrDefault(code, FaceCutStatus.COMPLETED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除人脸切片状态缓存
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void deleteFaceCutStatus(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
faceCutStatusCache.invalidate(String.valueOf(faceId));
|
||||||
|
log.debug("删除切片状态缓存: faceId={}", faceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 片段更新状态相关方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记无新片段(设置缓存键)
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选,为null时标记全局状态)
|
||||||
|
*/
|
||||||
|
public void markNoNewPieces(Long faceId, Long templateId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
log.warn("标记无新片段参数为空: faceId={}", faceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateId == null) {
|
||||||
|
// 全局标记:该人脸的所有模板都无新片段
|
||||||
|
faceNoPieceUpdateCache.put(String.valueOf(faceId), Boolean.TRUE);
|
||||||
|
log.debug("标记无新片段(全局): faceId={}", faceId);
|
||||||
|
} else {
|
||||||
|
// 模板级标记:该人脸在该模板下无新片段
|
||||||
|
faceNoPieceUpdateCache.put(faceId + ":" + templateId, Boolean.TRUE);
|
||||||
|
log.debug("标记无新片段(模板): faceId={}, templateId={}", faceId, templateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记有新片段(删除缓存键)
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选,为null时标记全局状态)
|
||||||
|
*/
|
||||||
|
public void markHasNewPieces(Long faceId, Long templateId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
log.warn("标记有新片段参数为空: faceId={}", faceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateId == null) {
|
||||||
|
// 全局标记:该人脸有新片段
|
||||||
|
faceNoPieceUpdateCache.invalidate(String.valueOf(faceId));
|
||||||
|
log.debug("标记有新片段(全局): faceId={}", faceId);
|
||||||
|
} else {
|
||||||
|
// 模板级标记:该人脸在该模板下有新片段
|
||||||
|
faceNoPieceUpdateCache.invalidate(faceId + ":" + templateId);
|
||||||
|
log.debug("标记有新片段(模板): faceId={}, templateId={}", faceId, templateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸片段更新状态
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选,为null时查询全局状态)
|
||||||
|
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
|
||||||
|
*/
|
||||||
|
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId, Long templateId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
log.warn("获取片段更新状态参数为空: faceId={}", faceId);
|
||||||
|
return FacePieceUpdateStatus.HAS_NEW_PIECES;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = templateId == null ? String.valueOf(faceId) : faceId + ":" + templateId;
|
||||||
|
boolean exists = faceNoPieceUpdateCache.getIfPresent(key) != null;
|
||||||
|
FacePieceUpdateStatus status = FacePieceUpdateStatus.fromKeyExists(exists);
|
||||||
|
|
||||||
|
if (templateId == null) {
|
||||||
|
log.debug("获取片段更新状态(全局): faceId={}, status={}", faceId, status.getDescription());
|
||||||
|
} else {
|
||||||
|
log.debug("获取片段更新状态(模板): faceId={}, templateId={}, status={}",
|
||||||
|
faceId, templateId, status.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸片段更新状态 - 全局版本
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return 片段更新状态,键存在=无新片段,键不存在=有新片段
|
||||||
|
*/
|
||||||
|
public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId) {
|
||||||
|
return getFacePieceUpdateStatus(faceId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有新片段
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选,为null时查询全局状态)
|
||||||
|
* @return true=有新片段,false=无新片段;如果templateId为null则默认返回true(有新片段)
|
||||||
|
*/
|
||||||
|
public boolean hasNewPieces(Long faceId, Long templateId) {
|
||||||
|
if (templateId == null) {
|
||||||
|
// 如果没有指定templateId,默认认为有新片段
|
||||||
|
log.debug("未指定templateId,默认返回有新片段: faceId={}", faceId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return getFacePieceUpdateStatus(faceId, templateId).hasNewPieces();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有新片段 - 全局版本
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @return true=有新片段,false=无新片段
|
||||||
|
*/
|
||||||
|
public boolean hasNewPieces(Long faceId) {
|
||||||
|
return getFacePieceUpdateStatus(faceId, null).hasNewPieces();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模板渲染状态相关方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置人脸模板渲染状态
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID
|
||||||
|
* @param status 渲染状态
|
||||||
|
*/
|
||||||
|
public void setTemplateRenderStatus(Long faceId, Long templateId, TemplateRenderStatus status) {
|
||||||
|
if (faceId == null || templateId == null || status == null) {
|
||||||
|
log.warn("设置模板渲染状态参数为空: faceId={}, templateId={}, status={}", faceId, templateId, status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
templateRenderCache.put(faceId + ":" + templateId, status.getCode());
|
||||||
|
log.debug("设置模板渲染状态: faceId={}, templateId={}, status={}", faceId, templateId, status.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人脸模板渲染状态
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID
|
||||||
|
* @return 渲染状态,缓存不存在时返回 null
|
||||||
|
*/
|
||||||
|
public TemplateRenderStatus getTemplateRenderStatus(Long faceId, Long templateId) {
|
||||||
|
if (faceId == null || templateId == null) {
|
||||||
|
log.warn("获取模板渲染状态参数为空: faceId={}, templateId={}", faceId, templateId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Integer code = templateRenderCache.getIfPresent(faceId + ":" + templateId);
|
||||||
|
if (code == null) {
|
||||||
|
log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId);
|
||||||
|
// 查询数据库
|
||||||
|
TaskEntity task = taskMapper.listLastFaceTemplateTask(faceId, templateId);
|
||||||
|
if (task == null) {
|
||||||
|
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE);
|
||||||
|
return TemplateRenderStatus.NONE;
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(2).equals(task.getStatus())) {
|
||||||
|
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING);
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(1).equals(task.getStatus())) {
|
||||||
|
setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED);
|
||||||
|
}
|
||||||
|
return TemplateRenderStatus.NONE;
|
||||||
|
}
|
||||||
|
return TemplateRenderStatus.fromCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除人脸模板渲染状态缓存
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID
|
||||||
|
*/
|
||||||
|
public void deleteTemplateRenderStatus(Long faceId, Long templateId) {
|
||||||
|
if (faceId == null || templateId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
templateRenderCache.invalidate(faceId + ":" + templateId);
|
||||||
|
log.debug("删除模板渲染状态缓存: faceId={}, templateId={}", faceId, templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除人脸的所有模板渲染状态(使用模式匹配)
|
||||||
|
* 注意:此操作可能影响性能,谨慎使用
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
*/
|
||||||
|
public void deleteAllTemplateRenderStatus(Long faceId) {
|
||||||
|
if (faceId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String prefix = faceId + ":";
|
||||||
|
long count = templateRenderCache.asMap().keySet().stream()
|
||||||
|
.filter(key -> key.startsWith(prefix))
|
||||||
|
.peek(templateRenderCache::invalidate)
|
||||||
|
.count();
|
||||||
|
if (count > 0) {
|
||||||
|
log.debug("批量删除模板渲染状态缓存: faceId={}, count={}", faceId, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,23 +3,14 @@ package com.ycwl.basic.biz;
|
|||||||
import com.ycwl.basic.enums.StatisticEnum;
|
import com.ycwl.basic.enums.StatisticEnum;
|
||||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.mapper.OrderMapper;
|
import com.ycwl.basic.mapper.OrderMapper;
|
||||||
import com.ycwl.basic.mapper.SourceMapper;
|
|
||||||
import com.ycwl.basic.mapper.StatisticsMapper;
|
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||||
import com.ycwl.basic.mapper.VideoMapper;
|
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
||||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
|
||||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||||
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
|
||||||
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.model.pc.order.resp.OrderItemVO;
|
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
|
||||||
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
|
||||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||||
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||||
@@ -42,16 +33,12 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class OrderBiz {
|
public class OrderBiz {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private VideoMapper videoMapper;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ScenicRepository scenicRepository;
|
private ScenicRepository scenicRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -69,16 +56,12 @@ public class OrderBiz {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private OrderMapper orderMapper;
|
private OrderMapper orderMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SourceMapper sourceMapper;
|
|
||||||
@Autowired
|
|
||||||
private ProfitSharingBiz profitSharingBiz;
|
private ProfitSharingBiz profitSharingBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VideoTaskRepository videoTaskRepository;
|
private VideoTaskRepository videoTaskRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private BrokerBiz brokerBiz;
|
private BrokerBiz brokerBiz;
|
||||||
@Autowired
|
@Autowired
|
||||||
private CouponBiz couponBiz;
|
|
||||||
@Autowired
|
|
||||||
@Lazy
|
@Lazy
|
||||||
private PrinterService printerService;
|
private PrinterService printerService;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -88,9 +71,9 @@ public class OrderBiz {
|
|||||||
PriceObj priceObj = new PriceObj();
|
PriceObj priceObj = new PriceObj();
|
||||||
priceObj.setGoodsType(goodsType);
|
priceObj.setGoodsType(goodsType);
|
||||||
priceObj.setGoodsId(goodsId);
|
priceObj.setGoodsId(goodsId);
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
|
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
|
||||||
// 景区全免
|
// 景区全免
|
||||||
priceObj.setFree(true);
|
priceObj.setFree(true);
|
||||||
priceObj.setPrice(BigDecimal.ZERO);
|
priceObj.setPrice(BigDecimal.ZERO);
|
||||||
@@ -104,10 +87,7 @@ public class OrderBiz {
|
|||||||
if (video == null) {
|
if (video == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
priceObj.setFaceId(video.getFaceId());
|
||||||
if (task != null) {
|
|
||||||
priceObj.setFaceId(task.getFaceId());
|
|
||||||
}
|
|
||||||
TemplateRespVO template = templateRepository.getTemplate(video.getTemplateId());
|
TemplateRespVO template = templateRepository.getTemplate(video.getTemplateId());
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
return priceObj;
|
return priceObj;
|
||||||
@@ -247,10 +227,6 @@ public class OrderBiz {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
|
||||||
Integer couponRecordId = order.getCouponRecordId();
|
|
||||||
if (couponRecordId != null) {
|
|
||||||
couponBiz.userUseCoupon(order.getMemberId(), order.getFaceId(), couponRecordId, orderId);
|
|
||||||
}
|
|
||||||
StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq();
|
StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq();
|
||||||
statisticsRecordAddReq.setMemberId(order.getMemberId());
|
statisticsRecordAddReq.setMemberId(order.getMemberId());
|
||||||
Long enterType = statisticsMapper.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
|
Long enterType = statisticsMapper.getUserRecentEnterType(order.getMemberId(), order.getCreateAt());
|
||||||
@@ -313,15 +289,4 @@ public class OrderBiz {
|
|||||||
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
|
profitSharingBiz.revokeProfitSharing(order.getScenicId(), orderId, "订单已退款");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查用户是否购买了指定商品,并额外校验订单的faceId是否匹配
|
|
||||||
* @param userId 用户ID
|
|
||||||
* @param faceId 人脸ID
|
|
||||||
* @param goodsType 商品类型
|
|
||||||
* @param goodsId 商品ID
|
|
||||||
* @return 是否已购买且faceId匹配
|
|
||||||
*/
|
|
||||||
public boolean checkUserBuyFaceItem(Long userId, Long faceId, int goodsType, Long goodsId) {
|
|
||||||
return orderRepository.checkUserBuyFaceItem(userId, faceId, goodsType, goodsId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
|||||||
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
import com.ycwl.basic.model.pc.price.resp.GoodsListRespVO;
|
||||||
import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
|
import com.ycwl.basic.model.pc.price.resp.SimpleGoodsRespVO;
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||||
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
import com.ycwl.basic.product.capability.ProductTypeCapability;
|
||||||
@@ -17,6 +17,7 @@ import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
|||||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
|
import com.ycwl.basic.repository.OrderRepository;
|
||||||
import com.ycwl.basic.repository.PriceRepository;
|
import com.ycwl.basic.repository.PriceRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
import com.ycwl.basic.repository.TemplateRepository;
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
@@ -45,16 +46,13 @@ public class PriceBiz {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FaceRepository faceRepository;
|
private FaceRepository faceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
|
||||||
private FaceService faceService;
|
|
||||||
@Autowired
|
|
||||||
private CouponBiz couponBiz;
|
|
||||||
@Autowired
|
|
||||||
private MemberRelationRepository memberRelationRepository;
|
private MemberRelationRepository memberRelationRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private PuzzleTemplateMapper puzzleTemplateMapper;
|
private PuzzleTemplateMapper puzzleTemplateMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
|
private IProductTypeCapabilityManagementService productTypeCapabilityManagementService;
|
||||||
|
@Autowired
|
||||||
|
private OrderRepository orderRepository;
|
||||||
|
|
||||||
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
||||||
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
List<GoodsListRespVO> goodsList = new ArrayList<>();
|
||||||
@@ -67,12 +65,12 @@ public class PriceBiz {
|
|||||||
goods.setGoodsType(0);
|
goods.setGoodsType(0);
|
||||||
return goods;
|
return goods;
|
||||||
}).forEach(goodsList::add);
|
}).forEach(goodsList::add);
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
||||||
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
||||||
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,11 +131,13 @@ public class PriceBiz {
|
|||||||
|
|
||||||
case "PHOTO_LOG":
|
case "PHOTO_LOG":
|
||||||
// 从 template 表查询pLog模板
|
// 从 template 表查询pLog模板
|
||||||
goodsList.add(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
|
||||||
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
List<PuzzleTemplateEntity> puzzleList = puzzleTemplateMapper.list(scenicId, null, null);
|
||||||
puzzleList.stream()
|
puzzleList.stream()
|
||||||
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
.map(template -> new SimpleGoodsRespVO(template.getId(), template.getName(), productType))
|
||||||
.forEach(goodsList::add);
|
.forEach(goodsList::add);
|
||||||
|
if (!puzzleList.isEmpty()) {
|
||||||
|
goodsList.addFirst(new SimpleGoodsRespVO(scenicId, "pLog图<景区打包>", productType));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "RECORDING_SET":
|
case "RECORDING_SET":
|
||||||
@@ -220,39 +220,15 @@ public class PriceBiz {
|
|||||||
if (face != null && !face.getMemberId().equals(userId)) {
|
if (face != null && !face.getMemberId().equals(userId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
|
if (Boolean.TRUE.equals(scenicConfig.getBoolean("all_free"))) {
|
||||||
// 景区全免
|
// 景区全免
|
||||||
respVO.setFree(true);
|
respVO.setFree(true);
|
||||||
respVO.setSlashPrice(BigDecimal.ZERO);
|
respVO.setSlashPrice(BigDecimal.ZERO);
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (type) {
|
|
||||||
case 0: // 单个定价
|
|
||||||
CouponRecordQueryResp recordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, goodsIds);
|
|
||||||
if (recordQueryResp.isUsable()) {
|
|
||||||
respVO.setCouponId(recordQueryResp.getCouponId());
|
|
||||||
respVO.setCouponRecordId(recordQueryResp.getId());
|
|
||||||
CouponEntity coupon = recordQueryResp.getCoupon();
|
|
||||||
if (coupon != null) {
|
|
||||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case -1:
|
|
||||||
CouponRecordQueryResp oneCouponRecordQueryResp = couponBiz.queryUserCouponRecord(scenicId, userId, faceId, "-1");
|
|
||||||
if (oneCouponRecordQueryResp.isUsable()) {
|
|
||||||
respVO.setCouponId(oneCouponRecordQueryResp.getCouponId());
|
|
||||||
respVO.setCouponRecordId(oneCouponRecordQueryResp.getId());
|
|
||||||
CouponEntity coupon = oneCouponRecordQueryResp.getCoupon();
|
|
||||||
if (coupon != null) {
|
|
||||||
respVO.setCouponPrice(coupon.calculateDiscountPrice(priceConfig.getPrice()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
respVO.setConfigId(priceConfig.getId());
|
respVO.setConfigId(priceConfig.getId());
|
||||||
respVO.setGoodsIds(goodsIds);
|
respVO.setGoodsIds(goodsIds);
|
||||||
respVO.setType(type);
|
respVO.setType(type);
|
||||||
@@ -281,7 +257,7 @@ public class PriceBiz {
|
|||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
boolean hasPurchasedTemplate = orderBiz.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
boolean hasPurchasedTemplate = orderRepository.checkUserBuyFaceItem(userId, faceId, -1, videoEntities.getFirst().getVideoId());
|
||||||
if (!hasPurchasedTemplate) {
|
if (!hasPurchasedTemplate) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
break;
|
break;
|
||||||
@@ -292,16 +268,16 @@ public class PriceBiz {
|
|||||||
if (allContentsPurchased) {
|
if (allContentsPurchased) {
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
// 检查录像集
|
// 检查录像集
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"))) {
|
||||||
boolean hasPurchasedRecording = orderBiz.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
boolean hasPurchasedRecording = orderRepository.checkUserBuyFaceItem(userId, faceId, 1, faceId);
|
||||||
if (!hasPurchasedRecording) {
|
if (!hasPurchasedRecording) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查照片集
|
// 检查照片集
|
||||||
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
if (allContentsPurchased && !Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"))) {
|
||||||
boolean hasPurchasedPhoto = orderBiz.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
boolean hasPurchasedPhoto = orderRepository.checkUserBuyFaceItem(userId, faceId, 2, faceId);
|
||||||
if (!hasPurchasedPhoto) {
|
if (!hasPurchasedPhoto) {
|
||||||
allContentsPurchased = false;
|
allContentsPurchased = false;
|
||||||
}
|
}
|
||||||
@@ -315,9 +291,6 @@ public class PriceBiz {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
respVO.setShare(false);
|
respVO.setShare(false);
|
||||||
if (face == null || !face.getMemberId().equals(userId)) {
|
|
||||||
respVO.setShare(true);
|
|
||||||
}
|
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
package com.ycwl.basic.biz;
|
|
||||||
|
|
||||||
import com.ycwl.basic.mapper.FaceMapper;
|
|
||||||
import com.ycwl.basic.mapper.TaskMapper;
|
|
||||||
import com.ycwl.basic.mapper.VideoMapper;
|
|
||||||
import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO;
|
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
|
||||||
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
|
||||||
import com.ycwl.basic.model.pc.task.req.TaskReqQuery;
|
|
||||||
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
|
|
||||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
|
||||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
|
||||||
import com.ycwl.basic.repository.TemplateRepository;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class TaskStatusBiz {
|
|
||||||
public static final String TASK_STATUS_USER_CACHE_KEY = "task:status:user:%s:face:%s";
|
|
||||||
public static final String TASK_STATUS_FACE_CACHE_KEY = "task:status:face:%s";
|
|
||||||
public static final String TASK_STATUS_FACE_CACHE_KEY_CUT = "task:status:face:%s:cut";
|
|
||||||
public static final String TASK_STATUS_FACE_CACHE_KEY_TEMPLATE = "task:status:face:%s:tpl:%s";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
|
||||||
@Autowired
|
|
||||||
private FaceRepository faceRepository;
|
|
||||||
@Autowired
|
|
||||||
private TemplateRepository templateRepository;
|
|
||||||
@Autowired
|
|
||||||
private FaceMapper faceMapper;
|
|
||||||
@Autowired
|
|
||||||
private TaskMapper taskMapper;
|
|
||||||
@Autowired
|
|
||||||
private VideoMapper videoMapper;
|
|
||||||
@Autowired
|
|
||||||
private TemplateBiz templateBiz;
|
|
||||||
|
|
||||||
public boolean getUserHaveFace(Long userId, Long faceId) {
|
|
||||||
if (userId == null || faceId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
|
||||||
if (face == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (face.getMemberId().equals(userId)) {
|
|
||||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId), "1", 3600, TimeUnit.SECONDS);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFaceCutStatus(Long faceId, int status) {
|
|
||||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId), String.valueOf(status), 3600, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFaceTemplateStatus(Long faceId, Long templateId, Long videoId) {
|
|
||||||
redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId), String.valueOf(videoId), 3600, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VideoTaskStatusVO getScenicUserStatus(Long scenicId, Long userId) {
|
|
||||||
FaceRespVO lastFace = faceMapper.findLastFaceByScenicAndUserId(scenicId, userId);
|
|
||||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
|
||||||
if (lastFace == null) {
|
|
||||||
response.setStatus(-1);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
return getFaceStatus(lastFace.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public VideoTaskStatusVO getFaceStatus(Long faceId) {
|
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
|
||||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
|
||||||
if (face == null) {
|
|
||||||
response.setStatus(-1);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
response.setScenicId(face.getScenicId());
|
|
||||||
response.setFaceId(faceId);
|
|
||||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
|
|
||||||
response.setMaxCount(templateList.size());
|
|
||||||
int alreadyFinished = 0;
|
|
||||||
for (TemplateRespVO template : templateList) {
|
|
||||||
response.setTemplateId(template.getId());
|
|
||||||
long videoId = getFaceTemplateVideoId(faceId, template.getId());
|
|
||||||
if (videoId <= 0) {
|
|
||||||
response.setStatus(2);
|
|
||||||
} else {
|
|
||||||
response.setVideoId(videoId);
|
|
||||||
alreadyFinished++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.setCount(alreadyFinished);
|
|
||||||
if (alreadyFinished == 0) {
|
|
||||||
response.setStatus(0);
|
|
||||||
} else {
|
|
||||||
response.setStatus(1);
|
|
||||||
}
|
|
||||||
if (alreadyFinished == 0) {
|
|
||||||
int faceCutStatus = getFaceCutStatus(faceId);
|
|
||||||
if (faceCutStatus != 1) {
|
|
||||||
// 正在切片
|
|
||||||
if (templateBiz.determineTemplateCanGenerate(templateList.getFirst().getId(), faceId, false)) {
|
|
||||||
response.setStatus(2);
|
|
||||||
} else {
|
|
||||||
response.setStatus(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VideoTaskStatusVO getFaceTemplateStatus(Long faceId, Long templateId) {
|
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
|
||||||
VideoTaskStatusVO response = new VideoTaskStatusVO();
|
|
||||||
if (face == null) {
|
|
||||||
response.setStatus(-1);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
response.setScenicId(face.getScenicId());
|
|
||||||
response.setFaceId(faceId);
|
|
||||||
response.setTemplateId(templateId);
|
|
||||||
long videoId = getFaceTemplateVideoId(faceId, templateId);
|
|
||||||
if (videoId < 0) {
|
|
||||||
int faceCutStatus = getFaceCutStatus(faceId);
|
|
||||||
if (faceCutStatus != 1) {
|
|
||||||
// 正在切片
|
|
||||||
response.setStatus(2);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
} else if (videoId == 0) {
|
|
||||||
response.setStatus(2);
|
|
||||||
} else {
|
|
||||||
response.setVideoId(videoId);
|
|
||||||
response.setStatus(1);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getFaceCutStatus(Long faceId) {
|
|
||||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId))) {
|
|
||||||
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId));
|
|
||||||
if (status != null) {
|
|
||||||
return Integer.parseInt(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getFaceTemplateVideoId(Long faceId, Long templateId) {
|
|
||||||
if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId))) {
|
|
||||||
String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId));
|
|
||||||
if (status != null) {
|
|
||||||
return Long.parseLong(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TaskReqQuery taskReqQuery = new TaskReqQuery();
|
|
||||||
taskReqQuery.setFaceId(faceId);
|
|
||||||
taskReqQuery.setTemplateId(templateId);
|
|
||||||
List<TaskRespVO> list = taskMapper.list(taskReqQuery);
|
|
||||||
Optional<TaskRespVO> min = list.stream().min(Comparator.comparing(TaskRespVO::getCreateTime));
|
|
||||||
if (min.isPresent()) {
|
|
||||||
TaskRespVO task = min.get();
|
|
||||||
long taskStatus = 0;
|
|
||||||
if (task.getStatus() == 1) {
|
|
||||||
// 已完成
|
|
||||||
VideoEntity video = videoMapper.findByTaskId(task.getId());
|
|
||||||
if (video != null) {
|
|
||||||
taskStatus = video.getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFaceTemplateStatus(faceId, templateId, taskStatus);
|
|
||||||
} else {
|
|
||||||
// 从来没生成过
|
|
||||||
setFaceTemplateStatus(faceId, templateId, -1L);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.ycwl.basic.controller;
|
package com.ycwl.basic.controller;
|
||||||
|
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||||
@@ -91,4 +93,20 @@ public class VideoReviewController {
|
|||||||
throw new RuntimeException("导出失败: " + e.getMessage());
|
throw new RuntimeException("导出失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查视频是否已被购买
|
||||||
|
* 购买条件:
|
||||||
|
* 1. 直接购买视频(order_item中goods_type=0且goods_id=视频id)
|
||||||
|
* 2. 购买整个模板(order的face_id与video关联的task的face_id相同,goods_type=-1,goods_id为video的templateId)
|
||||||
|
*
|
||||||
|
* @param reqDTO 查询条件
|
||||||
|
* @return 购买状态及订单ID列表
|
||||||
|
*/
|
||||||
|
@PostMapping("/check-purchase")
|
||||||
|
public ApiResponse<VideoPurchaseCheckRespDTO> checkVideoPurchase(@RequestBody VideoPurchaseCheckReqDTO reqDTO) {
|
||||||
|
log.info("检查视频购买状态,videoId: {}", reqDTO.getVideoId());
|
||||||
|
VideoPurchaseCheckRespDTO respDTO = videoReviewService.checkVideoPurchase(reqDTO);
|
||||||
|
return ApiResponse.success(respDTO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ public class LyCompatibleController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private VideoRepository videoRepository;
|
private VideoRepository videoRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VideoMapper videoMapper;
|
|
||||||
@Autowired
|
|
||||||
private TaskTaskServiceImpl taskTaskServiceImpl;
|
|
||||||
@Autowired
|
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VideoTaskRepository videoTaskRepository;
|
private VideoTaskRepository videoTaskRepository;
|
||||||
@@ -213,12 +209,11 @@ public class LyCompatibleController {
|
|||||||
VideoEntity videoRespVO = videoRepository.getVideo(contentPageVO.getContentId());
|
VideoEntity videoRespVO = videoRepository.getVideo(contentPageVO.getContentId());
|
||||||
map.put("id", videoRespVO.getId().toString());
|
map.put("id", videoRespVO.getId().toString());
|
||||||
map.put("task_id", videoRespVO.getTaskId().toString());
|
map.put("task_id", videoRespVO.getTaskId().toString());
|
||||||
TaskEntity task = videoTaskRepository.getTaskById(videoRespVO.getTaskId());
|
if (videoRespVO.getFaceId() != null) {
|
||||||
if (task != null) {
|
map.put("face_id", String.valueOf(videoRespVO.getFaceId()));
|
||||||
map.put("face_id", String.valueOf(task.getFaceId()));
|
|
||||||
}
|
}
|
||||||
map.put("template_cover_image", contentPageVO.getTemplateCoverUrl());
|
map.put("template_cover_image", contentPageVO.getTemplateCoverUrl());
|
||||||
Date taskShotDate = taskTaskServiceImpl.getTaskShotDate(videoRespVO.getTaskId());
|
Date taskShotDate = videoTaskRepository.getTaskShotDate(videoRespVO.getTaskId());
|
||||||
map.put("shoottime", DateUtil.format(taskShotDate, "yyyy-MM-dd HH:mm"));
|
map.put("shoottime", DateUtil.format(taskShotDate, "yyyy-MM-dd HH:mm"));
|
||||||
map.put("openid", openId);
|
map.put("openid", openId);
|
||||||
map.put("scenicname", contentPageVO.getScenicName());
|
map.put("scenicname", contentPageVO.getScenicName());
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ public class AppGoodsController {
|
|||||||
// 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功
|
// 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功
|
||||||
@GetMapping("/task/face/{faceId}/template/{templateId}")
|
@GetMapping("/task/face/{faceId}/template/{templateId}")
|
||||||
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
||||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
|
||||||
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,8 +85,7 @@ public class AppOrderV2Controller {
|
|||||||
switch (productItem.getProductType()) {
|
switch (productItem.getProductType()) {
|
||||||
case VLOG_VIDEO -> {
|
case VLOG_VIDEO -> {
|
||||||
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
|
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
|
||||||
TaskEntity task = videoTaskRepository.getTaskById(video.getTaskId());
|
request.setFaceId(video.getFaceId());
|
||||||
request.setFaceId(task.getFaceId());
|
|
||||||
}
|
}
|
||||||
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public class AppTaskController {
|
|||||||
@GetMapping("/face/{faceId}")
|
@GetMapping("/face/{faceId}")
|
||||||
@IgnoreLogReq
|
@IgnoreLogReq
|
||||||
public ApiResponse<VideoTaskStatusVO> getTaskStatusByFaceId(@PathVariable("faceId") Long faceId) {
|
public ApiResponse<VideoTaskStatusVO> getTaskStatusByFaceId(@PathVariable("faceId") Long faceId) {
|
||||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
|
||||||
return ApiResponse.success(goodsService.getTaskStatusByFaceId(faceId));
|
return ApiResponse.success(goodsService.getTaskStatusByFaceId(faceId));
|
||||||
}
|
}
|
||||||
@GetMapping("/scenic/{scenicId}")
|
@GetMapping("/scenic/{scenicId}")
|
||||||
@@ -49,7 +48,6 @@ public class AppTaskController {
|
|||||||
@GetMapping("/face/{faceId}/template/{templateId}")
|
@GetMapping("/face/{faceId}/template/{templateId}")
|
||||||
@IgnoreLogReq
|
@IgnoreLogReq
|
||||||
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
public ApiResponse<VideoTaskStatusVO> getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) {
|
||||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
|
||||||
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.mapper.TemplateMapper;
|
||||||
|
import com.ycwl.basic.model.pc.template.entity.TemplateEntity;
|
||||||
|
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||||
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端模板接口
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/template/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppTemplateController {
|
||||||
|
|
||||||
|
private final TemplateRepository templateRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模板ID获取封面URL
|
||||||
|
*
|
||||||
|
* @param templateId 模板ID
|
||||||
|
* @return 模板封面URL
|
||||||
|
*/
|
||||||
|
@GetMapping("/cover/{templateId}")
|
||||||
|
public ApiResponse<String> getTemplateCoverUrl(@PathVariable("templateId") Long templateId) {
|
||||||
|
if (templateId == null) {
|
||||||
|
return ApiResponse.fail("模板ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateRespVO template = templateRepository.getTemplate(templateId);
|
||||||
|
if (template == null) {
|
||||||
|
return ApiResponse.fail("未找到对应的模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
String coverUrl = template.getCoverUrl();
|
||||||
|
if (coverUrl == null || coverUrl.isEmpty()) {
|
||||||
|
return ApiResponse.fail("该模板没有封面地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.success(coverUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.ycwl.basic.controller.mobile;
|
|||||||
|
|
||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
import com.ycwl.basic.model.mobile.video.dto.VideoViewPermissionDTO;
|
import com.ycwl.basic.model.mobile.video.dto.VideoViewPermissionDTO;
|
||||||
|
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
|
||||||
import com.ycwl.basic.model.task.req.VideoInfoReq;
|
import com.ycwl.basic.model.task.req.VideoInfoReq;
|
||||||
import com.ycwl.basic.repository.VideoRepository;
|
import com.ycwl.basic.repository.VideoRepository;
|
||||||
import com.ycwl.basic.service.mobile.VideoViewPermissionService;
|
import com.ycwl.basic.service.mobile.VideoViewPermissionService;
|
||||||
@@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -90,4 +92,33 @@ public class AppVideoController {
|
|||||||
return ApiResponse.fail("权限检查失败,请稍后重试");
|
return ApiResponse.fail("权限检查失败,请稍后重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过faceId和templateId(可选)查询最新的视频记录
|
||||||
|
*
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选)
|
||||||
|
* @return 最新的视频记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/latest")
|
||||||
|
public ApiResponse<VideoRespVO> getLatestByFaceId(
|
||||||
|
@RequestParam("faceId") Long faceId,
|
||||||
|
@RequestParam(value = "templateId", required = false) Long templateId) {
|
||||||
|
try {
|
||||||
|
log.debug("查询最新视频记录: faceId={}, templateId={}", faceId, templateId);
|
||||||
|
|
||||||
|
VideoRespVO video = videoRepository.queryLatestByFaceIdAndTemplateId(faceId, templateId);
|
||||||
|
|
||||||
|
if (video == null) {
|
||||||
|
log.info("未找到视频记录: faceId={}, templateId={}", faceId, templateId);
|
||||||
|
return ApiResponse.fail("未找到视频记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.success(video);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询最新视频记录失败: faceId={}, templateId={}", faceId, templateId, e);
|
||||||
|
return ApiResponse.fail("查询失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile.manage;
|
||||||
|
|
||||||
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
|
import com.ycwl.basic.model.mobile.video.dto.HlsStreamRequest;
|
||||||
|
import com.ycwl.basic.model.mobile.video.dto.HlsStreamResponse;
|
||||||
|
import com.ycwl.basic.service.mobile.HlsStreamService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端视频流控制器
|
||||||
|
* 提供HLS视频流播放列表生成功能
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-12-26
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/video-stream/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppVideoStreamController {
|
||||||
|
|
||||||
|
private final HlsStreamService hlsStreamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成设备视频的HLS播放列表(JSON格式)
|
||||||
|
* 返回包含m3u8内容和视频片段信息的JSON对象
|
||||||
|
*
|
||||||
|
* @param request HLS流请求参数
|
||||||
|
* @return HLS播放列表响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/hls/playlist")
|
||||||
|
public ApiResponse<HlsStreamResponse> generateHlsPlaylist(@Validated @RequestBody HlsStreamRequest request) {
|
||||||
|
log.info("收到HLS播放列表生成请求: deviceId={}, durationMinutes={}",
|
||||||
|
request.getDeviceId(), request.getDurationMinutes());
|
||||||
|
|
||||||
|
try {
|
||||||
|
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
|
||||||
|
log.info("HLS播放列表生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
|
||||||
|
response.getDeviceId(), response.getSegmentCount(), response.getTotalDurationSeconds());
|
||||||
|
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成HLS播放列表失败: deviceId={}", request.getDeviceId(), e);
|
||||||
|
return ApiResponse.buildResponse(500, null, "生成失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成设备视频的HLS播放列表(m3u8文件格式)
|
||||||
|
* 直接返回m3u8文件内容,可被视频播放器直接使用
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param durationMinutes 视频时长(分钟),默认2分钟
|
||||||
|
* @param eventPlaylist 是否为Event播放列表,默认true
|
||||||
|
* @param response HTTP响应对象
|
||||||
|
*/
|
||||||
|
@GetMapping("/hls/playlist.m3u8")
|
||||||
|
@IgnoreToken
|
||||||
|
public void generateHlsPlaylistFile(
|
||||||
|
@RequestParam Long deviceId,
|
||||||
|
@RequestParam(defaultValue = "2") Integer durationMinutes,
|
||||||
|
@RequestParam(defaultValue = "true") Boolean eventPlaylist,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
|
||||||
|
log.info("收到m3u8文件生成请求: deviceId={}, durationMinutes={}",
|
||||||
|
deviceId, durationMinutes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
HlsStreamRequest request = new HlsStreamRequest();
|
||||||
|
request.setDeviceId(deviceId);
|
||||||
|
request.setDurationMinutes(durationMinutes);
|
||||||
|
request.setEventPlaylist(eventPlaylist);
|
||||||
|
|
||||||
|
// 生成播放列表
|
||||||
|
HlsStreamResponse hlsResponse = hlsStreamService.generateHlsPlaylist(request);
|
||||||
|
|
||||||
|
log.info("m3u8文件生成成功: deviceId={}, segmentCount={}, totalDuration={}s",
|
||||||
|
deviceId, hlsResponse.getSegmentCount(), hlsResponse.getTotalDurationSeconds());
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.setContentType("application/vnd.apple.mpegurl");
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename=\"playlist.m3u8\"");
|
||||||
|
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
response.setHeader("Pragma", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
|
||||||
|
// 写入m3u8内容
|
||||||
|
response.getWriter().write(hlsResponse.getPlaylistContent());
|
||||||
|
response.getWriter().flush();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成m3u8文件失败: deviceId={}", deviceId, e);
|
||||||
|
try {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.getWriter().write("{\"code\":500,\"message\":\"生成失败: " + e.getMessage() + "\"}");
|
||||||
|
response.getWriter().flush();
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
log.error("写入错误响应失败", ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备最近的视频片段信息
|
||||||
|
* 仅返回视频片段列表,不包含m3u8内容
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param durationMinutes 视频时长(分钟),默认2分钟
|
||||||
|
* @return 视频片段列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/segments")
|
||||||
|
public ApiResponse<HlsStreamResponse> getVideoSegments(
|
||||||
|
@RequestParam Long deviceId,
|
||||||
|
@RequestParam(defaultValue = "2") Integer durationMinutes) {
|
||||||
|
|
||||||
|
log.info("收到视频片段查询请求: deviceId={}, durationMinutes={}",
|
||||||
|
deviceId, durationMinutes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
HlsStreamRequest request = new HlsStreamRequest();
|
||||||
|
request.setDeviceId(deviceId);
|
||||||
|
request.setDurationMinutes(durationMinutes);
|
||||||
|
request.setEventPlaylist(true);
|
||||||
|
|
||||||
|
HlsStreamResponse response = hlsStreamService.generateHlsPlaylist(request);
|
||||||
|
log.info("视频片段查询成功: deviceId={}, segmentCount={}",
|
||||||
|
deviceId, response.getSegmentCount());
|
||||||
|
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询视频片段失败: deviceId={}", deviceId, e);
|
||||||
|
return ApiResponse.buildResponse(500, null, "查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
package com.ycwl.basic.controller.pc;
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
|
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
|
||||||
|
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
||||||
|
import com.ycwl.basic.model.pc.device.req.VideoContinuityReportReq;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.task.DeviceVideoContinuityCheckTask;
|
import com.ycwl.basic.task.DeviceVideoContinuityCheckTask;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备视频连续性检查控制器
|
* 设备视频连续性检查控制器
|
||||||
* 提供查询设备视频连续性检查结果的接口
|
* 提供查询设备视频连续性检查结果的接口
|
||||||
@@ -23,10 +33,12 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class DeviceVideoContinuityController {
|
public class DeviceVideoContinuityController {
|
||||||
|
|
||||||
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";
|
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";
|
||||||
|
private static final int CACHE_TTL_HOURS = 24; // 缓存24小时
|
||||||
|
|
||||||
private final RedisTemplate<String, String> redisTemplate;
|
private final RedisTemplate<String, String> redisTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final DeviceVideoContinuityCheckTask checkTask;
|
private final DeviceVideoContinuityCheckTask checkTask;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询设备最近的视频连续性检查结果
|
* 查询设备最近的视频连续性检查结果
|
||||||
@@ -103,4 +115,72 @@ public class DeviceVideoContinuityController {
|
|||||||
return ApiResponse.buildResponse(500, null, "删除失败: " + e.getMessage());
|
return ApiResponse.buildResponse(500, null, "删除失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部工具上报视频连续性检查结果
|
||||||
|
* 通过设备编号(deviceNo)上报检查结果,无需认证
|
||||||
|
*
|
||||||
|
* @param reportReq 上报请求
|
||||||
|
* @return 上报结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/report")
|
||||||
|
@IgnoreToken
|
||||||
|
public ApiResponse<DeviceVideoContinuityCache> reportContinuityResult(
|
||||||
|
@Validated @RequestBody VideoContinuityReportReq reportReq) {
|
||||||
|
log.info("外部工具上报设备 {} 的视频连续性检查结果", reportReq.getDeviceNo());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 根据设备编号查询设备ID
|
||||||
|
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(reportReq.getDeviceNo());
|
||||||
|
if (device == null) {
|
||||||
|
log.warn("设备编号 {} 不存在", reportReq.getDeviceNo());
|
||||||
|
return ApiResponse.buildResponse(404, null, "设备不存在: " + reportReq.getDeviceNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deviceId = device.getId();
|
||||||
|
|
||||||
|
// 2. 构建缓存对象
|
||||||
|
DeviceVideoContinuityCache cache = new DeviceVideoContinuityCache();
|
||||||
|
cache.setDeviceId(deviceId);
|
||||||
|
cache.setCheckTime(new Date());
|
||||||
|
cache.setStartTime(reportReq.getStartTime());
|
||||||
|
cache.setEndTime(reportReq.getEndTime());
|
||||||
|
cache.setSupport(reportReq.getSupport());
|
||||||
|
cache.setContinuous(reportReq.getContinuous());
|
||||||
|
cache.setTotalVideos(reportReq.getTotalVideos());
|
||||||
|
cache.setTotalDurationMs(reportReq.getTotalDurationMs());
|
||||||
|
cache.setMaxAllowedGapMs(reportReq.getMaxAllowedGapMs() != null
|
||||||
|
? reportReq.getMaxAllowedGapMs() : 2000L);
|
||||||
|
cache.setGapCount(reportReq.getGaps() != null ? reportReq.getGaps().size() : 0);
|
||||||
|
|
||||||
|
// 3. 转换间隙信息
|
||||||
|
if (reportReq.getGaps() != null && !reportReq.getGaps().isEmpty()) {
|
||||||
|
List<DeviceVideoContinuityCache.GapInfo> gapInfos = reportReq.getGaps().stream()
|
||||||
|
.map(gap -> new DeviceVideoContinuityCache.GapInfo(
|
||||||
|
gap.getBeforeFileName(),
|
||||||
|
gap.getAfterFileName(),
|
||||||
|
gap.getGapMs(),
|
||||||
|
gap.getGapStartTime(),
|
||||||
|
gap.getGapEndTime()
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
cache.setGaps(gapInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 存储到Redis
|
||||||
|
String redisKey = REDIS_KEY_PREFIX + deviceId;
|
||||||
|
String cacheJson = objectMapper.writeValueAsString(cache);
|
||||||
|
redisTemplate.opsForValue().set(redisKey, cacheJson, CACHE_TTL_HOURS, TimeUnit.HOURS);
|
||||||
|
|
||||||
|
log.info("设备 {} (ID: {}) 视频连续性检查结果上报成功: continuous={}, videos={}, gaps={}",
|
||||||
|
reportReq.getDeviceNo(), deviceId, cache.getContinuous(),
|
||||||
|
cache.getTotalVideos(), cache.getGapCount());
|
||||||
|
|
||||||
|
return ApiResponse.success(cache);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("外部工具上报设备 {} 视频连续性检查结果失败", reportReq.getDeviceNo(), e);
|
||||||
|
return ApiResponse.buildResponse(500, null, "上报失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.model.pc.extraDevice.req.ExtraDevicePageQueryReq;
|
||||||
|
import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO;
|
||||||
|
import com.ycwl.basic.service.pc.ExtraDeviceService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部设备管理控制器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/extra_device/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExtraDeviceController {
|
||||||
|
|
||||||
|
private final ExtraDeviceService extraDeviceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询外部设备列表
|
||||||
|
*
|
||||||
|
* @param req 查询请求参数,包含scenicId(可选)、pageNum、pageSize
|
||||||
|
* @return 分页查询结果,包含设备ID、景区ID、景区名称、设备名称、标识、状态、心跳时间、在线状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/page")
|
||||||
|
public ApiResponse<PageInfo<ExtraDeviceRespVO>> page(@RequestBody ExtraDevicePageQueryReq req) {
|
||||||
|
log.info("分页查询外部设备列表, scenicId: {}, pageNum: {}, pageSize: {}",
|
||||||
|
req.getScenicId(), req.getPageNum(), req.getPageSize());
|
||||||
|
|
||||||
|
PageInfo<ExtraDeviceRespVO> pageInfo = extraDeviceService.pageQuery(req);
|
||||||
|
|
||||||
|
log.info("外部设备列表查询完成, total: {}, pages: {}",
|
||||||
|
pageInfo.getTotal(), pageInfo.getPages());
|
||||||
|
|
||||||
|
return ApiResponse.success(pageInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.service.ProfitShareIntegrationService;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账管理 V2 版本控制器 - 基于 zt-profitshare 集成服务
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/profit-share/v2")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProfitShareV2Controller {
|
||||||
|
|
||||||
|
private final ProfitShareIntegrationService profitShareIntegrationService;
|
||||||
|
|
||||||
|
// ========== 分账规则管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分账规则
|
||||||
|
*/
|
||||||
|
@PostMapping("/rules")
|
||||||
|
public ApiResponse<RuleVO> createRule(@Valid @RequestBody CreateRuleRequest request) {
|
||||||
|
log.info("创建分账规则, scenicId: {}, ruleName: {}, ruleType: {}",
|
||||||
|
request.getScenicId(), request.getRuleName(), request.getRuleType());
|
||||||
|
try {
|
||||||
|
RuleVO rule = profitShareIntegrationService.createRule(request);
|
||||||
|
return ApiResponse.success(rule);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建分账规则失败", e);
|
||||||
|
return ApiResponse.fail("创建分账规则失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账规则列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/rules")
|
||||||
|
public ApiResponse<PageResponse<RuleVO>> listRules(
|
||||||
|
@RequestParam(required = false) Long scenicId,
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String ruleType,
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
log.info("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
|
||||||
|
scenicId, status, ruleType, page, pageSize);
|
||||||
|
|
||||||
|
// 参数验证:限制pageSize最大值为100
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageResponse<RuleVO> response = profitShareIntegrationService.listRules(scenicId, status, ruleType, page, pageSize);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询分账规则列表失败", e);
|
||||||
|
return ApiResponse.fail("查询分账规则列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分账规则详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/rules/{id}")
|
||||||
|
public ApiResponse<RuleVO> getRule(@PathVariable Long id) {
|
||||||
|
log.info("获取分账规则详情, id: {}", id);
|
||||||
|
try {
|
||||||
|
RuleVO rule = profitShareIntegrationService.getRule(id);
|
||||||
|
return ApiResponse.success(rule);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取分账规则详情失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取分账规则详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分账规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}")
|
||||||
|
public ApiResponse<RuleVO> updateRule(@PathVariable Long id, @Valid @RequestBody CreateRuleRequest request) {
|
||||||
|
log.info("更新分账规则, id: {}", id);
|
||||||
|
try {
|
||||||
|
RuleVO rule = profitShareIntegrationService.updateRule(id, request);
|
||||||
|
return ApiResponse.success(rule);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新分账规则失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("更新分账规则失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用分账规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}/enable")
|
||||||
|
public ApiResponse<String> enableRule(@PathVariable Long id) {
|
||||||
|
log.info("启用分账规则, id: {}", id);
|
||||||
|
try {
|
||||||
|
profitShareIntegrationService.enableRule(id);
|
||||||
|
return ApiResponse.success("规则已启用");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("启用分账规则失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("启用分账规则失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用分账规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}/disable")
|
||||||
|
public ApiResponse<String> disableRule(@PathVariable Long id) {
|
||||||
|
log.info("禁用分账规则, id: {}", id);
|
||||||
|
try {
|
||||||
|
profitShareIntegrationService.disableRule(id);
|
||||||
|
return ApiResponse.success("规则已禁用");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("禁用分账规则失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("禁用分账规则失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分账规则
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/rules/{id}")
|
||||||
|
public ApiResponse<String> deleteRule(@PathVariable Long id) {
|
||||||
|
log.info("删除分账规则, id: {}", id);
|
||||||
|
try {
|
||||||
|
profitShareIntegrationService.deleteRule(id);
|
||||||
|
return ApiResponse.success("规则已删除");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除分账规则失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("删除分账规则失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 分账记录查询 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询景区分账记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/scenic/{scenicId}")
|
||||||
|
public ApiResponse<PageResponse<RecordVO>> getRecordsByScenic(
|
||||||
|
@PathVariable Long scenicId,
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
log.info("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
||||||
|
|
||||||
|
// 参数验证:限制pageSize最大值为100
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageResponse<RecordVO> response = profitShareIntegrationService.getRecordsByScenic(scenicId, page, pageSize);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询景区分账记录失败, scenicId: {}", scenicId, e);
|
||||||
|
return ApiResponse.fail("查询景区分账记录失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账记录详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/{id}")
|
||||||
|
public ApiResponse<RecordDetailVO> getRecordById(@PathVariable Long id) {
|
||||||
|
log.info("查询分账记录详情, id: {}", id);
|
||||||
|
try {
|
||||||
|
RecordDetailVO record = profitShareIntegrationService.getRecordById(id);
|
||||||
|
return ApiResponse.success(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询分账记录详情失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("查询分账记录详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按订单ID查询分账记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/order/{orderId}")
|
||||||
|
public ApiResponse<RecordDetailVO> getRecordByOrderId(@PathVariable String orderId) {
|
||||||
|
log.info("按订单ID查询分账记录, orderId: {}", orderId);
|
||||||
|
try {
|
||||||
|
RecordDetailVO record = profitShareIntegrationService.getRecordByOrderId(orderId);
|
||||||
|
return ApiResponse.success(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("按订单ID查询分账记录失败, orderId: {}", orderId, e);
|
||||||
|
return ApiResponse.fail("按订单ID查询分账记录失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 手动分账与计算 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发分账
|
||||||
|
*/
|
||||||
|
@PostMapping("/manual")
|
||||||
|
public ApiResponse<String> manualShare(@Valid @RequestBody ManualShareRequest request) {
|
||||||
|
log.info("手动触发分账, orderId: {}", request.getOrderId());
|
||||||
|
try {
|
||||||
|
profitShareIntegrationService.manualShare(request.getOrderId());
|
||||||
|
return ApiResponse.success("手动分账触发成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("手动触发分账失败, orderId: {}", request.getOrderId(), e);
|
||||||
|
return ApiResponse.fail("手动触发分账失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分账结果(不执行)
|
||||||
|
*/
|
||||||
|
@PostMapping("/calculate")
|
||||||
|
public ApiResponse<CalculateResultVO> calculateShare(@Valid @RequestBody CalculateShareRequest request) {
|
||||||
|
log.info("计算分账结果, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
|
||||||
|
try {
|
||||||
|
CalculateResultVO result = profitShareIntegrationService.calculateShare(request);
|
||||||
|
return ApiResponse.success(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("计算分账结果失败", e);
|
||||||
|
return ApiResponse.fail("计算分账结果失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 类型查询 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的类型列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/types")
|
||||||
|
public ApiResponse<TypesVO> getSupportedTypes() {
|
||||||
|
log.info("获取支持的类型列表");
|
||||||
|
try {
|
||||||
|
TypesVO types = profitShareIntegrationService.getSupportedTypes();
|
||||||
|
return ApiResponse.success(types);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取支持的类型列表失败", e);
|
||||||
|
return ApiResponse.fail("获取支持的类型列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,16 @@ import com.ycwl.basic.annotation.IgnoreToken;
|
|||||||
import com.ycwl.basic.model.jwt.JwtInfo;
|
import com.ycwl.basic.model.jwt.JwtInfo;
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||||
|
import com.ycwl.basic.model.printer.req.CreateVirtualOrderRequest;
|
||||||
import com.ycwl.basic.service.pc.SourceService;
|
import com.ycwl.basic.service.pc.SourceService;
|
||||||
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author:longbinbin
|
* @Author:longbinbin
|
||||||
* @Date:2024/12/3 15:45
|
* @Date:2024/12/3 15:45
|
||||||
@@ -21,6 +25,8 @@ public class SourceController {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SourceService sourceService;
|
private SourceService sourceService;
|
||||||
|
@Autowired
|
||||||
|
private PrinterService printerService;
|
||||||
|
|
||||||
// 分页查询视频源
|
// 分页查询视频源
|
||||||
@PostMapping("/page")
|
@PostMapping("/page")
|
||||||
@@ -45,5 +51,26 @@ public class SourceController {
|
|||||||
return sourceService.deleteById(id);
|
return sourceService.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建虚拟用户0元订单
|
||||||
|
* 用于后台直接从source创建订单,不需要真实用户
|
||||||
|
*
|
||||||
|
* @param request 请求参数
|
||||||
|
* @return 订单信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/createVirtualOrder")
|
||||||
|
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody CreateVirtualOrderRequest request) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> result = printerService.createVirtualOrder(
|
||||||
|
request.getSourceId(),
|
||||||
|
request.getScenicId(),
|
||||||
|
request.getPrinterId()
|
||||||
|
);
|
||||||
|
return ApiResponse.success(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ApiResponse.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,23 @@
|
|||||||
package com.ycwl.basic.controller.printer;
|
package com.ycwl.basic.controller.printer;
|
||||||
|
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
|
|
||||||
import com.ycwl.basic.mapper.FaceMapper;
|
|
||||||
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
|
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
|
||||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
|
||||||
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
|
|
||||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
|
||||||
import com.ycwl.basic.repository.SourceRepository;
|
|
||||||
import com.ycwl.basic.service.printer.PrinterService;
|
|
||||||
import com.ycwl.basic.service.task.TaskFaceService;
|
|
||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
|
||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
import com.ycwl.basic.storage.StorageFactory;
|
|
||||||
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
|
||||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||||
import com.ycwl.basic.mapper.SourceMapper;
|
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||||
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
|
||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||||
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
import com.ycwl.basic.model.printer.FaceRecognizeWithSourcesResp;
|
|
||||||
import com.ycwl.basic.repository.DeviceRepository;
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.repository.FaceRepository;
|
import com.ycwl.basic.repository.FaceRepository;
|
||||||
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.repository.SourceRepository;
|
||||||
import com.ycwl.basic.service.pc.FaceService;
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
|
||||||
import com.ycwl.basic.storage.utils.StorageUtil;
|
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import com.ycwl.basic.utils.SnowFlakeUtil;
|
|
||||||
import com.ycwl.basic.utils.WxMpUtil;
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -45,12 +32,8 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.ycwl.basic.constant.StorageConstant.USER_FACE;
|
|
||||||
|
|
||||||
@IgnoreToken
|
@IgnoreToken
|
||||||
// 打印机大屏对接接口
|
// 打印机大屏对接接口
|
||||||
@@ -62,14 +45,9 @@ public class PrinterTvController {
|
|||||||
private final DeviceRepository deviceRepository;
|
private final DeviceRepository deviceRepository;
|
||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
private final FaceRepository faceRepository;
|
private final FaceRepository faceRepository;
|
||||||
private final TaskFaceService faceService;
|
|
||||||
private final FaceService pcFaceService;
|
private final FaceService pcFaceService;
|
||||||
private final ScenicService scenicService;
|
|
||||||
private final SourceMapper sourceMapper;
|
|
||||||
private final FaceMapper faceMapper;
|
|
||||||
private final MemberRelationRepository memberRelationRepository;
|
private final MemberRelationRepository memberRelationRepository;
|
||||||
private final SourceRepository sourceRepository;
|
private final SourceRepository sourceRepository;
|
||||||
private final PrinterService printerService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取景区列表
|
* 获取景区列表
|
||||||
@@ -191,85 +169,37 @@ public class PrinterTvController {
|
|||||||
/**
|
/**
|
||||||
* 根据人脸样本ID查询图像素材
|
* 根据人脸样本ID查询图像素材
|
||||||
*
|
*
|
||||||
* @param faceSampleId 人脸样本ID
|
* @param faceId 人脸样本ID
|
||||||
* @return type=2且face_sample_id匹配的source记录
|
* @return 匹配的source记录
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{faceSampleId}/source")
|
@GetMapping("/{faceId}/source")
|
||||||
public ApiResponse<SourceEntity> getSourceByFaceSampleId(@PathVariable Long faceSampleId) {
|
public ApiResponse<List<SourceEntity>> getSourceByFaceId(@PathVariable Long faceId, @RequestParam(name = "type", required = false, defaultValue = "2") Integer type) {
|
||||||
SourceEntity source = sourceMapper.getBySampleIdAndType(faceSampleId, 2);
|
List<MemberSourceEntity> source = memberRelationRepository.listSourceByFaceRelation(faceId, type);
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
return ApiResponse.fail("未找到对应的图像素材");
|
return ApiResponse.success(Collections.emptyList());
|
||||||
}
|
}
|
||||||
return ApiResponse.success(source);
|
return ApiResponse.success(source.stream().map(item -> sourceRepository.getSource(item.getSourceId())).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打印机大屏人脸识别
|
* 打印机大屏人脸识别
|
||||||
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果和匹配到的图像素材
|
* 上传照片,在景区人脸库中搜索匹配的人脸样本,返回识别结果
|
||||||
|
*
|
||||||
|
* 使用 USER_FACE_DB_NAME+scenicId 对人脸进行去重检测:
|
||||||
|
* - 如果已存在相同人脸(打印机大屏用户,memberId=0),则返回已存在的 faceId
|
||||||
|
* - 否则创建新的人脸记录并添加到人脸库
|
||||||
*
|
*
|
||||||
* @param file 人脸照片文件
|
* @param file 人脸照片文件
|
||||||
* @param scenicId 景区ID
|
* @param scenicId 景区ID
|
||||||
* @return 人脸识别结果和匹配的source列表
|
* @return 人脸识别结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/{scenicId}/faceRecognize")
|
@PostMapping("/{scenicId}/faceRecognize")
|
||||||
public ApiResponse<FaceRecognizeWithSourcesResp> faceRecognize(
|
public ApiResponse<FaceRecognizeResp> faceRecognize(
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
@PathVariable Long scenicId) throws Exception {
|
@PathVariable Long scenicId) {
|
||||||
|
// 复用 faceUpload 方法的去重逻辑
|
||||||
// 1. 上传人脸照片到存储
|
// memberId=0L 表示打印机大屏用户,scene="tv" 为试点场景:仅执行识别/补救/落库/建关系
|
||||||
IStorageAdapter adapter = StorageFactory.use("faces");
|
FaceRecognizeResp resp = pcFaceService.faceUpload(file, scenicId, 0L, "tv");
|
||||||
String filePath = StorageUtil.joinPath(USER_FACE, DateUtil.format(new Date(), "yyyy-MM-dd"));
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
String suffix = originalFilename.split("\\.", 2)[1];
|
|
||||||
String fileName = UUID.randomUUID() + "." + suffix;
|
|
||||||
String faceUrl = adapter.uploadFile(file, filePath, fileName);
|
|
||||||
|
|
||||||
// 2. 保存人脸数据到数据库
|
|
||||||
Long faceId = SnowFlakeUtil.getLongId();
|
|
||||||
FaceEntity faceEntity = new FaceEntity();
|
|
||||||
faceEntity.setId(faceId);
|
|
||||||
faceEntity.setScenicId(scenicId);
|
|
||||||
faceEntity.setFaceUrl(faceUrl);
|
|
||||||
faceEntity.setCreateAt(new Date());
|
|
||||||
faceEntity.setMemberId(0L); // 打印机大屏端没有用户ID
|
|
||||||
faceMapper.add(faceEntity);
|
|
||||||
|
|
||||||
// 3. 在景区人脸库中搜索(注意:这里使用scenicId作为数据库名,搜索的是景区内的人脸样本)
|
|
||||||
pcFaceService.matchFaceId(faceId);
|
|
||||||
|
|
||||||
// 4. 自动添加照片到打印列表,并获取添加成功的照片列表
|
|
||||||
List<SourceEntity> addedSources = printerService.autoAddPhotosToPreferPrint(faceId);
|
|
||||||
|
|
||||||
// 5. 根据自动添加结果决定返回的sources
|
|
||||||
List<SourceEntity> sources;
|
|
||||||
if (addedSources != null && !addedSources.isEmpty()) {
|
|
||||||
// 如果自动添加成功,返回添加的照片列表
|
|
||||||
sources = addedSources;
|
|
||||||
} else {
|
|
||||||
// 如果自动添加为空,按原逻辑查询匹配到的图像素材(type=2)
|
|
||||||
sources = new ArrayList<>();
|
|
||||||
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
|
|
||||||
for (MemberSourceEntity memberSourceEntity : memberSourceEntities) {
|
|
||||||
SourceEntity source = sourceRepository.getSource(memberSourceEntity.getSourceId());
|
|
||||||
if (source != null) {
|
|
||||||
sources.add(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 构造响应
|
|
||||||
FaceRecognizeWithSourcesResp resp = new FaceRecognizeWithSourcesResp();
|
|
||||||
resp.setUrl(faceUrl);
|
|
||||||
resp.setFaceId(faceId);
|
|
||||||
resp.setScenicId(scenicId);
|
|
||||||
resp.setSources(sources);
|
|
||||||
// 只有当添加了照片时才返回二维码URL
|
|
||||||
if (addedSources != null && !addedSources.isEmpty()) {
|
|
||||||
resp.setQrcodeUrl("https://zhentuai.com/printer/v1/tv/face/" + faceId + "/qrcode");
|
|
||||||
} else {
|
|
||||||
resp.setQrcodeUrl(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse.success(resp);
|
return ApiResponse.success(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
src/main/java/com/ycwl/basic/enums/FaceCutStatus.java
Normal file
65
src/main/java/com/ycwl/basic/enums/FaceCutStatus.java
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package com.ycwl.basic.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸视频切片状态枚举
|
||||||
|
*/
|
||||||
|
public enum FaceCutStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正在切片中
|
||||||
|
*/
|
||||||
|
CUTTING(0, "正在切片中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切片已完成
|
||||||
|
*/
|
||||||
|
COMPLETED(1, "切片已完成"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待用户选择模板
|
||||||
|
*/
|
||||||
|
WAITING_USER_SELECT(2, "等待用户选择模板");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
FaceCutStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举
|
||||||
|
*/
|
||||||
|
public static FaceCutStatus fromCode(int code) {
|
||||||
|
for (FaceCutStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown FaceCutStatus code: " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举,如果不存在则返回默认值
|
||||||
|
* @param code 状态码
|
||||||
|
* @param defaultStatus 默认状态
|
||||||
|
* @return 枚举值
|
||||||
|
*/
|
||||||
|
public static FaceCutStatus fromCodeOrDefault(int code, FaceCutStatus defaultStatus) {
|
||||||
|
for (FaceCutStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.ycwl.basic.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸片段更新状态枚举
|
||||||
|
* 用于标记人脸对应的视频片段是否有新增更新
|
||||||
|
*/
|
||||||
|
public enum FacePieceUpdateStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有新片段
|
||||||
|
* Redis键不存在时的默认状态,代表有新的视频片段产生
|
||||||
|
*/
|
||||||
|
HAS_NEW_PIECES(0, "有新片段"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无新片段
|
||||||
|
* Redis键存在时的状态,代表当前没有新的视频片段
|
||||||
|
*/
|
||||||
|
NO_NEW_PIECES(1, "无新片段");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
FacePieceUpdateStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举
|
||||||
|
*/
|
||||||
|
public static FacePieceUpdateStatus fromCode(int code) {
|
||||||
|
for (FacePieceUpdateStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown FacePieceUpdateStatus code: " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据Redis键是否存在判断状态
|
||||||
|
* @param keyExists Redis键是否存在
|
||||||
|
* @return 键存在返回NO_NEW_PIECES,键不存在返回HAS_NEW_PIECES
|
||||||
|
*/
|
||||||
|
public static FacePieceUpdateStatus fromKeyExists(boolean keyExists) {
|
||||||
|
return keyExists ? NO_NEW_PIECES : HAS_NEW_PIECES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有新片段
|
||||||
|
*/
|
||||||
|
public boolean hasNewPieces() {
|
||||||
|
return this == HAS_NEW_PIECES;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java
Normal file
75
src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ycwl.basic.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸对应模板渲染状态枚举
|
||||||
|
*/
|
||||||
|
public enum TemplateRenderStatus {
|
||||||
|
|
||||||
|
NONE(0, "没有渲染"),
|
||||||
|
/**
|
||||||
|
* 正在渲染中
|
||||||
|
*/
|
||||||
|
RENDERING(1, "正在渲染中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已渲染完成
|
||||||
|
*/
|
||||||
|
RENDERED(2, "已渲染完成");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
TemplateRenderStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举
|
||||||
|
*/
|
||||||
|
public static TemplateRenderStatus fromCode(int code) {
|
||||||
|
for (TemplateRenderStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown TemplateRenderStatus code: " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举,如果不存在则返回默认值
|
||||||
|
* @param code 状态码
|
||||||
|
* @param defaultStatus 默认状态
|
||||||
|
* @return 枚举值
|
||||||
|
*/
|
||||||
|
public static TemplateRenderStatus fromCodeOrDefault(int code, TemplateRenderStatus defaultStatus) {
|
||||||
|
for (TemplateRenderStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否已完成渲染
|
||||||
|
*/
|
||||||
|
public boolean isRendered() {
|
||||||
|
return this == RENDERED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否正在渲染
|
||||||
|
*/
|
||||||
|
public boolean isRendering() {
|
||||||
|
return this == RENDERING;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java
Normal file
159
src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package com.ycwl.basic.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频任务状态枚举
|
||||||
|
* 用于前端展示任务状态
|
||||||
|
*/
|
||||||
|
public enum VideoTaskStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无效人脸(景区级别)
|
||||||
|
*/
|
||||||
|
INVALID_FACE_SCENIC(-2, "尚未录入有效人脸"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无效人脸(人脸级别)
|
||||||
|
*/
|
||||||
|
INVALID_FACE(-1, "尚未录入有效人脸"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待制作
|
||||||
|
* 人脸已录入,但尚未开始合成视频
|
||||||
|
*/
|
||||||
|
PENDING(0, "专属视频待制作"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合成成功
|
||||||
|
* 已为用户合成视频
|
||||||
|
*/
|
||||||
|
SUCCESS(1, "AI已为您合成视频"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合成中
|
||||||
|
* 正在合成专属视频
|
||||||
|
*/
|
||||||
|
PROCESSING(2, "专属视频合成中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合成失败
|
||||||
|
*/
|
||||||
|
FAILED(3, "视频合成失败"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切片中
|
||||||
|
* 正在检索新的视频片段
|
||||||
|
*/
|
||||||
|
CUTTING(4, "正在检索新的视频片段");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
VideoTaskStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举
|
||||||
|
*/
|
||||||
|
public static VideoTaskStatus fromCode(int code) {
|
||||||
|
for (VideoTaskStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown VideoTaskStatus code: " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code获取枚举,如果不存在则返回默认值
|
||||||
|
* @param code 状态码
|
||||||
|
* @param defaultStatus 默认状态
|
||||||
|
* @return 枚举值
|
||||||
|
*/
|
||||||
|
public static VideoTaskStatus fromCodeOrDefault(int code, VideoTaskStatus defaultStatus) {
|
||||||
|
for (VideoTaskStatus status : values()) {
|
||||||
|
if (status.code == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取前端展示文案
|
||||||
|
* @param count 已合成视频数量(仅SUCCESS状态使用)
|
||||||
|
* @return 展示文案
|
||||||
|
*/
|
||||||
|
public String getDisplayText(long count) {
|
||||||
|
if (this == SUCCESS && count > 0) {
|
||||||
|
return "AI已为您合成" + count + "个视频";
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据业务逻辑判断最终展示状态
|
||||||
|
* @param taskStatus 任务状态码
|
||||||
|
* @param cutStatus 切片状态码(来自FaceCutStatus)
|
||||||
|
* @param count 已合成视频数量
|
||||||
|
* @return 最终展示的状态
|
||||||
|
*/
|
||||||
|
public static VideoTaskStatus resolveDisplayStatus(int taskStatus, int cutStatus, long count) {
|
||||||
|
VideoTaskStatus status = fromCodeOrDefault(taskStatus, PENDING);
|
||||||
|
|
||||||
|
// 优先级1: 无效人脸状态
|
||||||
|
if (status == INVALID_FACE_SCENIC || status == INVALID_FACE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先级2: 切片状态优先(当任务状态为待制作且切片状态为正在切片时)
|
||||||
|
if (status == PENDING && cutStatus == 0) {
|
||||||
|
return CUTTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先级3: 返回任务状态
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最终展示文案
|
||||||
|
* @param taskStatus 任务状态码
|
||||||
|
* @param cutStatus 切片状态码
|
||||||
|
* @param count 已合成视频数量
|
||||||
|
* @return 展示文案
|
||||||
|
*/
|
||||||
|
public static String getDisplayText(int taskStatus, int cutStatus, long count) {
|
||||||
|
VideoTaskStatus status = resolveDisplayStatus(taskStatus, cutStatus, count);
|
||||||
|
return status.getDisplayText(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为成功状态
|
||||||
|
*/
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return this == SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为处理中状态
|
||||||
|
*/
|
||||||
|
public boolean isProcessing() {
|
||||||
|
return this == PROCESSING || this == CUTTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为失败状态
|
||||||
|
*/
|
||||||
|
public boolean isFailed() {
|
||||||
|
return this == FAILED || this == INVALID_FACE || this == INVALID_FACE_SCENIC;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,5 +21,11 @@ public enum FaceMatchingScene {
|
|||||||
* 仅识别
|
* 仅识别
|
||||||
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
|
* 只执行人脸识别,不处理后续业务逻辑(源文件关联、任务创建等)
|
||||||
*/
|
*/
|
||||||
RECOGNITION_ONLY
|
RECOGNITION_ONLY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机大屏识别试点
|
||||||
|
* 仅执行:识别、补救、落库、建关系
|
||||||
|
*/
|
||||||
|
PRINTER_TV_RECOGNIZE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,40 @@ public class FaceMatchingPipelineFactory {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ScenicConfigFacade scenicConfigFacade;
|
private ScenicConfigFacade scenicConfigFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建打印机大屏识别试点Pipeline
|
||||||
|
* 仅执行:识别、补救、落库、建关系
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - “准备上下文”属于基础能力,默认包含
|
||||||
|
* - “建关系”包含构建关联与持久化两步
|
||||||
|
*/
|
||||||
|
public Pipeline<FaceMatchingContext> createPrinterTvRecognizePipeline() {
|
||||||
|
PipelineBuilder<FaceMatchingContext> builder = new PipelineBuilder<>("PrinterTvRecognize");
|
||||||
|
|
||||||
|
// 1. 准备上下文
|
||||||
|
builder.addStage(prepareContextStage);
|
||||||
|
|
||||||
|
// 2. 执行人脸识别
|
||||||
|
builder.addStage(faceRecognitionStage);
|
||||||
|
|
||||||
|
// 3. 人脸识别补救
|
||||||
|
builder.addStage(faceRecoveryStage);
|
||||||
|
|
||||||
|
// 4. 更新人脸结果(落库)
|
||||||
|
builder.addStage(updateFaceResultStage);
|
||||||
|
|
||||||
|
// 5. 构建源文件关联(建关系)
|
||||||
|
builder.addStage(buildSourceRelationStage);
|
||||||
|
|
||||||
|
// 6. 持久化关联关系(建关系)
|
||||||
|
builder.addStage(persistRelationsStage);
|
||||||
|
|
||||||
|
log.debug("创建打印机大屏识别试点Pipeline: stageCount={}", builder.build().getStageCount());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建自动人脸匹配Pipeline
|
* 创建自动人脸匹配Pipeline
|
||||||
*
|
*
|
||||||
@@ -219,6 +253,7 @@ public class FaceMatchingPipelineFactory {
|
|||||||
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
|
case AUTO_MATCHING -> createAutoMatchingPipeline(isNew);
|
||||||
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
|
case CUSTOM_MATCHING -> createCustomMatchingPipeline();
|
||||||
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
|
case RECOGNITION_ONLY -> createRecognitionOnlyPipeline();
|
||||||
|
case PRINTER_TV_RECOGNIZE -> createPrinterTvRecognizePipeline();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ycwl.basic.face.pipeline.stages;
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.biz.TaskStatusBiz;
|
import com.ycwl.basic.biz.FaceStatusManager;
|
||||||
|
import com.ycwl.basic.enums.FaceCutStatus;
|
||||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
@@ -37,7 +38,7 @@ public class CreateTaskStage extends AbstractPipelineStage<FaceMatchingContext>
|
|||||||
private TaskService taskService;
|
private TaskService taskService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TaskStatusBiz taskStatusBiz;
|
private FaceStatusManager faceStatusManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -59,7 +60,7 @@ public class CreateTaskStage extends AbstractPipelineStage<FaceMatchingContext>
|
|||||||
return StageResult.success("自动创建任务成功");
|
return StageResult.success("自动创建任务成功");
|
||||||
} else {
|
} else {
|
||||||
// 配置为等待用户选择
|
// 配置为等待用户选择
|
||||||
taskStatusBiz.setFaceCutStatus(faceId, 2);
|
faceStatusManager.setFaceCutStatus(faceId, FaceCutStatus.WAITING_USER_SELECT);
|
||||||
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
|
log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId);
|
||||||
return StageResult.skipped("等待用户手动选择");
|
return StageResult.skipped("等待用户手动选择");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ public class PrepareContextStage extends AbstractPipelineStage<FaceMatchingConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 加载景区配置
|
// 3. 加载景区配置
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
context.setScenicConfig(scenicConfig);
|
context.setScenicConfig(configManager);
|
||||||
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
|
log.debug("加载景区配置成功: scenicId={}", face.getScenicId());
|
||||||
|
|
||||||
// 4. 加载人脸识别适配器
|
// 4. 加载人脸识别适配器
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ycwl.basic.face.pipeline.stages;
|
package com.ycwl.basic.face.pipeline.stages;
|
||||||
|
|
||||||
import com.ycwl.basic.biz.TaskStatusBiz;
|
import com.ycwl.basic.biz.FaceStatusManager;
|
||||||
|
import com.ycwl.basic.enums.FaceCutStatus;
|
||||||
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
|
||||||
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||||
@@ -28,7 +29,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContext> {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TaskStatusBiz taskStatusBiz;
|
private FaceStatusManager faceStatusManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -56,7 +57,7 @@ public class SetTaskStatusStage extends AbstractPipelineStage<FaceMatchingContex
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
taskStatusBiz.setFaceCutStatus(faceId, 0);
|
faceStatusManager.setFaceCutStatus(faceId, FaceCutStatus.CUTTING);
|
||||||
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
|
log.debug("设置新用户任务状态: faceId={}, status=0", faceId);
|
||||||
return StageResult.success("任务状态已设置");
|
return StageResult.success("任务状态已设置");
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Currently implemented:
|
|||||||
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
||||||
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
||||||
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
|
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
|
||||||
|
- **Profit Share Integration** (`com.ycwl.basic.integration.profitshare`): ZT-ProfitShare microservice integration for revenue sharing
|
||||||
- **Message Integration** (`com.ycwl.basic.integration.message`): ZT-Message Kafka producer integration
|
- **Message Integration** (`com.ycwl.basic.integration.message`): ZT-Message Kafka producer integration
|
||||||
|
|
||||||
### Integration Pattern
|
### Integration Pattern
|
||||||
@@ -1715,3 +1716,417 @@ integration:
|
|||||||
- Cache frequently accessed questionnaires
|
- Cache frequently accessed questionnaires
|
||||||
- Monitor response submission patterns
|
- Monitor response submission patterns
|
||||||
- Implement rate limiting for public questionnaires
|
- Implement rate limiting for public questionnaires
|
||||||
|
|
||||||
|
## Profit Share Integration (ZT-ProfitShare Microservice)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
The zt-profitshare microservice provides comprehensive revenue sharing management for scenic areas, supporting multiple payment systems (Alipay, WeChat, UnionPay) and various distribution rules. It offers both HTTP REST API for management operations and Kafka messaging for automatic profit sharing triggered by payment events.
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
#### Feign Client
|
||||||
|
- **ProfitShareClient**: Complete profit share operations (rules, records, manual sharing, calculations)
|
||||||
|
|
||||||
|
#### Services
|
||||||
|
- **ProfitShareIntegrationService**: High-level profit share operations (with automatic fallback for queries)
|
||||||
|
- **ProfitShareKafkaProducer**: Kafka message producer for profit share and refund events
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
```yaml
|
||||||
|
integration:
|
||||||
|
profitshare:
|
||||||
|
enabled: true
|
||||||
|
serviceName: zt-profitshare
|
||||||
|
connectTimeout: 5000
|
||||||
|
readTimeout: 10000
|
||||||
|
retryEnabled: false
|
||||||
|
maxRetries: 3
|
||||||
|
fallback:
|
||||||
|
profitshare:
|
||||||
|
enabled: true
|
||||||
|
ttlDays: 7
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
enabled: true
|
||||||
|
profit-share-topic: zt-profitshare
|
||||||
|
refund-topic: zt-refund
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage Examples
|
||||||
|
|
||||||
|
#### Rule Management Operations
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private ProfitShareIntegrationService profitShareService;
|
||||||
|
|
||||||
|
// Create profit share rule (direct operation, fails immediately on error)
|
||||||
|
CreateRuleRequest ruleRequest = new CreateRuleRequest();
|
||||||
|
ruleRequest.setScenicId(1001L);
|
||||||
|
ruleRequest.setRuleName("标准分账规则");
|
||||||
|
ruleRequest.setRuleType("percentage");
|
||||||
|
ruleRequest.setDescription("平台收取5%手续费");
|
||||||
|
|
||||||
|
// Add platform recipient
|
||||||
|
CreateRecipientRequest platform = new CreateRecipientRequest();
|
||||||
|
platform.setRecipientName("平台手续费");
|
||||||
|
platform.setRecipientType("platform");
|
||||||
|
platform.setAccountInfo("platform_001");
|
||||||
|
platform.setShareType("percentage");
|
||||||
|
platform.setShareValue(5.0);
|
||||||
|
platform.setPriority(1);
|
||||||
|
|
||||||
|
Map<String, Object> platformExt = new HashMap<>();
|
||||||
|
platformExt.put("payment_system", "alipay");
|
||||||
|
platformExt.put("sub_merchant_id", "platform_001");
|
||||||
|
platform.setExtConfig(platformExt);
|
||||||
|
|
||||||
|
// Add scenic recipient
|
||||||
|
CreateRecipientRequest scenic = new CreateRecipientRequest();
|
||||||
|
scenic.setRecipientName("景区收款账户");
|
||||||
|
scenic.setRecipientType("merchant");
|
||||||
|
scenic.setAccountInfo("merchant_001");
|
||||||
|
scenic.setShareType("percentage");
|
||||||
|
scenic.setShareValue(95.0);
|
||||||
|
scenic.setPriority(2);
|
||||||
|
|
||||||
|
Map<String, Object> scenicExt = new HashMap<>();
|
||||||
|
scenicExt.put("payment_system", "alipay");
|
||||||
|
scenicExt.put("sub_merchant_id", "scenic_001");
|
||||||
|
scenicExt.put("settle_period", "T+1");
|
||||||
|
scenic.setExtConfig(scenicExt);
|
||||||
|
|
||||||
|
ruleRequest.setRecipients(Arrays.asList(platform, scenic));
|
||||||
|
|
||||||
|
RuleVO createdRule = profitShareService.createRule(ruleRequest);
|
||||||
|
log.info("分账规则创建成功: {}", createdRule.getId());
|
||||||
|
|
||||||
|
// Get rule details (automatically falls back to cache on failure)
|
||||||
|
RuleVO rule = profitShareService.getRule(ruleId);
|
||||||
|
log.info("规则名称: {}, 状态: {}", rule.getRuleName(), rule.getStatus());
|
||||||
|
|
||||||
|
// List rules (automatically falls back to cache on failure)
|
||||||
|
PageResponse<RuleVO> rules = profitShareService.listRules(1001L, "active", "percentage", 1, 10);
|
||||||
|
log.info("查询到 {} 条规则", rules.getData().getTotal());
|
||||||
|
|
||||||
|
// Update rule (direct operation, fails immediately on error)
|
||||||
|
CreateRuleRequest updateRequest = new CreateRuleRequest();
|
||||||
|
updateRequest.setRuleName("更新后的分账规则");
|
||||||
|
RuleVO updated = profitShareService.updateRule(ruleId, updateRequest);
|
||||||
|
|
||||||
|
// Enable/disable rule (direct operations, fail immediately on error)
|
||||||
|
profitShareService.enableRule(ruleId);
|
||||||
|
profitShareService.disableRule(ruleId);
|
||||||
|
|
||||||
|
// Delete rule (direct operation, fails immediately on error)
|
||||||
|
profitShareService.deleteRule(ruleId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Record Query Operations (with Automatic Fallback)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Get scenic profit share records (automatically falls back to cache on failure)
|
||||||
|
PageResponse<RecordVO> records = profitShareService.getRecordsByScenic(1001L, 1, 10);
|
||||||
|
log.info("景区分账记录: {} 条", records.getData().getTotal());
|
||||||
|
|
||||||
|
records.getData().getList().forEach(record -> {
|
||||||
|
log.info("订单: {}, 金额: {}, 状态: {}",
|
||||||
|
record.getOrderId(), record.getTotalAmount(), record.getStatus());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get record detail by ID (automatically falls back to cache on failure)
|
||||||
|
RecordDetailVO detail = profitShareService.getRecordById(recordId);
|
||||||
|
log.info("分账记录详情:");
|
||||||
|
detail.getDetails().forEach(shareDetail -> {
|
||||||
|
log.info(" 接收人: {}, 金额: {}, 状态: {}",
|
||||||
|
shareDetail.getRecipientName(), shareDetail.getShareAmount(), shareDetail.getStatus());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get record by order ID (automatically falls back to cache on failure)
|
||||||
|
RecordDetailVO recordByOrder = profitShareService.getRecordByOrderId("ORDER_123456");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kafka Message Production
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private ProfitShareKafkaProducer profitShareProducer;
|
||||||
|
|
||||||
|
// Send profit share message after payment success
|
||||||
|
@Transactional
|
||||||
|
public void handleOrderPaymentSuccess(Order order) {
|
||||||
|
// 1. Update order status
|
||||||
|
order.setStatus("PAID");
|
||||||
|
order.setPaymentTime(new Date());
|
||||||
|
orderRepository.save(order);
|
||||||
|
|
||||||
|
// 2. Build profit share message
|
||||||
|
OrderMessage message = OrderMessage.of(
|
||||||
|
order.getOrderId(),
|
||||||
|
order.getScenicId(),
|
||||||
|
order.getTotalAmount().doubleValue(),
|
||||||
|
order.getPaymentChannel(), // "alipay", "wechat", "union"
|
||||||
|
order.getPaymentOrderId()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Send to Kafka (async profit sharing)
|
||||||
|
profitShareProducer.sendProfitShareMessage(message);
|
||||||
|
|
||||||
|
log.info("订单支付成功,已发送分账消息: orderId={}, amount={}",
|
||||||
|
order.getOrderId(), order.getTotalAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send refund message after refund success
|
||||||
|
@Transactional
|
||||||
|
public void handleOrderRefundSuccess(Order order, BigDecimal refundAmount) {
|
||||||
|
// 1. Update order status
|
||||||
|
order.setStatus("REFUNDED");
|
||||||
|
orderRepository.save(order);
|
||||||
|
|
||||||
|
// 2. Build refund message
|
||||||
|
RefundMessage message = RefundMessage.of(
|
||||||
|
order.getOrderId(),
|
||||||
|
order.getScenicId(),
|
||||||
|
refundAmount.doubleValue(),
|
||||||
|
order.getPaymentChannel(),
|
||||||
|
order.getRefundOrderId()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Send refund message
|
||||||
|
profitShareProducer.sendRefundMessage(message);
|
||||||
|
|
||||||
|
log.info("订单退款成功,已发送退款消息: orderId={}, amount={}",
|
||||||
|
order.getOrderId(), refundAmount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Manual Profit Sharing
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Manual trigger profit sharing (direct operation, fails immediately on error)
|
||||||
|
// Used for compensation scenarios or delayed profit sharing
|
||||||
|
profitShareService.manualShare("ORDER_123456");
|
||||||
|
log.info("手动分账触发成功");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Calculate Profit Share
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Calculate profit share without execution (automatically falls back to cache on failure)
|
||||||
|
CalculateShareRequest calcRequest = new CalculateShareRequest();
|
||||||
|
calcRequest.setScenicId(1001L);
|
||||||
|
calcRequest.setTotalAmount(1000.0);
|
||||||
|
calcRequest.setRuleType("percentage");
|
||||||
|
calcRequest.setRecipients(Arrays.asList(platform, scenic));
|
||||||
|
|
||||||
|
CalculateResultVO result = profitShareService.calculateShare(calcRequest);
|
||||||
|
log.info("总金额: {}, 分账明细:", result.getTotalAmount());
|
||||||
|
result.getDetails().forEach(detail -> {
|
||||||
|
log.info(" {}: {} 元", detail.getRecipientName(), detail.getShareAmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get supported types (automatically falls back to cache on failure)
|
||||||
|
TypesVO types = profitShareService.getSupportedTypes();
|
||||||
|
log.info("支持的规则类型: {}", types.getRuleTypes());
|
||||||
|
log.info("支持的接收人类型: {}", types.getRecipientTypes());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fallback Cache Management
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
// Check fallback cache status
|
||||||
|
boolean hasRuleCache = fallbackService.hasFallbackCache("zt-profitshare", "rule:1001");
|
||||||
|
boolean hasRecordsCache = fallbackService.hasFallbackCache("zt-profitshare", "records:scenic:1001:1:10");
|
||||||
|
|
||||||
|
// Get cache statistics
|
||||||
|
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats("zt-profitshare");
|
||||||
|
log.info("Profit share fallback cache: {} items, TTL: {} days",
|
||||||
|
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
|
||||||
|
|
||||||
|
// Clear specific cache
|
||||||
|
fallbackService.clearFallbackCache("zt-profitshare", "rule:1001");
|
||||||
|
|
||||||
|
// Clear all profit share caches
|
||||||
|
fallbackService.clearAllFallbackCache("zt-profitshare");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule Types and Configuration
|
||||||
|
|
||||||
|
#### Rule Types
|
||||||
|
- **percentage**: Percentage-based distribution (e.g., platform 5%, merchant 95%)
|
||||||
|
- **fixed_amount**: Fixed amount distribution
|
||||||
|
- **scaled_amount**: Tiered amount distribution based on order amount
|
||||||
|
|
||||||
|
#### Recipient Types
|
||||||
|
- **platform**: Platform service fee recipient
|
||||||
|
- **merchant**: Merchant/scenic area recipient
|
||||||
|
- **agent**: Agent/distributor recipient
|
||||||
|
|
||||||
|
#### Share Types
|
||||||
|
- **percentage**: Share as percentage of total amount
|
||||||
|
- **fixed_amount**: Share as fixed amount
|
||||||
|
|
||||||
|
#### Payment Systems
|
||||||
|
- **alipay**: Alipay payment system
|
||||||
|
- **wechat**: WeChat Pay payment system
|
||||||
|
- **union**: UnionPay payment system
|
||||||
|
|
||||||
|
### Extended Configuration Examples
|
||||||
|
|
||||||
|
#### Alipay Configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payment_system": "alipay",
|
||||||
|
"sub_merchant_id": "2088xxx",
|
||||||
|
"settle_period": "T+1",
|
||||||
|
"account_type": "ALIPAY_LOGON_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WeChat Configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payment_system": "wechat",
|
||||||
|
"sub_mch_id": "1234567890",
|
||||||
|
"settle_period": "T+0",
|
||||||
|
"account_type": "MERCHANT_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tiered Sharing Configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scales": [
|
||||||
|
{
|
||||||
|
"min_amount": 0,
|
||||||
|
"max_amount": 1000,
|
||||||
|
"share_value": 3.0,
|
||||||
|
"share_type": "percentage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"min_amount": 1000,
|
||||||
|
"max_amount": 5000,
|
||||||
|
"share_value": 5.0,
|
||||||
|
"share_type": "percentage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"min_amount": 5000,
|
||||||
|
"max_amount": 0,
|
||||||
|
"share_value": 8.0,
|
||||||
|
"share_type": "percentage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Record Status
|
||||||
|
|
||||||
|
- **pending**: Profit share request pending
|
||||||
|
- **processing**: Profit share in progress
|
||||||
|
- **success**: Profit share completed successfully
|
||||||
|
- **failed**: Profit share failed (requires manual intervention)
|
||||||
|
|
||||||
|
### Cache Key Design
|
||||||
|
|
||||||
|
- `rule:{id}` - Individual rule cache
|
||||||
|
- `rules:list:{scenicId}:{status}:{ruleType}:{page}:{size}` - Rule list cache
|
||||||
|
- `record:{id}` - Individual record cache
|
||||||
|
- `record:order:{orderId}` - Record by order ID cache
|
||||||
|
- `records:scenic:{scenicId}:{page}:{size}` - Scenic records cache
|
||||||
|
- `calculate:{scenicId}:{amount}` - Calculation cache
|
||||||
|
- `types` - Supported types cache
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
#### When to Use HTTP API vs Kafka
|
||||||
|
- **HTTP API**:
|
||||||
|
- Rule management (create, update, enable/disable, delete)
|
||||||
|
- Query operations (records, statistics)
|
||||||
|
- Manual compensation scenarios
|
||||||
|
- Profit share calculation (dry run)
|
||||||
|
|
||||||
|
- **Kafka Messages** (Recommended):
|
||||||
|
- Automatic profit sharing after order payment
|
||||||
|
- Automatic reversal after order refund
|
||||||
|
- Asynchronous processing with retry capability
|
||||||
|
- Decoupled from main order flow
|
||||||
|
|
||||||
|
#### Idempotency Handling
|
||||||
|
```java
|
||||||
|
// Check before sending Kafka message
|
||||||
|
if (profitShareRecordRepository.existsByOrderId(orderId)) {
|
||||||
|
log.warn("订单已发起分账,跳过: orderId={}", orderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record profit share request
|
||||||
|
ProfitShareRequest request = new ProfitShareRequest();
|
||||||
|
request.setOrderId(orderId);
|
||||||
|
request.setStatus("PENDING");
|
||||||
|
request.setCreatedAt(new Date());
|
||||||
|
profitShareRecordRepository.save(request);
|
||||||
|
|
||||||
|
// Send Kafka message
|
||||||
|
profitShareProducer.sendProfitShareMessage(message);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Monitoring and Alerts
|
||||||
|
```java
|
||||||
|
// Monitor Kafka message send success rate
|
||||||
|
metricRegistry.counter("profit_share.kafka.send.success").inc();
|
||||||
|
metricRegistry.counter("profit_share.kafka.send.failure").inc();
|
||||||
|
|
||||||
|
// Monitor profit share record status
|
||||||
|
metricRegistry.gauge("profit_share.records.pending", () ->
|
||||||
|
profitShareRecordRepository.countByStatus("pending"));
|
||||||
|
metricRegistry.gauge("profit_share.records.failed", () ->
|
||||||
|
profitShareRecordRepository.countByStatus("failed"));
|
||||||
|
|
||||||
|
// Alert on failures
|
||||||
|
if (!response.getSuccess()) {
|
||||||
|
alertService.send("分账服务异常", response.getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues and Solutions
|
||||||
|
|
||||||
|
#### Q1: Kafka message sent but no profit share record created?
|
||||||
|
**A**: Troubleshooting steps:
|
||||||
|
1. Check Kafka broker connectivity
|
||||||
|
2. Verify topic `zt-profitshare` exists
|
||||||
|
3. Check profit share service logs for consumption errors
|
||||||
|
4. Query record by order ID to verify processing status
|
||||||
|
|
||||||
|
#### Q2: Profit share calculation incorrect?
|
||||||
|
**A**: Verify:
|
||||||
|
- Amount unit is in Yuan (元), not Fen (分)
|
||||||
|
- Percentage shares sum up to 100% or less
|
||||||
|
- Check `min_amount` and `max_amount` limits
|
||||||
|
- Review recipient priority ordering
|
||||||
|
|
||||||
|
#### Q3: How to handle profit share failures?
|
||||||
|
**A**: Profit share service automatically retries (max 3 times). If still fails:
|
||||||
|
1. Query record detail for error message
|
||||||
|
2. Fix data or rule issues
|
||||||
|
3. Call manual share API to retry
|
||||||
|
4. Set up alerts for failed records
|
||||||
|
|
||||||
|
#### Q4: Can I modify rule after orders are processed?
|
||||||
|
**A**: Yes, but:
|
||||||
|
- New orders use the updated rule
|
||||||
|
- Existing profit share records are not affected
|
||||||
|
- Consider creating new rule instead of modifying active one
|
||||||
|
- Disable old rule and enable new one for clean transition
|
||||||
|
|
||||||
|
### Testing Profit Share Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run profit share integration tests
|
||||||
|
mvn test -Dtest=ProfitShareIntegrationServiceTest
|
||||||
|
|
||||||
|
# Run all integration tests
|
||||||
|
mvn test -Dtest="com.ycwl.basic.integration.*Test"
|
||||||
|
```
|
||||||
@@ -1,225 +1,153 @@
|
|||||||
package com.ycwl.basic.integration.common.service;
|
package com.ycwl.basic.integration.common.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import com.ycwl.basic.integration.common.config.IntegrationProperties;
|
import com.ycwl.basic.integration.common.config.IntegrationProperties;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 集成服务通用失败降级处理
|
* 集成服务通用失败降级处理
|
||||||
* 提供统一的降级策略,支持所有微服务集成
|
* 提供统一的降级策略,支持所有微服务集成
|
||||||
|
* 使用 Caffeine 内存缓存,缓存命中时直接返回避免打崩下游服务
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class IntegrationFallbackService {
|
public class IntegrationFallbackService {
|
||||||
|
|
||||||
private final RedisTemplate<String, String> redisTemplate;
|
|
||||||
private final IntegrationProperties integrationProperties;
|
private final IntegrationProperties integrationProperties;
|
||||||
|
private final Cache<String, String> fallbackCache;
|
||||||
|
|
||||||
// 默认降级缓存配置
|
|
||||||
private static final String DEFAULT_FALLBACK_PREFIX = "integration:fallback:";
|
private static final String DEFAULT_FALLBACK_PREFIX = "integration:fallback:";
|
||||||
private static final long DEFAULT_FALLBACK_TTL = 7; // 7天
|
private static final long CACHE_TTL_MINUTES = 1;
|
||||||
|
private static final long MAX_CACHE_SIZE = 50000;
|
||||||
|
|
||||||
/**
|
public IntegrationFallbackService(IntegrationProperties integrationProperties) {
|
||||||
* 执行操作,失败时降级到缓存结果
|
this.integrationProperties = integrationProperties;
|
||||||
*
|
this.fallbackCache = Caffeine.newBuilder()
|
||||||
* @param serviceName 服务名称 (如: zt-device, zt-scenic)
|
.expireAfterWrite(CACHE_TTL_MINUTES, TimeUnit.MINUTES)
|
||||||
* @param cacheKey 缓存键
|
.maximumSize(MAX_CACHE_SIZE)
|
||||||
* @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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建完整的缓存键
|
* 执行操作,优先返回缓存结果
|
||||||
|
* 策略:有缓存直接返回,无缓存调用远程并缓存结果
|
||||||
|
* 同一 cacheKey 有互斥锁,避免并发请求打崩下游服务
|
||||||
*/
|
*/
|
||||||
|
public <T> T executeWithFallback(String serviceName, String cacheKey, Supplier<T> operation, Class<T> resultClass) {
|
||||||
|
String fullKey = buildFullCacheKey(serviceName, cacheKey);
|
||||||
|
|
||||||
|
// Caffeine.get() 内置互斥锁:同一 key 只有一个线程执行 loader,其他线程等待
|
||||||
|
String cachedValue = fallbackCache.get(fullKey, k -> {
|
||||||
|
log.debug("[{}] 缓存未命中,调用远程, cacheKey: {}", serviceName, cacheKey);
|
||||||
|
T result = operation.get();
|
||||||
|
return result != null ? JacksonUtil.toJSONString(result) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseValue(serviceName, cacheKey, cachedValue, resultClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行操作,优先返回缓存结果(支持TypeReference泛型)
|
||||||
|
* 同一 cacheKey 有互斥锁,避免并发请求打崩下游服务
|
||||||
|
*/
|
||||||
|
public <T> T executeWithFallback(String serviceName, String cacheKey, Supplier<T> operation, TypeReference<T> typeReference) {
|
||||||
|
String fullKey = buildFullCacheKey(serviceName, cacheKey);
|
||||||
|
|
||||||
|
// Caffeine.get() 内置互斥锁:同一 key 只有一个线程执行 loader,其他线程等待
|
||||||
|
String cachedValue = fallbackCache.get(fullKey, k -> {
|
||||||
|
log.debug("[{}] 缓存未命中,调用远程, cacheKey: {}", serviceName, cacheKey);
|
||||||
|
T result = operation.get();
|
||||||
|
return result != null ? JacksonUtil.toJSONString(result) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseValue(serviceName, cacheKey, cachedValue, typeReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T parseValue(String serviceName, String cacheKey, String value, Class<T> resultClass) {
|
||||||
|
try {
|
||||||
|
return JacksonUtil.parseObject(value, resultClass);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[{}] 解析缓存失败, cacheKey: {}", serviceName, cacheKey, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T parseValue(String serviceName, String cacheKey, String value, TypeReference<T> typeReference) {
|
||||||
|
try {
|
||||||
|
return JacksonUtil.parseObject(value, typeReference);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[{}] 解析缓存失败, cacheKey: {}", serviceName, cacheKey, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearFallbackCache(String serviceName, String cacheKey) {
|
||||||
|
String fullKey = buildFullCacheKey(serviceName, cacheKey);
|
||||||
|
fallbackCache.invalidate(fullKey);
|
||||||
|
log.debug("[{}] 清除缓存, key: {}", serviceName, fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAllFallbackCache(String serviceName) {
|
||||||
|
String prefix = buildFullCacheKey(serviceName, "");
|
||||||
|
List<String> keysToRemove = fallbackCache.asMap().keySet().stream()
|
||||||
|
.filter(key -> key.startsWith(prefix))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!keysToRemove.isEmpty()) {
|
||||||
|
fallbackCache.invalidateAll(keysToRemove);
|
||||||
|
log.info("[{}] 批量清除缓存,共删除 {} 项", serviceName, keysToRemove.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFallbackCache(String serviceName, String cacheKey) {
|
||||||
|
String fullKey = buildFullCacheKey(serviceName, cacheKey);
|
||||||
|
return fallbackCache.getIfPresent(fullKey) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FallbackCacheStats getFallbackCacheStats(String serviceName) {
|
||||||
|
String prefix = buildFullCacheKey(serviceName, "");
|
||||||
|
long totalCount = fallbackCache.asMap().keySet().stream()
|
||||||
|
.filter(key -> key.startsWith(prefix))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
return FallbackCacheStats.builder()
|
||||||
|
.serviceName(serviceName)
|
||||||
|
.totalCacheCount((int) totalCount)
|
||||||
|
.cacheKeyPattern(prefix + "*")
|
||||||
|
.cacheTtlMinutes(CACHE_TTL_MINUTES)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private String buildFullCacheKey(String serviceName, String cacheKey) {
|
private String buildFullCacheKey(String serviceName, String cacheKey) {
|
||||||
String prefix = getFallbackPrefix(serviceName);
|
String prefix = getFallbackPrefix(serviceName);
|
||||||
return prefix + serviceName + ":" + cacheKey;
|
return prefix + serviceName + ":" + cacheKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取服务的降级缓存前缀
|
|
||||||
*/
|
|
||||||
private String getFallbackPrefix(String serviceName) {
|
private String getFallbackPrefix(String serviceName) {
|
||||||
if (!integrationProperties.getFallback().isEnabled()) {
|
if (!integrationProperties.getFallback().isEnabled()) {
|
||||||
return DEFAULT_FALLBACK_PREFIX;
|
return DEFAULT_FALLBACK_PREFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取服务特定的缓存前缀
|
|
||||||
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
|
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
|
||||||
if (serviceConfig != null && serviceConfig.getCachePrefix() != null) {
|
if (serviceConfig != null && serviceConfig.getCachePrefix() != null) {
|
||||||
return serviceConfig.getCachePrefix();
|
return serviceConfig.getCachePrefix();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用全局配置的前缀
|
|
||||||
return integrationProperties.getFallback().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) {
|
private IntegrationProperties.ServiceFallbackConfig getServiceFallbackConfig(String serviceName) {
|
||||||
switch (serviceName.toLowerCase()) {
|
switch (serviceName.toLowerCase()) {
|
||||||
case "zt-scenic":
|
case "zt-scenic":
|
||||||
@@ -231,27 +159,20 @@ public class IntegrationFallbackService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查服务是否启用降级功能
|
|
||||||
*/
|
|
||||||
public boolean isFallbackEnabled(String serviceName) {
|
public boolean isFallbackEnabled(String serviceName) {
|
||||||
if (!integrationProperties.getFallback().isEnabled()) {
|
if (!integrationProperties.getFallback().isEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
|
IntegrationProperties.ServiceFallbackConfig serviceConfig = getServiceFallbackConfig(serviceName);
|
||||||
return serviceConfig == null || serviceConfig.isEnabled();
|
return serviceConfig == null || serviceConfig.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 降级缓存统计信息
|
|
||||||
*/
|
|
||||||
@lombok.Builder
|
@lombok.Builder
|
||||||
@lombok.Data
|
@lombok.Data
|
||||||
public static class FallbackCacheStats {
|
public static class FallbackCacheStats {
|
||||||
private String serviceName;
|
private String serviceName;
|
||||||
private int totalCacheCount;
|
private int totalCacheCount;
|
||||||
private String cacheKeyPattern;
|
private String cacheKeyPattern;
|
||||||
private long fallbackTtlDays;
|
private long cacheTtlMinutes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ycwl.basic.integration.device.service;
|
package com.ycwl.basic.integration.device.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
import com.ycwl.basic.integration.common.response.CommonResponse;
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
@@ -49,6 +50,9 @@ public class DeviceStatusIntegrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有在线设备(带降级,使用TypeReference保留泛型信息)
|
||||||
|
*/
|
||||||
public List<DeviceStatusDTO> getAllOnlineDevices() {
|
public List<DeviceStatusDTO> getAllOnlineDevices() {
|
||||||
log.debug("获取所有在线设备");
|
log.debug("获取所有在线设备");
|
||||||
return fallbackService.executeWithFallback(
|
return fallbackService.executeWithFallback(
|
||||||
@@ -58,7 +62,7 @@ public class DeviceStatusIntegrationService {
|
|||||||
CommonResponse<List<DeviceStatusDTO>> response = deviceStatusClient.getAllOnlineDevices();
|
CommonResponse<List<DeviceStatusDTO>> response = deviceStatusClient.getAllOnlineDevices();
|
||||||
return handleResponse(response, "获取所有在线设备失败");
|
return handleResponse(response, "获取所有在线设备失败");
|
||||||
},
|
},
|
||||||
List.class
|
new TypeReference<List<DeviceStatusDTO>>() {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class KafkaIntegrationProperties {
|
|||||||
private boolean enabled = false;
|
private boolean enabled = false;
|
||||||
private String bootstrapServers = "100.64.0.12:39092";
|
private String bootstrapServers = "100.64.0.12:39092";
|
||||||
private String ztMessageTopic = "zt-message"; // topic for zt-message microservice
|
private String ztMessageTopic = "zt-message"; // topic for zt-message microservice
|
||||||
|
private String profitShareTopic = "zt-profitshare"; // topic for profit share messages
|
||||||
|
private String refundTopic = "zt-profitshare-refund"; // topic for refund messages
|
||||||
private Consumer consumer = new Consumer();
|
private Consumer consumer = new Consumer();
|
||||||
private Producer producer = new Producer();
|
private Producer producer = new Producer();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.client;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账服务Feign客户端
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@FeignClient(name = "zt-profitshare", contextId = "profit-share-v2", path = "/api/profit-share/v2")
|
||||||
|
public interface ProfitShareClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分账规则
|
||||||
|
*/
|
||||||
|
@PostMapping("/rules")
|
||||||
|
CommonResponse<RuleVO> createRule(@RequestBody CreateRuleRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账规则列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/rules")
|
||||||
|
CommonResponse<PageResponse<RuleVO>> listRules(@RequestParam(value = "scenic_id", required = false) Long scenicId,
|
||||||
|
@RequestParam(value = "status", required = false) String status,
|
||||||
|
@RequestParam(value = "rule_type", required = false) String ruleType,
|
||||||
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分账规则详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/rules/{id}")
|
||||||
|
CommonResponse<RuleVO> getRule(@PathVariable("id") Long ruleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分账规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}")
|
||||||
|
CommonResponse<RuleVO> updateRule(@PathVariable("id") Long ruleId,
|
||||||
|
@RequestBody CreateRuleRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分账规则
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/rules/{id}")
|
||||||
|
CommonResponse<Void> deleteRule(@PathVariable("id") Long ruleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}/enable")
|
||||||
|
CommonResponse<Void> enableRule(@PathVariable("id") Long ruleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用规则
|
||||||
|
*/
|
||||||
|
@PutMapping("/rules/{id}/disable")
|
||||||
|
CommonResponse<Void> disableRule(@PathVariable("id") Long ruleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询景区分账记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/scenic/{scenic_id}")
|
||||||
|
CommonResponse<PageResponse<RecordVO>> getRecordsByScenic(@PathVariable("scenic_id") Long scenicId,
|
||||||
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账记录详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/{id}")
|
||||||
|
CommonResponse<RecordDetailVO> getRecordById(@PathVariable("id") Long recordId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按订单ID查询分账记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/records/order/{order_id}")
|
||||||
|
CommonResponse<RecordDetailVO> getRecordByOrderId(@PathVariable("order_id") String orderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发分账
|
||||||
|
*/
|
||||||
|
@PostMapping("/manual")
|
||||||
|
CommonResponse<Void> manualShare(@RequestBody ManualShareRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分账金额(不执行)
|
||||||
|
*/
|
||||||
|
@PostMapping("/calculate")
|
||||||
|
CommonResponse<CalculateResultVO> calculateShare(@RequestBody CalculateShareRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的类型
|
||||||
|
*/
|
||||||
|
@GetMapping("/types")
|
||||||
|
CommonResponse<TypesVO> getSupportedTypes();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账服务集成配置
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "integration.profitshare")
|
||||||
|
public class ProfitShareIntegrationConfig {
|
||||||
|
|
||||||
|
public ProfitShareIntegrationConfig() {
|
||||||
|
log.info("ZT-ProfitShare集成配置初始化完成");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分账结果VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class CalculateResultVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_amount")
|
||||||
|
private Double totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账明细列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("details")
|
||||||
|
private List<CalculateDetailVO> details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账明细VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class CalculateDetailVO {
|
||||||
|
/**
|
||||||
|
* 接收人名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_name")
|
||||||
|
private String recipientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_type")
|
||||||
|
private String recipientType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_amount")
|
||||||
|
private Double shareAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_type")
|
||||||
|
private String shareType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账值
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_value")
|
||||||
|
private Double shareValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRecipientRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分账请求
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class CalculateShareRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_amount")
|
||||||
|
private Double totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_type")
|
||||||
|
private String ruleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账接收人列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipients")
|
||||||
|
private List<CreateRecipientRequest> recipients;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动分账请求
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ManualShareRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("order_id")
|
||||||
|
private String orderId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的类型VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class TypesVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则类型列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_types")
|
||||||
|
private List<String> ruleTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人类型列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_types")
|
||||||
|
private List<String> recipientTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账类型列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_types")
|
||||||
|
private List<String> shareTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("statuses")
|
||||||
|
private List<String> statuses;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.message;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单分账消息(发送到 zt-profitshare topic)
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class OrderMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("order_id")
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额(单位:元)
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_amount")
|
||||||
|
private Double totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付系统(alipay, wechat, union)
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_system")
|
||||||
|
private String paymentSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_order_id")
|
||||||
|
private String paymentOrderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix 时间戳(秒)
|
||||||
|
*/
|
||||||
|
@JsonProperty("timestamp")
|
||||||
|
private Long timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建订单消息
|
||||||
|
*/
|
||||||
|
public static OrderMessage of(String orderId, Long scenicId, Double totalAmount, String paymentSystem, String paymentOrderId) {
|
||||||
|
return OrderMessage.builder()
|
||||||
|
.orderId(orderId)
|
||||||
|
.scenicId(scenicId)
|
||||||
|
.totalAmount(totalAmount)
|
||||||
|
.paymentSystem(paymentSystem)
|
||||||
|
.paymentOrderId(paymentOrderId)
|
||||||
|
.timestamp(System.currentTimeMillis() / 1000)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.message;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款消息(发送到 zt-profitshare-refund topic)
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class RefundMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("refund_order_id")
|
||||||
|
private String refundOrderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("original_order_id")
|
||||||
|
private String originalOrderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款金额(单位:元)
|
||||||
|
*/
|
||||||
|
@JsonProperty("refund_amount")
|
||||||
|
private Double refundAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款类型(full: 全额退款, partial: 部分退款)
|
||||||
|
*/
|
||||||
|
@JsonProperty("refund_type")
|
||||||
|
private String refundType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付系统(alipay, wechat, union)
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_system")
|
||||||
|
private String paymentSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix 时间戳(秒)
|
||||||
|
*/
|
||||||
|
@JsonProperty("timestamp")
|
||||||
|
private Long timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建退款消息
|
||||||
|
*/
|
||||||
|
public static RefundMessage of(String refundOrderId, String originalOrderId, Double refundAmount, String refundType, String paymentSystem) {
|
||||||
|
return RefundMessage.builder()
|
||||||
|
.refundOrderId(refundOrderId)
|
||||||
|
.originalOrderId(originalOrderId)
|
||||||
|
.refundAmount(refundAmount)
|
||||||
|
.refundType(refundType)
|
||||||
|
.paymentSystem(paymentSystem)
|
||||||
|
.timestamp(System.currentTimeMillis() / 1000)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.record;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账记录详情VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class RecordDetailVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("order_id")
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_id")
|
||||||
|
private Long ruleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_amount")
|
||||||
|
private Double totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付系统
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_system")
|
||||||
|
private String paymentSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_order_id")
|
||||||
|
private String paymentOrderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@JsonProperty("status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账明细列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("details")
|
||||||
|
private List<ShareDetailVO> details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("updated_at")
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.record;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账记录VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class RecordVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("order_id")
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_id")
|
||||||
|
private Long ruleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_amount")
|
||||||
|
private Double totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付系统
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_system")
|
||||||
|
private String paymentSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("payment_order_id")
|
||||||
|
private String paymentOrderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(pending, processing, success, failed)
|
||||||
|
*/
|
||||||
|
@JsonProperty("status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("updated_at")
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.record;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账明细VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ShareDetailVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_name")
|
||||||
|
private String recipientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_type")
|
||||||
|
private String recipientType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("account_info")
|
||||||
|
private String accountInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_amount")
|
||||||
|
private Double shareAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@JsonProperty("status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.rule;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账接收人请求
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class CreateRecipientRequest {
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_name")
|
||||||
|
private String recipientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人类型(platform, merchant, agent)
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_type")
|
||||||
|
private String recipientType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("account_info")
|
||||||
|
private String accountInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账类型(percentage, fixed_amount)
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_type")
|
||||||
|
private String shareType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账值(百分比或固定金额)
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_value")
|
||||||
|
private Double shareValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("min_amount")
|
||||||
|
private Double minAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("max_amount")
|
||||||
|
private Double maxAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级
|
||||||
|
*/
|
||||||
|
@JsonProperty("priority")
|
||||||
|
private Integer priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展配置
|
||||||
|
*/
|
||||||
|
@JsonProperty("ext_config")
|
||||||
|
private Map<String, Object> extConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要调支付分账
|
||||||
|
*/
|
||||||
|
@JsonProperty("need_payment_call")
|
||||||
|
private Integer needPaymentCall;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.rule;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分账规则请求
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class CreateRuleRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_name")
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则类型(percentage, fixed_amount, scaled_amount)
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_type")
|
||||||
|
private String ruleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则描述
|
||||||
|
*/
|
||||||
|
@JsonProperty("description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账接收人列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipients")
|
||||||
|
private List<CreateRecipientRequest> recipients;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.rule;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账接收人VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class RecipientVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_name")
|
||||||
|
private String recipientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipient_type")
|
||||||
|
private String recipientType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户信息
|
||||||
|
*/
|
||||||
|
@JsonProperty("account_info")
|
||||||
|
private String accountInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_type")
|
||||||
|
private String shareType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账值
|
||||||
|
*/
|
||||||
|
@JsonProperty("share_value")
|
||||||
|
private Double shareValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("min_amount")
|
||||||
|
private Double minAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大分账金额
|
||||||
|
*/
|
||||||
|
@JsonProperty("max_amount")
|
||||||
|
private Double maxAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级
|
||||||
|
*/
|
||||||
|
@JsonProperty("priority")
|
||||||
|
private Integer priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展配置
|
||||||
|
*/
|
||||||
|
@JsonProperty("ext_config")
|
||||||
|
private Map<String, Object> extConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@JsonProperty("status")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.dto.rule;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账规则VO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class RuleVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则名称
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_name")
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("rule_type")
|
||||||
|
private String ruleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则描述
|
||||||
|
*/
|
||||||
|
@JsonProperty("description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(active, inactive)
|
||||||
|
*/
|
||||||
|
@JsonProperty("status")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账接收人列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("recipients")
|
||||||
|
private List<RecipientVO> recipients;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("updated_at")
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
|
import com.ycwl.basic.integration.profitshare.client.ProfitShareClient;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账集成服务
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProfitShareIntegrationService {
|
||||||
|
|
||||||
|
private final ProfitShareClient profitShareClient;
|
||||||
|
private final IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
private static final String SERVICE_NAME = "zt-profitshare";
|
||||||
|
|
||||||
|
// ==================== 规则管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分账规则(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public RuleVO createRule(CreateRuleRequest request) {
|
||||||
|
log.debug("创建分账规则, scenicId: {}, ruleName: {}", request.getScenicId(), request.getRuleName());
|
||||||
|
CommonResponse<RuleVO> response = profitShareClient.createRule(request);
|
||||||
|
return handleResponse(response, "创建分账规则失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账规则列表(带fallback)
|
||||||
|
*/
|
||||||
|
public PageResponse<RuleVO> listRules(Long scenicId, String status, String ruleType, Integer page, Integer pageSize) {
|
||||||
|
log.debug("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
|
||||||
|
scenicId, status, ruleType, page, pageSize);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
String.format("rules:list:%s:%s:%s:%d:%d", scenicId, status, ruleType, page, pageSize),
|
||||||
|
() -> {
|
||||||
|
CommonResponse<PageResponse<RuleVO>> response = profitShareClient.listRules(scenicId, status, ruleType, page, pageSize);
|
||||||
|
return handleResponse(response, "查询分账规则列表失败");
|
||||||
|
},
|
||||||
|
PageResponse.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分账规则详情(带fallback)
|
||||||
|
*/
|
||||||
|
public RuleVO getRule(Long ruleId) {
|
||||||
|
log.debug("获取分账规则详情, ruleId: {}", ruleId);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"rule:" + ruleId,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<RuleVO> response = profitShareClient.getRule(ruleId);
|
||||||
|
return handleResponse(response, "获取分账规则详情失败");
|
||||||
|
},
|
||||||
|
RuleVO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分账规则(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public RuleVO updateRule(Long ruleId, CreateRuleRequest request) {
|
||||||
|
log.debug("更新分账规则, ruleId: {}", ruleId);
|
||||||
|
CommonResponse<RuleVO> response = profitShareClient.updateRule(ruleId, request);
|
||||||
|
return handleResponse(response, "更新分账规则失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分账规则(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public void deleteRule(Long ruleId) {
|
||||||
|
log.debug("删除分账规则, ruleId: {}", ruleId);
|
||||||
|
CommonResponse<Void> response = profitShareClient.deleteRule(ruleId);
|
||||||
|
handleResponse(response, "删除分账规则失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用规则(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public void enableRule(Long ruleId) {
|
||||||
|
log.debug("启用分账规则, ruleId: {}", ruleId);
|
||||||
|
CommonResponse<Void> response = profitShareClient.enableRule(ruleId);
|
||||||
|
handleResponse(response, "启用分账规则失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用规则(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public void disableRule(Long ruleId) {
|
||||||
|
log.debug("禁用分账规则, ruleId: {}", ruleId);
|
||||||
|
CommonResponse<Void> response = profitShareClient.disableRule(ruleId);
|
||||||
|
handleResponse(response, "禁用分账规则失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 分账记录查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询景区分账记录(带fallback)
|
||||||
|
*/
|
||||||
|
public PageResponse<RecordVO> getRecordsByScenic(Long scenicId, Integer page, Integer pageSize) {
|
||||||
|
log.debug("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
String.format("records:scenic:%d:%d:%d", scenicId, page, pageSize),
|
||||||
|
() -> {
|
||||||
|
CommonResponse<PageResponse<RecordVO>> response = profitShareClient.getRecordsByScenic(scenicId, page, pageSize);
|
||||||
|
return handleResponse(response, "查询景区分账记录失败");
|
||||||
|
},
|
||||||
|
PageResponse.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分账记录详情(带fallback)
|
||||||
|
*/
|
||||||
|
public RecordDetailVO getRecordById(Long recordId) {
|
||||||
|
log.debug("查询分账记录详情, recordId: {}", recordId);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"record:" + recordId,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<RecordDetailVO> response = profitShareClient.getRecordById(recordId);
|
||||||
|
return handleResponse(response, "查询分账记录详情失败");
|
||||||
|
},
|
||||||
|
RecordDetailVO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按订单ID查询分账记录(带fallback)
|
||||||
|
*/
|
||||||
|
public RecordDetailVO getRecordByOrderId(String orderId) {
|
||||||
|
log.debug("按订单ID查询分账记录, orderId: {}", orderId);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"record:order:" + orderId,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<RecordDetailVO> response = profitShareClient.getRecordByOrderId(orderId);
|
||||||
|
return handleResponse(response, "按订单ID查询分账记录失败");
|
||||||
|
},
|
||||||
|
RecordDetailVO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 分账操作 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发分账(直接操作,无fallback)
|
||||||
|
*/
|
||||||
|
public void manualShare(String orderId) {
|
||||||
|
log.debug("手动触发分账, orderId: {}", orderId);
|
||||||
|
ManualShareRequest request = ManualShareRequest.builder()
|
||||||
|
.orderId(orderId)
|
||||||
|
.build();
|
||||||
|
CommonResponse<Void> response = profitShareClient.manualShare(request);
|
||||||
|
handleResponse(response, "手动触发分账失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分账金额(不执行)(带fallback)
|
||||||
|
*/
|
||||||
|
public CalculateResultVO calculateShare(CalculateShareRequest request) {
|
||||||
|
log.debug("计算分账金额, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
String.format("calculate:%d:%.2f", request.getScenicId(), request.getTotalAmount()),
|
||||||
|
() -> {
|
||||||
|
CommonResponse<CalculateResultVO> response = profitShareClient.calculateShare(request);
|
||||||
|
return handleResponse(response, "计算分账金额失败");
|
||||||
|
},
|
||||||
|
CalculateResultVO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的类型(带fallback)
|
||||||
|
*/
|
||||||
|
public TypesVO getSupportedTypes() {
|
||||||
|
log.debug("获取支持的类型");
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"types",
|
||||||
|
() -> {
|
||||||
|
CommonResponse<TypesVO> response = profitShareClient.getSupportedTypes();
|
||||||
|
return handleResponse(response, "获取支持的类型失败");
|
||||||
|
},
|
||||||
|
TypesVO.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package com.ycwl.basic.integration.profitshare.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.message.OrderMessage;
|
||||||
|
import com.ycwl.basic.integration.profitshare.dto.message.RefundMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
|
import org.springframework.kafka.support.SendResult;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分账Kafka消息生产者
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-01-11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
|
||||||
|
public class ProfitShareKafkaProducer {
|
||||||
|
|
||||||
|
public static final String DEFAULT_PROFITSHARE_TOPIC = "zt-profitshare";
|
||||||
|
public static final String DEFAULT_REFUND_TOPIC = "zt-profitshare-refund";
|
||||||
|
|
||||||
|
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final KafkaIntegrationProperties kafkaProps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送分账消息(订单支付成功后调用)
|
||||||
|
*/
|
||||||
|
public void sendProfitShareMessage(OrderMessage message) {
|
||||||
|
validate(message);
|
||||||
|
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getProfitShareTopic())
|
||||||
|
? kafkaProps.getProfitShareTopic()
|
||||||
|
: DEFAULT_PROFITSHARE_TOPIC;
|
||||||
|
String key = message.getOrderId();
|
||||||
|
String payload = toJson(message);
|
||||||
|
|
||||||
|
log.info("[PROFIT-SHARE] producing to topic={}, key={}, orderId={}, scenicId={}, amount={}",
|
||||||
|
topic, key, message.getOrderId(), message.getScenicId(), message.getTotalAmount());
|
||||||
|
|
||||||
|
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
log.error("[PROFIT-SHARE] produce failed: orderId={}, error={}", message.getOrderId(), ex.getMessage(), ex);
|
||||||
|
} else if (metadata != null) {
|
||||||
|
log.info("[PROFIT-SHARE] produced: orderId={}, partition={}, offset={}",
|
||||||
|
message.getOrderId(), metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送退款消息(订单退款成功后调用)
|
||||||
|
*/
|
||||||
|
public CompletableFuture<SendResult<String, String>> sendRefundMessage(RefundMessage message) {
|
||||||
|
validateRefund(message);
|
||||||
|
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getRefundTopic())
|
||||||
|
? kafkaProps.getRefundTopic()
|
||||||
|
: DEFAULT_REFUND_TOPIC;
|
||||||
|
String key = message.getOriginalOrderId();
|
||||||
|
String payload = toJson(message);
|
||||||
|
|
||||||
|
log.info("[REFUND] producing to topic={}, key={}, refundOrderId={}, originalOrderId={}, amount={}, type={}",
|
||||||
|
topic, key, message.getRefundOrderId(), message.getOriginalOrderId(), message.getRefundAmount(), message.getRefundType());
|
||||||
|
|
||||||
|
return kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
log.error("[REFUND] produce failed: refundOrderId={}, error={}", message.getRefundOrderId(), ex.getMessage(), ex);
|
||||||
|
} else if (metadata != null) {
|
||||||
|
log.info("[REFUND] produced: refundOrderId={}, partition={}, offset={}",
|
||||||
|
message.getRefundOrderId(), metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(OrderMessage msg) {
|
||||||
|
if (msg == null) {
|
||||||
|
throw new IllegalArgumentException("OrderMessage is null");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getOrderId())) {
|
||||||
|
throw new IllegalArgumentException("orderId is required");
|
||||||
|
}
|
||||||
|
if (msg.getScenicId() == null || msg.getScenicId() <= 0) {
|
||||||
|
throw new IllegalArgumentException("scenicId is required and must be positive");
|
||||||
|
}
|
||||||
|
if (msg.getTotalAmount() == null || msg.getTotalAmount() <= 0) {
|
||||||
|
throw new IllegalArgumentException("totalAmount is required and must be positive");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getPaymentSystem())) {
|
||||||
|
throw new IllegalArgumentException("paymentSystem is required");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getPaymentOrderId())) {
|
||||||
|
throw new IllegalArgumentException("paymentOrderId is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRefund(RefundMessage msg) {
|
||||||
|
if (msg == null) {
|
||||||
|
throw new IllegalArgumentException("RefundMessage is null");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getRefundOrderId())) {
|
||||||
|
throw new IllegalArgumentException("refundOrderId is required");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getOriginalOrderId())) {
|
||||||
|
throw new IllegalArgumentException("originalOrderId is required");
|
||||||
|
}
|
||||||
|
if (msg.getRefundAmount() == null || msg.getRefundAmount() <= 0) {
|
||||||
|
throw new IllegalArgumentException("refundAmount is required and must be positive");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getRefundType())) {
|
||||||
|
throw new IllegalArgumentException("refundType is required");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(msg.getPaymentSystem())) {
|
||||||
|
throw new IllegalArgumentException("paymentSystem is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toJson(Object obj) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(obj);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException("failed to serialize message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ycwl.basic.integration.render.service;
|
package com.ycwl.basic.integration.render.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
import com.ycwl.basic.integration.common.response.CommonResponse;
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
@@ -29,7 +30,7 @@ public class RenderWorkerConfigIntegrationService {
|
|||||||
private static final String SERVICE_NAME = "zt-render-worker";
|
private static final String SERVICE_NAME = "zt-render-worker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取工作器所有配置(带降级)
|
* 获取工作器所有配置(带降级,使用TypeReference保留泛型信息)
|
||||||
*/
|
*/
|
||||||
public List<RenderWorkerConfigV2DTO> getWorkerConfigs(Long workerId) {
|
public List<RenderWorkerConfigV2DTO> getWorkerConfigs(Long workerId) {
|
||||||
log.debug("获取渲染工作器配置列表, workerId: {}", workerId);
|
log.debug("获取渲染工作器配置列表, workerId: {}", workerId);
|
||||||
@@ -42,7 +43,7 @@ public class RenderWorkerConfigIntegrationService {
|
|||||||
List<RenderWorkerConfigV2DTO> configs = handleResponse(response, "获取渲染工作器配置列表失败");
|
List<RenderWorkerConfigV2DTO> configs = handleResponse(response, "获取渲染工作器配置列表失败");
|
||||||
return configs != null ? configs : Collections.emptyList();
|
return configs != null ? configs : Collections.emptyList();
|
||||||
},
|
},
|
||||||
List.class
|
new TypeReference<List<RenderWorkerConfigV2DTO>>() {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +71,12 @@ public class RenderWorkerConfigIntegrationService {
|
|||||||
log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
|
log.debug("获取渲染工作器平铺配置, workerId: {}", workerId);
|
||||||
return fallbackService.executeWithFallback(
|
return fallbackService.executeWithFallback(
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
"worker:config:" + workerId,
|
"worker:flat:config:" + workerId,
|
||||||
() -> {
|
() -> {
|
||||||
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
|
List<RenderWorkerConfigV2DTO> configs = getWorkerConfigsInternal(workerId);
|
||||||
return flattenConfigs(configs);
|
return flattenConfigs(configs);
|
||||||
},
|
},
|
||||||
Map.class
|
new TypeReference<Map<String, Object>>() {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ycwl.basic.integration.scenic.service;
|
package com.ycwl.basic.integration.scenic.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
import com.ycwl.basic.integration.common.response.CommonResponse;
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
@@ -22,6 +23,9 @@ public class ScenicConfigIntegrationService {
|
|||||||
|
|
||||||
private static final String SERVICE_NAME = "zt-scenic";
|
private static final String SERVICE_NAME = "zt-scenic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取景区配置列表(带降级,使用TypeReference保留泛型信息)
|
||||||
|
*/
|
||||||
public List<ScenicConfigV2DTO> listConfigs(Long scenicId) {
|
public List<ScenicConfigV2DTO> listConfigs(Long scenicId) {
|
||||||
log.debug("获取景区配置列表, scenicId: {}", scenicId);
|
log.debug("获取景区配置列表, scenicId: {}", scenicId);
|
||||||
return fallbackService.executeWithFallback(
|
return fallbackService.executeWithFallback(
|
||||||
@@ -31,7 +35,7 @@ public class ScenicConfigIntegrationService {
|
|||||||
CommonResponse<List<ScenicConfigV2DTO>> response = scenicConfigV2Client.listConfigs(scenicId);
|
CommonResponse<List<ScenicConfigV2DTO>> response = scenicConfigV2Client.listConfigs(scenicId);
|
||||||
return handleResponse(response, "获取景区配置列表失败");
|
return handleResponse(response, "获取景区配置列表失败");
|
||||||
},
|
},
|
||||||
List.class
|
new TypeReference<List<ScenicConfigV2DTO>>() {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package com.ycwl.basic.mapper;
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
|
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
|
||||||
|
import com.ycwl.basic.model.pc.extraDevice.resp.ExtraDeviceRespVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ExtraDeviceMapper {
|
public interface ExtraDeviceMapper {
|
||||||
List<DeviceRespVO> listExtraDeviceByScenicId(Long scenicId);
|
List<DeviceRespVO> listExtraDeviceByScenicId(Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询外部设备列表
|
||||||
|
* @param scenicId 景区ID (可选)
|
||||||
|
* @return 外部设备列表
|
||||||
|
*/
|
||||||
|
List<ExtraDeviceRespVO> pageQuery(@Param("scenicId") Long scenicId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.ycwl.basic.model.pc.order.req.OrderReqQuery;
|
|||||||
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
|
import com.ycwl.basic.model.pc.order.resp.OrderAppRespVO;
|
||||||
import com.ycwl.basic.model.pc.order.resp.OrderRespVO;
|
import com.ycwl.basic.model.pc.order.resp.OrderRespVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -61,4 +62,19 @@ public interface OrderMapper {
|
|||||||
List<OrderItemEntity> getOrderItems(Long orderId);
|
List<OrderItemEntity> getOrderItems(Long orderId);
|
||||||
|
|
||||||
OrderEntity getUserBuyFaceItem(Long memberId, Long faceId, int goodsType, Long goodsId);
|
OrderEntity getUserBuyFaceItem(Long memberId, Long faceId, int goodsType, Long goodsId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询购买了指定视频的所有订单ID(直接购买)
|
||||||
|
* @param videoId 视频ID
|
||||||
|
* @return 订单ID列表
|
||||||
|
*/
|
||||||
|
List<Long> getOrderIdsByVideoId(Long videoId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询购买了指定模板和faceId的所有订单ID
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID
|
||||||
|
* @return 订单ID列表
|
||||||
|
*/
|
||||||
|
List<Long> getOrderIdsByFaceIdAndTemplateId(@Param("faceId") Long faceId, @Param("templateId") Long templateId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,4 +165,12 @@ public interface SourceMapper {
|
|||||||
* @return 删除的记录数
|
* @return 删除的记录数
|
||||||
*/
|
*/
|
||||||
int deleteRelationsByFaceIdAndType(Long faceId, Integer type);
|
int deleteRelationsByFaceIdAndType(Long faceId, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定faceId和type的免费关联记录数
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param type 素材类型
|
||||||
|
* @return 免费记录数
|
||||||
|
*/
|
||||||
|
int countFreeRelationsByFaceIdAndType(Long faceId, Integer type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,4 +57,6 @@ public interface TaskMapper {
|
|||||||
List<TaskRespVO> selectNotRunningByScenicList(String scenicOnly);
|
List<TaskRespVO> selectNotRunningByScenicList(String scenicOnly);
|
||||||
|
|
||||||
List<TaskEntity> selectAllFailed();
|
List<TaskEntity> selectAllFailed();
|
||||||
|
|
||||||
|
TaskEntity listLastFaceTemplateTask(Long faceId, Long templateId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,12 @@ public interface VideoMapper {
|
|||||||
* @return 已购买记录数量
|
* @return 已购买记录数量
|
||||||
*/
|
*/
|
||||||
int countBuyRecordByVideoId(Long videoId);
|
int countBuyRecordByVideoId(Long videoId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过faceId和templateId(可选)查询最新的视频记录
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选)
|
||||||
|
* @return 最新的视频记录
|
||||||
|
*/
|
||||||
|
VideoRespVO queryLatestByFaceIdAndTemplateId(@NonNull Long faceId, Long templateId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
|
|||||||
/**
|
/**
|
||||||
* 查询所有机位评价数据(用于后端计算平均值)
|
* 查询所有机位评价数据(用于后端计算平均值)
|
||||||
*
|
*
|
||||||
* @return 机位评价列表(嵌套Map结构)
|
* @return 机位评价列表(Map结构: 机位ID -> 评分)
|
||||||
*/
|
*/
|
||||||
List<Map<String, Map<String, Integer>>> selectAllCameraPositionRatings();
|
List<Map<String, Integer>> selectAllCameraPositionRatings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.video.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HLS视频流请求参数
|
||||||
|
* 用于生成设备视频的HLS播放列表
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-12-26
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class HlsStreamRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "设备ID不能为空")
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频时长(分钟),默认2分钟
|
||||||
|
* 获取最近N分钟的视频
|
||||||
|
*/
|
||||||
|
private Integer durationMinutes = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为Event播放列表
|
||||||
|
* true: 使用EVENT类型(适合固定时长的视频回放)
|
||||||
|
* false: 使用VOD类型(适合点播)
|
||||||
|
*/
|
||||||
|
private Boolean eventPlaylist = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.video.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HLS视频流响应
|
||||||
|
* 包含生成的m3u8播放列表内容
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-12-26
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class HlsStreamResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID
|
||||||
|
*/
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* m3u8播放列表内容
|
||||||
|
*/
|
||||||
|
private String playlistContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频片段数量
|
||||||
|
*/
|
||||||
|
private Integer segmentCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总时长(秒)
|
||||||
|
*/
|
||||||
|
private Double totalDurationSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频片段列表
|
||||||
|
*/
|
||||||
|
private List<VideoSegment> segments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放列表类型(EVENT/VOD)
|
||||||
|
*/
|
||||||
|
private String playlistType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频片段信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class VideoSegment {
|
||||||
|
/**
|
||||||
|
* 视频片段URL
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 片段时长(秒)
|
||||||
|
*/
|
||||||
|
private Double duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 片段序号
|
||||||
|
*/
|
||||||
|
private Integer sequence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 片段开始时间
|
||||||
|
*/
|
||||||
|
private String startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 片段结束时间
|
||||||
|
*/
|
||||||
|
private String endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.ycwl.basic.model.pc.device.req;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部工具上报视频连续性检查结果的请求DTO
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-12-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class VideoContinuityReportReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备编号(必填)
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "设备编号不能为空")
|
||||||
|
private String deviceNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查的开始时间(必填)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "检查开始时间不能为空")
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查的结束时间(必填)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "检查结束时间不能为空")
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否支持连续性检查(必填)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "是否支持连续性检查不能为空")
|
||||||
|
private Boolean support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频是否连续(必填)
|
||||||
|
* true: 所有间隙都在允许范围内
|
||||||
|
* false: 存在超出允许范围的间隙
|
||||||
|
*/
|
||||||
|
@NotNull(message = "视频是否连续不能为空")
|
||||||
|
private Boolean continuous;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频总数(必填)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "视频总数不能为空")
|
||||||
|
private Integer totalVideos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总时长,单位毫秒(必填)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "总时长不能为空")
|
||||||
|
private Long totalDurationMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许的最大间隙,单位毫秒(选填,默认2000ms)
|
||||||
|
*/
|
||||||
|
private Long maxAllowedGapMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 间隙列表(选填,当continuous=false时应提供)
|
||||||
|
*/
|
||||||
|
private List<GapInfoReq> gaps = Collections.emptyList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 间隙信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class GapInfoReq {
|
||||||
|
/**
|
||||||
|
* 前一个文件名
|
||||||
|
*/
|
||||||
|
private String beforeFileName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后一个文件名
|
||||||
|
*/
|
||||||
|
private String afterFileName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 间隙时长,单位毫秒
|
||||||
|
*/
|
||||||
|
private Long gapMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 间隙开始时间
|
||||||
|
*/
|
||||||
|
private Date gapStartTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 间隙结束时间
|
||||||
|
*/
|
||||||
|
private Date gapEndTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.model.pc.extraDevice.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部设备分页查询请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExtraDevicePageQueryReq {
|
||||||
|
/**
|
||||||
|
* 景区ID (支持 Long 或字符串格式的Long)
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码,默认1
|
||||||
|
*/
|
||||||
|
private Integer pageNum = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页大小,默认20
|
||||||
|
*/
|
||||||
|
private Integer pageSize = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.ycwl.basic.model.pc.extraDevice.resp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部设备响应VO
|
||||||
|
* 对应文档中的 ExtraDeviceResp 结构
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExtraDeviceRespVO {
|
||||||
|
/**
|
||||||
|
* 设备ID (JSON输出为字符串)
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID (JSON输出为字符串)
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区名称 (从景区服务获取)
|
||||||
|
*/
|
||||||
|
private String scenicName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备标识
|
||||||
|
*/
|
||||||
|
private String ident;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库状态
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心跳时间 (格式: yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
private String keepaliveAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线状态: 1=在线, 0=离线
|
||||||
|
* 判断逻辑:5分钟内有心跳则在线
|
||||||
|
*/
|
||||||
|
private Integer online;
|
||||||
|
}
|
||||||
@@ -35,21 +35,6 @@ public class MemberRespVO {
|
|||||||
*/
|
*/
|
||||||
// 真实姓名
|
// 真实姓名
|
||||||
private String realName;
|
private String realName;
|
||||||
/**
|
|
||||||
* 推客优惠码
|
|
||||||
*/
|
|
||||||
// 推客优惠码
|
|
||||||
private String promoCode;
|
|
||||||
/**
|
|
||||||
* 推客id
|
|
||||||
*/
|
|
||||||
// 推客id
|
|
||||||
private Long brokerId;
|
|
||||||
/**
|
|
||||||
* 是否同意用户协议,1同意0未同意
|
|
||||||
*/
|
|
||||||
// 是否同意用户协议,1同意0未同意
|
|
||||||
private Integer agreement;
|
|
||||||
/**
|
/**
|
||||||
* 电话号码
|
* 电话号码
|
||||||
*/
|
*/
|
||||||
@@ -70,11 +55,4 @@ public class MemberRespVO {
|
|||||||
*/
|
*/
|
||||||
// 城市
|
// 城市
|
||||||
private String city;
|
private String city;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
|
||||||
private Date createDate;
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
|
||||||
private Date updateAt;
|
|
||||||
private Integer orderCount;
|
|
||||||
// 是否开启服务通知 0关闭 1开启
|
|
||||||
private Integer isServiceNotification;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.util.Date;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("scenic_config")
|
@TableName("scenic_config")
|
||||||
|
@Deprecated
|
||||||
public class ScenicConfigEntity {
|
public class ScenicConfigEntity {
|
||||||
@TableId
|
@TableId
|
||||||
private Long id;
|
private Long id;
|
||||||
@@ -102,10 +103,6 @@ public class ScenicConfigEntity {
|
|||||||
private Float faceScoreThreshold;
|
private Float faceScoreThreshold;
|
||||||
private StorageType storeType;
|
private StorageType storeType;
|
||||||
private String storeConfigJson;
|
private String storeConfigJson;
|
||||||
private StorageType tmpStoreType;
|
|
||||||
private String tmpStoreConfigJson;
|
|
||||||
private StorageType localStoreType;
|
|
||||||
private String localStoreConfigJson;
|
|
||||||
private BigDecimal brokerDirectRate;
|
private BigDecimal brokerDirectRate;
|
||||||
private Integer faceDetectHelperThreshold;
|
private Integer faceDetectHelperThreshold;
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ public class VideoEntity {
|
|||||||
*/
|
*/
|
||||||
private Long taskId;
|
private Long taskId;
|
||||||
/**
|
/**
|
||||||
* 执行任务的机器ID,render_worker.id
|
* 人脸ID,对应face.id
|
||||||
*/
|
*/
|
||||||
private Long workerId;
|
private Long faceId;
|
||||||
/**
|
/**
|
||||||
* 视频链接
|
* 视频链接
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频购买检查请求DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class VideoPurchaseCheckReqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频ID
|
||||||
|
*/
|
||||||
|
private Long videoId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频购买检查响应DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class VideoPurchaseCheckRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频ID
|
||||||
|
*/
|
||||||
|
private Long videoId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已被购买
|
||||||
|
*/
|
||||||
|
private Boolean isPurchased;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买该视频的订单ID列表(包括直接购买和通过模板购买)
|
||||||
|
*/
|
||||||
|
private List<Long> orderIds;
|
||||||
|
}
|
||||||
@@ -27,9 +27,8 @@ public class VideoReviewAddReqDTO {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 机位评价JSON(可选)
|
* 机位评价JSON(可选)
|
||||||
* 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
|
* 格式: {"12345": 5, "12346": 4}
|
||||||
* 外层key为机位ID,内层Map为该机位的各维度评分
|
* key为机位ID,value为该机位的评分(1-5)
|
||||||
* 评分维度: 清晰度, 构图, 色彩, 整体效果
|
|
||||||
*/
|
*/
|
||||||
private Map<String, Map<String, Integer>> cameraPositionRating;
|
private Map<String, Integer> cameraPositionRating;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ public class VideoReviewRespDTO {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 机位评价JSON
|
* 机位评价JSON
|
||||||
* 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
|
* 格式: {"12345": 5, "12346": 4}
|
||||||
* 外层key为机位ID,内层Map为该机位的各维度评分
|
* key为机位ID,value为该机位的评分(1-5)
|
||||||
*/
|
*/
|
||||||
private Map<String, Map<String, Integer>> cameraPositionRating;
|
private Map<String, Integer> cameraPositionRating;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ public class VideoReviewStatisticsRespDTO {
|
|||||||
private List<ScenicReviewRank> scenicRankList;
|
private List<ScenicReviewRank> scenicRankList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 机位评价维度统计
|
* 机位评价统计
|
||||||
* key: 维度名称, value: 平均分
|
* key: 机位ID, value: 该机位的平均评分
|
||||||
*/
|
*/
|
||||||
private Map<String, BigDecimal> cameraPositionAverage;
|
private Map<String, BigDecimal> cameraPositionAverage;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.ycwl.basic.handler.NestedMapTypeHandler;
|
import com.ycwl.basic.handler.MapTypeHandler;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.apache.ibatis.type.JdbcType;
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
|
||||||
@@ -51,11 +51,11 @@ public class VideoReviewEntity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 机位评价JSON
|
* 机位评价JSON
|
||||||
* 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
|
* 格式: {"12345": 5, "12346": 4}
|
||||||
* 外层key为机位ID,内层Map为该机位的各维度评分
|
* key为机位ID,value为该机位的评分(1-5)
|
||||||
*/
|
*/
|
||||||
@TableField(typeHandler = NestedMapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||||
private Map<String, Map<String, Integer>> cameraPositionRating;
|
private Map<String, Integer> cameraPositionRating;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.model.printer.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建虚拟用户0元订单请求参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CreateVirtualOrderRequest {
|
||||||
|
/**
|
||||||
|
* source记录ID
|
||||||
|
*/
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机ID(可选)
|
||||||
|
*/
|
||||||
|
private Integer printerId;
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.ycwl.basic.model.snowFlake;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.io.Serial;
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Created by liuhongguang on 2019年10月27日
|
|
||||||
* @Description
|
|
||||||
*/
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Data
|
|
||||||
public class UniqueId implements Serializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 0 + 41 + 5 + 5 + 12
|
|
||||||
* 固定 + 时间戳 + 工作机器ID + 数据中心ID + 序列号
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Serial
|
|
||||||
private static final long serialVersionUID = 8632670752020316524L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作机器ID、数据中心ID、序列号、上次生成ID的时间戳
|
|
||||||
*/
|
|
||||||
// 机器ID
|
|
||||||
private long machineId;
|
|
||||||
|
|
||||||
// 数据中心ID
|
|
||||||
private long datacenterId;
|
|
||||||
|
|
||||||
// 毫秒内序列
|
|
||||||
private long sequence;
|
|
||||||
|
|
||||||
// 时间戳
|
|
||||||
private long timestamp;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "UniqueIdRespVo{" +
|
|
||||||
"服务机器ID=" + machineId +
|
|
||||||
", 数据中心ID=" + datacenterId +
|
|
||||||
", 毫秒内的序列=" + sequence +
|
|
||||||
", 生成时间与预设时间戳间隔=" + timestamp +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.ycwl.basic.model.snowFlake;
|
|
||||||
|
|
||||||
|
|
||||||
public class UniqueIdMetaData {
|
|
||||||
/**
|
|
||||||
* 取当前系统启动时间为参考起始时间,
|
|
||||||
* 取1995-04-01为参考日
|
|
||||||
*/
|
|
||||||
// public static final long START_TIME = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
|
|
||||||
public static final long START_TIME = 796665600000L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 机器ID所占位数
|
|
||||||
*/
|
|
||||||
// 机器位数
|
|
||||||
public static final long MACHINE_ID_BITS = 5L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 机器ID最大值31,0-31
|
|
||||||
*/
|
|
||||||
// 机器ID最大
|
|
||||||
public static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据中心ID所占位数
|
|
||||||
*/
|
|
||||||
// 数据中心ID所占位数
|
|
||||||
public static final long DATACENTER_ID_BITS = 5L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据中心ID最大值31,0-31
|
|
||||||
*/
|
|
||||||
// 数据中心ID最大值
|
|
||||||
public static final long MAX_DATACENTER_ID = ~(-1L << MACHINE_ID_BITS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sequence所占位数
|
|
||||||
*/
|
|
||||||
// 序列所占位数
|
|
||||||
public static final long SEQUENCE_BITS = 12L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 机器ID偏移量12
|
|
||||||
*/
|
|
||||||
// 机器ID偏移量
|
|
||||||
public static final long MACHINE_SHIFT_BITS = SEQUENCE_BITS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据中心ID偏移量12+5=17
|
|
||||||
*/
|
|
||||||
// 数据中心ID偏移量
|
|
||||||
public static final long DATACENTER_SHIFT_BITS = SEQUENCE_BITS + MACHINE_ID_BITS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时间戳的偏移量12+5+5=22
|
|
||||||
*/
|
|
||||||
// 时间戳偏移量
|
|
||||||
public static final long TIMESTAMP_LEFT_SHIFT_BITS = SEQUENCE_BITS + MACHINE_ID_BITS + DATACENTER_ID_BITS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sequence掩码4095
|
|
||||||
*/
|
|
||||||
// 序列掩码
|
|
||||||
public static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 机器ID掩码1023
|
|
||||||
*/
|
|
||||||
// 机器ID掩码
|
|
||||||
public static final long MACHINE_MASK = ~(-1L << MACHINE_ID_BITS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据中心掩码1023
|
|
||||||
*/
|
|
||||||
// 数据中心掩码
|
|
||||||
public static final long DATACENTER_MASK = ~(-1L << MACHINE_ID_BITS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时间戳掩码2的41次方减1
|
|
||||||
*/
|
|
||||||
// 时间戳掩码
|
|
||||||
public static final long TIMESTAMP_MASK = ~(-1L << 41L);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -200,6 +200,7 @@ public class WxMpPayAdapter implements IPayAdapter {
|
|||||||
Transaction parse = parser.parse(requestParam, Transaction.class);
|
Transaction parse = parser.parse(requestParam, Transaction.class);
|
||||||
resp.setValid(true);
|
resp.setValid(true);
|
||||||
resp.setOrderNo(parse.getOutTradeNo());
|
resp.setOrderNo(parse.getOutTradeNo());
|
||||||
|
resp.setTransactionId(parse.getTransactionId());
|
||||||
if (parse.getAmount() != null) {
|
if (parse.getAmount() != null) {
|
||||||
resp.setOrderPrice(parse.getAmount().getTotal());
|
resp.setOrderPrice(parse.getAmount().getTotal());
|
||||||
resp.setPayPrice(parse.getAmount().getPayerTotal());
|
resp.setPayPrice(parse.getAmount().getPayerTotal());
|
||||||
@@ -281,6 +282,7 @@ public class WxMpPayAdapter implements IPayAdapter {
|
|||||||
resp.setRefundNo(refund.getOutRefundNo());
|
resp.setRefundNo(refund.getOutRefundNo());
|
||||||
} else {
|
} else {
|
||||||
resp.setSuccess(false);
|
resp.setSuccess(false);
|
||||||
|
resp.setMessage(refund.getStatus().name());
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
@@ -313,6 +315,7 @@ public class WxMpPayAdapter implements IPayAdapter {
|
|||||||
.build();
|
.build();
|
||||||
RefundNotification parse = parser.parse(requestParam, RefundNotification.class);
|
RefundNotification parse = parser.parse(requestParam, RefundNotification.class);
|
||||||
resp.setValid(true);
|
resp.setValid(true);
|
||||||
|
resp.setRefundTransactionId(parse.getRefundId());
|
||||||
resp.setOriginalResponse(parse);
|
resp.setOriginalResponse(parse);
|
||||||
if (parse.getRefundStatus() == SUCCESS) {
|
if (parse.getRefundStatus() == SUCCESS) {
|
||||||
//退款成功
|
//退款成功
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.math.BigDecimal;
|
|||||||
public class PayResponse {
|
public class PayResponse {
|
||||||
private boolean valid;
|
private boolean valid;
|
||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
private String transactionId;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Object originalResponse;
|
private Object originalResponse;
|
||||||
private Integer orderPrice;
|
private Integer orderPrice;
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ import lombok.Data;
|
|||||||
public class RefundOrderResponse {
|
public class RefundOrderResponse {
|
||||||
private boolean success;
|
private boolean success;
|
||||||
private String refundNo;
|
private String refundNo;
|
||||||
|
private String message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class RefundResponse {
|
|||||||
private boolean valid;
|
private boolean valid;
|
||||||
private String orderNo;
|
private String orderNo;
|
||||||
private String refundNo;
|
private String refundNo;
|
||||||
|
private String refundTransactionId;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Object originalResponse;
|
private Object originalResponse;
|
||||||
private Integer orderPrice;
|
private Integer orderPrice;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ public enum CouponStatus { CLAIMED("claimed", ...), USED("used", ...), EXPIRED("
|
|||||||
|
|
||||||
#### 关键特性
|
#### 关键特性
|
||||||
- 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`)控制适用商品
|
- 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`)控制适用商品
|
||||||
|
- 属性门槛:通过 `requiredAttributeKeys`(JSON) 配置,要求在可折扣商品范围内任一商品出现任一属性Key(属性Key为后端与运营约定的字符串);商品属性由服务端根据商品能力配置(`ProductTypeCapability.metadata.pricingAttributeKeys`)计算写入 `ProductItem.attributeKeys`
|
||||||
- 消费限制:支持最小消费金额、最大折扣限制
|
- 消费限制:支持最小消费金额、最大折扣限制
|
||||||
- 时效性:基于时间的有效期控制
|
- 时效性:基于时间的有效期控制
|
||||||
- **用户领取数量限制**:通过 `userClaimLimit` 字段控制单个用户可领取优惠券的最大数量(v1.0.0新增)
|
- **用户领取数量限制**:通过 `userClaimLimit` 字段控制单个用户可领取优惠券的最大数量(v1.0.0新增)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.pricing.enums.ProductType;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商品项DTO
|
* 商品项DTO
|
||||||
@@ -50,4 +51,9 @@ public class ProductItem {
|
|||||||
* 景区ID
|
* 景区ID
|
||||||
*/
|
*/
|
||||||
private String scenicId;
|
private String scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品属性Key列表(服务端计算填充,客户端传入会被忽略)
|
||||||
|
*/
|
||||||
|
private List<String> attributeKeys;
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,12 @@ public class PriceCouponConfig {
|
|||||||
*/
|
*/
|
||||||
private String applicableProducts;
|
private String applicableProducts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠券使用门槛:要求在可折扣商品范围内出现指定属性Key(JSON)
|
||||||
|
* 为空表示不限制
|
||||||
|
*/
|
||||||
|
private String requiredAttributeKeys;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发行总量
|
* 发行总量
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
|||||||
* 插入优惠券配置
|
* 插入优惠券配置
|
||||||
*/
|
*/
|
||||||
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
|
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
|
||||||
"max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " +
|
"max_discount, applicable_products, required_attribute_keys, total_quantity, used_quantity, valid_from, valid_until, " +
|
||||||
"is_active, scenic_id, create_time, update_time) VALUES " +
|
"is_active, scenic_id, create_time, update_time) VALUES " +
|
||||||
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
|
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
|
||||||
"#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " +
|
"#{applicableProducts}, #{requiredAttributeKeys}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " +
|
||||||
"#{isActive}, #{scenicId}, NOW(), NOW())")
|
"#{isActive}, #{scenicId}, NOW(), NOW())")
|
||||||
int insertCoupon(PriceCouponConfig coupon);
|
int insertCoupon(PriceCouponConfig coupon);
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
|||||||
*/
|
*/
|
||||||
@Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " +
|
@Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " +
|
||||||
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
|
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
|
||||||
"applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " +
|
"applicable_products = #{applicableProducts}, required_attribute_keys = #{requiredAttributeKeys}, total_quantity = #{totalQuantity}, " +
|
||||||
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " +
|
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " +
|
||||||
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
|
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateCoupon(PriceCouponConfig coupon);
|
int updateCoupon(PriceCouponConfig coupon);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ycwl.basic.pricing.service.impl;
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||||
@@ -27,8 +29,11 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CouponManagementServiceImpl implements ICouponManagementService {
|
public class CouponManagementServiceImpl implements ICouponManagementService {
|
||||||
|
|
||||||
|
private static final TypeReference<List<String>> STRING_LIST_TYPE = new TypeReference<>() {};
|
||||||
|
|
||||||
private final PriceCouponConfigMapper couponConfigMapper;
|
private final PriceCouponConfigMapper couponConfigMapper;
|
||||||
private final PriceCouponClaimRecordMapper claimRecordMapper;
|
private final PriceCouponClaimRecordMapper claimRecordMapper;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
// ==================== 优惠券配置管理 ====================
|
// ==================== 优惠券配置管理 ====================
|
||||||
|
|
||||||
@@ -37,6 +42,8 @@ public class CouponManagementServiceImpl implements ICouponManagementService {
|
|||||||
public Long createCouponConfig(PriceCouponConfig config) {
|
public Long createCouponConfig(PriceCouponConfig config) {
|
||||||
log.info("创建优惠券配置: {}", config.getCouponName());
|
log.info("创建优惠券配置: {}", config.getCouponName());
|
||||||
|
|
||||||
|
validateCouponConfig(config);
|
||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
if (config.getUsedQuantity() == null) {
|
if (config.getUsedQuantity() == null) {
|
||||||
config.setUsedQuantity(0);
|
config.setUsedQuantity(0);
|
||||||
@@ -60,6 +67,8 @@ public class CouponManagementServiceImpl implements ICouponManagementService {
|
|||||||
public boolean updateCouponConfig(PriceCouponConfig config) {
|
public boolean updateCouponConfig(PriceCouponConfig config) {
|
||||||
log.info("更新优惠券配置,ID: {}", config.getId());
|
log.info("更新优惠券配置,ID: {}", config.getId());
|
||||||
|
|
||||||
|
validateCouponConfig(config);
|
||||||
|
|
||||||
PriceCouponConfig existing = couponConfigMapper.selectById(config.getId());
|
PriceCouponConfig existing = couponConfigMapper.selectById(config.getId());
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
log.error("优惠券配置不存在,ID: {}", config.getId());
|
log.error("优惠券配置不存在,ID: {}", config.getId());
|
||||||
@@ -76,6 +85,32 @@ public class CouponManagementServiceImpl implements ICouponManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateCouponConfig(PriceCouponConfig config) {
|
||||||
|
validateRequiredAttributeKeys(config.getRequiredAttributeKeys());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRequiredAttributeKeys(String requiredAttributeKeys) {
|
||||||
|
if (requiredAttributeKeys == null || requiredAttributeKeys.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> keys;
|
||||||
|
try {
|
||||||
|
keys = objectMapper.readValue(requiredAttributeKeys, STRING_LIST_TYPE);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("requiredAttributeKeys格式错误,必须是JSON数组字符串,例如 [\"TYPE_3\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys == null || keys.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasBlankKey = keys.stream().anyMatch(key -> key == null || key.trim().isEmpty());
|
||||||
|
if (hasBlankKey) {
|
||||||
|
throw new IllegalArgumentException("requiredAttributeKeys不能包含空值");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean deleteCouponConfig(Long id) {
|
public boolean deleteCouponConfig(Long id) {
|
||||||
|
|||||||
@@ -118,23 +118,63 @@ public class CouponServiceImpl implements ICouponService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查商品类型限制
|
// 3. 检查商品类型限制(用于确定可折扣商品范围)
|
||||||
if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) {
|
List<ProductItem> discountableProducts = products;
|
||||||
return true;
|
if (coupon.getApplicableProducts() != null && !coupon.getApplicableProducts().isEmpty()) {
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<String> applicableProductTypes = objectMapper.readValue(
|
List<String> applicableProductTypes = objectMapper.readValue(
|
||||||
coupon.getApplicableProducts(), new TypeReference<List<String>>() {});
|
coupon.getApplicableProducts(), new TypeReference<List<String>>() {});
|
||||||
|
|
||||||
for (ProductItem product : products) {
|
discountableProducts = products.stream()
|
||||||
if (applicableProductTypes.contains(product.getProductType().getCode())) {
|
.filter(product -> applicableProductTypes.contains(product.getProductType().getCode()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (discountableProducts.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析适用商品类型失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查属性门槛:要求在可折扣商品范围内,任一商品出现任一属性Key
|
||||||
|
if (coupon.getRequiredAttributeKeys() == null || coupon.getRequiredAttributeKeys().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<String> requiredAttributeKeys = objectMapper.readValue(
|
||||||
|
coupon.getRequiredAttributeKeys(), new TypeReference<List<String>>() {});
|
||||||
|
if (requiredAttributeKeys == null || requiredAttributeKeys.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ProductItem product : discountableProducts) {
|
||||||
|
List<String> attributeKeys = product.getAttributeKeys();
|
||||||
|
if (attributeKeys == null || attributeKeys.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String requiredKey : requiredAttributeKeys) {
|
||||||
|
if (requiredKey == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = requiredKey.trim();
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeKeys.contains(key)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析适用商品类型失败", e);
|
log.error("解析优惠券属性门槛失败", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 价格计算服务实现
|
* 价格计算服务实现
|
||||||
@@ -27,6 +31,8 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||||
|
|
||||||
|
private static final String CAPABILITY_METADATA_ATTRIBUTE_KEYS = "pricingAttributeKeys";
|
||||||
|
|
||||||
private final IProductConfigService productConfigService;
|
private final IProductConfigService productConfigService;
|
||||||
private final ICouponService couponService;
|
private final ICouponService couponService;
|
||||||
private final IPriceBundleService bundleService;
|
private final IPriceBundleService bundleService;
|
||||||
@@ -46,6 +52,13 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
return capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED;
|
return capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isQuantityBasedPricing(ProductTypeCapability capability) {
|
||||||
|
if (capability == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
||||||
if (request.getProducts() == null || request.getProducts().isEmpty()) {
|
if (request.getProducts() == null || request.getProducts().isEmpty()) {
|
||||||
@@ -166,9 +179,16 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||||
BigDecimal originalTotalAmount = BigDecimal.ZERO;
|
BigDecimal originalTotalAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
Map<String, ProductTypeCapability> capabilityCache = new HashMap<>();
|
||||||
|
Map<String, List<String>> attributeKeysCache = new HashMap<>();
|
||||||
|
|
||||||
for (ProductItem product : products) {
|
for (ProductItem product : products) {
|
||||||
|
String productTypeCode = product.getProductType().getCode();
|
||||||
|
ProductTypeCapability capability = capabilityCache.computeIfAbsent(
|
||||||
|
productTypeCode, productTypeCapabilityService::getCapability);
|
||||||
|
|
||||||
// 计算实际价格和原价(传入景区ID)
|
// 计算实际价格和原价(传入景区ID)
|
||||||
ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product, scenicId);
|
ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product, scenicId, capability);
|
||||||
|
|
||||||
product.setUnitPrice(priceInfo.getActualPrice());
|
product.setUnitPrice(priceInfo.getActualPrice());
|
||||||
product.setOriginalPrice(priceInfo.getOriginalPrice());
|
product.setOriginalPrice(priceInfo.getOriginalPrice());
|
||||||
@@ -188,6 +208,51 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> buildProductAttributeKeys(ProductTypeCapability capability) {
|
||||||
|
if (capability == null || capability.getMetadata() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object rawValue = capability.getMetadata().get(CAPABILITY_METADATA_ATTRIBUTE_KEYS);
|
||||||
|
if (rawValue == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> result = new LinkedHashSet<>();
|
||||||
|
if (rawValue instanceof List<?> rawList) {
|
||||||
|
for (Object item : rawList) {
|
||||||
|
if (item instanceof String rawKey) {
|
||||||
|
addAttributeKey(result, rawKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (rawValue instanceof String rawString) {
|
||||||
|
String[] parts = rawString.split(",");
|
||||||
|
for (String part : parts) {
|
||||||
|
addAttributeKey(result, part);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("商品类型能力metadata中{}字段类型不支持: productType={}, valueType={}",
|
||||||
|
CAPABILITY_METADATA_ATTRIBUTE_KEYS,
|
||||||
|
capability.getProductType(),
|
||||||
|
rawValue.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.isEmpty() ? List.of() : List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAttributeKey(Set<String> target, String rawKey) {
|
||||||
|
if (rawKey == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = rawKey.trim();
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal calculateSingleProductPrice(ProductItem product) {
|
private BigDecimal calculateSingleProductPrice(ProductItem product) {
|
||||||
ProductType productType = product.getProductType();
|
ProductType productType = product.getProductType();
|
||||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||||
@@ -245,7 +310,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product, Long scenicId) {
|
private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product, Long scenicId,
|
||||||
|
ProductTypeCapability capability) {
|
||||||
ProductType productType = product.getProductType();
|
ProductType productType = product.getProductType();
|
||||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||||
|
|
||||||
@@ -269,7 +335,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
actualPrice = baseConfig.getBasePrice();
|
actualPrice = baseConfig.getBasePrice();
|
||||||
originalPrice = baseConfig.getOriginalPrice();
|
originalPrice = baseConfig.getOriginalPrice();
|
||||||
|
|
||||||
if (isQuantityBasedPricing(productType.getCode())) {
|
if (isQuantityBasedPricing(capability)) {
|
||||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
if (originalPrice != null) {
|
if (originalPrice != null) {
|
||||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
@@ -289,7 +355,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
actualPrice = defaultConfig.getBasePrice();
|
actualPrice = defaultConfig.getBasePrice();
|
||||||
originalPrice = defaultConfig.getOriginalPrice();
|
originalPrice = defaultConfig.getOriginalPrice();
|
||||||
|
|
||||||
if (isQuantityBasedPricing(productType.getCode())) {
|
if (isQuantityBasedPricing(capability)) {
|
||||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
if (originalPrice != null) {
|
if (originalPrice != null) {
|
||||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
@@ -308,7 +374,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
actualPrice = baseConfig.getBasePrice();
|
actualPrice = baseConfig.getBasePrice();
|
||||||
originalPrice = baseConfig.getOriginalPrice();
|
originalPrice = baseConfig.getOriginalPrice();
|
||||||
|
|
||||||
if (isQuantityBasedPricing(productType.getCode())) {
|
if (isQuantityBasedPricing(capability)) {
|
||||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
if (originalPrice != null) {
|
if (originalPrice != null) {
|
||||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ public class VoucherPrintServiceImpl implements VoucherPrintService {
|
|||||||
// 验证faceId是否属于当前用户
|
// 验证faceId是否属于当前用户
|
||||||
validateFaceOwnership(request.getFaceId(), currentUserId);
|
validateFaceOwnership(request.getFaceId(), currentUserId);
|
||||||
|
|
||||||
ScenicConfigManager config = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
Long brokerId = config.getLong("voucher_broker_id");
|
Long brokerId = configManager.getLong("voucher_broker_id");
|
||||||
if (brokerId != null) {
|
if (brokerId != null) {
|
||||||
if (!request.getBrokerId().equals(brokerId)) {
|
if (!request.getBrokerId().equals(brokerId)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -192,11 +192,11 @@ public class VoucherPrintServiceImpl implements VoucherPrintService {
|
|||||||
}
|
}
|
||||||
request.setScenicId(face.getScenicId());
|
request.setScenicId(face.getScenicId());
|
||||||
|
|
||||||
ScenicConfigManager config = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
if (!Boolean.TRUE.equals(config.getBoolean("booking_enable"))) {
|
if (!Boolean.TRUE.equals(configManager.getBoolean("booking_enable"))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Long brokerId = config.getLong("booking_broker_id");
|
Long brokerId = configManager.getLong("booking_broker_id");
|
||||||
if (brokerId != null) {
|
if (brokerId != null) {
|
||||||
if (!request.getBrokerId().equals(brokerId)) {
|
if (!request.getBrokerId().equals(brokerId)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -370,9 +370,9 @@ public class VoucherPrintServiceImpl implements VoucherPrintService {
|
|||||||
content += "<CB>"+voucherPrintResp.getCode()+"</CB>";
|
content += "<CB>"+voucherPrintResp.getCode()+"</CB>";
|
||||||
content += "<C>"+voucherPrintResp.getType()+"</C>";
|
content += "<C>"+voucherPrintResp.getType()+"</C>";
|
||||||
content += "<C>有效期:"+sdf2.format(new Date())+"</C>";
|
content += "<C>有效期:"+sdf2.format(new Date())+"</C>";
|
||||||
ScenicConfigManager config = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
if (Strings.isNotBlank(config.getString("ticket_print_sn"))) {
|
if (Strings.isNotBlank(configManager.getString("ticket_print_sn"))) {
|
||||||
FeiETicketPrinter.doPrint(config.getString("ticket_print_sn"), content, 1);
|
FeiETicketPrinter.doPrint(configManager.getString("ticket_print_sn"), content, 1);
|
||||||
} else {
|
} else {
|
||||||
log.warn("打印没有配置->内容:\n{}", content);
|
log.warn("打印没有配置->内容:\n{}", content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ public class PuzzleTemplateDTO {
|
|||||||
*/
|
*/
|
||||||
private Long scenicId;
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动添加到打印队列:1-开启 0-关闭
|
||||||
|
*/
|
||||||
|
private Integer autoAddPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以打印:1-可以 0-不可以
|
||||||
|
*/
|
||||||
|
private Integer canPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户查看区域(裁切区域),格式:x,y,w,h
|
||||||
|
*/
|
||||||
|
private String userArea;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 元素列表
|
* 元素列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ public class TemplateCreateRequest {
|
|||||||
*/
|
*/
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动添加到打印队列:1-开启 0-关闭
|
||||||
|
*/
|
||||||
|
private Integer autoAddPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以打印:1-可以 0-不可以
|
||||||
|
*/
|
||||||
|
private Integer canPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户查看区域(裁切区域),格式:x,y,w,h
|
||||||
|
*/
|
||||||
|
private String userArea;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态:0-禁用 1-启用
|
* 状态:0-禁用 1-启用
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ public class PuzzleGenerationRecordEntity {
|
|||||||
@TableField("result_image_url")
|
@TableField("result_image_url")
|
||||||
private String resultImageUrl;
|
private String resultImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始图片URL(未裁切的图片,用于打印)
|
||||||
|
*/
|
||||||
|
@TableField("original_image_url")
|
||||||
|
private String originalImageUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件大小(字节)
|
* 文件大小(字节)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -97,6 +97,24 @@ public class PuzzleTemplateEntity {
|
|||||||
@TableField("scenic_id")
|
@TableField("scenic_id")
|
||||||
private Long scenicId;
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动添加到打印队列:1-开启 0-关闭
|
||||||
|
*/
|
||||||
|
@TableField("auto_add_print")
|
||||||
|
private Integer autoAddPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以打印:1-可以 0-不可以
|
||||||
|
*/
|
||||||
|
@TableField("can_print")
|
||||||
|
private Integer canPrint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户查看区域(裁切区域),格式:x,y,w,h
|
||||||
|
*/
|
||||||
|
@TableField("user_area")
|
||||||
|
private String userArea;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public interface PuzzleGenerationRecordMapper {
|
|||||||
*/
|
*/
|
||||||
int updateSuccess(@Param("id") Long id,
|
int updateSuccess(@Param("id") Long id,
|
||||||
@Param("resultImageUrl") String resultImageUrl,
|
@Param("resultImageUrl") String resultImageUrl,
|
||||||
|
@Param("originalImageUrl") String originalImageUrl,
|
||||||
@Param("resultFileSize") Long resultFileSize,
|
@Param("resultFileSize") Long resultFileSize,
|
||||||
@Param("resultWidth") Integer resultWidth,
|
@Param("resultWidth") Integer resultWidth,
|
||||||
@Param("resultHeight") Integer resultHeight,
|
@Param("resultHeight") Integer resultHeight,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
|||||||
import com.ycwl.basic.puzzle.util.PuzzleDuplicationDetector;
|
import com.ycwl.basic.puzzle.util.PuzzleDuplicationDetector;
|
||||||
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
import com.ycwl.basic.puzzle.util.PuzzleImageRenderer;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
import com.ycwl.basic.storage.StorageFactory;
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
import com.ycwl.basic.utils.WxMpUtil;
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -60,6 +61,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
@Lazy
|
@Lazy
|
||||||
private final PuzzleDuplicationDetector duplicationDetector;
|
private final PuzzleDuplicationDetector duplicationDetector;
|
||||||
|
@Lazy
|
||||||
|
private final PrinterService printerService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
||||||
@@ -136,30 +139,64 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
// 9. 渲染图片
|
// 9. 渲染图片
|
||||||
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
|
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
|
||||||
|
|
||||||
// 10. 上传到OSS
|
// 10. 上传原图到OSS(未裁切)
|
||||||
String imageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
String originalImageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
||||||
log.info("图片上传成功: url={}", imageUrl);
|
log.info("原图上传成功: url={}", originalImageUrl);
|
||||||
|
|
||||||
// 11. 更新记录为成功
|
// 11. 处理用户区域裁切
|
||||||
|
String finalImageUrl = originalImageUrl; // 默认使用原图
|
||||||
|
BufferedImage finalImage = resultImage;
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(template.getUserArea())) {
|
||||||
|
try {
|
||||||
|
BufferedImage croppedImage = cropImage(resultImage, template.getUserArea());
|
||||||
|
finalImageUrl = uploadImage(croppedImage, template.getCode() + "_cropped", request.getOutputFormat(), request.getQuality());
|
||||||
|
finalImage = croppedImage;
|
||||||
|
log.info("裁切后图片上传成功: userArea={}, url={}", template.getUserArea(), finalImageUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("图片裁切失败,使用原图: userArea={}", template.getUserArea(), e);
|
||||||
|
// 裁切失败时使用原图
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 更新记录为成功
|
||||||
long duration = (int) (System.currentTimeMillis() - startTime);
|
long duration = (int) (System.currentTimeMillis() - startTime);
|
||||||
long fileSize = estimateFileSize(resultImage, request.getOutputFormat());
|
long fileSize = estimateFileSize(finalImage, request.getOutputFormat());
|
||||||
recordMapper.updateSuccess(
|
recordMapper.updateSuccess(
|
||||||
record.getId(),
|
record.getId(),
|
||||||
imageUrl,
|
finalImageUrl,
|
||||||
|
originalImageUrl,
|
||||||
fileSize,
|
fileSize,
|
||||||
resultImage.getWidth(),
|
finalImage.getWidth(),
|
||||||
resultImage.getHeight(),
|
finalImage.getHeight(),
|
||||||
(int) duration
|
(int) duration
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info("拼图生成成功(新生成): recordId={}, imageUrl={}, duration={}ms",
|
log.info("拼图生成成功(新生成): recordId={}, originalUrl={}, finalUrl={}, duration={}ms",
|
||||||
record.getId(), imageUrl, duration);
|
record.getId(), originalImageUrl, finalImageUrl, duration);
|
||||||
|
|
||||||
|
// 13. 检查是否自动添加到打印队列
|
||||||
|
if (template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
|
||||||
|
try {
|
||||||
|
Integer printRecordId = printerService.addUserPhotoFromPuzzle(
|
||||||
|
request.getUserId(),
|
||||||
|
resolvedScenicId,
|
||||||
|
request.getFaceId(),
|
||||||
|
originalImageUrl, // 使用原图URL添加到打印队列
|
||||||
|
record.getId()
|
||||||
|
);
|
||||||
|
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
|
||||||
|
// 添加失败不影响拼图生成流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return PuzzleGenerateResponse.success(
|
return PuzzleGenerateResponse.success(
|
||||||
imageUrl,
|
finalImageUrl,
|
||||||
fileSize,
|
fileSize,
|
||||||
resultImage.getWidth(),
|
finalImage.getWidth(),
|
||||||
resultImage.getHeight(),
|
finalImage.getHeight(),
|
||||||
(int) duration,
|
(int) duration,
|
||||||
record.getId(),
|
record.getId(),
|
||||||
false, // isDuplicate=false
|
false, // isDuplicate=false
|
||||||
@@ -405,4 +442,43 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
|
|
||||||
return templateScenicId;
|
return templateScenicId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 裁切图片
|
||||||
|
* @param image 原图
|
||||||
|
* @param userArea 裁切区域,格式:x,y,w,h
|
||||||
|
* @return 裁切后的图片
|
||||||
|
*/
|
||||||
|
private BufferedImage cropImage(BufferedImage image, String userArea) {
|
||||||
|
if (StrUtil.isBlank(userArea)) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] parts = userArea.split(",");
|
||||||
|
if (parts.length != 4) {
|
||||||
|
throw new IllegalArgumentException("userArea格式错误,应为:x,y,w,h");
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = Integer.parseInt(parts[0].trim());
|
||||||
|
int y = Integer.parseInt(parts[1].trim());
|
||||||
|
int w = Integer.parseInt(parts[2].trim());
|
||||||
|
int h = Integer.parseInt(parts[3].trim());
|
||||||
|
|
||||||
|
// 边界检查
|
||||||
|
if (x < 0 || y < 0 || w <= 0 || h <= 0) {
|
||||||
|
throw new IllegalArgumentException("裁切区域参数必须为正数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x + w > image.getWidth() || y + h > image.getHeight()) {
|
||||||
|
throw new IllegalArgumentException("裁切区域超出图片边界");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行裁切
|
||||||
|
return image.getSubimage(x, y, w, h);
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("userArea格式错误,参数必须为数字", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.ycwl.basic.mapper.MpNotifyConfigMapper;
|
|||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.mp.MpNotifyConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpNotifyConfigEntity;
|
||||||
import com.ycwl.basic.model.pc.mp.ScenicMpNotifyVO;
|
import com.ycwl.basic.model.pc.mp.ScenicMpNotifyVO;
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
|
|
||||||
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
|
||||||
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
|
||||||
import com.ycwl.basic.pay.enums.PayAdapterType;
|
import com.ycwl.basic.pay.enums.PayAdapterType;
|
||||||
@@ -59,78 +58,6 @@ public class ScenicRepository {
|
|||||||
return scenicEntity;
|
return scenicEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public ScenicConfigEntity getScenicConfig(Long scenicId) {
|
|
||||||
ScenicConfigManager scenicConfigManager = getScenicConfigManager(scenicId);
|
|
||||||
ScenicConfigEntity config = new ScenicConfigEntity();
|
|
||||||
|
|
||||||
// 基础配置
|
|
||||||
config.setScenicId(scenicId);
|
|
||||||
if (scenicConfigManager == null) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 业务流程配置
|
|
||||||
config.setBookRoutine(scenicConfigManager.getInteger("book_routine"));
|
|
||||||
config.setForceFinishTime(scenicConfigManager.getInteger("force_finish_time"));
|
|
||||||
config.setTourTime(scenicConfigManager.getInteger("tour_time"));
|
|
||||||
|
|
||||||
// 存储时间配置
|
|
||||||
config.setSampleStoreDay(scenicConfigManager.getInteger("sample_store_day"));
|
|
||||||
config.setFaceStoreDay(scenicConfigManager.getInteger("face_store_day"));
|
|
||||||
config.setVideoStoreDay(scenicConfigManager.getInteger("video_store_day"));
|
|
||||||
config.setVideoSourceStoreDay(scenicConfigManager.getInteger("video_source_store_day"));
|
|
||||||
config.setImageSourceStoreDay(scenicConfigManager.getInteger("image_source_store_day"));
|
|
||||||
config.setUserSourceExpireDay(scenicConfigManager.getInteger("user_source_expire_day"));
|
|
||||||
|
|
||||||
// 功能开关配置
|
|
||||||
config.setAllFree(scenicConfigManager.getBoolean("all_free"));
|
|
||||||
config.setDisableSourceVideo(scenicConfigManager.getBoolean("disable_source_video"));
|
|
||||||
config.setDisableSourceImage(scenicConfigManager.getBoolean("disable_source_image"));
|
|
||||||
config.setVoucherEnable(scenicConfigManager.getBoolean("voucher_enable"));
|
|
||||||
|
|
||||||
// 模板和防录屏配置
|
|
||||||
config.setTemplateNewVideoType(scenicConfigManager.getInteger("template_new_video_type"));
|
|
||||||
config.setAntiScreenRecordType(scenicConfigManager.getInteger("anti_screen_record_type"));
|
|
||||||
|
|
||||||
// 人脸识别配置
|
|
||||||
config.setFaceScoreThreshold(scenicConfigManager.getFloat("face_score_threshold"));
|
|
||||||
config.setFaceDetectHelperThreshold(scenicConfigManager.getInteger("face_detect_helper_threshold"));
|
|
||||||
config.setFaceType(scenicConfigManager.getEnum("face_type", FaceBodyAdapterType.class));
|
|
||||||
config.setFaceConfigJson(scenicConfigManager.getString("face_config_json"));
|
|
||||||
|
|
||||||
// 存储配置
|
|
||||||
config.setStoreType(scenicConfigManager.getEnum("store_type", StorageType.class));
|
|
||||||
config.setStoreConfigJson(scenicConfigManager.getString("store_config_json"));
|
|
||||||
config.setTmpStoreType(scenicConfigManager.getEnum("tmp_store_type", StorageType.class));
|
|
||||||
config.setTmpStoreConfigJson(scenicConfigManager.getString("tmp_store_config_json"));
|
|
||||||
config.setLocalStoreType(scenicConfigManager.getEnum("local_store_type", StorageType.class));
|
|
||||||
config.setLocalStoreConfigJson(scenicConfigManager.getString("local_store_config_json"));
|
|
||||||
|
|
||||||
// 支付配置
|
|
||||||
config.setPayType(scenicConfigManager.getEnum("pay_type", PayAdapterType.class));
|
|
||||||
config.setPayConfigJson(scenicConfigManager.getString("pay_config_json"));
|
|
||||||
|
|
||||||
// 推客配置
|
|
||||||
config.setBrokerDirectRate(scenicConfigManager.getBigDecimal("broker_direct_rate"));
|
|
||||||
|
|
||||||
// 水印配置
|
|
||||||
config.setWatermarkType(scenicConfigManager.getString("watermark_type"));
|
|
||||||
config.setWatermarkScenicText(scenicConfigManager.getString("watermark_scenic_text"));
|
|
||||||
config.setWatermarkDtFormat(scenicConfigManager.getString("watermark_dt_format"));
|
|
||||||
|
|
||||||
// 提示信息配置
|
|
||||||
config.setImageSourcePackHint(scenicConfigManager.getString("image_source_pack_hint"));
|
|
||||||
config.setVideoSourcePackHint(scenicConfigManager.getString("video_source_pack_hint"));
|
|
||||||
config.setExtraNotificationTime(scenicConfigManager.getString("extra_notification_time"));
|
|
||||||
|
|
||||||
// 免费数量配置
|
|
||||||
config.setPhotoFreeNum(scenicConfigManager.getInteger("photo_free_num"));
|
|
||||||
config.setVideoFreeNum(scenicConfigManager.getInteger("video_free_num"));
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MpConfigEntity getScenicMpConfig(Long scenicId) {
|
public MpConfigEntity getScenicMpConfig(Long scenicId) {
|
||||||
if (redisTemplate.hasKey(String.format(SCENIC_MP_CACHE_KEY, scenicId))) {
|
if (redisTemplate.hasKey(String.format(SCENIC_MP_CACHE_KEY, scenicId))) {
|
||||||
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_MP_CACHE_KEY, scenicId)), MpConfigEntity.class);
|
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_MP_CACHE_KEY, scenicId)), MpConfigEntity.class);
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ public class VideoRepository {
|
|||||||
public static final String VIDEO_CACHE_KEY = "video:%s";
|
public static final String VIDEO_CACHE_KEY = "video:%s";
|
||||||
public static final String VIDEO_BY_TASK_ID_CACHE_KEY = "video:task:%s";
|
public static final String VIDEO_BY_TASK_ID_CACHE_KEY = "video:task:%s";
|
||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
|
||||||
private PriceBiz priceBiz;
|
|
||||||
@Autowired
|
|
||||||
private IVoucherService iVoucherService;
|
|
||||||
@Autowired
|
|
||||||
private MemberRelationRepository memberRelationRepository;
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
public VideoEntity getVideo(Long videoId) {
|
public VideoEntity getVideo(Long videoId) {
|
||||||
@@ -134,4 +129,14 @@ public class VideoRepository {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过faceId和templateId(可选)查询最新的视频记录
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param templateId 模板ID(可选)
|
||||||
|
* @return 最新的视频记录
|
||||||
|
*/
|
||||||
|
public VideoRespVO queryLatestByFaceIdAndTemplateId(Long faceId, Long templateId) {
|
||||||
|
return videoMapper.queryLatestByFaceIdAndTemplateId(faceId, templateId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,29 +53,6 @@ public class VideoTaskRepository {
|
|||||||
redisTemplate.delete(String.format(TASK_CACHE_KEY, taskId));
|
redisTemplate.delete(String.format(TASK_CACHE_KEY, taskId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Date getTaskShotDate(Long taskId) {
|
|
||||||
TaskRespVO taskRespVO = taskMapper.getById(taskId);
|
|
||||||
if (taskRespVO == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Date shotTime = taskRespVO.getCreateTime();
|
|
||||||
JacksonUtil.JSONObjectCompat paramJson = JacksonUtil.JSONObjectCompat.parseObject(taskRespVO.getTaskParams());
|
|
||||||
if (paramJson != null) {
|
|
||||||
Optional<String> any = paramJson.keySet().stream().filter(StringUtils::isNumeric).findAny();
|
|
||||||
if (any.isPresent()) {
|
|
||||||
var jsonArray = paramJson.getJSONArray(any.get());
|
|
||||||
if (jsonArray != null && !jsonArray.isEmpty()) {
|
|
||||||
JacksonUtil.JSONObjectCompat jsonObject = jsonArray.get(0);
|
|
||||||
if (jsonObject.getString("createTime") != null) {
|
|
||||||
shotTime = DateUtil.parse(jsonObject.getString("createTime"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shotTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTaskDeviceNum(Long taskId) {
|
public Integer getTaskDeviceNum(Long taskId) {
|
||||||
TaskEntity task = getTaskById(taskId);
|
TaskEntity task = getTaskById(taskId);
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
@@ -131,6 +108,32 @@ public class VideoTaskRepository {
|
|||||||
return deviceCount.get();
|
return deviceCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Date getTaskShotDate(Long taskId) {
|
||||||
|
TaskEntity task = getTaskById(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Date shotTime = task.getCreateTime();
|
||||||
|
Map<String, Object> paramJson = JacksonUtil.parseObject(task.getTaskParams(), Map.class);
|
||||||
|
if (paramJson != null) {
|
||||||
|
Optional<String> any = paramJson.keySet().stream().filter(StringUtils::isNumeric).findAny();
|
||||||
|
if (any.isPresent()) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> jsonArray = (List<Map<String, Object>>) paramJson.get(any.get());
|
||||||
|
if (jsonArray != null && !jsonArray.isEmpty()) {
|
||||||
|
Map<String, Object> jsonObject = jsonArray.get(0);
|
||||||
|
if (jsonObject.containsKey("createTime")) {
|
||||||
|
Object createTimeObj = jsonObject.get("createTime");
|
||||||
|
if (createTimeObj instanceof Number) {
|
||||||
|
shotTime = new Date(((Number) createTimeObj).longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shotTime;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查任务是否可以更新
|
* 检查任务是否可以更新
|
||||||
* @param taskId 任务ID
|
* @param taskId 任务ID
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.ycwl.basic.service;
|
package com.ycwl.basic.service;
|
||||||
|
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||||
@@ -45,4 +47,15 @@ public interface VideoReviewService {
|
|||||||
* @throws IOException IO异常
|
* @throws IOException IO异常
|
||||||
*/
|
*/
|
||||||
void exportReviews(VideoReviewListReqDTO reqDTO, OutputStream outputStream) throws IOException;
|
void exportReviews(VideoReviewListReqDTO reqDTO, OutputStream outputStream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查视频是否已被购买
|
||||||
|
* 购买条件:
|
||||||
|
* 1. 直接购买视频(order_item中goods_type=0且goods_id=视频id)
|
||||||
|
* 2. 购买整个模板(order的face_id与video关联的task的face_id相同,goods_type=-1,goods_id为video的templateId)
|
||||||
|
*
|
||||||
|
* @param reqDTO 查询条件
|
||||||
|
* @return 购买状态及订单ID列表
|
||||||
|
*/
|
||||||
|
VideoPurchaseCheckRespDTO checkVideoPurchase(VideoPurchaseCheckReqDTO reqDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ import com.github.pagehelper.PageInfo;
|
|||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
import com.ycwl.basic.exception.BaseException;
|
import com.ycwl.basic.exception.BaseException;
|
||||||
import com.ycwl.basic.exception.BizException;
|
import com.ycwl.basic.exception.BizException;
|
||||||
|
import com.ycwl.basic.mapper.OrderMapper;
|
||||||
|
import com.ycwl.basic.mapper.TaskMapper;
|
||||||
import com.ycwl.basic.mapper.VideoMapper;
|
import com.ycwl.basic.mapper.VideoMapper;
|
||||||
import com.ycwl.basic.mapper.VideoReviewMapper;
|
import com.ycwl.basic.mapper.VideoReviewMapper;
|
||||||
|
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||||
|
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
||||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
|
||||||
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
|
||||||
import com.ycwl.basic.repository.DeviceRepository;
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||||
@@ -31,7 +37,6 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视频评价Service实现类
|
* 视频评价Service实现类
|
||||||
@@ -49,6 +54,12 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DeviceRepository deviceRepository;
|
private DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderMapper orderMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskMapper taskMapper;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -160,7 +171,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
// 2. 收集所有机位ID并批量查询机位名称
|
// 2. 收集所有机位ID并批量查询机位名称
|
||||||
Set<Long> allDeviceIds = new LinkedHashSet<>();
|
Set<Long> allDeviceIds = new LinkedHashSet<>();
|
||||||
for (VideoReviewRespDTO review : list) {
|
for (VideoReviewRespDTO review : list) {
|
||||||
Map<String, Map<String, Integer>> cameraRating = review.getCameraPositionRating();
|
Map<String, Integer> cameraRating = review.getCameraPositionRating();
|
||||||
if (cameraRating != null && !cameraRating.isEmpty()) {
|
if (cameraRating != null && !cameraRating.isEmpty()) {
|
||||||
// 收集机位ID (按顺序)
|
// 收集机位ID (按顺序)
|
||||||
for (String deviceIdStr : cameraRating.keySet()) {
|
for (String deviceIdStr : cameraRating.keySet()) {
|
||||||
@@ -195,12 +206,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
|
||||||
// 5. 创建单元格自动换行样式
|
// 5. 生成动态表头 - 使用机位名称作为表头
|
||||||
CellStyle wrapStyle = workbook.createCellStyle();
|
|
||||||
wrapStyle.setWrapText(true);
|
|
||||||
wrapStyle.setVerticalAlignment(VerticalAlignment.TOP);
|
|
||||||
|
|
||||||
// 6. 生成动态表头 - 使用机位名称作为表头
|
|
||||||
Row headerRow = sheet.createRow(0);
|
Row headerRow = sheet.createRow(0);
|
||||||
List<String> headerList = new ArrayList<>();
|
List<String> headerList = new ArrayList<>();
|
||||||
headerList.add("评价ID");
|
headerList.add("评价ID");
|
||||||
@@ -228,7 +234,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
cell.setCellStyle(headerStyle);
|
cell.setCellStyle(headerStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 填充数据
|
// 6. 填充数据
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
int rowNum = 1;
|
int rowNum = 1;
|
||||||
|
|
||||||
@@ -246,37 +252,21 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
row.createCell(colIndex++).setCellValue(review.getContent());
|
row.createCell(colIndex++).setCellValue(review.getContent());
|
||||||
|
|
||||||
// 机位评价列 - 按表头顺序填充
|
// 机位评价列 - 按表头顺序填充
|
||||||
Map<String, Map<String, Integer>> cameraRating = review.getCameraPositionRating();
|
Map<String, Integer> cameraRating = review.getCameraPositionRating();
|
||||||
for (Long deviceId : sortedDeviceIds) {
|
for (Long deviceId : sortedDeviceIds) {
|
||||||
String deviceIdStr = String.valueOf(deviceId);
|
String deviceIdStr = String.valueOf(deviceId);
|
||||||
Map<String, Integer> dimensions = null;
|
Integer rating = null;
|
||||||
|
|
||||||
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) {
|
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) {
|
||||||
dimensions = cameraRating.get(deviceIdStr);
|
rating = cameraRating.get(deviceIdStr);
|
||||||
}
|
|
||||||
|
|
||||||
// 构建单元格内容: 只显示评分维度(不再重复机位名称)
|
|
||||||
StringBuilder cellContent = new StringBuilder();
|
|
||||||
|
|
||||||
if (dimensions != null && !dimensions.isEmpty()) {
|
|
||||||
// 按维度名排序,保证一致性
|
|
||||||
List<Map.Entry<String, Integer>> sortedDimensions = dimensions.entrySet().stream()
|
|
||||||
.sorted(Map.Entry.comparingByKey())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, Integer> dimEntry : sortedDimensions) {
|
|
||||||
if (!first) {
|
|
||||||
cellContent.append("\n");
|
|
||||||
}
|
|
||||||
cellContent.append(dimEntry.getKey()).append(":").append(dimEntry.getValue());
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Cell cell = row.createCell(colIndex++);
|
Cell cell = row.createCell(colIndex++);
|
||||||
cell.setCellValue(cellContent.toString());
|
if (rating != null) {
|
||||||
cell.setCellStyle(wrapStyle);
|
cell.setCellValue(rating);
|
||||||
|
} else {
|
||||||
|
cell.setCellValue("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间列
|
// 时间列
|
||||||
@@ -284,50 +274,92 @@ public class VideoReviewServiceImpl implements VideoReviewService {
|
|||||||
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
|
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 自动调整列宽
|
// 7. 自动调整列宽
|
||||||
for (int i = 0; i < headerList.size(); i++) {
|
for (int i = 0; i < headerList.size(); i++) {
|
||||||
sheet.autoSizeColumn(i);
|
sheet.autoSizeColumn(i);
|
||||||
// 对于机位列,设置最小宽度以便换行内容显示完整
|
|
||||||
if (i >= 7 && i < 7 + sortedDeviceIds.size()) {
|
|
||||||
int currentWidth = sheet.getColumnWidth(i);
|
|
||||||
sheet.setColumnWidth(i, Math.max(currentWidth, 5000)); // 最小25个字符宽度
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 写入输出流
|
// 8. 写入输出流
|
||||||
workbook.write(outputStream);
|
workbook.write(outputStream);
|
||||||
workbook.close();
|
workbook.close();
|
||||||
|
|
||||||
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size());
|
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoPurchaseCheckRespDTO checkVideoPurchase(VideoPurchaseCheckReqDTO reqDTO) {
|
||||||
|
// 参数校验
|
||||||
|
if (reqDTO.getVideoId() == null) {
|
||||||
|
throw new BaseException("视频ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long videoId = reqDTO.getVideoId();
|
||||||
|
|
||||||
|
// 查询视频信息
|
||||||
|
VideoEntity video = videoMapper.getEntity(videoId);
|
||||||
|
if (video == null) {
|
||||||
|
throw new BaseException("视频不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoPurchaseCheckRespDTO respDTO = new VideoPurchaseCheckRespDTO();
|
||||||
|
respDTO.setVideoId(videoId);
|
||||||
|
|
||||||
|
List<Long> allOrderIds = new ArrayList<>();
|
||||||
|
|
||||||
|
// 情况1:直接购买视频的订单(goods_type=0, goods_id=视频id)
|
||||||
|
List<Long> directOrderIds = orderMapper.getOrderIdsByVideoId(videoId);
|
||||||
|
if (directOrderIds != null && !directOrderIds.isEmpty()) {
|
||||||
|
allOrderIds.addAll(directOrderIds);
|
||||||
|
log.info("视频[{}]直接购买订单数: {}", videoId, directOrderIds.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况2:通过购买模板间接拥有(goods_type=-1, goods_id=templateId)
|
||||||
|
// 通过video的faceId查询购买模板的订单
|
||||||
|
if (video.getFaceId() != null && video.getTemplateId() != null) {
|
||||||
|
List<Long> templateOrderIds = orderMapper.getOrderIdsByFaceIdAndTemplateId(
|
||||||
|
video.getFaceId(),
|
||||||
|
video.getTemplateId()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (templateOrderIds != null && !templateOrderIds.isEmpty()) {
|
||||||
|
allOrderIds.addAll(templateOrderIds);
|
||||||
|
log.info("视频[{}]通过模板[{}]购买订单数: {}, faceId: {}",
|
||||||
|
videoId, video.getTemplateId(), templateOrderIds.size(), video.getFaceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respDTO.setOrderIds(allOrderIds);
|
||||||
|
respDTO.setIsPurchased(!allOrderIds.isEmpty());
|
||||||
|
|
||||||
|
log.info("视频[{}]购买检查完成,是否被购买: {}, 总订单数: {}",
|
||||||
|
videoId, respDTO.getIsPurchased(), allOrderIds.size());
|
||||||
|
|
||||||
|
return respDTO;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算机位评价各维度的平均值
|
* 计算各机位的平均评分
|
||||||
*/
|
*/
|
||||||
private Map<String, BigDecimal> calculateCameraPositionAverage() {
|
private Map<String, BigDecimal> calculateCameraPositionAverage() {
|
||||||
List<Map<String, Map<String, Integer>>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
|
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
|
||||||
|
|
||||||
if (allRatings == null || allRatings.isEmpty()) {
|
if (allRatings == null || allRatings.isEmpty()) {
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计各维度的总分和次数
|
// 统计各机位的总分和次数
|
||||||
Map<String, List<Integer>> dimensionScores = new HashMap<>();
|
Map<String, List<Integer>> deviceScores = new HashMap<>();
|
||||||
for (Map<String, Map<String, Integer>> rating : allRatings) {
|
for (Map<String, Integer> rating : allRatings) {
|
||||||
if (rating == null) continue;
|
if (rating == null) continue;
|
||||||
// 遍历每个机位
|
// 遍历每个机位的评分
|
||||||
for (Map<String, Integer> deviceRatings : rating.values()) {
|
for (Map.Entry<String, Integer> entry : rating.entrySet()) {
|
||||||
if (deviceRatings == null) continue;
|
deviceScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
|
||||||
// 遍历该机位的每个维度
|
|
||||||
for (Map.Entry<String, Integer> entry : deviceRatings.entrySet()) {
|
|
||||||
dimensionScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算平均值
|
// 计算平均值
|
||||||
Map<String, BigDecimal> result = new HashMap<>();
|
Map<String, BigDecimal> result = new HashMap<>();
|
||||||
for (Map.Entry<String, List<Integer>> entry : dimensionScores.entrySet()) {
|
for (Map.Entry<String, List<Integer>> entry : deviceScores.entrySet()) {
|
||||||
double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||||
result.put(entry.getKey(), BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
|
result.put(entry.getKey(), BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.ycwl.basic.service.mobile;
|
||||||
|
|
||||||
|
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.device.dto.device.DeviceV2DTO;
|
||||||
|
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
||||||
|
import com.ycwl.basic.model.mobile.video.dto.HlsStreamRequest;
|
||||||
|
import com.ycwl.basic.model.mobile.video.dto.HlsStreamResponse;
|
||||||
|
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
|
||||||
|
import com.ycwl.basic.repository.DeviceRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HLS视频流服务
|
||||||
|
* 用于生成设备视频的HLS播放列表(m3u8)
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-12-26
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class HlsStreamService {
|
||||||
|
|
||||||
|
private final DeviceIntegrationService deviceIntegrationService;
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
private static final String M3U8_HEADER = "#EXTM3U";
|
||||||
|
private static final String M3U8_VERSION = "#EXT-X-VERSION:3";
|
||||||
|
private static final String M3U8_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE:0";
|
||||||
|
private static final String M3U8_ALLOW_CACHE = "#EXT-X-ALLOW-CACHE:YES";
|
||||||
|
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成设备视频的HLS播放列表
|
||||||
|
*
|
||||||
|
* @param request HLS流请求参数
|
||||||
|
* @return HLS播放列表响应
|
||||||
|
*/
|
||||||
|
public HlsStreamResponse generateHlsPlaylist(HlsStreamRequest request) {
|
||||||
|
log.info("开始生成HLS播放列表: deviceId={}, durationMinutes={}",
|
||||||
|
request.getDeviceId(), request.getDurationMinutes());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取设备信息
|
||||||
|
DeviceV2DTO device = deviceIntegrationService.getDevice(request.getDeviceId());
|
||||||
|
if (device == null) {
|
||||||
|
throw new RuntimeException("设备不存在: " + request.getDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备配置
|
||||||
|
DeviceConfigEntity config = deviceRepository.getDeviceConfig(request.getDeviceId());
|
||||||
|
if (config == null) {
|
||||||
|
throw new RuntimeException("设备配置不存在: " + request.getDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备的存储操作器
|
||||||
|
IDeviceStorageOperator operator = DeviceFactory.getDeviceStorageOperator(device, config);
|
||||||
|
if (operator == null) {
|
||||||
|
throw new RuntimeException("设备未配置存储操作器: " + request.getDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算时间范围:当前时间向前N分钟
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
Date endDate = calendar.getTime();
|
||||||
|
calendar.add(Calendar.MINUTE, -request.getDurationMinutes());
|
||||||
|
Date startDate = calendar.getTime();
|
||||||
|
|
||||||
|
log.info("查询视频文件范围: deviceId={}, startDate={}, endDate={}",
|
||||||
|
request.getDeviceId(), DATE_FORMAT.format(startDate), DATE_FORMAT.format(endDate));
|
||||||
|
|
||||||
|
// 获取视频文件列表
|
||||||
|
List<FileObject> fileList = operator.getFileListByDtRange(startDate, endDate);
|
||||||
|
if (fileList == null || fileList.isEmpty()) {
|
||||||
|
log.warn("未找到视频文件: deviceId={}, startDate={}, endDate={}",
|
||||||
|
request.getDeviceId(), DATE_FORMAT.format(startDate), DATE_FORMAT.format(endDate));
|
||||||
|
return buildEmptyResponse(request.getDeviceId(), request.getEventPlaylist());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按创建时间排序(升序)
|
||||||
|
fileList.sort(Comparator.comparing(FileObject::getCreateTime));
|
||||||
|
|
||||||
|
log.info("找到 {} 个视频文件", fileList.size());
|
||||||
|
|
||||||
|
// 生成播放列表
|
||||||
|
return buildHlsResponse(request.getDeviceId(), fileList, request.getEventPlaylist());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成HLS播放列表失败: deviceId={}", request.getDeviceId(), e);
|
||||||
|
throw new RuntimeException("生成HLS播放列表失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建HLS响应
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param fileList 视频文件列表
|
||||||
|
* @param isEventPlaylist 是否为Event播放列表
|
||||||
|
* @return HLS响应
|
||||||
|
*/
|
||||||
|
private HlsStreamResponse buildHlsResponse(Long deviceId, List<FileObject> fileList, Boolean isEventPlaylist) {
|
||||||
|
StringBuilder m3u8Content = new StringBuilder();
|
||||||
|
List<HlsStreamResponse.VideoSegment> segments = new ArrayList<>();
|
||||||
|
|
||||||
|
// 添加m3u8头部
|
||||||
|
m3u8Content.append(M3U8_HEADER).append("\n");
|
||||||
|
m3u8Content.append(M3U8_VERSION).append("\n");
|
||||||
|
|
||||||
|
// 设置播放列表类型
|
||||||
|
String playlistType = Boolean.TRUE.equals(isEventPlaylist) ? "EVENT" : "VOD";
|
||||||
|
m3u8Content.append("#EXT-X-PLAYLIST-TYPE:").append(playlistType).append("\n");
|
||||||
|
|
||||||
|
// 计算目标时长(使用视频片段的平均时长)
|
||||||
|
double avgDuration = calculateAverageDuration(fileList);
|
||||||
|
int targetDuration = (int) Math.ceil(avgDuration);
|
||||||
|
m3u8Content.append("#EXT-X-TARGETDURATION:").append(targetDuration).append("\n");
|
||||||
|
|
||||||
|
m3u8Content.append(M3U8_MEDIA_SEQUENCE).append("\n");
|
||||||
|
m3u8Content.append(M3U8_ALLOW_CACHE).append("\n");
|
||||||
|
|
||||||
|
// 添加视频片段
|
||||||
|
double totalDuration = 0.0;
|
||||||
|
int sequence = 0;
|
||||||
|
|
||||||
|
for (FileObject file : fileList) {
|
||||||
|
// 计算片段时长(秒)
|
||||||
|
double duration = calculateSegmentDuration(file);
|
||||||
|
totalDuration += duration;
|
||||||
|
|
||||||
|
// 添加片段信息到m3u8
|
||||||
|
m3u8Content.append("#EXTINF:").append(String.format("%.3f", duration)).append(",\n");
|
||||||
|
m3u8Content.append(file.getUrl().replace("-internal.aliyuncs.com", ".aliyuncs.com")).append("\n");
|
||||||
|
|
||||||
|
// 记录片段信息
|
||||||
|
HlsStreamResponse.VideoSegment segment = HlsStreamResponse.VideoSegment.builder()
|
||||||
|
.url(file.getUrl())
|
||||||
|
.duration(duration)
|
||||||
|
.sequence(sequence++)
|
||||||
|
.startTime(file.getCreateTime() != null ? DATE_FORMAT.format(file.getCreateTime()) : null)
|
||||||
|
.endTime(file.getEndTime() != null ? DATE_FORMAT.format(file.getEndTime()) : null)
|
||||||
|
.build();
|
||||||
|
segments.add(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加结束标记(VOD类型需要)
|
||||||
|
if (!"EVENT".equals(playlistType)) {
|
||||||
|
m3u8Content.append("#EXT-X-ENDLIST\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HlsStreamResponse.builder()
|
||||||
|
.deviceId(deviceId)
|
||||||
|
.playlistContent(m3u8Content.toString())
|
||||||
|
.segmentCount(fileList.size())
|
||||||
|
.totalDurationSeconds(totalDuration)
|
||||||
|
.segments(segments)
|
||||||
|
.playlistType(playlistType)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建空响应
|
||||||
|
*/
|
||||||
|
private HlsStreamResponse buildEmptyResponse(Long deviceId, Boolean isEventPlaylist) {
|
||||||
|
String playlistType = Boolean.TRUE.equals(isEventPlaylist) ? "EVENT" : "VOD";
|
||||||
|
StringBuilder m3u8Content = new StringBuilder();
|
||||||
|
|
||||||
|
m3u8Content.append(M3U8_HEADER).append("\n");
|
||||||
|
m3u8Content.append(M3U8_VERSION).append("\n");
|
||||||
|
m3u8Content.append("#EXT-X-PLAYLIST-TYPE:").append(playlistType).append("\n");
|
||||||
|
m3u8Content.append("#EXT-X-TARGETDURATION:0\n");
|
||||||
|
m3u8Content.append(M3U8_MEDIA_SEQUENCE).append("\n");
|
||||||
|
|
||||||
|
if (!"EVENT".equals(playlistType)) {
|
||||||
|
m3u8Content.append("#EXT-X-ENDLIST\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HlsStreamResponse.builder()
|
||||||
|
.deviceId(deviceId)
|
||||||
|
.playlistContent(m3u8Content.toString())
|
||||||
|
.segmentCount(0)
|
||||||
|
.totalDurationSeconds(0.0)
|
||||||
|
.segments(Collections.emptyList())
|
||||||
|
.playlistType(playlistType)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算视频片段的平均时长(秒)
|
||||||
|
*/
|
||||||
|
private double calculateAverageDuration(List<FileObject> fileList) {
|
||||||
|
if (fileList == null || fileList.isEmpty()) {
|
||||||
|
return 10.0; // 默认10秒
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Double> durations = fileList.stream()
|
||||||
|
.map(this::calculateSegmentDuration)
|
||||||
|
.filter(d -> d > 0)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (durations.isEmpty()) {
|
||||||
|
return 10.0; // 默认10秒
|
||||||
|
}
|
||||||
|
|
||||||
|
return durations.stream()
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个视频片段的时长(秒)
|
||||||
|
*/
|
||||||
|
private double calculateSegmentDuration(FileObject file) {
|
||||||
|
if (file.getCreateTime() != null && file.getEndTime() != null) {
|
||||||
|
long durationMs = file.getEndTime().getTime() - file.getCreateTime().getTime();
|
||||||
|
if (durationMs > 0) {
|
||||||
|
return durationMs / 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果无法计算,返回默认值10秒
|
||||||
|
return 10.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,10 +53,9 @@ public class VideoViewPermissionService {
|
|||||||
log.warn("视频缺少景区信息: videoId={}", videoId);
|
log.warn("视频缺少景区信息: videoId={}", videoId);
|
||||||
return createErrorPermission("视频信息不完整");
|
return createErrorPermission("视频信息不完整");
|
||||||
}
|
}
|
||||||
TaskEntity taskById = videoTaskRepository.getTaskById(video.getTaskId());
|
|
||||||
|
|
||||||
// 检查用户是否已购买
|
// 检查用户是否已购买
|
||||||
IsBuyRespVO buy = orderBiz.isBuy(scenicId, userId, taskById.getFaceId(), 0, videoId);
|
IsBuyRespVO buy = orderBiz.isBuy(scenicId, userId, video.getFaceId(), 0, videoId);
|
||||||
if (buy != null && (buy.isBuy() || buy.isFree())) {
|
if (buy != null && (buy.isBuy() || buy.isFree())) {
|
||||||
// 已购买,不限制查看
|
// 已购买,不限制查看
|
||||||
log.debug("用户已购买视频,无查看限制: userId={}, videoId={}", userId, videoId);
|
log.debug("用户已购买视频,无查看限制: userId={}, videoId={}", userId, videoId);
|
||||||
@@ -123,10 +122,9 @@ public class VideoViewPermissionService {
|
|||||||
if (scenicId == null) {
|
if (scenicId == null) {
|
||||||
return createErrorPermission("视频信息不完整");
|
return createErrorPermission("视频信息不完整");
|
||||||
}
|
}
|
||||||
TaskEntity taskById = videoTaskRepository.getTaskById(video.getTaskId());
|
|
||||||
|
|
||||||
// 检查用户是否已购买
|
// 检查用户是否已购买
|
||||||
IsBuyRespVO buy = orderBiz.isBuy(scenicId, userId, taskById.getFaceId(), 0, videoId);
|
IsBuyRespVO buy = orderBiz.isBuy(scenicId, userId, video.getFaceId(), 0, videoId);
|
||||||
if (buy != null && (buy.isBuy() || buy.isFree())) {
|
if (buy != null && (buy.isBuy() || buy.isFree())) {
|
||||||
// 已购买,不限制查看
|
// 已购买,不限制查看
|
||||||
log.debug("用户已购买视频,无查看限制: userId={}, videoId={}", userId, videoId);
|
log.debug("用户已购买视频,无查看限制: userId={}, videoId={}", userId, videoId);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user