You've already forked FrameTour-BE
Compare commits
31 Commits
2a3b4ca19f
...
1df6a4bc23
| Author | SHA1 | Date | |
|---|---|---|---|
| 1df6a4bc23 | |||
| 981a4ba7bd | |||
| 017ced34fa | |||
| a9ae00d580 | |||
| 99f75b6805 | |||
| 295815f1fa | |||
| 010bac1091 | |||
| eb9b781fd3 | |||
| 8d3dae32f3 | |||
| 43775f550b | |||
| 24f72091b3 | |||
| cc62fb4c18 | |||
| d1962ed615 | |||
| e1023b6ea8 | |||
| aec5e57df7 | |||
| 52ce26e630 | |||
| 32297dc29c | |||
| 21d8c56e82 | |||
| f8374519c3 | |||
| 44f5008fd1 | |||
| 6e0ebcd1bd | |||
| 5caf9a0ebf | |||
| 06bc2c2020 | |||
| 81dc2f1b86 | |||
| 41e90bab9c | |||
| b4628bd3e8 | |||
| cfb4284b7c | |||
| 5a61432dc9 | |||
| 91160a1adb | |||
| 991a8b10e3 | |||
| ab1e8cf7ef |
8
pom.xml
8
pom.xml
@@ -303,6 +303,14 @@
|
|||||||
<artifactId>poi-ooxml</artifactId>
|
<artifactId>poi-ooxml</artifactId>
|
||||||
<version>5.4.0</version>
|
<version>5.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ClickHouse JDBC Driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.clickhouse</groupId>
|
||||||
|
<artifactId>clickhouse-jdbc</artifactId>
|
||||||
|
<version>0.8.5</version>
|
||||||
|
<classifier>all</classifier>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.ycwl.basic.clickhouse.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计数据查询服务接口
|
||||||
|
* 用于抽象 t_stats 和 t_stats_record 表的查询
|
||||||
|
* 支持 MySQL 和 ClickHouse 两种实现
|
||||||
|
*/
|
||||||
|
public interface StatsQueryService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计预览视频人数
|
||||||
|
*/
|
||||||
|
Integer countPreviewVideoOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计扫码访问人数
|
||||||
|
*/
|
||||||
|
Integer countScanCodeOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计推送订阅人数
|
||||||
|
*/
|
||||||
|
Integer countPushOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计上传头像人数
|
||||||
|
*/
|
||||||
|
Integer countUploadFaceOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计生成视频人数
|
||||||
|
*/
|
||||||
|
Integer countCompleteVideoOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计生成视频条数
|
||||||
|
*/
|
||||||
|
Integer countCompleteOfVideo(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计总访问人数
|
||||||
|
*/
|
||||||
|
Integer countTotalVisitorOfMember(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计预览视频条数
|
||||||
|
*/
|
||||||
|
Integer countPreviewOfVideo(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户分销员 ID 列表
|
||||||
|
*/
|
||||||
|
List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户最近进入类型
|
||||||
|
*/
|
||||||
|
Long getUserRecentEnterType(Long memberId, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户项目 ID 列表
|
||||||
|
*/
|
||||||
|
List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计分销员扫码次数
|
||||||
|
*/
|
||||||
|
Integer countBrokerScanCount(Long brokerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计分销员扫码数据
|
||||||
|
*/
|
||||||
|
List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按小时统计扫码人数
|
||||||
|
*/
|
||||||
|
List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计扫码人数
|
||||||
|
*/
|
||||||
|
List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query);
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package com.ycwl.basic.clickhouse.service.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||||
|
import com.ycwl.basic.mapper.TaskMapper;
|
||||||
|
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClickHouse 统计数据查询服务实现
|
||||||
|
* 当 clickhouse.enabled=true 时启用
|
||||||
|
*
|
||||||
|
* 注意:ClickHouse JDBC 驱动 0.6.x 对参数绑定支持有问题,
|
||||||
|
* 因此使用字符串格式化方式构建 SQL(参数均为内部生成的数值或日期,无 SQL 注入风险)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||||
|
public class ClickHouseStatsQueryServiceImpl implements StatsQueryService {
|
||||||
|
|
||||||
|
private static final TimeZone CLICKHOUSE_TIMEZONE = TimeZone.getTimeZone("Asia/Shanghai");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建日期格式化器(SimpleDateFormat 非线程安全,每次创建新实例)
|
||||||
|
*/
|
||||||
|
private SimpleDateFormat createDateFormat() {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
|
||||||
|
return sdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建日期时间格式化器
|
||||||
|
*/
|
||||||
|
private SimpleDateFormat createDateTimeFormat() {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
|
||||||
|
return sdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("clickHouseJdbcTemplate")
|
||||||
|
private NamedParameterJdbcTemplate namedJdbcTemplate;
|
||||||
|
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskMapper taskMapper;
|
||||||
|
|
||||||
|
private JdbcTemplate getJdbcTemplate() {
|
||||||
|
if (jdbcTemplate == null) {
|
||||||
|
jdbcTemplate = namedJdbcTemplate.getJdbcTemplate();
|
||||||
|
}
|
||||||
|
return jdbcTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间为 ClickHouse 可识别的字符串
|
||||||
|
*/
|
||||||
|
private String formatDateTime(Date date) {
|
||||||
|
return date != null ? "'" + createDateTimeFormat().format(date) + "'" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期为 ClickHouse 可识别的字符串
|
||||||
|
*/
|
||||||
|
private String formatDate(Date date) {
|
||||||
|
return date != null ? "'" + createDateFormat().format(date) + "'" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接“进入景区”的 trace_id 子查询。
|
||||||
|
* <p>
|
||||||
|
* ClickHouse 上 t_stats_record 往往按时间分区/排序;给子查询补充时间范围可显著减少扫描量。
|
||||||
|
*/
|
||||||
|
private void appendEnterScenicTraceIdSubQuery(StringBuilder sql, Long scenicId, Date startTime, Date endTime) {
|
||||||
|
sql.append("SELECT DISTINCT trace_id FROM t_stats_record ");
|
||||||
|
sql.append("WHERE action = 'ENTER_SCENIC' ");
|
||||||
|
if (scenicId != null) {
|
||||||
|
sql.append("AND identifier = '").append(scenicId).append("' ");
|
||||||
|
}
|
||||||
|
if (startTime != null) {
|
||||||
|
sql.append("AND create_time >= ").append(formatDateTime(startTime)).append(" ");
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
sql.append("AND create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.action = 'LOAD' ");
|
||||||
|
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
|
||||||
|
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
|
||||||
|
sql.append("AND JSONExtractString(r.params, 'share') = '' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countScanCodeOfMember(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'LAUNCH' ");
|
||||||
|
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPushOfMember(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'PERM_REQ' ");
|
||||||
|
sql.append("AND r.identifier = 'NOTIFY' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countUploadFaceOfMember(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
|
||||||
|
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> listFaceIdsWithUpload(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT DISTINCT r.identifier FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.action = 'FACE_UPLOAD' ");
|
||||||
|
sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForList(sql.toString(), String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
|
||||||
|
List<String> faceIds = listFaceIdsWithUpload(query);
|
||||||
|
if (faceIds == null || faceIds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return taskMapper.countCompletedTaskMembersByFaceIds(faceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countCompleteOfVideo(CommonQueryReq query) {
|
||||||
|
List<String> faceIds = listFaceIdsWithUpload(query);
|
||||||
|
if (faceIds == null || faceIds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return taskMapper.countCompletedTasksByFaceIds(faceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'LAUNCH' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPreviewOfVideo(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("WITH JSONExtractString(params, 'id') AS videoId, ");
|
||||||
|
sql.append(" JSONExtractString(params, 'share') AS share ");
|
||||||
|
sql.append("SELECT toInt32(uniqExact(videoId)) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'LOAD' ");
|
||||||
|
sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
|
||||||
|
sql.append("AND videoId != '' ");
|
||||||
|
sql.append("AND share = '' ");
|
||||||
|
if (query.getStartTime() != null) {
|
||||||
|
sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
|
||||||
|
}
|
||||||
|
if (query.getEndTime() != null) {
|
||||||
|
sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt64(r.identifier) AS identifier ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.action = 'CODE_SCAN' ");
|
||||||
|
sql.append(" AND s.member_id = ").append(memberId).append(" ");
|
||||||
|
if (startTime != null) {
|
||||||
|
sql.append(" AND r.create_time >= ").append(formatDateTime(startTime)).append(" ");
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||||
|
}
|
||||||
|
sql.append("GROUP BY r.identifier ");
|
||||||
|
sql.append("ORDER BY max(r.create_time) DESC");
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getUserRecentEnterType(Long memberId, Date endTime) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT JSONExtractInt(r.params, 'scene') AS scene ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.action = 'LAUNCH' ");
|
||||||
|
sql.append(" AND s.member_id = ").append(memberId).append(" ");
|
||||||
|
if (endTime != null) {
|
||||||
|
sql.append(" AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
|
||||||
|
}
|
||||||
|
sql.append("ORDER BY r.create_time DESC LIMIT 1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getJdbcTemplate().queryForObject(sql.toString(), Long.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT toInt64(r.identifier) AS identifier ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE s.member_id = ").append(memberId).append(" ");
|
||||||
|
sql.append(" AND r.action = 'ENTER_PROJECT' ");
|
||||||
|
sql.append(" AND r.create_time < ").append(formatDateTime(endTime)).append(" ");
|
||||||
|
sql.append(" AND r.create_time > ").append(formatDateTime(startTime)).append(" ");
|
||||||
|
sql.append("ORDER BY r.create_time DESC LIMIT 1");
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForList(sql.toString(), Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countBrokerScanCount(Long brokerId) {
|
||||||
|
String sql = "SELECT count(1) AS count FROM t_stats_record " +
|
||||||
|
"WHERE action = 'CODE_SCAN' AND identifier = '" + brokerId + "'";
|
||||||
|
|
||||||
|
return getJdbcTemplate().queryForObject(sql, Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
|
||||||
|
SimpleDateFormat dateFormat = createDateFormat();
|
||||||
|
String startDateStr = dateFormat.format(startTime);
|
||||||
|
String endDateStr = dateFormat.format(endTime);
|
||||||
|
String startDateTimeStr = "'" + startDateStr + " 00:00:00'";
|
||||||
|
String endDateTimeStr = "'" + endDateStr + " 23:59:59'";
|
||||||
|
|
||||||
|
String sql = "SELECT toDate(create_time) AS date, count(DISTINCT id) AS scanCount " +
|
||||||
|
"FROM t_stats_record " +
|
||||||
|
"WHERE action = 'CODE_SCAN' " +
|
||||||
|
" AND identifier = '" + brokerId + "' " +
|
||||||
|
" AND create_time >= " + startDateTimeStr + " " +
|
||||||
|
" AND create_time <= " + endDateTimeStr + " " +
|
||||||
|
"GROUP BY toDate(create_time) " +
|
||||||
|
"ORDER BY toDate(create_time)";
|
||||||
|
|
||||||
|
return getJdbcTemplate().query(sql, (rs, rowNum) -> {
|
||||||
|
HashMap<String, Object> map = new HashMap<>();
|
||||||
|
map.put("date", rs.getDate("date"));
|
||||||
|
map.put("scanCount", rs.getLong("scanCount"));
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
|
||||||
|
sql.append(" uniqExact(s.member_id) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'LAUNCH' ");
|
||||||
|
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||||
|
sql.append("AND s.create_time BETWEEN ").append(formatDateTime(query.getStartTime()));
|
||||||
|
sql.append(" AND ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
sql.append("GROUP BY toStartOfHour(s.create_time) ");
|
||||||
|
sql.append("ORDER BY toStartOfHour(s.create_time)");
|
||||||
|
|
||||||
|
return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||||
|
HashMap<String, String> map = new HashMap<>();
|
||||||
|
map.put("t", rs.getString("t"));
|
||||||
|
map.put("count", rs.getString("count"));
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
|
||||||
|
sql.append(" uniqExact(s.member_id) AS count ");
|
||||||
|
sql.append("FROM t_stats_record r ");
|
||||||
|
sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
|
||||||
|
sql.append("WHERE r.trace_id IN (");
|
||||||
|
appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
|
sql.append(") ");
|
||||||
|
sql.append("AND r.action = 'LAUNCH' ");
|
||||||
|
sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
|
||||||
|
sql.append("AND s.create_time BETWEEN ").append(formatDateTime(query.getStartTime()));
|
||||||
|
sql.append(" AND ").append(formatDateTime(query.getEndTime())).append(" ");
|
||||||
|
sql.append("GROUP BY toStartOfDay(s.create_time) ");
|
||||||
|
sql.append("ORDER BY toStartOfDay(s.create_time)");
|
||||||
|
|
||||||
|
return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
|
||||||
|
HashMap<String, String> map = new HashMap<>();
|
||||||
|
map.put("t", rs.getString("t"));
|
||||||
|
map.put("count", rs.getString("count"));
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.ycwl.basic.clickhouse.service.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||||
|
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||||
|
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 统计数据查询服务实现
|
||||||
|
* 当 clickhouse.enabled 未启用时使用此实现(兜底)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "false", matchIfMissing = true)
|
||||||
|
public class MySqlStatsQueryServiceImpl implements StatsQueryService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StatisticsMapper statisticsMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPreviewVideoOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countPreviewVideoOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countScanCodeOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countScanCodeOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPushOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countPushOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countUploadFaceOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countUploadFaceOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countCompleteVideoOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countCompleteVideoOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countCompleteOfVideo(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countCompleteOfVideo(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countTotalVisitorOfMember(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countTotalVisitorOfMember(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countPreviewOfVideo(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.countPreviewOfVideo(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||||
|
return statisticsMapper.getBrokerIdListForUser(memberId, startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getUserRecentEnterType(Long memberId, Date endTime) {
|
||||||
|
return statisticsMapper.getUserRecentEnterType(memberId, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
|
||||||
|
return statisticsMapper.getProjectIdListForUser(memberId, startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer countBrokerScanCount(Long brokerId) {
|
||||||
|
return statisticsMapper.countBrokerScanCount(brokerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
|
||||||
|
return statisticsMapper.getDailyScanStats(brokerId, startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.scanCodeMemberChartByHour(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
|
||||||
|
return statisticsMapper.scanCodeMemberChartByDate(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.config;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClickHouse 数据源配置
|
||||||
|
* 用于 t_stats 和 t_stats_record 表的查询
|
||||||
|
*
|
||||||
|
* 使用 NamedParameterJdbcTemplate 而非 MyBatis,以避免干扰 MyBatis-Plus 的自动配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||||
|
public class ClickHouseDataSourceConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClickHouse 数据源(非 Primary)
|
||||||
|
*/
|
||||||
|
@Bean(name = "clickHouseDataSource")
|
||||||
|
@ConfigurationProperties(prefix = "clickhouse.datasource")
|
||||||
|
public DataSource clickHouseDataSource() {
|
||||||
|
return new HikariDataSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "clickHouseJdbcTemplate")
|
||||||
|
public NamedParameterJdbcTemplate clickHouseJdbcTemplate(
|
||||||
|
@Qualifier("clickHouseDataSource") DataSource dataSource) {
|
||||||
|
return new NamedParameterJdbcTemplate(dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ycwl.basic.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 主数据源配置
|
||||||
|
*
|
||||||
|
* 当 ClickHouse 启用时,需要显式配置 MySQL 数据源并标记为 @Primary,
|
||||||
|
* 以确保 MyBatis-Plus 和其他组件使用正确的数据源
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
|
||||||
|
public class MySqlPrimaryDataSourceConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 数据源属性
|
||||||
|
*/
|
||||||
|
@Primary
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties(prefix = "spring.datasource")
|
||||||
|
public DataSourceProperties mysqlDataSourceProperties() {
|
||||||
|
return new DataSourceProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 主数据源
|
||||||
|
* 使用 @Primary 确保这是默认数据源
|
||||||
|
*/
|
||||||
|
@Primary
|
||||||
|
@Bean(name = "dataSource")
|
||||||
|
public DataSource mysqlDataSource(DataSourceProperties properties) {
|
||||||
|
return properties.initializeDataSourceBuilder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.ycwl.basic.config;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import com.ycwl.basic.interceptor.AuthInterceptor;
|
import com.ycwl.basic.interceptor.AuthInterceptor;
|
||||||
import com.ycwl.basic.stats.interceptor.StatsInterceptor;
|
import com.ycwl.basic.puzzle.edge.interceptor.PuzzleEdgeWorkerIpInterceptor;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -25,20 +25,19 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AuthInterceptor authInterceptor;
|
private AuthInterceptor authInterceptor;
|
||||||
@Autowired
|
@Autowired
|
||||||
private StatsInterceptor statsInterceptor;
|
private PuzzleEdgeWorkerIpInterceptor puzzleEdgeWorkerIpInterceptor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(puzzleEdgeWorkerIpInterceptor)
|
||||||
|
.addPathPatterns("/puzzle/render/v1/**");
|
||||||
registry.addInterceptor(authInterceptor)
|
registry.addInterceptor(authInterceptor)
|
||||||
// 拦截除指定接口外的所有请求,通过判断 注解 来决定是否需要做登录验证
|
// 拦截除指定接口外的所有请求,通过判断 注解 来决定是否需要做登录验证
|
||||||
.addPathPatterns("/**")
|
.addPathPatterns("/**")
|
||||||
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/doc.html/**", "/error", "/");
|
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/api-docs", "/doc.html/**", "/error", "/");
|
||||||
registry.addInterceptor(statsInterceptor)
|
|
||||||
.addPathPatterns("/api/mobile/**");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -64,23 +64,17 @@ public class AppFaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{faceId}")
|
@GetMapping("/{faceId}")
|
||||||
public ApiResponse<FaceRespVO> getById(@PathVariable("faceId") Long faceId) {
|
public ApiResponse<FaceEntity> getById(@PathVariable("faceId") Long faceId) {
|
||||||
return faceService.getById(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
return ApiResponse.success(face);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{faceId}")
|
@DeleteMapping("/{faceId}")
|
||||||
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
|
public ApiResponse<String> deleteFace(@PathVariable("faceId") Long faceId) {
|
||||||
// 添加权限检查:验证当前用户是否拥有该 face
|
|
||||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
|
||||||
Long userId = worker.getUserId();
|
|
||||||
|
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
if (face == null) {
|
if (face == null) {
|
||||||
throw new BaseException("人脸数据不存在");
|
throw new BaseException("人脸数据不存在");
|
||||||
}
|
}
|
||||||
if (!face.getMemberId().equals(userId)) {
|
|
||||||
throw new BaseException("无权删除此人脸");
|
|
||||||
}
|
|
||||||
|
|
||||||
return faceService.deleteFace(faceId);
|
return faceService.deleteFace(faceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package com.ycwl.basic.controller.mobile;
|
|
||||||
|
|
||||||
import com.github.pagehelper.PageInfo;
|
|
||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
|
||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
|
||||||
import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
|
|
||||||
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
|
|
||||||
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
|
|
||||||
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
|
||||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
|
||||||
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.resp.ScenicConfigResp;
|
|
||||||
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
|
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
|
||||||
import com.ycwl.basic.service.mobile.AppScenicService;
|
|
||||||
import com.ycwl.basic.service.pc.FaceService;
|
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author:longbinbin
|
|
||||||
* @Date:2024/12/5 10:22
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/mobile/scenic/v1")
|
|
||||||
// 景区相关接口
|
|
||||||
public class AppScenicController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private FaceService faceService;
|
|
||||||
@Autowired
|
|
||||||
private AppScenicService appScenicService;
|
|
||||||
@Autowired
|
|
||||||
private ScenicRepository scenicRepository;
|
|
||||||
private static final List<String> ENABLED_USER_IDs = new ArrayList<>(){{
|
|
||||||
add("3932535453961555968");
|
|
||||||
add("3936121342868459520");
|
|
||||||
add("3936940597855784960");
|
|
||||||
add("4049850382325780480");
|
|
||||||
}};
|
|
||||||
|
|
||||||
// 分页查询景区列表
|
|
||||||
@PostMapping("/page")
|
|
||||||
public ApiResponse<PageInfo<ScenicEntity>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
|
|
||||||
String userId = BaseContextHandler.getUserId();
|
|
||||||
if (ENABLED_USER_IDs.contains(userId)) {
|
|
||||||
return appScenicService.pageQuery(scenicReqQuery);
|
|
||||||
} else {
|
|
||||||
return ApiResponse.success(new PageInfo<>(new ArrayList<>()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 根据id查询景区详情
|
|
||||||
@IgnoreToken
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ApiResponse<ScenicRespVO> getDetails(@PathVariable Long id){
|
|
||||||
return appScenicService.getDetails(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/config")
|
|
||||||
@IgnoreToken
|
|
||||||
public ApiResponse<ScenicConfigResp> getConfig(@PathVariable Long id){
|
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(id);
|
|
||||||
ScenicConfigResp resp = new ScenicConfigResp();
|
|
||||||
resp.setWatermarkUrl(scenicConfig.getString("watermark_url"));
|
|
||||||
resp.setVideoStoreDay(scenicConfig.getInteger("video_store_day"));
|
|
||||||
resp.setAntiScreenRecordType(scenicConfig.getInteger("anti_screen_record_type"));
|
|
||||||
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable", false));
|
|
||||||
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable", false));
|
|
||||||
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting", false));
|
|
||||||
resp.setImageSourcePackHint(scenicConfig.getString("image_source_pack_hint"));
|
|
||||||
resp.setVideoSourcePackHint(scenicConfig.getString("video_source_pack_hint"));
|
|
||||||
resp.setShareBeforeBuy(scenicConfig.getBoolean("share_before_buy"));
|
|
||||||
resp.setFaceSelectFirst(scenicConfig.getBoolean("face_select_first", false));
|
|
||||||
resp.setPrintEnableSource(scenicConfig.getBoolean("print_enable_source", true));
|
|
||||||
resp.setPrintForceFaceUpload(scenicConfig.getBoolean("print_force_face_upload", false));
|
|
||||||
resp.setPrintEnableManual(scenicConfig.getBoolean("print_enable_manual", true));
|
|
||||||
resp.setSceneMode(scenicConfig.getInteger("scene_mode", 0));
|
|
||||||
resp.setPrintEnable(scenicConfig.getBoolean("print_enable", false));
|
|
||||||
resp.setShowMyPagePaid(scenicConfig.getBoolean("show_my_page_paid", true));
|
|
||||||
resp.setShowMyPageUnpaid(scenicConfig.getBoolean("show_my_page_unpaid", true));
|
|
||||||
return ApiResponse.success(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询景区设备总数和拍到用户的机位数量
|
|
||||||
@GetMapping("/{scenicId}/deviceCount/")
|
|
||||||
public ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(@PathVariable Long scenicId){
|
|
||||||
return appScenicService.deviceCountByScenicId(scenicId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 景区视频源素材列表
|
|
||||||
@GetMapping("/contentList/")
|
|
||||||
public ApiResponse<List<ContentPageVO>> contentList() {
|
|
||||||
return faceService.contentListUseDefaultFace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 景区视频源素材列表
|
|
||||||
@GetMapping("/face/{faceId}/contentList")
|
|
||||||
public ApiResponse<List<ContentPageVO>> contentList(@PathVariable Long faceId) {
|
|
||||||
List<ContentPageVO> contentPageVOS = faceService.faceContentList(faceId);
|
|
||||||
return ApiResponse.success(contentPageVOS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/nearby")
|
|
||||||
public ApiResponse<List<ScenicAppVO>> nearby(@RequestBody ScenicIndexVO scenicIndexVO) {
|
|
||||||
List<ScenicAppVO> list = appScenicService.scenicListByLnLa(scenicIndexVO);
|
|
||||||
return ApiResponse.success(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,18 +4,16 @@ package com.ycwl.basic.controller.printer;
|
|||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
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.mobile.face.FaceRecognizeResp;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
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.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.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import com.ycwl.basic.utils.WxMpUtil;
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
@@ -32,7 +30,6 @@ 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.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@IgnoreToken
|
@IgnoreToken
|
||||||
@@ -46,8 +43,7 @@ public class PrinterTvController {
|
|||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
private final FaceRepository faceRepository;
|
private final FaceRepository faceRepository;
|
||||||
private final FaceService pcFaceService;
|
private final FaceService pcFaceService;
|
||||||
private final MemberRelationRepository memberRelationRepository;
|
private final SourceMapper sourceMapper;
|
||||||
private final SourceRepository sourceRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取景区列表
|
* 获取景区列表
|
||||||
@@ -167,18 +163,16 @@ public class PrinterTvController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据人脸样本ID查询图像素材
|
* 根据人脸ID查询图像素材
|
||||||
*
|
*
|
||||||
* @param faceId 人脸样本ID
|
* @param faceId 人脸ID
|
||||||
|
* @param type 素材类型(默认为2-图片)
|
||||||
* @return 匹配的source记录
|
* @return 匹配的source记录
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{faceId}/source")
|
@GetMapping("/{faceId}/source")
|
||||||
public ApiResponse<List<SourceEntity>> getSourceByFaceId(@PathVariable Long faceId, @RequestParam(name = "type", required = false, defaultValue = "2") Integer type) {
|
public ApiResponse<List<SourceEntity>> getSourceByFaceId(@PathVariable Long faceId, @RequestParam(name = "type", required = false, defaultValue = "2") Integer type) {
|
||||||
List<MemberSourceEntity> source = memberRelationRepository.listSourceByFaceRelation(faceId, type);
|
List<SourceEntity> sources = sourceMapper.listSourceByFaceRelation(faceId, type);
|
||||||
if (source == null) {
|
return ApiResponse.success(sources);
|
||||||
return ApiResponse.success(Collections.emptyList());
|
|
||||||
}
|
|
||||||
return ApiResponse.success(source.stream().map(item -> sourceRepository.getSource(item.getSourceId())).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ public class PuzzleGenerationOrchestrator {
|
|||||||
generateRequest.setFaceId(faceId);
|
generateRequest.setFaceId(faceId);
|
||||||
generateRequest.setBusinessType("face_matching");
|
generateRequest.setBusinessType("face_matching");
|
||||||
generateRequest.setTemplateCode(template.getCode());
|
generateRequest.setTemplateCode(template.getCode());
|
||||||
generateRequest.setOutputFormat("PNG");
|
generateRequest.setOutputFormat("JPEG");
|
||||||
generateRequest.setQuality(90);
|
generateRequest.setQuality(80);
|
||||||
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
||||||
generateRequest.setRequireRuleMatch(true);
|
generateRequest.setRequireRuleMatch(true);
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
|
|||||||
return resp;
|
return resp;
|
||||||
} else if (errorCode == 222204) {
|
} else if (errorCode == 222204) {
|
||||||
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
|
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
|
||||||
log.warn("无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
|
// log.warn("无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
|
||||||
String base64Image = downloadImageAsBase64(faceUrl);
|
String base64Image = downloadImageAsBase64(faceUrl);
|
||||||
if (base64Image != null) {
|
if (base64Image != null) {
|
||||||
// 重试时也不需要限流,由外层调度器控制
|
// 重试时也不需要限流,由外层调度器控制
|
||||||
@@ -338,7 +338,7 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
|
|||||||
return resp;
|
return resp;
|
||||||
} else if (errorCode == 222204) {
|
} else if (errorCode == 222204) {
|
||||||
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
|
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
|
||||||
log.warn("搜索人脸时无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
|
// log.warn("搜索人脸时无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
|
||||||
String base64Image = downloadImageAsBase64(faceUrl);
|
String base64Image = downloadImageAsBase64(faceUrl);
|
||||||
if (base64Image != null) {
|
if (base64Image != null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import com.ycwl.basic.model.pc.broker.resp.BrokerRecordRespVO;
|
|||||||
import com.ycwl.basic.model.pc.broker.resp.DailySummaryRespVO;
|
import com.ycwl.basic.model.pc.broker.resp.DailySummaryRespVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,4 +31,11 @@ public interface BrokerRecordMapper {
|
|||||||
int update(BrokerRecord brokerRecord);
|
int update(BrokerRecord brokerRecord);
|
||||||
|
|
||||||
List<DailySummaryRespVO> getDailySummaryByBrokerId(Long brokerId, Date startTime, Date endTime);
|
List<DailySummaryRespVO> getDailySummaryByBrokerId(Long brokerId, Date startTime, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计分销员订单数据(不含扫码统计,已迁移到 ClickHouse)
|
||||||
|
*/
|
||||||
|
List<HashMap<String, Object>> getDailyOrderStats(@Param("brokerId") Long brokerId,
|
||||||
|
@Param("startTime") Date startTime,
|
||||||
|
@Param("endTime") Date endTime);
|
||||||
}
|
}
|
||||||
@@ -173,4 +173,12 @@ public interface SourceMapper {
|
|||||||
* @return 免费记录数
|
* @return 免费记录数
|
||||||
*/
|
*/
|
||||||
int countFreeRelationsByFaceIdAndType(Long faceId, Integer type);
|
int countFreeRelationsByFaceIdAndType(Long faceId, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据faceId和type直接查询关联的source列表(避免N+1查询)
|
||||||
|
* @param faceId 人脸ID
|
||||||
|
* @param type 素材类型
|
||||||
|
* @return source实体列表
|
||||||
|
*/
|
||||||
|
List<SourceEntity> listSourceByFaceRelation(Long faceId, Integer type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
|
|||||||
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
import com.ycwl.basic.model.mobile.statistic.req.StatisticsRecordAddReq;
|
||||||
import com.ycwl.basic.model.mobile.statistic.resp.AppStatisticsFunnelVO;
|
import com.ycwl.basic.model.mobile.statistic.resp.AppStatisticsFunnelVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -108,6 +109,18 @@ public interface StatisticsMapper {
|
|||||||
|
|
||||||
List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query);
|
List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计分销员扫码次数
|
||||||
|
*/
|
||||||
|
Integer countBrokerScanCount(Long brokerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计分销员扫码数据
|
||||||
|
*/
|
||||||
|
List<HashMap<String, Object>> getDailyScanStats(@Param("brokerId") Long brokerId,
|
||||||
|
@Param("startTime") Date startTime,
|
||||||
|
@Param("endTime") Date endTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计订单数量和金额(包含推送订单和现场订单)
|
* 统计订单数量和金额(包含推送订单和现场订单)
|
||||||
* @param query
|
* @param query
|
||||||
|
|||||||
@@ -59,4 +59,16 @@ public interface TaskMapper {
|
|||||||
List<TaskEntity> selectAllFailed();
|
List<TaskEntity> selectAllFailed();
|
||||||
|
|
||||||
TaskEntity listLastFaceTemplateTask(Long faceId, Long templateId);
|
TaskEntity listLastFaceTemplateTask(Long faceId, Long templateId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 face_id 列表统计已完成任务的用户数
|
||||||
|
* 用于 ClickHouse 迁移后的跨库统计
|
||||||
|
*/
|
||||||
|
Integer countCompletedTaskMembersByFaceIds(@Param("faceIds") List<String> faceIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 face_id 列表统计已完成任务数
|
||||||
|
* 用于 ClickHouse 迁移后的跨库统计
|
||||||
|
*/
|
||||||
|
Integer countCompletedTasksByFaceIds(@Param("faceIds") List<String> faceIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
|||||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -18,6 +19,7 @@ import java.util.Map;
|
|||||||
* 2. 类型安全:根据枚举类型查找策略
|
* 2. 类型安全:根据枚举类型查找策略
|
||||||
* 3. 失败快速:找不到策略时抛出异常
|
* 3. 失败快速:找不到策略时抛出异常
|
||||||
*/
|
*/
|
||||||
|
@Lazy
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DuplicatePurchaseCheckerFactory {
|
public class DuplicatePurchaseCheckerFactory {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.order.strategy.DuplicateCheckContext;
|
|||||||
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
|
||||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,6 +14,7 @@ import org.springframework.stereotype.Component;
|
|||||||
* 检查逻辑:
|
* 检查逻辑:
|
||||||
* 不执行任何检查,直接通过
|
* 不执行任何检查,直接通过
|
||||||
*/
|
*/
|
||||||
|
@Lazy
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class NoCheckDuplicateChecker implements IDuplicatePurchaseChecker {
|
public class NoCheckDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.ycwl.basic.pricing.enums.ProductType;
|
|||||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,6 +30,7 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* SQL查询: WHERE order_id = ? AND product_type = ?
|
* SQL查询: WHERE order_id = ? AND product_type = ?
|
||||||
*/
|
*/
|
||||||
|
@Lazy
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class ParentResourceDuplicateChecker implements IDuplicatePurchaseChecker {
|
public class ParentResourceDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.ycwl.basic.pricing.enums.ProductType;
|
|||||||
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,6 +30,7 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* SQL查询: WHERE order_id = ? AND product_type = ? AND product_id = ?
|
* SQL查询: WHERE order_id = ? AND product_type = ? AND product_id = ?
|
||||||
*/
|
*/
|
||||||
|
@Lazy
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class UniqueResourceDuplicateChecker implements IDuplicatePurchaseChecker {
|
public class UniqueResourceDuplicateChecker implements IDuplicatePurchaseChecker {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class PuzzleGenerateController {
|
|||||||
log.warn("拼图生成参数错误: {}", e.getMessage());
|
log.warn("拼图生成参数错误: {}", e.getMessage());
|
||||||
return ApiResponse.fail(e.getMessage());
|
return ApiResponse.fail(e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("拼图生成失败", e);
|
log.error("拼图生成失败:{}", e.getMessage());
|
||||||
return ApiResponse.fail("图片生成失败,请稍后重试");
|
return ApiResponse.fail("图片生成失败,请稍后重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘 Worker 接入安全配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "puzzle.edge.worker.security")
|
||||||
|
public class PuzzleEdgeWorkerSecurityProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用访问 IP 校验
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许访问的 IPv4 CIDR(默认:100.64.0.0/24)
|
||||||
|
*/
|
||||||
|
private String allowedIpCidr = "100.64.0.0/24";
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.controller;
|
||||||
|
|
||||||
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeTaskFailRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeTaskSuccessRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeUploadUrlsResponse;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeWorkerAuthRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeWorkerSyncRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeWorkerSyncResponse;
|
||||||
|
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puzzle 边缘渲染对接接口
|
||||||
|
* 模式:边缘客户端拉取任务(含上传地址) → 上传 → 上报结果
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@IgnoreToken
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/puzzle/render/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PuzzleEdgeRenderTaskController {
|
||||||
|
|
||||||
|
private final PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService;
|
||||||
|
|
||||||
|
@PostMapping("/worker/sync")
|
||||||
|
public ApiResponse<PuzzleEdgeWorkerSyncResponse> sync(@RequestBody PuzzleEdgeWorkerSyncRequest req) {
|
||||||
|
return ApiResponse.success(puzzleEdgeRenderTaskService.sync(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/task/{taskId}/uploadUrls")
|
||||||
|
public ApiResponse<PuzzleEdgeUploadUrlsResponse> uploadUrls(@PathVariable Long taskId,
|
||||||
|
@RequestBody PuzzleEdgeWorkerAuthRequest req) {
|
||||||
|
return ApiResponse.success(puzzleEdgeRenderTaskService.getUploadUrls(taskId, req != null ? req.getAccessKey() : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/task/{taskId}/success")
|
||||||
|
public ApiResponse<String> success(@PathVariable Long taskId, @RequestBody PuzzleEdgeTaskSuccessRequest req) {
|
||||||
|
puzzleEdgeRenderTaskService.taskSuccess(taskId, req);
|
||||||
|
return ApiResponse.success("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/task/{taskId}/fail")
|
||||||
|
public ApiResponse<String> fail(@PathVariable Long taskId, @RequestBody PuzzleEdgeTaskFailRequest req) {
|
||||||
|
puzzleEdgeRenderTaskService.taskFail(taskId, req);
|
||||||
|
return ApiResponse.success("OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeRenderTaskDTO {
|
||||||
|
private Long id;
|
||||||
|
private Long recordId;
|
||||||
|
private Long templateId;
|
||||||
|
private String templateCode;
|
||||||
|
private Long scenicId;
|
||||||
|
private Long faceId;
|
||||||
|
private Integer attemptCount;
|
||||||
|
private String outputFormat;
|
||||||
|
private Integer outputQuality;
|
||||||
|
private Map<String, Object> payload;
|
||||||
|
/**
|
||||||
|
* 上传地址(预签名URL),用于 Worker 直接上传产物
|
||||||
|
*/
|
||||||
|
private PuzzleEdgeUploadUrlsResponse upload;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeTaskFailRequest {
|
||||||
|
private String accessKey;
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeTaskSuccessRequest {
|
||||||
|
private String accessKey;
|
||||||
|
|
||||||
|
private Long resultFileSize;
|
||||||
|
private Integer resultWidth;
|
||||||
|
private Integer resultHeight;
|
||||||
|
private Integer renderDurationMs;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeUploadUrlsResponse {
|
||||||
|
private UploadTarget original;
|
||||||
|
private UploadTarget cropped;
|
||||||
|
private Date expireAt;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UploadTarget {
|
||||||
|
private String method;
|
||||||
|
private String url;
|
||||||
|
private String publicUrl;
|
||||||
|
private Map<String, String> headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeWorkerAuthRequest {
|
||||||
|
private String accessKey;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeWorkerSyncRequest {
|
||||||
|
private String accessKey;
|
||||||
|
private Integer maxTasks;
|
||||||
|
private ClientStatusReqVo clientStatus;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PuzzleEdgeWorkerSyncResponse {
|
||||||
|
private List<PuzzleEdgeRenderTaskDTO> tasks = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puzzle 边缘渲染任务实体
|
||||||
|
* 对应表:puzzle_edge_render_task
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("puzzle_edge_render_task")
|
||||||
|
public class PuzzleEdgeRenderTaskEntity {
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@TableField("record_id")
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
@TableField("template_id")
|
||||||
|
private Long templateId;
|
||||||
|
|
||||||
|
@TableField("template_code")
|
||||||
|
private String templateCode;
|
||||||
|
|
||||||
|
@TableField("scenic_id")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
@TableField("face_id")
|
||||||
|
private Long faceId;
|
||||||
|
|
||||||
|
@TableField("content_hash")
|
||||||
|
private String contentHash;
|
||||||
|
|
||||||
|
@TableField("status")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@TableField("worker_id")
|
||||||
|
private Long workerId;
|
||||||
|
|
||||||
|
@TableField("lease_expire_time")
|
||||||
|
private Date leaseExpireTime;
|
||||||
|
|
||||||
|
@TableField("attempt_count")
|
||||||
|
private Integer attemptCount;
|
||||||
|
|
||||||
|
@TableField("output_format")
|
||||||
|
private String outputFormat;
|
||||||
|
|
||||||
|
@TableField("output_quality")
|
||||||
|
private Integer outputQuality;
|
||||||
|
|
||||||
|
@TableField("original_object_key")
|
||||||
|
private String originalObjectKey;
|
||||||
|
|
||||||
|
@TableField("cropped_object_key")
|
||||||
|
private String croppedObjectKey;
|
||||||
|
|
||||||
|
@TableField("payload_json")
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
@TableField("error_message")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@TableField("create_time")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@TableField("update_time")
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.interceptor;
|
||||||
|
|
||||||
|
import com.ycwl.basic.puzzle.edge.config.PuzzleEdgeWorkerSecurityProperties;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.IpUtils;
|
||||||
|
import com.ycwl.basic.utils.Ipv4CidrMatcher;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘 Worker 接口访问 IP 校验
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PuzzleEdgeWorkerIpInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private static final String FORBIDDEN_MESSAGE = "非法来源IP";
|
||||||
|
|
||||||
|
private final PuzzleEdgeWorkerSecurityProperties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
if (properties == null || !properties.isEnabled()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientIp = IpUtils.getIpAddr(request);
|
||||||
|
if (Ipv4CidrMatcher.matches(clientIp, properties.getAllowedIpCidr())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("拒绝边缘 Worker 请求: uri={}, ip={}, allowedIpCidr={}",
|
||||||
|
request != null ? request.getRequestURI() : null,
|
||||||
|
clientIp,
|
||||||
|
properties.getAllowedIpCidr());
|
||||||
|
|
||||||
|
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write(JacksonUtil.toJson(ApiResponse.buildResponse(HttpStatus.FORBIDDEN.value(), FORBIDDEN_MESSAGE)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.puzzle.edge.entity.PuzzleEdgeRenderTaskEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PuzzleEdgeRenderTaskMapper {
|
||||||
|
|
||||||
|
PuzzleEdgeRenderTaskEntity getById(@Param("id") Long id);
|
||||||
|
|
||||||
|
int insert(PuzzleEdgeRenderTaskEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一条可领取任务ID:PENDING 或 RUNNING但租约已过期
|
||||||
|
*/
|
||||||
|
Long findNextClaimableTaskId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取任务(并写入租约与attempt)
|
||||||
|
*/
|
||||||
|
int claimTask(@Param("taskId") Long taskId,
|
||||||
|
@Param("workerId") Long workerId,
|
||||||
|
@Param("leaseExpireTime") Date leaseExpireTime);
|
||||||
|
|
||||||
|
int markSuccess(@Param("taskId") Long taskId, @Param("workerId") Long workerId);
|
||||||
|
|
||||||
|
int markFail(@Param("taskId") Long taskId,
|
||||||
|
@Param("workerId") Long workerId,
|
||||||
|
@Param("errorMessage") String errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,780 @@
|
|||||||
|
package com.ycwl.basic.puzzle.edge.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.ycwl.basic.model.pc.renderWorker.entity.RenderWorkerEntity;
|
||||||
|
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeRenderTaskDTO;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeTaskFailRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeTaskSuccessRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeUploadUrlsResponse;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeWorkerSyncRequest;
|
||||||
|
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeWorkerSyncResponse;
|
||||||
|
import com.ycwl.basic.puzzle.edge.entity.PuzzleEdgeRenderTaskEntity;
|
||||||
|
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||||
|
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||||
|
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||||
|
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||||
|
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||||
|
import com.ycwl.basic.repository.RenderWorkerRepository;
|
||||||
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puzzle 边缘渲染任务服务(中心端)
|
||||||
|
* 负责:任务创建、任务拉取、签发上传URL、回报成功/失败落库
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PuzzleEdgeRenderTaskService {
|
||||||
|
|
||||||
|
private static final int STATUS_PENDING = 0;
|
||||||
|
private static final int STATUS_RUNNING = 1;
|
||||||
|
private static final int STATUS_SUCCESS = 2;
|
||||||
|
private static final int STATUS_FAIL = 3;
|
||||||
|
|
||||||
|
private static final int MAX_SYNC_TASKS = 5;
|
||||||
|
private static final long LEASE_MILLIS = TimeUnit.SECONDS.toMillis(20);
|
||||||
|
private static final long UPLOAD_URL_EXPIRE_MILLIS = TimeUnit.HOURS.toMillis(1);
|
||||||
|
private static final int MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
private static final long TASK_CACHE_EXPIRE_HOURS = 6L;
|
||||||
|
private static final long TASK_CACHE_MAX_SIZE = 20000L;
|
||||||
|
private static final long WAIT_FUTURE_EXPIRE_MILLIS = TimeUnit.MINUTES.toMillis(10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务等待结果
|
||||||
|
*/
|
||||||
|
public static class TaskWaitResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final String errorMessage;
|
||||||
|
private final String imageUrl;
|
||||||
|
|
||||||
|
private TaskWaitResult(boolean success, String errorMessage, String imageUrl) {
|
||||||
|
this.success = success;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TaskWaitResult success(String imageUrl) {
|
||||||
|
return new TaskWaitResult(true, null, imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TaskWaitResult fail(String errorMessage) {
|
||||||
|
return new TaskWaitResult(false, errorMessage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待 future 的包装,包含创建时间用于过期清理
|
||||||
|
*/
|
||||||
|
private static class WaitFutureEntry {
|
||||||
|
final CompletableFuture<TaskWaitResult> future;
|
||||||
|
final long createTimeMillis;
|
||||||
|
|
||||||
|
WaitFutureEntry(CompletableFuture<TaskWaitResult> future) {
|
||||||
|
this.future = future;
|
||||||
|
this.createTimeMillis = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired(long nowMillis) {
|
||||||
|
return nowMillis - createTimeMillis > WAIT_FUTURE_EXPIRE_MILLIS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务内存池(单实例、允许丢失):仅用作 Worker 拉取与状态落地的中间态
|
||||||
|
*/
|
||||||
|
private final Cache<Long, PuzzleEdgeRenderTaskEntity> taskCache = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(TASK_CACHE_EXPIRE_HOURS, TimeUnit.HOURS)
|
||||||
|
.maximumSize(TASK_CACHE_MAX_SIZE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final AtomicLong taskIdSequence = new AtomicLong(System.currentTimeMillis());
|
||||||
|
private final Object taskLock = new Object();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务等待 future 池:用于伪同步等待任务完成
|
||||||
|
*/
|
||||||
|
private final ConcurrentHashMap<Long, WaitFutureEntry> waitFutures = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final PuzzleGenerationRecordMapper recordMapper;
|
||||||
|
private final PuzzleRepository puzzleRepository;
|
||||||
|
private final PrinterService printerService;
|
||||||
|
private final RenderWorkerRepository renderWorkerRepository;
|
||||||
|
|
||||||
|
public PuzzleEdgeWorkerSyncResponse sync(PuzzleEdgeWorkerSyncRequest req) {
|
||||||
|
RenderWorkerEntity worker = requireWorker(req != null ? req.getAccessKey() : null);
|
||||||
|
|
||||||
|
ClientStatusReqVo clientStatus = req != null ? req.getClientStatus() : null;
|
||||||
|
if (clientStatus != null) {
|
||||||
|
renderWorkerRepository.setWorkerHostStatus(worker.getId(), clientStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxTasks = req != null && req.getMaxTasks() != null ? req.getMaxTasks() : 1;
|
||||||
|
if (maxTasks <= 0) {
|
||||||
|
maxTasks = 1;
|
||||||
|
}
|
||||||
|
if (maxTasks > MAX_SYNC_TASKS) {
|
||||||
|
maxTasks = MAX_SYNC_TASKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PuzzleEdgeWorkerSyncResponse resp = new PuzzleEdgeWorkerSyncResponse();
|
||||||
|
for (int i = 0; i < maxTasks; i++) {
|
||||||
|
PuzzleEdgeRenderTaskEntity task = claimOne(worker.getId());
|
||||||
|
if (task == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PuzzleEdgeRenderTaskDTO dto = toTaskDTOOrFail(task, worker.getId());
|
||||||
|
if (dto != null) {
|
||||||
|
resp.getTasks().add(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PuzzleEdgeUploadUrlsResponse getUploadUrls(Long taskId, String accessKey) {
|
||||||
|
RenderWorkerEntity worker = requireWorker(accessKey);
|
||||||
|
PuzzleEdgeRenderTaskEntity task = getAndCheckRunningTask(taskId, worker.getId());
|
||||||
|
return buildUploadUrls(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void taskSuccess(Long taskId, PuzzleEdgeTaskSuccessRequest req) {
|
||||||
|
RenderWorkerEntity worker = requireWorker(req != null ? req.getAccessKey() : null);
|
||||||
|
PuzzleEdgeRenderTaskEntity task = getAndCheckTaskOwned(taskId, worker.getId());
|
||||||
|
if (task.getStatus() != null && task.getStatus() == STATUS_SUCCESS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.getStatus() == null || task.getStatus() != STATUS_RUNNING) {
|
||||||
|
throw new IllegalArgumentException("任务状态非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = tryMarkSuccess(task, worker.getId());
|
||||||
|
if (!updated) {
|
||||||
|
throw new IllegalStateException("任务状态更新失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
PuzzleGenerationRecordEntity record = recordMapper.getById(task.getRecordId());
|
||||||
|
if (record == null) {
|
||||||
|
log.warn("边缘渲染任务回报成功,但生成记录不存在: taskId={}, recordId={}", taskId, task.getRecordId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IStorageAdapter storage = StorageFactory.use();
|
||||||
|
String originalImageUrl = storage.getUrl(task.getOriginalObjectKey());
|
||||||
|
String resultImageUrl = StrUtil.isNotBlank(task.getCroppedObjectKey())
|
||||||
|
? storage.getUrl(task.getCroppedObjectKey())
|
||||||
|
: originalImageUrl;
|
||||||
|
|
||||||
|
Long resultFileSize = req != null ? req.getResultFileSize() : null;
|
||||||
|
Integer resultWidth = req != null ? req.getResultWidth() : null;
|
||||||
|
Integer resultHeight = req != null ? req.getResultHeight() : null;
|
||||||
|
Integer renderDurationMs = req != null ? req.getRenderDurationMs() : null;
|
||||||
|
|
||||||
|
recordMapper.updateSuccess(
|
||||||
|
record.getId(),
|
||||||
|
resultImageUrl,
|
||||||
|
originalImageUrl,
|
||||||
|
resultFileSize,
|
||||||
|
resultWidth,
|
||||||
|
resultHeight,
|
||||||
|
renderDurationMs
|
||||||
|
);
|
||||||
|
|
||||||
|
// 通知等待方任务完成
|
||||||
|
completeWaitFuture(taskId, TaskWaitResult.success(resultImageUrl));
|
||||||
|
|
||||||
|
PuzzleTemplateEntity template = puzzleRepository.getTemplateById(task.getTemplateId());
|
||||||
|
if (template != null && template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
|
||||||
|
try {
|
||||||
|
Integer printRecordId = printerService.addUserPhotoFromPuzzle(
|
||||||
|
record.getUserId(),
|
||||||
|
record.getScenicId(),
|
||||||
|
record.getFaceId(),
|
||||||
|
originalImageUrl,
|
||||||
|
record.getId()
|
||||||
|
);
|
||||||
|
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void taskFail(Long taskId, PuzzleEdgeTaskFailRequest req) {
|
||||||
|
RenderWorkerEntity worker = requireWorker(req != null ? req.getAccessKey() : null);
|
||||||
|
PuzzleEdgeRenderTaskEntity task = getAndCheckTaskOwned(taskId, worker.getId());
|
||||||
|
if (task.getStatus() != null && task.getStatus() == STATUS_FAIL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.getStatus() == null || task.getStatus() != STATUS_RUNNING) {
|
||||||
|
throw new IllegalArgumentException("任务状态非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
String errorMessage = req != null && StrUtil.isNotBlank(req.getErrorMessage())
|
||||||
|
? req.getErrorMessage()
|
||||||
|
: "边缘渲染失败";
|
||||||
|
|
||||||
|
boolean updated = tryMarkFail(task, worker.getId(), errorMessage);
|
||||||
|
if (!updated) {
|
||||||
|
throw new IllegalStateException("任务状态更新失败");
|
||||||
|
}
|
||||||
|
recordMapper.updateFail(task.getRecordId(), errorMessage);
|
||||||
|
|
||||||
|
// 通知等待方任务失败
|
||||||
|
completeWaitFuture(taskId, TaskWaitResult.fail(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行超时自动失败并重试:
|
||||||
|
* - task.status=RUNNING 且 leaseExpireTime 已过期:视为本次尝试超时
|
||||||
|
* - attemptCount < MAX_RETRY_ATTEMPTS:重置为 PENDING 等待重试
|
||||||
|
* - attemptCount >= MAX_RETRY_ATTEMPTS:最终失败并落库
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 1000L)
|
||||||
|
public void timeoutFailAndRetry() {
|
||||||
|
List<Long> retryRecordIds = new ArrayList<>();
|
||||||
|
Map<Long, String> failRecordMessages = new HashMap<>();
|
||||||
|
Map<Long, String> failTaskMessages = new HashMap<>(); // taskId -> errorMessage
|
||||||
|
|
||||||
|
synchronized (taskLock) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (PuzzleEdgeRenderTaskEntity task : taskCache.asMap().values()) {
|
||||||
|
if (task == null || task.getId() == null || task.getStatus() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.getStatus() != STATUS_RUNNING) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date leaseExpireTime = task.getLeaseExpireTime();
|
||||||
|
if (leaseExpireTime == null || leaseExpireTime.getTime() >= now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int attemptCount = task.getAttemptCount() != null ? task.getAttemptCount() : 0;
|
||||||
|
if (attemptCount >= MAX_RETRY_ATTEMPTS) {
|
||||||
|
String errorMessage = String.format("边缘渲染任务超时(%d秒),重试次数耗尽: attemptCount=%d",
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(LEASE_MILLIS), attemptCount);
|
||||||
|
log.warn("边缘渲染任务最终失败: taskId={}, recordId={}, {}", task.getId(), task.getRecordId(), errorMessage);
|
||||||
|
|
||||||
|
task.setStatus(STATUS_FAIL);
|
||||||
|
task.setWorkerId(null);
|
||||||
|
task.setLeaseExpireTime(null);
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
task.setUpdateTime(new Date(now));
|
||||||
|
if (task.getRecordId() != null) {
|
||||||
|
failRecordMessages.put(task.getRecordId(), errorMessage);
|
||||||
|
}
|
||||||
|
// 记录需要通知的任务
|
||||||
|
failTaskMessages.put(task.getId(), errorMessage);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("边缘渲染任务超时,准备重试: taskId={}, recordId={}, attemptCount={}",
|
||||||
|
task.getId(), task.getRecordId(), attemptCount);
|
||||||
|
|
||||||
|
task.setStatus(STATUS_PENDING);
|
||||||
|
task.setWorkerId(null);
|
||||||
|
task.setLeaseExpireTime(null);
|
||||||
|
task.setErrorMessage("边缘渲染任务超时,等待重试");
|
||||||
|
task.setUpdateTime(new Date(now));
|
||||||
|
if (task.getRecordId() != null) {
|
||||||
|
retryRecordIds.add(task.getRecordId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Long recordId : retryRecordIds) {
|
||||||
|
incrementRecordRetryCount(recordId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Long, String> entry : failRecordMessages.entrySet()) {
|
||||||
|
recordMapper.updateFail(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知等待方任务最终失败
|
||||||
|
for (Map.Entry<Long, String> entry : failTaskMessages.entrySet()) {
|
||||||
|
completeWaitFuture(entry.getKey(), TaskWaitResult.fail(entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期的等待 future
|
||||||
|
cleanupExpiredWaitFutures();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的等待 future,防止内存泄漏
|
||||||
|
*/
|
||||||
|
private void cleanupExpiredWaitFutures() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Iterator<Map.Entry<Long, WaitFutureEntry>> iterator = waitFutures.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<Long, WaitFutureEntry> entry = iterator.next();
|
||||||
|
if (entry.getValue().isExpired(now)) {
|
||||||
|
entry.getValue().future.complete(TaskWaitResult.fail("等待超时(内部清理)"));
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementRecordRetryCount(Long recordId) {
|
||||||
|
if (recordId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
|
||||||
|
if (record == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int currentRetryCount = record.getRetryCount() != null ? record.getRetryCount() : 0;
|
||||||
|
PuzzleGenerationRecordEntity update = new PuzzleGenerationRecordEntity();
|
||||||
|
update.setId(recordId);
|
||||||
|
update.setRetryCount(currentRetryCount + 1);
|
||||||
|
recordMapper.update(update);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("更新生成记录重试次数失败: recordId={}, error={}", recordId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建边缘渲染任务(供中心业务侧调用)
|
||||||
|
*/
|
||||||
|
public Long createRenderTask(PuzzleGenerationRecordEntity record,
|
||||||
|
PuzzleTemplateEntity template,
|
||||||
|
List<PuzzleElementEntity> sortedElements,
|
||||||
|
Map<String, String> finalDynamicData,
|
||||||
|
String outputFormat,
|
||||||
|
Integer quality) {
|
||||||
|
if (record == null || record.getId() == null) {
|
||||||
|
throw new IllegalArgumentException("record不能为空");
|
||||||
|
}
|
||||||
|
if (template == null || template.getId() == null) {
|
||||||
|
throw new IllegalArgumentException("template不能为空");
|
||||||
|
}
|
||||||
|
if (sortedElements == null) {
|
||||||
|
sortedElements = List.of();
|
||||||
|
}
|
||||||
|
if (finalDynamicData == null) {
|
||||||
|
finalDynamicData = Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedFormat = normalizeOutputFormat(outputFormat);
|
||||||
|
Integer outputQuality = quality != null ? quality : 90;
|
||||||
|
String ext = "PNG".equals(normalizedFormat) ? "png" : "jpeg";
|
||||||
|
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
|
||||||
|
|
||||||
|
String originalObjectKey = String.format("puzzle/%s/%s", template.getCode(), fileName);
|
||||||
|
String croppedObjectKey = StrUtil.isNotBlank(template.getUserArea())
|
||||||
|
? String.format("puzzle/%s_cropped/%s", template.getCode(), fileName)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("recordId", record.getId());
|
||||||
|
|
||||||
|
Map<String, Object> templatePayload = new HashMap<>();
|
||||||
|
templatePayload.put("id", template.getId());
|
||||||
|
templatePayload.put("code", template.getCode());
|
||||||
|
templatePayload.put("canvasWidth", template.getCanvasWidth());
|
||||||
|
templatePayload.put("canvasHeight", template.getCanvasHeight());
|
||||||
|
templatePayload.put("backgroundType", template.getBackgroundType());
|
||||||
|
templatePayload.put("backgroundColor", template.getBackgroundColor());
|
||||||
|
templatePayload.put("backgroundImage", template.getBackgroundImage());
|
||||||
|
templatePayload.put("userArea", template.getUserArea());
|
||||||
|
payload.put("template", templatePayload);
|
||||||
|
|
||||||
|
List<Map<String, Object>> elementPayloadList = new ArrayList<>();
|
||||||
|
for (PuzzleElementEntity e : sortedElements) {
|
||||||
|
Map<String, Object> elementPayload = new HashMap<>();
|
||||||
|
elementPayload.put("id", e.getId());
|
||||||
|
elementPayload.put("type", e.getElementType());
|
||||||
|
elementPayload.put("key", e.getElementKey());
|
||||||
|
elementPayload.put("name", e.getElementName());
|
||||||
|
elementPayload.put("x", e.getXPosition());
|
||||||
|
elementPayload.put("y", e.getYPosition());
|
||||||
|
elementPayload.put("width", e.getWidth());
|
||||||
|
elementPayload.put("height", e.getHeight());
|
||||||
|
elementPayload.put("zIndex", e.getZIndex());
|
||||||
|
elementPayload.put("rotation", e.getRotation());
|
||||||
|
elementPayload.put("opacity", e.getOpacity());
|
||||||
|
elementPayload.put("config", e.getConfig());
|
||||||
|
elementPayloadList.add(elementPayload);
|
||||||
|
}
|
||||||
|
payload.put("elements", elementPayloadList);
|
||||||
|
payload.put("dynamicData", finalDynamicData);
|
||||||
|
|
||||||
|
Map<String, Object> outputPayload = new HashMap<>();
|
||||||
|
outputPayload.put("format", normalizedFormat);
|
||||||
|
outputPayload.put("quality", outputQuality);
|
||||||
|
payload.put("output", outputPayload);
|
||||||
|
|
||||||
|
PuzzleEdgeRenderTaskEntity task = new PuzzleEdgeRenderTaskEntity();
|
||||||
|
task.setRecordId(record.getId());
|
||||||
|
task.setTemplateId(template.getId());
|
||||||
|
task.setTemplateCode(template.getCode());
|
||||||
|
task.setScenicId(record.getScenicId());
|
||||||
|
task.setFaceId(record.getFaceId());
|
||||||
|
task.setContentHash(record.getContentHash());
|
||||||
|
task.setStatus(STATUS_PENDING);
|
||||||
|
task.setAttemptCount(0);
|
||||||
|
task.setOutputFormat(normalizedFormat);
|
||||||
|
task.setOutputQuality(outputQuality);
|
||||||
|
task.setOriginalObjectKey(originalObjectKey);
|
||||||
|
task.setCroppedObjectKey(croppedObjectKey);
|
||||||
|
task.setPayloadJson(JacksonUtil.toJson(payload));
|
||||||
|
|
||||||
|
Long taskId = taskIdSequence.incrementAndGet();
|
||||||
|
Date now = new Date();
|
||||||
|
task.setId(taskId);
|
||||||
|
task.setCreateTime(now);
|
||||||
|
task.setUpdateTime(now);
|
||||||
|
|
||||||
|
taskCache.put(taskId, task);
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册任务等待,返回用于等待的 CompletableFuture
|
||||||
|
* 调用方应在 createRenderTask 之后立即调用此方法
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return CompletableFuture,可用于同步等待或异步处理
|
||||||
|
*/
|
||||||
|
public CompletableFuture<TaskWaitResult> registerWait(Long taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
CompletableFuture<TaskWaitResult> failedFuture = new CompletableFuture<>();
|
||||||
|
failedFuture.complete(TaskWaitResult.fail("taskId不能为空"));
|
||||||
|
return failedFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<TaskWaitResult> future = new CompletableFuture<>();
|
||||||
|
waitFutures.put(taskId, new WaitFutureEntry(future));
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 伪同步等待任务完成
|
||||||
|
* 阻塞当前线程直到任务成功、失败或超时
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param timeoutMs 超时时间(毫秒)
|
||||||
|
* @return 任务结果,包含成功/失败状态和相关信息
|
||||||
|
*/
|
||||||
|
public TaskWaitResult waitForTask(Long taskId, long timeoutMs) {
|
||||||
|
if (taskId == null) {
|
||||||
|
return TaskWaitResult.fail("taskId不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查任务是否已完成
|
||||||
|
PuzzleEdgeRenderTaskEntity task = taskCache.getIfPresent(taskId);
|
||||||
|
if (task != null) {
|
||||||
|
if (task.getStatus() != null && task.getStatus() == STATUS_SUCCESS) {
|
||||||
|
return buildSuccessResult(task);
|
||||||
|
}
|
||||||
|
if (task.getStatus() != null && task.getStatus() == STATUS_FAIL) {
|
||||||
|
return TaskWaitResult.fail(task.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或创建等待 future
|
||||||
|
WaitFutureEntry entry = waitFutures.computeIfAbsent(taskId, k -> new WaitFutureEntry(new CompletableFuture<>()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return entry.future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
waitFutures.remove(taskId);
|
||||||
|
return TaskWaitResult.fail("等待任务超时");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
waitFutures.remove(taskId);
|
||||||
|
return TaskWaitResult.fail("等待被中断");
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
waitFutures.remove(taskId);
|
||||||
|
return TaskWaitResult.fail("等待出错: " + e.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建任务并同步等待结果(便捷方法)
|
||||||
|
*/
|
||||||
|
public TaskWaitResult createAndWait(PuzzleGenerationRecordEntity record,
|
||||||
|
PuzzleTemplateEntity template,
|
||||||
|
List<PuzzleElementEntity> sortedElements,
|
||||||
|
Map<String, String> finalDynamicData,
|
||||||
|
String outputFormat,
|
||||||
|
Integer quality,
|
||||||
|
long timeoutMs) {
|
||||||
|
Long taskId = createRenderTask(record, template, sortedElements, finalDynamicData, outputFormat, quality);
|
||||||
|
registerWait(taskId);
|
||||||
|
return waitForTask(taskId, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskWaitResult buildSuccessResult(PuzzleEdgeRenderTaskEntity task) {
|
||||||
|
IStorageAdapter storage = StorageFactory.use();
|
||||||
|
String imageUrl = storage.getUrl(task.getOriginalObjectKey());
|
||||||
|
return TaskWaitResult.success(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成任务等待(内部调用)
|
||||||
|
*/
|
||||||
|
private void completeWaitFuture(Long taskId, TaskWaitResult result) {
|
||||||
|
WaitFutureEntry entry = waitFutures.remove(taskId);
|
||||||
|
if (entry != null && entry.future != null) {
|
||||||
|
entry.future.complete(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PuzzleEdgeRenderTaskDTO toTaskDTOOrFail(PuzzleEdgeRenderTaskEntity task, Long workerId) {
|
||||||
|
try {
|
||||||
|
PuzzleEdgeRenderTaskDTO dto = new PuzzleEdgeRenderTaskDTO();
|
||||||
|
dto.setId(task.getId());
|
||||||
|
dto.setRecordId(task.getRecordId());
|
||||||
|
dto.setTemplateId(task.getTemplateId());
|
||||||
|
dto.setTemplateCode(task.getTemplateCode());
|
||||||
|
dto.setScenicId(task.getScenicId());
|
||||||
|
dto.setFaceId(task.getFaceId());
|
||||||
|
dto.setAttemptCount(task.getAttemptCount());
|
||||||
|
dto.setOutputFormat(task.getOutputFormat());
|
||||||
|
dto.setOutputQuality(task.getOutputQuality());
|
||||||
|
dto.setPayload(JacksonUtil.fromJson(task.getPayloadJson(), new TypeReference<Map<String, Object>>() {}));
|
||||||
|
dto.setUpload(buildUploadUrls(task));
|
||||||
|
return dto;
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errorMessage = "任务数据组装失败: " + e.getMessage();
|
||||||
|
log.error("边缘渲染任务组装失败: taskId={}, recordId={}", task.getId(), task.getRecordId(), e);
|
||||||
|
tryMarkFail(task, workerId, errorMessage);
|
||||||
|
recordMapper.updateFail(task.getRecordId(), errorMessage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PuzzleEdgeUploadUrlsResponse buildUploadUrls(PuzzleEdgeRenderTaskEntity task) {
|
||||||
|
String outputFormat = normalizeOutputFormat(task.getOutputFormat());
|
||||||
|
String contentType = resolveContentType(outputFormat);
|
||||||
|
Date expireAt = new Date(System.currentTimeMillis() + UPLOAD_URL_EXPIRE_MILLIS);
|
||||||
|
|
||||||
|
IStorageAdapter storage = StorageFactory.use();
|
||||||
|
|
||||||
|
PuzzleEdgeUploadUrlsResponse resp = new PuzzleEdgeUploadUrlsResponse();
|
||||||
|
resp.setExpireAt(expireAt);
|
||||||
|
|
||||||
|
Map<String, String> headers = Map.of("Content-Type", contentType);
|
||||||
|
|
||||||
|
PuzzleEdgeUploadUrlsResponse.UploadTarget original = new PuzzleEdgeUploadUrlsResponse.UploadTarget();
|
||||||
|
original.setMethod("PUT");
|
||||||
|
original.setHeaders(headers);
|
||||||
|
original.setPublicUrl(storage.getUrl(task.getOriginalObjectKey()));
|
||||||
|
original.setUrl(storage.getUrlForUpload(expireAt, contentType, task.getOriginalObjectKey()));
|
||||||
|
resp.setOriginal(original);
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(task.getCroppedObjectKey())) {
|
||||||
|
PuzzleEdgeUploadUrlsResponse.UploadTarget cropped = new PuzzleEdgeUploadUrlsResponse.UploadTarget();
|
||||||
|
cropped.setMethod("PUT");
|
||||||
|
cropped.setHeaders(headers);
|
||||||
|
cropped.setPublicUrl(storage.getUrl(task.getCroppedObjectKey()));
|
||||||
|
cropped.setUrl(storage.getUrlForUpload(expireAt, contentType, task.getCroppedObjectKey()));
|
||||||
|
resp.setCropped(cropped);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PuzzleEdgeRenderTaskEntity claimOne(Long workerId) {
|
||||||
|
synchronized (taskLock) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Long selectedTaskId = null;
|
||||||
|
for (Map.Entry<Long, PuzzleEdgeRenderTaskEntity> entry : taskCache.asMap().entrySet()) {
|
||||||
|
PuzzleEdgeRenderTaskEntity task = entry.getValue();
|
||||||
|
if (!isClaimable(task, now)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long id = entry.getKey();
|
||||||
|
if (selectedTaskId == null || id < selectedTaskId) {
|
||||||
|
selectedTaskId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTaskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PuzzleEdgeRenderTaskEntity task = taskCache.getIfPresent(selectedTaskId);
|
||||||
|
if (!isClaimable(task, now)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date leaseExpireTime = new Date(now + LEASE_MILLIS);
|
||||||
|
task.setWorkerId(workerId);
|
||||||
|
task.setStatus(STATUS_RUNNING);
|
||||||
|
task.setLeaseExpireTime(leaseExpireTime);
|
||||||
|
task.setAttemptCount((task.getAttemptCount() != null ? task.getAttemptCount() : 0) + 1);
|
||||||
|
task.setUpdateTime(new Date(now));
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PuzzleEdgeRenderTaskEntity getAndCheckRunningTask(Long taskId, Long workerId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
throw new IllegalArgumentException("taskId不能为空");
|
||||||
|
}
|
||||||
|
synchronized (taskLock) {
|
||||||
|
PuzzleEdgeRenderTaskEntity task = taskCache.getIfPresent(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
throw new IllegalArgumentException("任务不存在");
|
||||||
|
}
|
||||||
|
if (task.getStatus() == null || task.getStatus() != STATUS_RUNNING) {
|
||||||
|
throw new IllegalArgumentException("任务状态非法");
|
||||||
|
}
|
||||||
|
if (task.getWorkerId() == null || !task.getWorkerId().equals(workerId)) {
|
||||||
|
throw new IllegalArgumentException("任务不属于当前worker");
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PuzzleEdgeRenderTaskEntity getAndCheckTaskOwned(Long taskId, Long workerId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
throw new IllegalArgumentException("taskId不能为空");
|
||||||
|
}
|
||||||
|
synchronized (taskLock) {
|
||||||
|
PuzzleEdgeRenderTaskEntity task = taskCache.getIfPresent(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
throw new IllegalArgumentException("任务不存在");
|
||||||
|
}
|
||||||
|
if (task.getWorkerId() == null || !task.getWorkerId().equals(workerId)) {
|
||||||
|
throw new IllegalArgumentException("任务不属于当前worker");
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryMarkSuccess(PuzzleEdgeRenderTaskEntity task, Long workerId) {
|
||||||
|
synchronized (taskLock) {
|
||||||
|
if (task == null || task.getId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.getStatus() == null || task.getStatus() != STATUS_RUNNING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.getWorkerId() == null || !task.getWorkerId().equals(workerId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.setStatus(STATUS_SUCCESS);
|
||||||
|
task.setLeaseExpireTime(null);
|
||||||
|
task.setErrorMessage(null);
|
||||||
|
task.setUpdateTime(new Date());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryMarkFail(PuzzleEdgeRenderTaskEntity task, Long workerId, String errorMessage) {
|
||||||
|
synchronized (taskLock) {
|
||||||
|
if (task == null || task.getId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.getStatus() == null || task.getStatus() != STATUS_RUNNING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.getWorkerId() == null || !task.getWorkerId().equals(workerId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.setStatus(STATUS_FAIL);
|
||||||
|
task.setLeaseExpireTime(null);
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
task.setUpdateTime(new Date());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isClaimable(PuzzleEdgeRenderTaskEntity task, long nowMillis) {
|
||||||
|
if (task == null || task.getId() == null || task.getStatus() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.getStatus() == STATUS_PENDING) {
|
||||||
|
int attemptCount = task.getAttemptCount() != null ? task.getAttemptCount() : 0;
|
||||||
|
return attemptCount < MAX_RETRY_ATTEMPTS;
|
||||||
|
}
|
||||||
|
if (task.getStatus() != STATUS_RUNNING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date leaseExpireTime = task.getLeaseExpireTime();
|
||||||
|
if (leaseExpireTime == null || leaseExpireTime.getTime() >= nowMillis) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int attemptCount = task.getAttemptCount() != null ? task.getAttemptCount() : 0;
|
||||||
|
return attemptCount < MAX_RETRY_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderWorkerEntity requireWorker(String accessKey) {
|
||||||
|
if (StrUtil.isBlank(accessKey)) {
|
||||||
|
throw new IllegalArgumentException("accessKey不能为空");
|
||||||
|
}
|
||||||
|
RenderWorkerEntity worker = renderWorkerRepository.getWorkerByAccessKey(accessKey);
|
||||||
|
if (worker == null) {
|
||||||
|
throw new IllegalArgumentException("worker不存在");
|
||||||
|
}
|
||||||
|
if (worker.getStatus() == null || worker.getStatus() != 1) {
|
||||||
|
throw new IllegalArgumentException("worker未启用");
|
||||||
|
}
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOutputFormat(String format) {
|
||||||
|
String outputFormat = StrUtil.isNotBlank(format) ? format.toUpperCase() : "PNG";
|
||||||
|
if ("JPG".equals(outputFormat)) {
|
||||||
|
outputFormat = "JPEG";
|
||||||
|
}
|
||||||
|
if (!"PNG".equals(outputFormat) && !"JPEG".equals(outputFormat)) {
|
||||||
|
outputFormat = "PNG";
|
||||||
|
}
|
||||||
|
return outputFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveContentType(String outputFormat) {
|
||||||
|
return "PNG".equals(outputFormat) ? "image/png" : "image/jpeg";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
package com.ycwl.basic.puzzle.repository;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||||
|
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||||
|
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||||
|
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||||
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼图模块缓存仓库
|
||||||
|
* 提供模板和元素的缓存读取,减少数据库查询
|
||||||
|
*
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Repository
|
||||||
|
public class PuzzleRepository {
|
||||||
|
|
||||||
|
private final PuzzleTemplateMapper templateMapper;
|
||||||
|
private final PuzzleElementMapper elementMapper;
|
||||||
|
private final RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板缓存KEY(根据code)
|
||||||
|
*/
|
||||||
|
private static final String PUZZLE_TEMPLATE_BY_CODE_KEY = "puzzle:template:code:%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板缓存KEY(根据id)
|
||||||
|
*/
|
||||||
|
private static final String PUZZLE_TEMPLATE_BY_ID_KEY = "puzzle:template:id:%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元素列表缓存KEY(根据templateId)
|
||||||
|
*/
|
||||||
|
private static final String PUZZLE_ELEMENTS_BY_TEMPLATE_KEY = "puzzle:elements:templateId:%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存过期时间(小时)
|
||||||
|
*/
|
||||||
|
private static final long CACHE_EXPIRE_HOURS = 24;
|
||||||
|
|
||||||
|
public PuzzleRepository(
|
||||||
|
PuzzleTemplateMapper templateMapper,
|
||||||
|
PuzzleElementMapper elementMapper,
|
||||||
|
RedisTemplate<String, String> redisTemplate) {
|
||||||
|
this.templateMapper = templateMapper;
|
||||||
|
this.elementMapper = elementMapper;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模板缓存 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模板编码获取模板(优先从缓存读取)
|
||||||
|
*
|
||||||
|
* @param code 模板编码
|
||||||
|
* @return 模板实体,不存在返回null
|
||||||
|
*/
|
||||||
|
public PuzzleTemplateEntity getTemplateByCode(String code) {
|
||||||
|
String cacheKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code);
|
||||||
|
|
||||||
|
// 1. 尝试从缓存读取
|
||||||
|
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||||
|
if (Boolean.TRUE.equals(hasKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
if (cacheValue != null) {
|
||||||
|
log.debug("从缓存读取模板: code={}", code);
|
||||||
|
return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从数据库查询
|
||||||
|
PuzzleTemplateEntity template = templateMapper.getByCode(code);
|
||||||
|
if (template == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入缓存
|
||||||
|
cacheTemplate(template);
|
||||||
|
log.debug("模板缓存写入: code={}, id={}", code, template.getId());
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模板ID获取模板(优先从缓存读取)
|
||||||
|
*
|
||||||
|
* @param id 模板ID
|
||||||
|
* @return 模板实体,不存在返回null
|
||||||
|
*/
|
||||||
|
public PuzzleTemplateEntity getTemplateById(Long id) {
|
||||||
|
String cacheKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||||
|
|
||||||
|
// 1. 尝试从缓存读取
|
||||||
|
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||||
|
if (Boolean.TRUE.equals(hasKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
if (cacheValue != null) {
|
||||||
|
log.debug("从缓存读取模板: id={}", id);
|
||||||
|
return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从数据库查询
|
||||||
|
PuzzleTemplateEntity template = templateMapper.getById(id);
|
||||||
|
if (template == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入缓存
|
||||||
|
cacheTemplate(template);
|
||||||
|
log.debug("模板缓存写入: id={}, code={}", id, template.getCode());
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存模板(同时缓存 byId 和 byCode)
|
||||||
|
*/
|
||||||
|
private void cacheTemplate(PuzzleTemplateEntity template) {
|
||||||
|
if (template == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String json = JacksonUtil.toJSONString(template);
|
||||||
|
|
||||||
|
// 同时缓存 byId 和 byCode
|
||||||
|
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, template.getId());
|
||||||
|
String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, template.getCode());
|
||||||
|
|
||||||
|
redisTemplate.opsForValue().set(idKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||||
|
redisTemplate.opsForValue().set(codeKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除模板缓存
|
||||||
|
*
|
||||||
|
* @param id 模板ID
|
||||||
|
* @param code 模板编码(可为null,此时需要先查询获取)
|
||||||
|
*/
|
||||||
|
public void clearTemplateCache(Long id, String code) {
|
||||||
|
// 如果没有传code,尝试从缓存或数据库获取
|
||||||
|
if (code == null && id != null) {
|
||||||
|
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(idKey);
|
||||||
|
if (cacheValue != null) {
|
||||||
|
PuzzleTemplateEntity template = JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
|
||||||
|
code = template.getCode();
|
||||||
|
} else {
|
||||||
|
PuzzleTemplateEntity template = templateMapper.getById(id);
|
||||||
|
if (template != null) {
|
||||||
|
code = template.getCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 byId 缓存
|
||||||
|
if (id != null) {
|
||||||
|
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
|
||||||
|
redisTemplate.delete(idKey);
|
||||||
|
log.debug("清除模板缓存: id={}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 byCode 缓存
|
||||||
|
if (code != null) {
|
||||||
|
String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code);
|
||||||
|
redisTemplate.delete(codeKey);
|
||||||
|
log.debug("清除模板缓存: code={}", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时清除该模板的元素缓存
|
||||||
|
if (id != null) {
|
||||||
|
clearElementsCache(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 元素缓存 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模板ID获取元素列表(优先从缓存读取)
|
||||||
|
*
|
||||||
|
* @param templateId 模板ID
|
||||||
|
* @return 元素列表
|
||||||
|
*/
|
||||||
|
public List<PuzzleElementEntity> getElementsByTemplateId(Long templateId) {
|
||||||
|
String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId);
|
||||||
|
|
||||||
|
// 1. 尝试从缓存读取
|
||||||
|
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||||
|
if (Boolean.TRUE.equals(hasKey)) {
|
||||||
|
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
if (cacheValue != null) {
|
||||||
|
log.debug("从缓存读取元素列表: templateId={}", templateId);
|
||||||
|
return JacksonUtil.parseObject(cacheValue, new TypeReference<List<PuzzleElementEntity>>() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从数据库查询
|
||||||
|
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(templateId);
|
||||||
|
|
||||||
|
// 3. 写入缓存(即使是空列表也要缓存,避免缓存穿透)
|
||||||
|
String json = JacksonUtil.toJSONString(elements);
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||||
|
log.debug("元素列表缓存写入: templateId={}, count={}", templateId, elements.size());
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除元素缓存
|
||||||
|
*
|
||||||
|
* @param templateId 模板ID
|
||||||
|
*/
|
||||||
|
public void clearElementsCache(Long templateId) {
|
||||||
|
String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId);
|
||||||
|
redisTemplate.delete(cacheKey);
|
||||||
|
log.debug("清除元素缓存: templateId={}", templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 批量清除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有拼图相关缓存(慎用)
|
||||||
|
* 使用 SCAN 命令避免 KEYS 阻塞
|
||||||
|
*/
|
||||||
|
public void clearAllPuzzleCache() {
|
||||||
|
log.warn("开始清除所有拼图缓存...");
|
||||||
|
|
||||||
|
// 使用 SCAN 删除模板缓存
|
||||||
|
deleteByPattern("puzzle:template:*");
|
||||||
|
// 使用 SCAN 删除元素缓存
|
||||||
|
deleteByPattern("puzzle:elements:*");
|
||||||
|
|
||||||
|
log.warn("拼图缓存清除完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模式删除缓存
|
||||||
|
* 使用 SCAN 命令避免阻塞
|
||||||
|
*/
|
||||||
|
private void deleteByPattern(String pattern) {
|
||||||
|
try {
|
||||||
|
var keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
log.debug("删除缓存: pattern={}, count={}", pattern, keys.size());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除缓存失败: pattern={}", pattern, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,29 @@ import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
|||||||
public interface IPuzzleGenerateService {
|
public interface IPuzzleGenerateService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成拼图图片
|
* 生成拼图图片(默认同步模式)
|
||||||
*
|
*
|
||||||
* @param request 生成请求
|
* @param request 生成请求
|
||||||
* @return 生成结果(包含图片URL等信息)
|
* @return 生成结果(包含图片URL等信息)
|
||||||
*/
|
*/
|
||||||
PuzzleGenerateResponse generate(PuzzleGenerateRequest request);
|
PuzzleGenerateResponse generate(PuzzleGenerateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步生成拼图图片
|
||||||
|
* <p>立即执行并阻塞等待结果返回</p>
|
||||||
|
*
|
||||||
|
* @param request 生成请求
|
||||||
|
* @return 生成结果(包含图片URL等信息)
|
||||||
|
*/
|
||||||
|
PuzzleGenerateResponse generateSync(PuzzleGenerateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步生成拼图图片
|
||||||
|
* <p>提交到队列,由固定大小的线程池异步处理,不等待结果</p>
|
||||||
|
* <p>队列满时会降级为同步执行(CallerRunsPolicy)</p>
|
||||||
|
*
|
||||||
|
* @param request 生成请求
|
||||||
|
* @return 生成记录ID(可用于后续追踪状态)
|
||||||
|
*/
|
||||||
|
Long generateAsync(PuzzleGenerateRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import cn.hutool.json.JSONUtil;
|
|||||||
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
|
||||||
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
|
||||||
|
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
|
||||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||||
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
|
||||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||||
import com.ycwl.basic.puzzle.fill.FillResult;
|
import com.ycwl.basic.puzzle.fill.FillResult;
|
||||||
import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine;
|
import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine;
|
||||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
|
||||||
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
|
||||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||||
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
|
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;
|
||||||
@@ -20,7 +20,6 @@ import com.ycwl.basic.repository.ScenicRepository;
|
|||||||
import com.ycwl.basic.service.printer.PrinterService;
|
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.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -47,36 +46,233 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
||||||
|
|
||||||
private final PuzzleTemplateMapper templateMapper;
|
private final PuzzleRepository puzzleRepository;
|
||||||
private final PuzzleElementMapper elementMapper;
|
|
||||||
private final PuzzleGenerationRecordMapper recordMapper;
|
private final PuzzleGenerationRecordMapper recordMapper;
|
||||||
@Lazy
|
|
||||||
private final PuzzleImageRenderer imageRenderer;
|
private final PuzzleImageRenderer imageRenderer;
|
||||||
@Lazy
|
|
||||||
private final PuzzleElementFillEngine fillEngine;
|
private final PuzzleElementFillEngine fillEngine;
|
||||||
@Lazy
|
|
||||||
private final ScenicRepository scenicRepository;
|
private final ScenicRepository scenicRepository;
|
||||||
@Lazy
|
|
||||||
private final PuzzleDuplicationDetector duplicationDetector;
|
private final PuzzleDuplicationDetector duplicationDetector;
|
||||||
@Lazy
|
|
||||||
private final PrinterService printerService;
|
private final PrinterService printerService;
|
||||||
|
private final PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService;
|
||||||
|
|
||||||
|
public PuzzleGenerateServiceImpl(
|
||||||
|
PuzzleRepository puzzleRepository,
|
||||||
|
PuzzleGenerationRecordMapper recordMapper,
|
||||||
|
@Lazy PuzzleImageRenderer imageRenderer,
|
||||||
|
@Lazy PuzzleElementFillEngine fillEngine,
|
||||||
|
@Lazy ScenicRepository scenicRepository,
|
||||||
|
@Lazy PuzzleDuplicationDetector duplicationDetector,
|
||||||
|
@Lazy PrinterService printerService,
|
||||||
|
PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService) {
|
||||||
|
this.puzzleRepository = puzzleRepository;
|
||||||
|
this.recordMapper = recordMapper;
|
||||||
|
this.imageRenderer = imageRenderer;
|
||||||
|
this.fillEngine = fillEngine;
|
||||||
|
this.scenicRepository = scenicRepository;
|
||||||
|
this.duplicationDetector = duplicationDetector;
|
||||||
|
this.printerService = printerService;
|
||||||
|
this.puzzleEdgeRenderTaskService = puzzleEdgeRenderTaskService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
public PuzzleGenerateResponse generate(PuzzleGenerateRequest request) {
|
||||||
|
// 默认使用同步模式
|
||||||
|
return generateSync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PuzzleGenerateResponse generateSync(PuzzleGenerateRequest request) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
log.info("开始同步生成拼图(边缘渲染模式): templateCode={}, userId={}, faceId={}",
|
||||||
|
request.getTemplateCode(), request.getUserId(), request.getFaceId());
|
||||||
|
|
||||||
|
// 1. 参数校验
|
||||||
|
validateRequest(request);
|
||||||
|
|
||||||
|
// 2. 查询模板(使用缓存)
|
||||||
|
PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode());
|
||||||
|
if (template == null) {
|
||||||
|
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
if (template.getStatus() != 1) {
|
||||||
|
throw new IllegalArgumentException("模板已禁用: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
|
||||||
|
|
||||||
|
// 3. 查询并排序元素
|
||||||
|
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||||
|
if (elements.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
|
||||||
|
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||||
|
|
||||||
|
// 4. 构建dynamicData
|
||||||
|
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
|
||||||
|
|
||||||
|
// 5. 重复图片检测(可能抛出DuplicateImageException)
|
||||||
|
duplicationDetector.detectDuplicateImages(finalDynamicData, elements);
|
||||||
|
|
||||||
|
// 6. 内容去重检测
|
||||||
|
String contentHash = duplicationDetector.calculateContentHash(finalDynamicData);
|
||||||
|
PuzzleGenerationRecordEntity duplicateRecord = duplicationDetector.findDuplicateRecord(
|
||||||
|
template.getId(), contentHash, resolvedScenicId
|
||||||
|
);
|
||||||
|
if (duplicateRecord != null) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
|
||||||
|
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
|
||||||
|
return PuzzleGenerateResponse.success(
|
||||||
|
duplicateRecord.getResultImageUrl(),
|
||||||
|
duplicateRecord.getResultFileSize(),
|
||||||
|
duplicateRecord.getResultWidth(),
|
||||||
|
duplicateRecord.getResultHeight(),
|
||||||
|
(int) duration,
|
||||||
|
duplicateRecord.getId(),
|
||||||
|
true,
|
||||||
|
duplicateRecord.getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 创建生成记录
|
||||||
|
PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
|
||||||
|
record.setContentHash(contentHash);
|
||||||
|
recordMapper.insert(record);
|
||||||
|
|
||||||
|
// 8. 创建边缘渲染任务并等待完成
|
||||||
|
Long taskId = puzzleEdgeRenderTaskService.createRenderTask(
|
||||||
|
record,
|
||||||
|
template,
|
||||||
|
elements,
|
||||||
|
finalDynamicData,
|
||||||
|
request.getOutputFormat(),
|
||||||
|
request.getQuality()
|
||||||
|
);
|
||||||
|
puzzleEdgeRenderTaskService.registerWait(taskId);
|
||||||
|
|
||||||
|
log.info("同步拼图任务已提交边缘渲染,等待完成: recordId={}, taskId={}", record.getId(), taskId);
|
||||||
|
|
||||||
|
// 9. 等待任务完成(30秒超时)
|
||||||
|
PuzzleEdgeRenderTaskService.TaskWaitResult waitResult =
|
||||||
|
puzzleEdgeRenderTaskService.waitForTask(taskId, 30_000);
|
||||||
|
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
if (waitResult.isSuccess()) {
|
||||||
|
log.info("同步拼图边缘渲染完成: recordId={}, taskId={}, imageUrl={}, duration={}ms",
|
||||||
|
record.getId(), taskId, waitResult.getImageUrl(), duration);
|
||||||
|
|
||||||
|
// 重新查询记录获取完整信息(边缘渲染回调已更新)
|
||||||
|
PuzzleGenerationRecordEntity updatedRecord = recordMapper.getById(record.getId());
|
||||||
|
if (updatedRecord != null && updatedRecord.getResultImageUrl() != null) {
|
||||||
|
return PuzzleGenerateResponse.success(
|
||||||
|
updatedRecord.getResultImageUrl(),
|
||||||
|
updatedRecord.getResultFileSize(),
|
||||||
|
updatedRecord.getResultWidth(),
|
||||||
|
updatedRecord.getResultHeight(),
|
||||||
|
(int) duration,
|
||||||
|
updatedRecord.getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回调可能还未完全写入,使用等待结果中的URL
|
||||||
|
return PuzzleGenerateResponse.success(
|
||||||
|
waitResult.getImageUrl(),
|
||||||
|
null,
|
||||||
|
template.getCanvasWidth(),
|
||||||
|
template.getCanvasHeight(),
|
||||||
|
(int) duration,
|
||||||
|
record.getId()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.error("同步拼图边缘渲染失败: recordId={}, taskId={}, error={}, duration={}ms",
|
||||||
|
record.getId(), taskId, waitResult.getErrorMessage(), duration);
|
||||||
|
throw new RuntimeException("拼图生成失败: " + waitResult.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long generateAsync(PuzzleGenerateRequest request) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
log.info("开始创建异步拼图边缘渲染任务: templateCode={}, userId={}, faceId={}",
|
||||||
|
request.getTemplateCode(), request.getUserId(), request.getFaceId());
|
||||||
|
|
||||||
|
// 1. 参数校验
|
||||||
|
validateRequest(request);
|
||||||
|
|
||||||
|
// 2. 查询模板(使用缓存)
|
||||||
|
PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode());
|
||||||
|
if (template == null) {
|
||||||
|
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
if (template.getStatus() != 1) {
|
||||||
|
throw new IllegalArgumentException("模板已禁用: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
|
||||||
|
|
||||||
|
// 3. 查询并排序元素
|
||||||
|
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||||
|
if (elements.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
|
||||||
|
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||||
|
|
||||||
|
// 4. 构建dynamicData
|
||||||
|
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
|
||||||
|
|
||||||
|
// 5. 重复图片检测(可能抛出DuplicateImageException)
|
||||||
|
duplicationDetector.detectDuplicateImages(finalDynamicData, elements);
|
||||||
|
|
||||||
|
// 6. 内容去重检测
|
||||||
|
String contentHash = duplicationDetector.calculateContentHash(finalDynamicData);
|
||||||
|
PuzzleGenerationRecordEntity duplicateRecord = duplicationDetector.findDuplicateRecord(
|
||||||
|
template.getId(), contentHash, resolvedScenicId
|
||||||
|
);
|
||||||
|
if (duplicateRecord != null) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
|
||||||
|
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
|
||||||
|
return duplicateRecord.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 创建生成记录
|
||||||
|
PuzzleGenerationRecordEntity record = createRecord(template, request, resolvedScenicId);
|
||||||
|
record.setContentHash(contentHash);
|
||||||
|
recordMapper.insert(record);
|
||||||
|
|
||||||
|
// 8. 创建边缘渲染任务
|
||||||
|
Long taskId = puzzleEdgeRenderTaskService.createRenderTask(
|
||||||
|
record,
|
||||||
|
template,
|
||||||
|
elements,
|
||||||
|
finalDynamicData,
|
||||||
|
request.getOutputFormat(),
|
||||||
|
request.getQuality()
|
||||||
|
);
|
||||||
|
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("异步拼图任务已进入边缘渲染队列: recordId={}, taskId={}, templateCode={}, duration={}ms",
|
||||||
|
record.getId(), taskId, request.getTemplateCode(), duration);
|
||||||
|
|
||||||
|
return record.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心生成逻辑(同步执行)
|
||||||
|
*/
|
||||||
|
private PuzzleGenerateResponse doGenerate(PuzzleGenerateRequest request) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
log.info("开始生成拼图: templateCode={}, userId={}, faceId={}",
|
log.info("开始生成拼图: templateCode={}, userId={}, faceId={}",
|
||||||
request.getTemplateCode(), request.getUserId(), request.getFaceId());
|
request.getTemplateCode(), request.getUserId(), request.getFaceId());
|
||||||
|
|
||||||
// 业务层校验:faceId 必填
|
// 参数校验
|
||||||
if (request.getFaceId() == null) {
|
validateRequest(request);
|
||||||
throw new IllegalArgumentException("人脸ID不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 查询模板和元素
|
// 1. 查询模板和元素(使用缓存)
|
||||||
PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode());
|
PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode());
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode());
|
||||||
}
|
}
|
||||||
@@ -88,7 +284,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
// 2. 校验景区隔离
|
// 2. 校验景区隔离
|
||||||
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
|
Long resolvedScenicId = resolveScenicId(template, request.getScenicId());
|
||||||
|
|
||||||
List<PuzzleElementEntity> elements = elementMapper.getByTemplateId(template.getId());
|
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||||
if (elements.isEmpty()) {
|
if (elements.isEmpty()) {
|
||||||
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||||
}
|
}
|
||||||
@@ -135,16 +331,66 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
record.setContentHash(contentHash);
|
record.setContentHash(contentHash);
|
||||||
recordMapper.insert(record);
|
recordMapper.insert(record);
|
||||||
|
|
||||||
|
// 9. 执行核心生成逻辑
|
||||||
|
return doGenerateInternal(request, template, resolvedScenicId, record, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验请求参数
|
||||||
|
*/
|
||||||
|
private void validateRequest(PuzzleGenerateRequest request) {
|
||||||
|
if (request.getFaceId() == null) {
|
||||||
|
throw new IllegalArgumentException("人脸ID不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心生成逻辑(内部方法,同步/异步共用)
|
||||||
|
* 注意:此方法会在调用线程中执行渲染和上传操作
|
||||||
|
*
|
||||||
|
* @param request 生成请求
|
||||||
|
* @param template 模板
|
||||||
|
* @param resolvedScenicId 景区ID
|
||||||
|
* @param record 生成记录(已插入数据库)
|
||||||
|
* @return 生成结果(异步模式下不关心返回值)
|
||||||
|
*/
|
||||||
|
private PuzzleGenerateResponse doGenerateInternal(PuzzleGenerateRequest request,
|
||||||
|
PuzzleTemplateEntity template,
|
||||||
|
Long resolvedScenicId,
|
||||||
|
PuzzleGenerationRecordEntity record) {
|
||||||
|
return doGenerateInternal(request, template, resolvedScenicId, record, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心生成逻辑(内部方法,同步/异步共用)
|
||||||
|
*/
|
||||||
|
private PuzzleGenerateResponse doGenerateInternal(PuzzleGenerateRequest request,
|
||||||
|
PuzzleTemplateEntity template,
|
||||||
|
Long resolvedScenicId,
|
||||||
|
PuzzleGenerationRecordEntity record,
|
||||||
|
long startTime) {
|
||||||
|
List<PuzzleElementEntity> elements = puzzleRepository.getElementsByTemplateId(template.getId());
|
||||||
|
if (elements.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按z-index排序元素
|
||||||
|
elements.sort(Comparator.comparing(PuzzleElementEntity::getZIndex,
|
||||||
|
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||||
|
|
||||||
|
// 准备dynamicData
|
||||||
|
Map<String, String> finalDynamicData = buildDynamicData(template, request, resolvedScenicId, elements);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 9. 渲染图片
|
// 渲染图片
|
||||||
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
|
BufferedImage resultImage = imageRenderer.render(template, elements, finalDynamicData);
|
||||||
|
|
||||||
// 10. 上传原图到OSS(未裁切)
|
// 上传原图到OSS(未裁切)
|
||||||
String originalImageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
String originalImageUrl = uploadImage(resultImage, template.getCode(), request.getOutputFormat(), request.getQuality());
|
||||||
log.info("原图上传成功: url={}", originalImageUrl);
|
log.info("原图上传成功: url={}", originalImageUrl);
|
||||||
|
|
||||||
// 11. 处理用户区域裁切
|
// 处理用户区域裁切
|
||||||
String finalImageUrl = originalImageUrl; // 默认使用原图
|
String finalImageUrl = originalImageUrl;
|
||||||
BufferedImage finalImage = resultImage;
|
BufferedImage finalImage = resultImage;
|
||||||
|
|
||||||
if (StrUtil.isNotBlank(template.getUserArea())) {
|
if (StrUtil.isNotBlank(template.getUserArea())) {
|
||||||
@@ -155,12 +401,11 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
log.info("裁切后图片上传成功: userArea={}, url={}", template.getUserArea(), finalImageUrl);
|
log.info("裁切后图片上传成功: userArea={}, url={}", template.getUserArea(), finalImageUrl);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("图片裁切失败,使用原图: userArea={}", template.getUserArea(), e);
|
log.error("图片裁切失败,使用原图: userArea={}", template.getUserArea(), e);
|
||||||
// 裁切失败时使用原图
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. 更新记录为成功
|
// 更新记录为成功
|
||||||
long duration = (int) (System.currentTimeMillis() - startTime);
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
long fileSize = estimateFileSize(finalImage, request.getOutputFormat());
|
long fileSize = estimateFileSize(finalImage, request.getOutputFormat());
|
||||||
recordMapper.updateSuccess(
|
recordMapper.updateSuccess(
|
||||||
record.getId(),
|
record.getId(),
|
||||||
@@ -172,23 +417,22 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
(int) duration
|
(int) duration
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info("拼图生成成功(新生成): recordId={}, originalUrl={}, finalUrl={}, duration={}ms",
|
log.info("拼图生成成功: recordId={}, originalUrl={}, finalUrl={}, duration={}ms",
|
||||||
record.getId(), originalImageUrl, finalImageUrl, duration);
|
record.getId(), originalImageUrl, finalImageUrl, duration);
|
||||||
|
|
||||||
// 13. 检查是否自动添加到打印队列
|
// 检查是否自动添加到打印队列
|
||||||
if (template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
|
if (template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
|
||||||
try {
|
try {
|
||||||
Integer printRecordId = printerService.addUserPhotoFromPuzzle(
|
Integer printRecordId = printerService.addUserPhotoFromPuzzle(
|
||||||
request.getUserId(),
|
request.getUserId(),
|
||||||
resolvedScenicId,
|
resolvedScenicId,
|
||||||
request.getFaceId(),
|
request.getFaceId(),
|
||||||
originalImageUrl, // 使用原图URL添加到打印队列
|
originalImageUrl,
|
||||||
record.getId()
|
record.getId()
|
||||||
);
|
);
|
||||||
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
|
log.info("自动添加到打印队列成功: recordId={}, printRecordId={}", record.getId(), printRecordId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
|
log.error("自动添加到打印队列失败: recordId={}", record.getId(), e);
|
||||||
// 添加失败不影响拼图生成流程
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,13 +443,12 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
|
|||||||
finalImage.getHeight(),
|
finalImage.getHeight(),
|
||||||
(int) duration,
|
(int) duration,
|
||||||
record.getId(),
|
record.getId(),
|
||||||
false, // isDuplicate=false
|
false,
|
||||||
null // originalRecordId=null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("拼图生成失败: templateCode={}", request.getTemplateCode(), e);
|
log.error("拼图生成失败: templateCode={}", request.getTemplateCode(), e);
|
||||||
// 更新记录为失败
|
|
||||||
recordMapper.updateFail(record.getId(), e.getMessage());
|
recordMapper.updateFail(record.getId(), e.getMessage());
|
||||||
throw new RuntimeException("图片生成失败: " + e.getMessage(), e);
|
throw new RuntimeException("图片生成失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
|||||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||||
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper;
|
||||||
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper;
|
||||||
|
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
|
||||||
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -37,6 +38,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
|
|
||||||
private final PuzzleTemplateMapper templateMapper;
|
private final PuzzleTemplateMapper templateMapper;
|
||||||
private final PuzzleElementMapper elementMapper;
|
private final PuzzleElementMapper elementMapper;
|
||||||
|
private final PuzzleRepository puzzleRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -70,6 +72,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果修改了编码,检查新编码是否已存在
|
// 如果修改了编码,检查新编码是否已存在
|
||||||
|
String oldCode = existing.getCode();
|
||||||
if (request.getCode() != null && !request.getCode().equals(existing.getCode())) {
|
if (request.getCode() != null && !request.getCode().equals(existing.getCode())) {
|
||||||
int count = templateMapper.countByCode(request.getCode(), id);
|
int count = templateMapper.countByCode(request.getCode(), id);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
@@ -82,6 +85,12 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
templateMapper.update(entity);
|
templateMapper.update(entity);
|
||||||
|
|
||||||
|
// 清除缓存(如果修改了code,需要同时清除新旧code的缓存)
|
||||||
|
puzzleRepository.clearTemplateCache(id, oldCode);
|
||||||
|
if (request.getCode() != null && !request.getCode().equals(oldCode)) {
|
||||||
|
puzzleRepository.clearTemplateCache(null, request.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
log.info("拼图模板更新成功: id={}", id);
|
log.info("拼图模板更新成功: id={}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +109,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
templateMapper.deleteById(id);
|
templateMapper.deleteById(id);
|
||||||
elementMapper.deleteByTemplateId(id);
|
elementMapper.deleteByTemplateId(id);
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
puzzleRepository.clearTemplateCache(id, existing.getCode());
|
||||||
|
|
||||||
log.info("拼图模板删除成功: id={}, 同时删除了关联的元素", id);
|
log.info("拼图模板删除成功: id={}, 同时删除了关联的元素", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +208,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
// 4. 插入数据库
|
// 4. 插入数据库
|
||||||
elementMapper.insert(entity);
|
elementMapper.insert(entity);
|
||||||
|
|
||||||
|
// 5. 清除元素缓存
|
||||||
|
puzzleRepository.clearElementsCache(request.getTemplateId());
|
||||||
|
|
||||||
log.info("元素添加成功: id={}, type={}, key={}",
|
log.info("元素添加成功: id={}, type={}, key={}",
|
||||||
entity.getId(), entity.getElementType(), entity.getElementKey());
|
entity.getId(), entity.getElementType(), entity.getElementKey());
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
@@ -225,6 +240,8 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
// 3. 批量插入
|
// 3. 批量插入
|
||||||
if (!entityList.isEmpty()) {
|
if (!entityList.isEmpty()) {
|
||||||
elementMapper.batchInsert(entityList);
|
elementMapper.batchInsert(entityList);
|
||||||
|
// 4. 清除元素缓存
|
||||||
|
puzzleRepository.clearElementsCache(templateId);
|
||||||
log.info("批量添加元素成功: templateId={}, count={}", templateId, entityList.size());
|
log.info("批量添加元素成功: templateId={}, count={}", templateId, entityList.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,6 +310,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
|
|
||||||
log.info("批量替换元素完成: templateId={}, deleted={}, updated={}, inserted={}",
|
log.info("批量替换元素完成: templateId={}, deleted={}, updated={}, inserted={}",
|
||||||
templateId, deletedCount, updatedCount, insertedCount);
|
templateId, deletedCount, updatedCount, insertedCount);
|
||||||
|
|
||||||
|
// 7. 清除元素缓存
|
||||||
|
puzzleRepository.clearElementsCache(templateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -314,6 +334,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
elementMapper.update(entity);
|
elementMapper.update(entity);
|
||||||
|
|
||||||
|
// 4. 清除元素缓存
|
||||||
|
puzzleRepository.clearElementsCache(existing.getTemplateId());
|
||||||
|
|
||||||
log.info("元素更新成功: id={}", id);
|
log.info("元素更新成功: id={}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +352,10 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elementMapper.deleteById(id);
|
elementMapper.deleteById(id);
|
||||||
|
|
||||||
|
// 清除元素缓存
|
||||||
|
puzzleRepository.clearElementsCache(existing.getTemplateId());
|
||||||
|
|
||||||
log.info("元素删除成功: id={}", id);
|
log.info("元素删除成功: id={}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,11 @@ import java.util.List;
|
|||||||
* @Date:2024/12/6 10:23
|
* @Date:2024/12/6 10:23
|
||||||
*/
|
*/
|
||||||
public interface AppScenicService {
|
public interface AppScenicService {
|
||||||
ApiResponse<PageInfo<ScenicEntity>> pageQuery(ScenicReqQuery scenicReqQuery);
|
|
||||||
|
|
||||||
ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(Long scenicId);
|
|
||||||
|
|
||||||
ApiResponse<ScenicRespVO> getDetails(Long id);
|
ApiResponse<ScenicRespVO> getDetails(Long id);
|
||||||
|
|
||||||
ApiResponse<ScenicLoginRespVO> login(ScenicLoginReq scenicLoginReq) throws Exception;
|
ApiResponse<ScenicLoginRespVO> login(ScenicLoginReq scenicLoginReq) throws Exception;
|
||||||
|
|
||||||
ApiResponse<ScenicRegisterRespVO> register(ScenicRegisterReq scenicRegisterReq);
|
ApiResponse<ScenicRegisterRespVO> register(ScenicRegisterReq scenicRegisterReq);
|
||||||
|
|
||||||
List<ScenicAppVO> scenicListByLnLa(ScenicIndexVO scenicIndexVO);
|
|
||||||
|
|
||||||
ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId);
|
ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,34 +69,6 @@ public class AppScenicServiceImpl implements AppScenicService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ScenicRepository scenicRepository;
|
private ScenicRepository scenicRepository;
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApiResponse<PageInfo<ScenicEntity>> pageQuery(ScenicReqQuery scenicReqQuery) {
|
|
||||||
|
|
||||||
ScenicReqQuery query = new ScenicReqQuery();
|
|
||||||
query.setPageSize(1000);
|
|
||||||
query.setStatus("1");
|
|
||||||
List<ScenicV2DTO> scenicList = scenicRepository.list(query);
|
|
||||||
List<ScenicEntity> list = scenicList.stream().map(scenic -> {
|
|
||||||
return scenicRepository.getScenic(Long.valueOf(scenic.getId()));
|
|
||||||
}).toList();
|
|
||||||
PageInfo<ScenicEntity> pageInfo = new PageInfo<>(list);
|
|
||||||
return ApiResponse.success(pageInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(Long scenicId) {
|
|
||||||
JwtInfo worker = JwtTokenUtil.getWorker();
|
|
||||||
// 通过zt-device服务获取设备统计
|
|
||||||
PageResponse<DeviceV2DTO> deviceListResponse = deviceIntegrationService.getScenicActiveDevices(scenicId, 1, 1000);
|
|
||||||
ScenicDeviceCountVO scenicDeviceCountVO = new ScenicDeviceCountVO();
|
|
||||||
if (deviceListResponse != null && deviceListResponse.getList() != null) {
|
|
||||||
scenicDeviceCountVO.setTotalDeviceCount(deviceListResponse.getList().size());
|
|
||||||
} else {
|
|
||||||
scenicDeviceCountVO.setTotalDeviceCount(0);
|
|
||||||
}
|
|
||||||
return ApiResponse.success(scenicDeviceCountVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<ScenicRespVO> getDetails(Long id) {
|
public ApiResponse<ScenicRespVO> getDetails(Long id) {
|
||||||
ScenicEntity scenic = scenicRepository.getScenic(id);
|
ScenicEntity scenic = scenicRepository.getScenic(id);
|
||||||
@@ -218,95 +190,6 @@ public class AppScenicServiceImpl implements AppScenicService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ScenicAppVO> scenicListByLnLa(ScenicIndexVO scenicIndexVO) {
|
|
||||||
// 参数校验
|
|
||||||
if (scenicIndexVO == null) {
|
|
||||||
log.warn("scenicListByLnLa 接收到空参数");
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
if (scenicIndexVO.getLatitude() == null || scenicIndexVO.getLongitude() == null) {
|
|
||||||
log.warn("scenicListByLnLa 缺少必要的经纬度参数, latitude={}, longitude={}",
|
|
||||||
scenicIndexVO.getLatitude(), scenicIndexVO.getLongitude());
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 scenicRepository 获取所有景区(1000个)
|
|
||||||
ScenicReqQuery query = new ScenicReqQuery();
|
|
||||||
query.setPageNum(1);
|
|
||||||
query.setPageSize(1000);
|
|
||||||
List<ScenicV2DTO> scenicList = scenicRepository.list(query);
|
|
||||||
|
|
||||||
if (scenicList == null || scenicList.isEmpty()) {
|
|
||||||
log.info("未查询到任何景区数据");
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ScenicAppVO> list = new ArrayList<>();
|
|
||||||
|
|
||||||
// 为每个景区获取详细信息(包含经纬度)
|
|
||||||
for (ScenicV2DTO scenicDTO : scenicList) {
|
|
||||||
try {
|
|
||||||
// ID 格式校验
|
|
||||||
if (StringUtils.isBlank(scenicDTO.getId())) {
|
|
||||||
log.warn("景区 ID 为空,跳过该景区");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取景区详细信息(包含经纬度)
|
|
||||||
ScenicEntity scenicEntity = scenicRepository.getScenic(Long.parseLong(scenicDTO.getId()));
|
|
||||||
if (scenicEntity == null) {
|
|
||||||
log.warn("景区详情查询失败, scenicId={}", scenicDTO.getId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scenicEntity.getLatitude() == null || scenicEntity.getLongitude() == null) {
|
|
||||||
log.warn("景区缺少经纬度信息, scenicId={}, scenicName={}",
|
|
||||||
scenicEntity.getId(), scenicEntity.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算距离
|
|
||||||
BigDecimal distance = calculateDistance(
|
|
||||||
scenicIndexVO.getLatitude(),
|
|
||||||
scenicIndexVO.getLongitude(),
|
|
||||||
scenicEntity.getLatitude(),
|
|
||||||
scenicEntity.getLongitude()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据距离和范围筛选景区
|
|
||||||
if (scenicEntity.getRadius() != null &&
|
|
||||||
distance.compareTo(scenicEntity.getRadius().multiply(BigDecimal.valueOf(1_000L))) < 0) {
|
|
||||||
|
|
||||||
// 转换为 ScenicAppVO
|
|
||||||
ScenicAppVO scenicAppVO = new ScenicAppVO();
|
|
||||||
scenicAppVO.setId(scenicEntity.getId());
|
|
||||||
scenicAppVO.setName(scenicEntity.getName());
|
|
||||||
scenicAppVO.setCoverUrl(scenicEntity.getCoverUrl());
|
|
||||||
scenicAppVO.setRadius(scenicEntity.getRadius());
|
|
||||||
scenicAppVO.setDistance(distance);
|
|
||||||
|
|
||||||
// 获取设备数量
|
|
||||||
List<DeviceV2DTO> devices = deviceRepository.getAllDeviceByScenicId(scenicEntity.getId());
|
|
||||||
scenicAppVO.setDeviceNum(devices != null ? devices.size() : 0);
|
|
||||||
|
|
||||||
list.add(scenicAppVO);
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.error("景区 ID 格式错误,无法转换为 Long 类型, scenicId={}, error={}",
|
|
||||||
scenicDTO.getId(), e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("处理景区信息时发生异常, scenicId={}, error={}",
|
|
||||||
scenicDTO != null ? scenicDTO.getId() : "unknown", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("根据经纬度筛选景区完成, 输入坐标=({}, {}), 符合条件的景区数量={}",
|
|
||||||
scenicIndexVO.getLatitude(), scenicIndexVO.getLongitude(), list.size());
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId) {
|
public ApiResponse<List<DeviceRespVO>> getDevices(Long scenicId) {
|
||||||
PageResponse<DeviceV2DTO> deviceV2ListResponse = deviceIntegrationService.listDevices(1, 1000, null, null, null, 1, scenicId, null);
|
PageResponse<DeviceV2DTO> deviceV2ListResponse = deviceIntegrationService.listDevices(1, 1000, null, null, null, 1, scenicId, null);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.service.mobile.impl;
|
|||||||
import cn.hutool.core.date.DateField;
|
import cn.hutool.core.date.DateField;
|
||||||
import cn.hutool.core.date.DateUnit;
|
import cn.hutool.core.date.DateUnit;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import com.ycwl.basic.enums.StatisticEnum;
|
import com.ycwl.basic.enums.StatisticEnum;
|
||||||
import com.ycwl.basic.mapper.StatisticsMapper;
|
import com.ycwl.basic.mapper.StatisticsMapper;
|
||||||
@@ -41,6 +42,9 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StatisticsMapper statisticsMapper;
|
private StatisticsMapper statisticsMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StatsQueryService statsQueryService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付订单金额、预览_支付转化率、扫码_付费用户转化率
|
* 支付订单金额、预览_支付转化率、扫码_付费用户转化率
|
||||||
@@ -210,19 +214,19 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
|||||||
// Integer cameraShotOfMemberNum=statisticsMapper.countCameraShotOfMember(query);
|
// Integer cameraShotOfMemberNum=statisticsMapper.countCameraShotOfMember(query);
|
||||||
//扫码访问人数
|
//扫码访问人数
|
||||||
// 扫小程序码或景区码进入访问的用户数,包括授权用户(使用OpenID进行精准统计)和未授权用户(使用 UUID统计访问)。但当用户授权时,获取OpenID并与UUID关联,删除本地UUID,避免重复记录。
|
// 扫小程序码或景区码进入访问的用户数,包括授权用户(使用OpenID进行精准统计)和未授权用户(使用 UUID统计访问)。但当用户授权时,获取OpenID并与UUID关联,删除本地UUID,避免重复记录。
|
||||||
Integer scanCodeVisitorOfMemberNum=statisticsMapper.countScanCodeOfMember(query);
|
Integer scanCodeVisitorOfMemberNum=statsQueryService.countScanCodeOfMember(query);
|
||||||
//上传头像(人脸)人数
|
//上传头像(人脸)人数
|
||||||
// 上传了人脸的用户数(包括本地临时ID和获取到OpenID的,同一设备微信获取到OpenID要覆盖掉之前生成的临时ID),上传多张人脸都只算一个人。
|
// 上传了人脸的用户数(包括本地临时ID和获取到OpenID的,同一设备微信获取到OpenID要覆盖掉之前生成的临时ID),上传多张人脸都只算一个人。
|
||||||
Integer uploadFaceOfMemberNum=statisticsMapper.countUploadFaceOfMember(query);
|
Integer uploadFaceOfMemberNum=statsQueryService.countUploadFaceOfMember(query);
|
||||||
//推送订阅人数
|
//推送订阅人数
|
||||||
// 只要点了允许通知,哪怕只勾选1条订阅都算
|
// 只要点了允许通知,哪怕只勾选1条订阅都算
|
||||||
Integer pushOfMemberNum =statisticsMapper.countPushOfMember(query);
|
Integer pushOfMemberNum =statsQueryService.countPushOfMember(query);
|
||||||
//生成视频人数
|
//生成视频人数
|
||||||
// 生成过Vlog视频的用户ID数,要注意屏蔽掉以前没有片段也能生成的情况
|
// 生成过Vlog视频的用户ID数,要注意屏蔽掉以前没有片段也能生成的情况
|
||||||
Integer completeVideoOfMemberNum =statisticsMapper.countCompleteVideoOfMember(query);
|
Integer completeVideoOfMemberNum =statsQueryService.countCompleteVideoOfMember(query);
|
||||||
//预览视频人数
|
//预览视频人数
|
||||||
// 购买前播放了5秒的视频条数。
|
// 购买前播放了5秒的视频条数。
|
||||||
Integer previewVideoOfMemberNum =statisticsMapper.countPreviewVideoOfMember(query);
|
Integer previewVideoOfMemberNum =statsQueryService.countPreviewVideoOfMember(query);
|
||||||
if (previewVideoOfMemberNum==null){
|
if (previewVideoOfMemberNum==null){
|
||||||
previewVideoOfMemberNum=0;
|
previewVideoOfMemberNum=0;
|
||||||
}
|
}
|
||||||
@@ -233,13 +237,13 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
|||||||
Integer payOfMemberNum =statisticsMapper.countPayOfMember(query);
|
Integer payOfMemberNum =statisticsMapper.countPayOfMember(query);
|
||||||
//总访问人数
|
//总访问人数
|
||||||
// 通过任何途径访问到小程序的总人数,包括授权用户和未授权用户。
|
// 通过任何途径访问到小程序的总人数,包括授权用户和未授权用户。
|
||||||
Integer totalVisitorOfMemberNum =statisticsMapper.countTotalVisitorOfMember(query);
|
Integer totalVisitorOfMemberNum =statsQueryService.countTotalVisitorOfMember(query);
|
||||||
// Integer totalVisitorOfMemberNum =scanCodeVisitorOfMemberNum;
|
// Integer totalVisitorOfMemberNum =scanCodeVisitorOfMemberNum;
|
||||||
//生成视频条数
|
//生成视频条数
|
||||||
// 仅指代生成的Vlog条数,不包含录像原片。
|
// 仅指代生成的Vlog条数,不包含录像原片。
|
||||||
Integer completeOfVideoNum =statisticsMapper.countCompleteOfVideo(query);
|
Integer completeOfVideoNum =statsQueryService.countCompleteOfVideo(query);
|
||||||
//预览视频条数
|
//预览视频条数
|
||||||
Integer previewOfVideoNum =statisticsMapper.countPreviewOfVideo(query);
|
Integer previewOfVideoNum =statsQueryService.countPreviewOfVideo(query);
|
||||||
//支付订单数
|
//支付订单数
|
||||||
Integer payOfOrderNum =statisticsMapper.countPayOfOrder(query);
|
Integer payOfOrderNum =statisticsMapper.countPayOfOrder(query);
|
||||||
//支付订单金额
|
//支付订单金额
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ public interface FaceService {
|
|||||||
|
|
||||||
List<ContentPageVO> faceContentList(Long faceId);
|
List<ContentPageVO> faceContentList(Long faceId);
|
||||||
|
|
||||||
ApiResponse<List<ContentPageVO>> contentListUseDefaultFace();
|
|
||||||
|
|
||||||
void bindFace(Long faceId, Long memberId);
|
void bindFace(Long faceId, Long memberId);
|
||||||
|
|
||||||
String bindWxaCode(Long faceId);
|
String bindWxaCode(Long faceId);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.ycwl.basic.service.pc.impl;
|
package com.ycwl.basic.service.pc.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||||
import com.ycwl.basic.mapper.BrokerRecordMapper;
|
import com.ycwl.basic.mapper.BrokerRecordMapper;
|
||||||
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.req.BrokerRecordReqQuery;
|
import com.ycwl.basic.model.pc.broker.req.BrokerRecordReqQuery;
|
||||||
@@ -11,8 +13,9 @@ import com.ycwl.basic.service.pc.BrokerRecordService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author:longbinbin
|
* @Author:longbinbin
|
||||||
@@ -22,6 +25,8 @@ import java.util.List;
|
|||||||
public class BrokerRecordServiceImpl implements BrokerRecordService {
|
public class BrokerRecordServiceImpl implements BrokerRecordService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private BrokerRecordMapper brokerRecordMapper;
|
private BrokerRecordMapper brokerRecordMapper;
|
||||||
|
@Autowired
|
||||||
|
private StatsQueryService statsQueryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageInfo<BrokerRecordRespVO> pageQuery(BrokerRecordReqQuery brokerRecordReqQuery) {
|
public PageInfo<BrokerRecordRespVO> pageQuery(BrokerRecordReqQuery brokerRecordReqQuery) {
|
||||||
@@ -58,7 +63,52 @@ public class BrokerRecordServiceImpl implements BrokerRecordService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DailySummaryRespVO> getDailySummaryByBrokerId(Long brokerId, Date startTime, Date endTime) {
|
public List<DailySummaryRespVO> getDailySummaryByBrokerId(Long brokerId, Date startTime, Date endTime) {
|
||||||
return brokerRecordMapper.getDailySummaryByBrokerId(brokerId, startTime, endTime);
|
// 从 MySQL 获取订单数据
|
||||||
|
List<HashMap<String, Object>> orderStats = brokerRecordMapper.getDailyOrderStats(brokerId, startTime, endTime);
|
||||||
|
|
||||||
|
// 从 ClickHouse/MySQL 获取扫码数据
|
||||||
|
List<HashMap<String, Object>> scanStats = statsQueryService.getDailyScanStats(brokerId, startTime, endTime);
|
||||||
|
|
||||||
|
// 将扫码数据转换为 Map 便于查找
|
||||||
|
Map<String, Long> scanCountByDate = new HashMap<>();
|
||||||
|
if (scanStats != null) {
|
||||||
|
for (HashMap<String, Object> stat : scanStats) {
|
||||||
|
Object dateObj = stat.get("date");
|
||||||
|
String dateKey = dateObj != null ? DateUtil.formatDate((Date) dateObj) : null;
|
||||||
|
Object scanCountObj = stat.get("scanCount");
|
||||||
|
Long scanCount = scanCountObj != null ? ((Number) scanCountObj).longValue() : 0L;
|
||||||
|
if (dateKey != null) {
|
||||||
|
scanCountByDate.put(dateKey, scanCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并数据
|
||||||
|
List<DailySummaryRespVO> result = new ArrayList<>();
|
||||||
|
for (HashMap<String, Object> orderStat : orderStats) {
|
||||||
|
DailySummaryRespVO vo = new DailySummaryRespVO();
|
||||||
|
|
||||||
|
Object dateObj = orderStat.get("date");
|
||||||
|
if (dateObj instanceof Date) {
|
||||||
|
vo.setDate((Date) dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
String dateKey = dateObj != null ? DateUtil.formatDate((Date) dateObj) : null;
|
||||||
|
vo.setScanCount(scanCountByDate.getOrDefault(dateKey, 0L));
|
||||||
|
|
||||||
|
Object orderCountObj = orderStat.get("orderCount");
|
||||||
|
vo.setOrderCount(orderCountObj != null ? ((Number) orderCountObj).longValue() : 0L);
|
||||||
|
|
||||||
|
Object totalOrderPriceObj = orderStat.get("totalOrderPrice");
|
||||||
|
vo.setTotalOrderPrice(totalOrderPriceObj != null ? new BigDecimal(totalOrderPriceObj.toString()) : BigDecimal.ZERO);
|
||||||
|
|
||||||
|
Object totalBrokerPriceObj = orderStat.get("totalBrokerPrice");
|
||||||
|
vo.setTotalBrokerPrice(totalBrokerPriceObj != null ? new BigDecimal(totalBrokerPriceObj.toString()) : BigDecimal.ZERO);
|
||||||
|
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.ycwl.basic.service.pc.impl;
|
|||||||
|
|
||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.clickhouse.service.StatsQueryService;
|
||||||
import com.ycwl.basic.mapper.BrokerMapper;
|
import com.ycwl.basic.mapper.BrokerMapper;
|
||||||
import com.ycwl.basic.model.pc.broker.entity.BrokerEntity;
|
import com.ycwl.basic.model.pc.broker.entity.BrokerEntity;
|
||||||
import com.ycwl.basic.model.pc.broker.req.BrokerReqQuery;
|
import com.ycwl.basic.model.pc.broker.req.BrokerReqQuery;
|
||||||
@@ -27,6 +28,8 @@ public class BrokerServiceImpl implements BrokerService {
|
|||||||
private BrokerMapper brokerMapper;
|
private BrokerMapper brokerMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ScenicRepository scenicRepository;
|
private ScenicRepository scenicRepository;
|
||||||
|
@Autowired
|
||||||
|
private StatsQueryService statsQueryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageInfo<BrokerRespVO> pageQuery(BrokerReqQuery brokerReqQuery) {
|
public PageInfo<BrokerRespVO> pageQuery(BrokerReqQuery brokerReqQuery) {
|
||||||
@@ -41,11 +44,14 @@ public class BrokerServiceImpl implements BrokerService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||||
|
|
||||||
// 设置景区名称
|
// 设置景区名称和扫码次数
|
||||||
list.forEach(item -> {
|
list.forEach(item -> {
|
||||||
if (item.getScenicId() != null) {
|
if (item.getScenicId() != null) {
|
||||||
item.setScenicName(scenicNames.get(item.getScenicId()));
|
item.setScenicName(scenicNames.get(item.getScenicId()));
|
||||||
}
|
}
|
||||||
|
// 从 ClickHouse/MySQL 查询分销员扫码次数
|
||||||
|
Integer scanCount = statsQueryService.countBrokerScanCount(item.getId());
|
||||||
|
item.setBrokerScanCount(scanCount != null ? scanCount.longValue() : 0L);
|
||||||
});
|
});
|
||||||
|
|
||||||
PageInfo<BrokerRespVO> pageInfo = new PageInfo(list);
|
PageInfo<BrokerRespVO> pageInfo = new PageInfo(list);
|
||||||
@@ -64,11 +70,14 @@ public class BrokerServiceImpl implements BrokerService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||||
|
|
||||||
// 设置景区名称
|
// 设置景区名称和扫码次数
|
||||||
list.forEach(item -> {
|
list.forEach(item -> {
|
||||||
if (item.getScenicId() != null) {
|
if (item.getScenicId() != null) {
|
||||||
item.setScenicName(scenicNames.get(item.getScenicId()));
|
item.setScenicName(scenicNames.get(item.getScenicId()));
|
||||||
}
|
}
|
||||||
|
// 从 ClickHouse/MySQL 查询分销员扫码次数
|
||||||
|
Integer scanCount = statsQueryService.countBrokerScanCount(item.getId());
|
||||||
|
item.setBrokerScanCount(scanCount != null ? scanCount.longValue() : 0L);
|
||||||
});
|
});
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -677,13 +677,6 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
return contentList;
|
return contentList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApiResponse<List<ContentPageVO>> contentListUseDefaultFace() {
|
|
||||||
FaceRespVO lastFaceByUserId = faceMapper.findLastFaceByUserId(BaseContextHandler.getUserId());
|
|
||||||
List<ContentPageVO> contentPageVOS = faceContentList(lastFaceByUserId.getId());
|
|
||||||
return ApiResponse.success(contentPageVOS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindFace(Long faceId, Long memberId) {
|
public void bindFace(Long faceId, Long memberId) {
|
||||||
FaceEntity face = faceRepository.getFace(faceId);
|
FaceEntity face = faceRepository.getFace(faceId);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
|
|||||||
import com.ycwl.basic.service.task.TaskFaceService;
|
import com.ycwl.basic.service.task.TaskFaceService;
|
||||||
import com.ycwl.basic.service.task.TaskService;
|
import com.ycwl.basic.service.task.TaskService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -40,7 +41,11 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,7 +151,7 @@ public class FaceMatchingOrchestrator {
|
|||||||
processSourceRelations(context, searchResult, faceId, isNew);
|
processSourceRelations(context, searchResult, faceId, isNew);
|
||||||
|
|
||||||
// 步骤7: 异步生成拼图模板
|
// 步骤7: 异步生成拼图模板
|
||||||
asyncGeneratePuzzleTemplate(context.face.getScenicId(), faceId, context.face.getMemberId());
|
asyncGeneratePuzzleTemplate(context.face.getScenicId(), faceId, context.face.getMemberId(), scene);
|
||||||
|
|
||||||
return searchResult;
|
return searchResult;
|
||||||
|
|
||||||
@@ -354,8 +359,10 @@ public class FaceMatchingOrchestrator {
|
|||||||
/**
|
/**
|
||||||
* 步骤8: 异步生成拼图模板
|
* 步骤8: 异步生成拼图模板
|
||||||
* 在人脸匹配完成后,异步为该景区的所有启用的拼图模板生成图片
|
* 在人脸匹配完成后,异步为该景区的所有启用的拼图模板生成图片
|
||||||
|
*
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
private void asyncGeneratePuzzleTemplate(Long scenicId, Long faceId, Long memberId) {
|
private void asyncGeneratePuzzleTemplate(Long scenicId, Long faceId, Long memberId, String scene) {
|
||||||
if (redisTemplate.hasKey("puzzle_generated:face:" + faceId)) {
|
if (redisTemplate.hasKey("puzzle_generated:face:" + faceId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -363,7 +370,6 @@ public class FaceMatchingOrchestrator {
|
|||||||
"puzzle_generated:face:" + faceId,
|
"puzzle_generated:face:" + faceId,
|
||||||
"1",
|
"1",
|
||||||
60 * 10, TimeUnit.SECONDS);
|
60 * 10, TimeUnit.SECONDS);
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
try {
|
||||||
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
|
log.info("开始异步生成景区拼图模板: scenicId={}, faceId={}", scenicId, faceId);
|
||||||
|
|
||||||
@@ -397,15 +403,8 @@ public class FaceMatchingOrchestrator {
|
|||||||
baseDynamicData.put("scenicText", scenicBasic.getName());
|
baseDynamicData.put("scenicText", scenicBasic.getName());
|
||||||
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
baseDynamicData.put("dateStr", DateUtil.format(new Date(), "yyyy.MM.dd"));
|
||||||
|
|
||||||
// 使用虚拟线程池并行生成所有模板
|
templateList
|
||||||
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
.forEach(template -> {
|
||||||
java.util.concurrent.atomic.AtomicInteger failCount = new java.util.concurrent.atomic.AtomicInteger(0);
|
|
||||||
|
|
||||||
try (java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
|
|
||||||
// 为每个模板创建一个异步任务
|
|
||||||
List<java.util.concurrent.CompletableFuture<Void>> futures = templateList.stream()
|
|
||||||
.map(template -> java.util.concurrent.CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
log.info("开始生成拼图: scenicId={}, templateCode={}, templateName={}",
|
||||||
scenicId, template.getCode(), template.getName());
|
scenicId, template.getCode(), template.getName());
|
||||||
|
|
||||||
@@ -416,38 +415,21 @@ public class FaceMatchingOrchestrator {
|
|||||||
generateRequest.setFaceId(faceId);
|
generateRequest.setFaceId(faceId);
|
||||||
generateRequest.setBusinessType("face_matching");
|
generateRequest.setBusinessType("face_matching");
|
||||||
generateRequest.setTemplateCode(template.getCode());
|
generateRequest.setTemplateCode(template.getCode());
|
||||||
generateRequest.setOutputFormat("PNG");
|
generateRequest.setOutputFormat("JPEG");
|
||||||
generateRequest.setQuality(90);
|
generateRequest.setQuality(80);
|
||||||
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
generateRequest.setDynamicData(new HashMap<>(baseDynamicData));
|
||||||
generateRequest.setRequireRuleMatch(true);
|
generateRequest.setRequireRuleMatch(true);
|
||||||
|
if (template.getAutoAddPrint() > 0 && Strings.CI.equals(scene, "printer")) {
|
||||||
// 调用拼图生成服务
|
puzzleGenerateService.generateSync(generateRequest);
|
||||||
PuzzleGenerateResponse response = puzzleGenerateService.generate(generateRequest);
|
} else {
|
||||||
|
puzzleGenerateService.generateAsync(generateRequest);
|
||||||
log.info("拼图生成成功: scenicId={}, templateCode={}, imageUrl={}",
|
|
||||||
scenicId, template.getCode(), response.getImageUrl());
|
|
||||||
successCount.incrementAndGet();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("拼图生成失败: scenicId={}, templateCode={}, templateName={}",
|
|
||||||
scenicId, template.getCode(), template.getName(), e);
|
|
||||||
failCount.incrementAndGet();
|
|
||||||
}
|
}
|
||||||
}, executor))
|
});
|
||||||
.toList();
|
|
||||||
|
|
||||||
// 等待所有任务完成
|
|
||||||
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("景区拼图模板批量生成完成: scenicId={}, 总数={}, 成功={}, 失败={}",
|
|
||||||
scenicId, templateList.size(), successCount.get(), failCount.get());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 异步任务失败不影响主流程,仅记录日志
|
// 异步任务失败不影响主流程,仅记录日志
|
||||||
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
log.error("异步生成拼图模板失败: scenicId={}, faceId={}", scenicId, faceId, e);
|
||||||
}
|
}
|
||||||
}, "PuzzleTemplateGenerator-" + scenicId).start();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ public class VideoRecreationHandler {
|
|||||||
}).toList()
|
}).toList()
|
||||||
.stream().map(FaceSampleEntity::getId).toList();
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
|
|
||||||
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
|
||||||
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
log.debug("视频重切逻辑:视频数量 {}, 照片数量 {}", videoCount, photoCount);
|
||||||
|
|
||||||
// 只有照片数量大于视频数量时才创建重切任务
|
// 只有照片数量大于视频数量时才创建重切任务
|
||||||
|
|||||||
@@ -400,17 +400,20 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
obj.setGoodsType(3);
|
obj.setGoodsType(3);
|
||||||
|
|
||||||
// 按照 sourceId 分类照片
|
// 按照 sourceId 分类照片
|
||||||
// sourceId > 0: 普通照片打印 (PHOTO_PRINT)
|
// sourceId > 0 且 source 表存在: 普通照片打印 (PHOTO_PRINT)
|
||||||
|
// sourceId > 0 且 source 表不存在: 拼图打印 (PUZZLE),归类为特效照片价格
|
||||||
// sourceId == null: 手机照片打印 (PHOTO_PRINT_MU)
|
// sourceId == null: 手机照片打印 (PHOTO_PRINT_MU)
|
||||||
// sourceId == 0: 特效照片打印 (PHOTO_PRINT_FX)
|
// sourceId == 0: 特效照片打印 (PHOTO_PRINT_FX)
|
||||||
long normalCount = userPhotoList.stream()
|
long normalCount = userPhotoList.stream()
|
||||||
.filter(item -> Objects.nonNull(item.getQuantity())
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
&& item.getSourceId() != null && item.getSourceId() > 0)
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) != null)
|
||||||
.mapToInt(MemberPrintResp::getQuantity)
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
.sum();
|
.sum();
|
||||||
List<String> normalAttrs = userPhotoList.stream()
|
List<String> normalAttrs = userPhotoList.stream()
|
||||||
.filter(item -> Objects.nonNull(item.getQuantity())
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
&& item.getSourceId() != null && item.getSourceId() > 0)
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) != null)
|
||||||
.map(MemberPrintResp::getSourceId)
|
.map(MemberPrintResp::getSourceId)
|
||||||
.map(id -> {
|
.map(id -> {
|
||||||
SourceEntity source = sourceRepository.getSource(id);
|
SourceEntity source = sourceRepository.getSource(id);
|
||||||
@@ -436,7 +439,15 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
.mapToInt(MemberPrintResp::getQuantity)
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
long totalCount = normalCount + mobileCount + effectCount;
|
// 拼图:sourceId > 0 但 source 表中不存在(即来自 puzzle_generation_record 表)
|
||||||
|
long puzzleCount = userPhotoList.stream()
|
||||||
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) == null)
|
||||||
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
long totalCount = normalCount + mobileCount + effectCount + puzzleCount;
|
||||||
|
|
||||||
if (totalCount == 0) {
|
if (totalCount == 0) {
|
||||||
// 如果没有照片,返回零价格
|
// 如果没有照片,返回零价格
|
||||||
@@ -449,6 +460,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
// 构建价格计算请求
|
// 构建价格计算请求
|
||||||
PriceCalculationRequest request = new PriceCalculationRequest();
|
PriceCalculationRequest request = new PriceCalculationRequest();
|
||||||
request.setUserId(memberId);
|
request.setUserId(memberId);
|
||||||
|
request.setScenicId(scenicId);
|
||||||
|
|
||||||
// 创建商品项列表
|
// 创建商品项列表
|
||||||
List<ProductItem> productItems = new ArrayList<>();
|
List<ProductItem> productItems = new ArrayList<>();
|
||||||
@@ -479,15 +491,15 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加特效照片打印商品项 (sourceId == 0)
|
// 添加特效照片打印商品项 (sourceId == 0)
|
||||||
if (effectCount > 0) {
|
if (effectCount > 0 || puzzleCount > 0) {
|
||||||
ProductItem effectPhotoItem = new ProductItem();
|
ProductItem effectPhotoItem = new ProductItem();
|
||||||
effectPhotoItem.setProductType(ProductType.PHOTO_PRINT_FX);
|
effectPhotoItem.setProductType(ProductType.PHOTO_PRINT_FX);
|
||||||
effectPhotoItem.setProductId(scenicId.toString());
|
effectPhotoItem.setProductId(scenicId.toString());
|
||||||
effectPhotoItem.setQuantity(Long.valueOf(effectCount).intValue());
|
effectPhotoItem.setQuantity(Long.valueOf(effectCount + puzzleCount).intValue());
|
||||||
effectPhotoItem.setPurchaseCount(1);
|
effectPhotoItem.setPurchaseCount(1);
|
||||||
effectPhotoItem.setScenicId(scenicId.toString());
|
effectPhotoItem.setScenicId(scenicId.toString());
|
||||||
productItems.add(effectPhotoItem);
|
productItems.add(effectPhotoItem);
|
||||||
log.debug("特效照片打印数量: {}", effectCount);
|
log.debug("特效照片打印数量: {}", effectCount + puzzleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.setProducts(productItems);
|
request.setProducts(productItems);
|
||||||
@@ -548,7 +560,27 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
@Override
|
@Override
|
||||||
public List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req, Long faceId) {
|
public List<Integer> addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req, Long faceId) {
|
||||||
List<Integer> resultIds = new ArrayList<>();
|
List<Integer> resultIds = new ArrayList<>();
|
||||||
|
|
||||||
|
// 预先查询用户在该景区的已有打印记录,用于去重
|
||||||
|
List<MemberPrintResp> existingPhotos = printerMapper.listRelationByFaceId(memberId, scenicId, faceId);
|
||||||
|
Map<Long, Integer> sourceIdToMemberPrintId = new HashMap<>();
|
||||||
|
if (existingPhotos != null) {
|
||||||
|
for (MemberPrintResp photo : existingPhotos) {
|
||||||
|
if (photo.getSourceId() != null) {
|
||||||
|
sourceIdToMemberPrintId.put(photo.getSourceId(), photo.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.getIds().forEach(id -> {
|
req.getIds().forEach(id -> {
|
||||||
|
// 检查该sourceId是否已经添加过
|
||||||
|
if (sourceIdToMemberPrintId.containsKey(id)) {
|
||||||
|
Integer existingId = sourceIdToMemberPrintId.get(id);
|
||||||
|
log.info("sourceId={}已存在于打印列表中,返回已有记录: memberPrintId={}", id, existingId);
|
||||||
|
resultIds.add(existingId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
SourceRespVO byId = sourceMapper.getById(id);
|
SourceRespVO byId = sourceMapper.getById(id);
|
||||||
if (byId == null) {
|
if (byId == null) {
|
||||||
resultIds.add(null);
|
resultIds.add(null);
|
||||||
@@ -660,12 +692,14 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId, faceId);
|
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId, faceId);
|
||||||
|
|
||||||
// 按照 sourceId 分类照片
|
// 按照 sourceId 分类照片
|
||||||
// sourceId > 0: 普通照片打印 (PHOTO_PRINT)
|
// sourceId > 0 且 source 表存在: 普通照片打印 (PHOTO_PRINT)
|
||||||
|
// sourceId > 0 且 source 表不存在: 拼图打印 (PUZZLE),归类为特效照片价格
|
||||||
// sourceId == null: 手机照片打印 (PHOTO_PRINT_MU)
|
// sourceId == null: 手机照片打印 (PHOTO_PRINT_MU)
|
||||||
// sourceId == 0: 特效照片打印 (PHOTO_PRINT_FX)
|
// sourceId == 0: 特效照片打印 (PHOTO_PRINT_FX)
|
||||||
long normalCount = userPhotoList.stream()
|
long normalCount = userPhotoList.stream()
|
||||||
.filter(item -> Objects.nonNull(item.getQuantity())
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
&& item.getSourceId() != null && item.getSourceId() > 0)
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) != null)
|
||||||
.mapToInt(MemberPrintResp::getQuantity)
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
@@ -681,7 +715,14 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
.mapToInt(MemberPrintResp::getQuantity)
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
long totalCount = normalCount + mobileCount + effectCount;
|
// 拼图:sourceId > 0 但 source 表中不存在(即来自 puzzle_generation_record 表)
|
||||||
|
long puzzleCount = userPhotoList.stream()
|
||||||
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) == null)
|
||||||
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
|
.sum();
|
||||||
|
long totalCount = normalCount + mobileCount + effectCount + puzzleCount;
|
||||||
|
|
||||||
if (totalCount == 0) {
|
if (totalCount == 0) {
|
||||||
throw new BaseException("没有可打印的照片");
|
throw new BaseException("没有可打印的照片");
|
||||||
@@ -718,12 +759,30 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
|
|
||||||
// 添加普通照片打印商品项 (sourceId > 0)
|
// 添加普通照片打印商品项 (sourceId > 0)
|
||||||
if (normalCount > 0) {
|
if (normalCount > 0) {
|
||||||
|
List<String> normalAttrs = userPhotoList.stream()
|
||||||
|
.filter(item -> Objects.nonNull(item.getQuantity())
|
||||||
|
&& item.getSourceId() != null && item.getSourceId() > 0)
|
||||||
|
.filter(item -> sourceMapper.getById(item.getSourceId()) != null)
|
||||||
|
.map(MemberPrintResp::getSourceId)
|
||||||
|
.map(id -> {
|
||||||
|
SourceEntity source = sourceRepository.getSource(id);
|
||||||
|
if (source == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return source.getDeviceId();
|
||||||
|
})
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.toList();
|
||||||
|
|
||||||
ProductItem normalPhotoItem = new ProductItem();
|
ProductItem normalPhotoItem = new ProductItem();
|
||||||
normalPhotoItem.setProductType(ProductType.PHOTO_PRINT);
|
normalPhotoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||||
normalPhotoItem.setProductId(scenicId.toString());
|
normalPhotoItem.setProductId(scenicId.toString());
|
||||||
normalPhotoItem.setQuantity(Long.valueOf(normalCount).intValue());
|
normalPhotoItem.setQuantity(Long.valueOf(normalCount).intValue());
|
||||||
normalPhotoItem.setPurchaseCount(1);
|
normalPhotoItem.setPurchaseCount(1);
|
||||||
normalPhotoItem.setScenicId(scenicId.toString());
|
normalPhotoItem.setScenicId(scenicId.toString());
|
||||||
|
normalPhotoItem.setAttributeKeys(normalAttrs);
|
||||||
productItems.add(normalPhotoItem);
|
productItems.add(normalPhotoItem);
|
||||||
log.debug("创建订单-普通照片打印数量: {}", normalCount);
|
log.debug("创建订单-普通照片打印数量: {}", normalCount);
|
||||||
}
|
}
|
||||||
@@ -741,15 +800,15 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加特效照片打印商品项 (sourceId == 0)
|
// 添加特效照片打印商品项 (sourceId == 0)
|
||||||
if (effectCount > 0) {
|
if (effectCount > 0 || puzzleCount > 0) {
|
||||||
ProductItem effectPhotoItem = new ProductItem();
|
ProductItem effectPhotoItem = new ProductItem();
|
||||||
effectPhotoItem.setProductType(ProductType.PHOTO_PRINT_FX);
|
effectPhotoItem.setProductType(ProductType.PHOTO_PRINT_FX);
|
||||||
effectPhotoItem.setProductId(scenicId.toString());
|
effectPhotoItem.setProductId(scenicId.toString());
|
||||||
effectPhotoItem.setQuantity(Long.valueOf(effectCount).intValue());
|
effectPhotoItem.setQuantity(Long.valueOf(effectCount + puzzleCount).intValue());
|
||||||
effectPhotoItem.setPurchaseCount(1);
|
effectPhotoItem.setPurchaseCount(1);
|
||||||
effectPhotoItem.setScenicId(scenicId.toString());
|
effectPhotoItem.setScenicId(scenicId.toString());
|
||||||
productItems.add(effectPhotoItem);
|
productItems.add(effectPhotoItem);
|
||||||
log.debug("创建订单-特效照片打印数量: {}", effectCount);
|
log.debug("创建订单-特效照片打印数量: {}", effectCount + puzzleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.setProducts(productItems);
|
request.setProducts(productItems);
|
||||||
@@ -889,7 +948,11 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
SourceEntity source = null;
|
SourceEntity source = null;
|
||||||
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
||||||
source = sourceMapper.getEntity(item.getSourceId());
|
source = sourceMapper.getEntity(item.getSourceId());
|
||||||
|
if (source == null) {
|
||||||
|
context.setImageType(ImageType.PUZZLE); // 特殊
|
||||||
|
} else {
|
||||||
context.setSource(ImageSource.IPC);
|
context.setSource(ImageSource.IPC);
|
||||||
|
}
|
||||||
} else if (item.getSourceId() == null) {
|
} else if (item.getSourceId() == null) {
|
||||||
context.setSource(ImageSource.PHONE);
|
context.setSource(ImageSource.PHONE);
|
||||||
} else {
|
} else {
|
||||||
@@ -1220,7 +1283,7 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
resp.setFaceId(faceId);
|
resp.setFaceId(faceId);
|
||||||
resp.setScenicId(scenicId);
|
resp.setScenicId(scenicId);
|
||||||
try {
|
try {
|
||||||
faceService.matchFaceId(faceId);
|
faceService.matchFaceId(faceId, true, "printer");
|
||||||
if (existingFace == null) {
|
if (existingFace == null) {
|
||||||
autoAddPhotosToPreferPrint(faceId);
|
autoAddPhotosToPreferPrint(faceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ public class TaskFaceServiceImpl implements TaskFaceService {
|
|||||||
return entry.getValue().stream();
|
return entry.getValue().stream();
|
||||||
}).toList()
|
}).toList()
|
||||||
.stream().map(FaceSampleEntity::getId).toList();
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, sampleListIds.size(), faceSampleIds.size());
|
|
||||||
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
||||||
task.faceId = faceEntity.getId();
|
task.faceId = faceEntity.getId();
|
||||||
task.faceSampleIds = faceSampleIds;
|
task.faceSampleIds = faceSampleIds;
|
||||||
|
|||||||
@@ -146,6 +146,34 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
return worker;
|
return worker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个版本号
|
||||||
|
* @param v1 版本号1
|
||||||
|
* @param v2 版本号2
|
||||||
|
* @return 负数表示 v1 < v2,0 表示相等,正数表示 v1 > v2
|
||||||
|
*/
|
||||||
|
private int compareVersion(String v1, String v2) {
|
||||||
|
String[] parts1 = v1.split("\\.");
|
||||||
|
String[] parts2 = v2.split("\\.");
|
||||||
|
int maxLen = Math.max(parts1.length, parts2.length);
|
||||||
|
for (int i = 0; i < maxLen; i++) {
|
||||||
|
int num1 = i < parts1.length ? parseVersionPart(parts1[i]) : 0;
|
||||||
|
int num2 = i < parts2.length ? parseVersionPart(parts2[i]) : 0;
|
||||||
|
if (num1 != num2) {
|
||||||
|
return num1 - num2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseVersionPart(String part) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(part.replaceAll("[^0-9]", ""));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isWorkerSelfHostedScenic(Long scenicId) {
|
private boolean isWorkerSelfHostedScenic(Long scenicId) {
|
||||||
String cacheKey = String.format(WORKER_SELF_HOSTED_CACHE_KEY, scenicId);
|
String cacheKey = String.format(WORKER_SELF_HOSTED_CACHE_KEY, scenicId);
|
||||||
String cachedValue = redisTemplate.opsForValue().get(cacheKey);
|
String cachedValue = redisTemplate.opsForValue().get(cacheKey);
|
||||||
@@ -174,6 +202,13 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
worker.setStatus(null);
|
worker.setStatus(null);
|
||||||
// get status
|
// get status
|
||||||
ClientStatusReqVo clientStatus = req.getClientStatus();
|
ClientStatusReqVo clientStatus = req.getClientStatus();
|
||||||
|
// 版本校验:上报版本低于缓存版本时认为 worker 异常
|
||||||
|
ClientStatusReqVo cachedStatus = repository.getWorkerHostStatus(worker.getId());
|
||||||
|
if (cachedStatus != null && clientStatus != null
|
||||||
|
&& cachedStatus.getVersion() != null && clientStatus.getVersion() != null
|
||||||
|
&& compareVersion(clientStatus.getVersion(), cachedStatus.getVersion()) < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
repository.setWorkerHostStatus(worker.getId(), clientStatus);
|
repository.setWorkerHostStatus(worker.getId(), clientStatus);
|
||||||
TaskSyncRespVo resp = new TaskSyncRespVo();
|
TaskSyncRespVo resp = new TaskSyncRespVo();
|
||||||
// Template
|
// Template
|
||||||
@@ -283,7 +318,6 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
return entry.getValue().stream();
|
return entry.getValue().stream();
|
||||||
}).toList()
|
}).toList()
|
||||||
.stream().map(FaceSampleEntity::getId).toList();
|
.stream().map(FaceSampleEntity::getId).toList();
|
||||||
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(faceRespVO.getScenicId());
|
||||||
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId());
|
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(faceRespVO.getScenicId());
|
||||||
if (templateList == null || templateList.isEmpty()) {
|
if (templateList == null || templateList.isEmpty()) {
|
||||||
@@ -355,7 +389,6 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
return entry.getValue().stream();
|
return entry.getValue().stream();
|
||||||
}).toList()
|
}).toList()
|
||||||
.stream().map(FaceSampleEntity::getId).collect(Collectors.toList());
|
.stream().map(FaceSampleEntity::getId).collect(Collectors.toList());
|
||||||
log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size());
|
|
||||||
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
VideoPieceGetter.Task task = new VideoPieceGetter.Task();
|
||||||
task.faceId = faceId;
|
task.faceId = faceId;
|
||||||
task.faceSampleIds = faceSampleIds;
|
task.faceSampleIds = faceSampleIds;
|
||||||
@@ -399,15 +432,18 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
memberVideoEntity.setTemplateId(templateId);
|
memberVideoEntity.setTemplateId(templateId);
|
||||||
memberVideoEntity.setIsBuy(0);
|
memberVideoEntity.setIsBuy(0);
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
log.info("创建任务! faceId:{},templateId:{},taskParams:{}", faceId, templateId, sourcesMap);
|
log.info("创建任务! faceId:{},templateId:{}", faceId, templateId);
|
||||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
|
||||||
TaskEntity taskEntity = null;
|
TaskEntity taskEntity = null;
|
||||||
|
boolean isReuseOldTask = false;
|
||||||
if (Integer.valueOf(0).equals(scenicConfig.getInteger("template_new_video_type"))) {
|
if (Integer.valueOf(0).equals(scenicConfig.getInteger("template_new_video_type"))) {
|
||||||
log.info("景区{}启用:templateNewVideoType:全新视频原位替换", face.getScenicId());
|
log.info("景区{}启用:templateNewVideoType:全新视频原位替换", face.getScenicId());
|
||||||
taskReqQuery.setTemplateId(templateId);
|
taskReqQuery.setTemplateId(templateId);
|
||||||
|
taskReqQuery.setTaskParams(null); // 原位替换模式下,不按taskParams匹配
|
||||||
List<TaskEntity> templateTaskList = taskMapper.listEntity(taskReqQuery);
|
List<TaskEntity> templateTaskList = taskMapper.listEntity(taskReqQuery);
|
||||||
if (!templateTaskList.isEmpty()) {
|
if (!templateTaskList.isEmpty()) {
|
||||||
taskEntity = templateTaskList.getFirst();
|
taskEntity = templateTaskList.getFirst();
|
||||||
|
isReuseOldTask = true;
|
||||||
log.info("已有旧生成的视频:{}", taskEntity);
|
log.info("已有旧生成的视频:{}", taskEntity);
|
||||||
MemberVideoEntity taskVideoRelation = videoMapper.queryRelationByMemberTask(face.getMemberId(), taskEntity.getId());
|
MemberVideoEntity taskVideoRelation = videoMapper.queryRelationByMemberTask(face.getMemberId(), taskEntity.getId());
|
||||||
if (taskVideoRelation != null) {
|
if (taskVideoRelation != null) {
|
||||||
@@ -425,17 +461,25 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
taskEntity.setTemplateId(templateId);
|
taskEntity.setTemplateId(templateId);
|
||||||
taskEntity.setAutomatic(automatic ? 1 : 0);
|
taskEntity.setAutomatic(automatic ? 1 : 0);
|
||||||
}
|
}
|
||||||
taskEntity.setWorkerId(null);
|
|
||||||
taskEntity.setStatus(0);
|
taskEntity.setStatus(0);
|
||||||
taskEntity.setTaskParams(JacksonUtil.toJSONString(sourcesMap));
|
taskEntity.setTaskParams(JacksonUtil.toJSONString(sourcesMap));
|
||||||
|
if (isReuseOldTask) {
|
||||||
|
taskMapper.update(taskEntity);
|
||||||
|
taskMapper.deassign(taskEntity.getId());
|
||||||
|
log.info("更新旧任务! taskId:{}", taskEntity.getId());
|
||||||
|
} else {
|
||||||
taskMapper.add(taskEntity);
|
taskMapper.add(taskEntity);
|
||||||
|
}
|
||||||
memberVideoEntity.setTaskId(taskEntity.getId());
|
memberVideoEntity.setTaskId(taskEntity.getId());
|
||||||
} else {
|
} else {
|
||||||
log.info("重复task! faceId:{},templateId:{},taskParams:{}", faceId, templateId, sourcesMap);
|
TaskRespVO existingTask = list.getFirst();
|
||||||
memberVideoEntity.setTaskId(list.getFirst().getId());
|
log.info("重复task! faceId:{},templateId:{},taskId:{}", faceId, templateId, existingTask.getId());
|
||||||
VideoEntity video = videoMapper.findByTaskId(list.getFirst().getId());
|
videoTaskRepository.clearTaskCache(existingTask.getId());
|
||||||
|
|
||||||
|
memberVideoEntity.setTaskId(existingTask.getId());
|
||||||
|
VideoEntity video = videoRepository.getVideoByTaskId(existingTask.getId());
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
IsBuyRespVO isBuy = orderBiz.isBuy(list.getFirst().getScenicId(), face.getMemberId(), face.getId(), 0, video.getId());
|
IsBuyRespVO isBuy = orderBiz.isBuy(existingTask.getScenicId(), face.getMemberId(), face.getId(), 0, video.getId());
|
||||||
if (isBuy.isBuy()) {
|
if (isBuy.isBuy()) {
|
||||||
memberVideoEntity.setIsBuy(1);
|
memberVideoEntity.setIsBuy(1);
|
||||||
memberVideoEntity.setOrderId(isBuy.getOrderId());
|
memberVideoEntity.setOrderId(isBuy.getOrderId());
|
||||||
@@ -590,6 +634,10 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
public void sendVideoGeneratedServiceNotification(Long taskId, Long memberId) {
|
public void sendVideoGeneratedServiceNotification(Long taskId, Long memberId) {
|
||||||
MemberVideoEntity item = videoMapper.queryRelationByMemberTask(memberId, taskId);
|
MemberVideoEntity item = videoMapper.queryRelationByMemberTask(memberId, taskId);
|
||||||
MemberRespVO member = memberMapper.getById(memberId);
|
MemberRespVO member = memberMapper.getById(memberId);
|
||||||
|
if (member == null || item == null) {
|
||||||
|
log.warn("sendVideoGeneratedServiceNotification member or item is null, memberId:{}, taskId:{}", memberId, taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
String openId = member.getOpenId();
|
String openId = member.getOpenId();
|
||||||
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
|
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
|
||||||
if (StringUtils.isNotBlank(openId) && scenicMp != null) {
|
if (StringUtils.isNotBlank(openId) && scenicMp != null) {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.biz;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class StatsBiz {
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.controller;
|
|
||||||
|
|
||||||
import com.ycwl.basic.annotation.IgnoreToken;
|
|
||||||
import com.ycwl.basic.stats.dto.AddTraceReq;
|
|
||||||
import com.ycwl.basic.stats.service.StatsService;
|
|
||||||
import com.ycwl.basic.stats.util.StatsUtil;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/trace/v1")
|
|
||||||
public class TraceController {
|
|
||||||
@Autowired
|
|
||||||
private StatsService statsService;
|
|
||||||
@IgnoreToken
|
|
||||||
@PostMapping("/start")
|
|
||||||
public void startTrace(HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
String traceId = request.getHeader("traceId");
|
|
||||||
if (traceId == null || traceId.isEmpty()) {
|
|
||||||
traceId = StatsUtil.createUuid();
|
|
||||||
response.setHeader("Set-TraceId", traceId);
|
|
||||||
}
|
|
||||||
statsService.addStats(traceId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@IgnoreToken
|
|
||||||
@PostMapping("/add")
|
|
||||||
public void addTrace(HttpServletRequest request, HttpServletResponse response, @RequestBody AddTraceReq req) {
|
|
||||||
String traceId = request.getHeader("traceId");
|
|
||||||
if (traceId == null || traceId.isEmpty()) {
|
|
||||||
traceId = StatsUtil.createUuid();
|
|
||||||
response.setHeader("Set-TraceId", traceId);
|
|
||||||
}
|
|
||||||
if (StringUtils.isEmpty(req.getParams())) {
|
|
||||||
req.setParams(null);
|
|
||||||
}
|
|
||||||
statsService.addRecord(traceId, req.getAction(), req.getIdentifier(), req.getParams());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class AddTraceReq {
|
|
||||||
private String action;
|
|
||||||
private String identifier;
|
|
||||||
private String params;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("t_stats")
|
|
||||||
public class StatsEntity {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String traceId;
|
|
||||||
private Long memberId;
|
|
||||||
private Date createTime;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("t_stats_record")
|
|
||||||
public class StatsRecordEntity {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String traceId;
|
|
||||||
private String action;
|
|
||||||
private String identifier;
|
|
||||||
private String params;
|
|
||||||
private Date createTime;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.interceptor;
|
|
||||||
|
|
||||||
import com.ycwl.basic.annotation.IgnoreLogReq;
|
|
||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
|
||||||
import com.ycwl.basic.stats.service.StatsService;
|
|
||||||
import com.ycwl.basic.stats.util.StatsUtil;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.method.HandlerMethod;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class StatsInterceptor implements HandlerInterceptor {
|
|
||||||
@Lazy
|
|
||||||
@Autowired
|
|
||||||
private StatsService statsService;
|
|
||||||
|
|
||||||
// 在请求处理前执行
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
||||||
if (!(handler instanceof HandlerMethod)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// HandlerMethod handlerMethod = (HandlerMethod) handler;
|
|
||||||
// request.setAttribute("startTime", System.currentTimeMillis());
|
|
||||||
// String requestURI = request.getRequestURI();
|
|
||||||
// String method = request.getMethod();
|
|
||||||
String traceId = request.getHeader("traceId");
|
|
||||||
if (StringUtils.isEmpty(traceId)) {
|
|
||||||
return true;
|
|
||||||
// traceId = StatsUtil.createUuid();
|
|
||||||
// response.setHeader("Set-TraceId", traceId);
|
|
||||||
// statsService.addStats(traceId, null);
|
|
||||||
}
|
|
||||||
// if (handlerMethod.getMethodAnnotation(IgnoreLogReq.class) == null) {
|
|
||||||
// statsService.addRecord(traceId, "REQUEST",method + " " + requestURI, null);
|
|
||||||
// }
|
|
||||||
if (StringUtils.isNotEmpty(BaseContextHandler.getUserId())) {
|
|
||||||
statsService.updateStats(traceId, Long.valueOf(BaseContextHandler.getUserId()));
|
|
||||||
}
|
|
||||||
// 返回 true 继续后续流程,false 终止请求
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 控制器方法执行后,渲染视图前
|
|
||||||
@Override
|
|
||||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求完全完成后执行(无论是否异常)
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
|
||||||
// long startTime = (Long) request.getAttribute("startTime");
|
|
||||||
// long endTime = System.currentTimeMillis();
|
|
||||||
// System.out.println("【AfterCompletion】请求结束,耗时:" + (endTime - startTime) + "ms");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.ycwl.basic.stats.entity.StatsEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface StatsMapper extends BaseMapper<StatsEntity> {
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.ycwl.basic.stats.entity.StatsRecordEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface StatsRecordMapper extends BaseMapper<StatsRecordEntity> {
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.service;
|
|
||||||
|
|
||||||
public interface StatsService {
|
|
||||||
void addStats(String traceId, Long memberId);
|
|
||||||
|
|
||||||
void updateStats(String traceId, Long memberId);
|
|
||||||
|
|
||||||
void addRecord(String traceId, String action, String identifier, String params);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.service.impl;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.ycwl.basic.stats.entity.StatsEntity;
|
|
||||||
import com.ycwl.basic.stats.entity.StatsRecordEntity;
|
|
||||||
import com.ycwl.basic.stats.mapper.StatsMapper;
|
|
||||||
import com.ycwl.basic.stats.mapper.StatsRecordMapper;
|
|
||||||
import com.ycwl.basic.stats.service.StatsService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class StatsServiceImpl implements StatsService {
|
|
||||||
@Lazy
|
|
||||||
@Autowired
|
|
||||||
private StatsMapper statsMapper;
|
|
||||||
|
|
||||||
@Lazy
|
|
||||||
@Autowired
|
|
||||||
private StatsRecordMapper statsRecordMapper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addStats(String traceId, Long memberId) {
|
|
||||||
StatsEntity entity = new StatsEntity();
|
|
||||||
entity.setTraceId(traceId);
|
|
||||||
entity.setMemberId(memberId);
|
|
||||||
entity.setCreateTime(new Date());
|
|
||||||
statsMapper.insert(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateStats(String traceId, Long memberId) {
|
|
||||||
StatsEntity entity = new StatsEntity();
|
|
||||||
entity.setMemberId(memberId);
|
|
||||||
LambdaQueryWrapper<StatsEntity> queryWrapper = new LambdaQueryWrapper<>();
|
|
||||||
queryWrapper.eq(StatsEntity::getTraceId, traceId);
|
|
||||||
statsMapper.update(entity, queryWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addRecord(String traceId, String action, String identifier, String params) {
|
|
||||||
StatsRecordEntity entity = new StatsRecordEntity();
|
|
||||||
entity.setTraceId(traceId);
|
|
||||||
entity.setAction(action);
|
|
||||||
entity.setIdentifier(identifier);
|
|
||||||
entity.setParams(params);
|
|
||||||
entity.setCreateTime(new Date());
|
|
||||||
statsRecordMapper.insert(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.ycwl.basic.stats.util;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class StatsUtil {
|
|
||||||
public static String createUuid() {
|
|
||||||
return UUID.randomUUID().toString().replace("-", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,6 +36,9 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -43,7 +46,9 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
@@ -84,6 +89,16 @@ public class VideoPieceGetter {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FaceStatusManager faceStatusManager;
|
private FaceStatusManager faceStatusManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区设备配对关系缓存
|
||||||
|
* key: scenicId
|
||||||
|
* value: Map<deviceId, pairDeviceId>,空Map表示该景区无配对关系
|
||||||
|
*/
|
||||||
|
private final Cache<Long, Map<Long, Long>> pairDeviceCache = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(5, TimeUnit.MINUTES)
|
||||||
|
.maximumSize(500)
|
||||||
|
.build();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Task {
|
public static class Task {
|
||||||
public List<Long> faceSampleIds = new ArrayList<>();
|
public List<Long> faceSampleIds = new ArrayList<>();
|
||||||
@@ -153,17 +168,8 @@ public class VideoPieceGetter {
|
|||||||
task.callback.onInvoke();
|
task.callback.onInvoke();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<Long, Long> pairDeviceMap = new ConcurrentHashMap<>();
|
|
||||||
Long scenicId = list.getFirst().getScenicId();
|
Long scenicId = list.getFirst().getScenicId();
|
||||||
List<DeviceV2DTO> allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId);
|
Map<Long, Long> pairDeviceMap = pairDeviceCache.get(scenicId, this::loadPairDeviceMap);
|
||||||
allDeviceByScenicId.forEach(device -> {
|
|
||||||
Long deviceId = device.getId();
|
|
||||||
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
|
|
||||||
Long pairDevice = deviceConfig.getLong("pair_device");
|
|
||||||
if (pairDevice != null) {
|
|
||||||
pairDeviceMap.putIfAbsent(deviceId, pairDevice);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Map<Long, List<FaceSampleEntity>> collection = list.stream()
|
Map<Long, List<FaceSampleEntity>> collection = list.stream()
|
||||||
.filter(faceSample -> {
|
.filter(faceSample -> {
|
||||||
if (templatePlaceholder != null) {
|
if (templatePlaceholder != null) {
|
||||||
@@ -176,12 +182,14 @@ public class VideoPieceGetter {
|
|||||||
templatePlaceholder.forEach(deviceId -> {
|
templatePlaceholder.forEach(deviceId -> {
|
||||||
currentUnFinPlaceholder.computeIfAbsent(deviceId, k -> new AtomicInteger(0)).incrementAndGet();
|
currentUnFinPlaceholder.computeIfAbsent(deviceId, k -> new AtomicInteger(0)).incrementAndGet();
|
||||||
});
|
});
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("[Placeholder初始化] 有templateId,初始化完成:placeholder总数={}, 不同设备数={}, 详细计数={}",
|
log.debug("[Placeholder初始化] 有templateId,初始化完成:placeholder总数={}, 不同设备数={}, 详细计数={}",
|
||||||
templatePlaceholder.size(),
|
templatePlaceholder.size(),
|
||||||
currentUnFinPlaceholder.size(),
|
currentUnFinPlaceholder.size(),
|
||||||
currentUnFinPlaceholder.entrySet().stream()
|
currentUnFinPlaceholder.entrySet().stream()
|
||||||
.map(e -> e.getKey() + "=" + e.getValue().get())
|
.map(e -> e.getKey() + "=" + e.getValue().get())
|
||||||
.collect(Collectors.joining(", ")));
|
.collect(Collectors.joining(", ")));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
collection.keySet().forEach(deviceId -> {
|
collection.keySet().forEach(deviceId -> {
|
||||||
currentUnFinPlaceholder.put(deviceId.toString(), new AtomicInteger(1));
|
currentUnFinPlaceholder.put(deviceId.toString(), new AtomicInteger(1));
|
||||||
@@ -190,16 +198,8 @@ public class VideoPieceGetter {
|
|||||||
currentUnFinPlaceholder.size());
|
currentUnFinPlaceholder.size());
|
||||||
}
|
}
|
||||||
collection.values().forEach(faceSampleList -> {
|
collection.values().forEach(faceSampleList -> {
|
||||||
executor.execute(() -> {
|
|
||||||
AtomicBoolean isFirst = new AtomicBoolean(true);
|
|
||||||
faceSampleList.forEach(faceSample -> {
|
faceSampleList.forEach(faceSample -> {
|
||||||
if (!isFirst.get()) {
|
executor.execute(() -> {
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException ignore) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFirst.set(false);
|
|
||||||
// 处理关联设备:如果当前设备是某个主设备的配对设备,也处理主设备
|
// 处理关联设备:如果当前设备是某个主设备的配对设备,也处理主设备
|
||||||
if (pairDeviceMap.containsValue(faceSample.getDeviceId())) {
|
if (pairDeviceMap.containsValue(faceSample.getDeviceId())) {
|
||||||
pairDeviceMap.entrySet().stream()
|
pairDeviceMap.entrySet().stream()
|
||||||
@@ -212,12 +212,12 @@ public class VideoPieceGetter {
|
|||||||
AtomicInteger pairCount = currentUnFinPlaceholder.get(pairDeviceId.toString());
|
AtomicInteger pairCount = currentUnFinPlaceholder.get(pairDeviceId.toString());
|
||||||
if (pairCount != null) {
|
if (pairCount != null) {
|
||||||
int remaining = pairCount.decrementAndGet();
|
int remaining = pairCount.decrementAndGet();
|
||||||
log.info("[计数器更新] 关联设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
|
// log.info("[计数器更新] 关联设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
|
||||||
pairDeviceId, remaining, currentUnFinPlaceholder.size());
|
// pairDeviceId, remaining, currentUnFinPlaceholder.size());
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
currentUnFinPlaceholder.remove(pairDeviceId.toString());
|
currentUnFinPlaceholder.remove(pairDeviceId.toString());
|
||||||
log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
|
// log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
|
||||||
pairDeviceId, currentUnFinPlaceholder.size());
|
// pairDeviceId, currentUnFinPlaceholder.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,34 +229,25 @@ public class VideoPieceGetter {
|
|||||||
AtomicInteger count = currentUnFinPlaceholder.get(faceSample.getDeviceId().toString());
|
AtomicInteger count = currentUnFinPlaceholder.get(faceSample.getDeviceId().toString());
|
||||||
if (count != null) {
|
if (count != null) {
|
||||||
int remaining = count.decrementAndGet();
|
int remaining = count.decrementAndGet();
|
||||||
log.info("[计数器更新] 设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
|
// log.info("[计数器更新] 设备 {} 计数器递减,剩余={}, currentUnFinPlaceholder总数={}",
|
||||||
faceSample.getDeviceId(), remaining, currentUnFinPlaceholder.size());
|
// faceSample.getDeviceId(), remaining, currentUnFinPlaceholder.size());
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
|
currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
|
||||||
log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
|
// log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除,剩余设备数={}",
|
||||||
faceSample.getDeviceId(), currentUnFinPlaceholder.size());
|
// faceSample.getDeviceId(), currentUnFinPlaceholder.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有templateId,检查是否所有placeholder都已满足
|
// 如果有templateId,检查是否所有placeholder都已满足
|
||||||
if (templatePlaceholder != null) {
|
if (templatePlaceholder != null) {
|
||||||
int totalPlaceholderCount = templatePlaceholder.size();
|
|
||||||
int remainingCount = currentUnFinPlaceholder.values().stream()
|
|
||||||
.mapToInt(AtomicInteger::get)
|
|
||||||
.sum();
|
|
||||||
log.info("[进度检查] 当前进度:已完成 {}/{},剩余 {} 个placeholder未满足,剩余设备数={}",
|
|
||||||
totalPlaceholderCount - remainingCount, totalPlaceholderCount, remainingCount,
|
|
||||||
currentUnFinPlaceholder.size());
|
|
||||||
|
|
||||||
if (currentUnFinPlaceholder.isEmpty()) {
|
if (currentUnFinPlaceholder.isEmpty()) {
|
||||||
if (!invoke.get()) {
|
// 使用 compareAndSet 保证原子性,避免多线程重复调用 callback
|
||||||
invoke.set(true);
|
if (invoke.compareAndSet(false, true)) {
|
||||||
log.info("[Callback调用] 所有placeholder已满足,currentUnFinPlaceholder为空,提前调用callback");
|
log.info("[Callback调用] 所有placeholder已满足,currentUnFinPlaceholder为空,提前调用callback");
|
||||||
task.getCallback().onInvoke();
|
task.getCallback().onInvoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
if (task.faceId != null) {
|
if (task.faceId != null) {
|
||||||
// 经过切片后,可能有新的人脸切片生成,需要更新人脸状态
|
// 经过切片后,可能有新的人脸切片生成,需要更新人脸状态
|
||||||
templateRepository.getTemplateListByScenicId(scenicId).forEach(template -> {
|
templateRepository.getTemplateListByScenicId(scenicId).forEach(template -> {
|
||||||
@@ -265,19 +256,18 @@ public class VideoPieceGetter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000L);
|
|
||||||
log.info("executor等待被结束![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
|
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
executor.awaitTermination(3, TimeUnit.MINUTES);
|
executor.awaitTermination(3, TimeUnit.MINUTES);
|
||||||
log.info("executor已结束![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
|
log.info("executor已结束![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
|
||||||
executor.close();
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
log.info("executor已中断![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
|
log.info("executor已中断![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
|
||||||
} finally {
|
} finally {
|
||||||
|
executor.close();
|
||||||
if (null != task.getCallback()) {
|
if (null != task.getCallback()) {
|
||||||
if (!invoke.get()) {
|
// 使用 compareAndSet 保证原子性,避免多线程重复调用 callback
|
||||||
invoke.set(true);
|
if (invoke.compareAndSet(false, true)) {
|
||||||
log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}",
|
log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}",
|
||||||
currentUnFinPlaceholder.size());
|
currentUnFinPlaceholder.size());
|
||||||
task.getCallback().onInvoke();
|
task.getCallback().onInvoke();
|
||||||
@@ -735,4 +725,27 @@ public class VideoPieceGetter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载景区的设备配对关系
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 设备配对关系Map,无配对关系时返回空Map(而非null,避免重复查询)
|
||||||
|
*/
|
||||||
|
private Map<Long, Long> loadPairDeviceMap(Long scenicId) {
|
||||||
|
List<DeviceV2DTO> allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId);
|
||||||
|
if (allDeviceByScenicId == null || allDeviceByScenicId.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<Long, Long> pairDeviceMap = new HashMap<>();
|
||||||
|
allDeviceByScenicId.forEach(device -> {
|
||||||
|
Long deviceId = device.getId();
|
||||||
|
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
|
||||||
|
Long pairDevice = deviceConfig.getLong("pair_device");
|
||||||
|
if (pairDevice != null) {
|
||||||
|
pairDeviceMap.putIfAbsent(deviceId, pairDevice);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.debug("加载景区 {} 设备配对关系,共 {} 对", scenicId, pairDeviceMap.size());
|
||||||
|
return pairDeviceMap.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(pairDeviceMap);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/main/java/com/ycwl/basic/utils/Ipv4CidrMatcher.java
Normal file
91
src/main/java/com/ycwl/basic/utils/Ipv4CidrMatcher.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package com.ycwl.basic.utils;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPv4 CIDR 匹配工具
|
||||||
|
*/
|
||||||
|
public final class Ipv4CidrMatcher {
|
||||||
|
|
||||||
|
private Ipv4CidrMatcher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 IPv4 是否命中 CIDR(如:100.64.0.0/24)。
|
||||||
|
* - 若 cidr 不包含 '/',则按“完全相等”处理。
|
||||||
|
* - 仅支持 IPv4;IPv6 返回 false。
|
||||||
|
*/
|
||||||
|
public static boolean matches(String ip, String cidr) {
|
||||||
|
if (StringUtils.isBlank(ip) || StringUtils.isBlank(cidr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanedIp = ip.trim();
|
||||||
|
if (cleanedIp.contains(":")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanedCidr = cidr.trim();
|
||||||
|
int slashIndex = cleanedCidr.indexOf('/');
|
||||||
|
if (slashIndex < 0) {
|
||||||
|
return cleanedIp.equals(cleanedCidr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String networkPart = cleanedCidr.substring(0, slashIndex).trim();
|
||||||
|
String prefixPart = cleanedCidr.substring(slashIndex + 1).trim();
|
||||||
|
|
||||||
|
Integer ipInt = parseIpv4ToInt(cleanedIp);
|
||||||
|
Integer networkInt = parseIpv4ToInt(networkPart);
|
||||||
|
if (ipInt == null || networkInt == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer prefixLength = parsePrefixLength(prefixPart);
|
||||||
|
if (prefixLength == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mask = prefixLength == 0 ? 0 : (-1 << (32 - prefixLength));
|
||||||
|
return (ipInt & mask) == (networkInt & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer parsePrefixLength(String prefixPart) {
|
||||||
|
try {
|
||||||
|
int prefixLength = Integer.parseInt(prefixPart);
|
||||||
|
if (prefixLength < 0 || prefixLength > 32) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return prefixLength;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer parseIpv4ToInt(String ip) {
|
||||||
|
if (StringUtils.isBlank(ip)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] parts = ip.trim().split("\\.");
|
||||||
|
if (parts.length != 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long result = 0;
|
||||||
|
for (String part : parts) {
|
||||||
|
int value;
|
||||||
|
try {
|
||||||
|
value = Integer.parseInt(part);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 0 || value > 255) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = (result << 8) | value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,17 @@ spring:
|
|||||||
lifecycle:
|
lifecycle:
|
||||||
timeout-per-shutdown-phase: 60s
|
timeout-per-shutdown-phase: 60s
|
||||||
|
|
||||||
|
# ClickHouse 配置
|
||||||
|
clickhouse:
|
||||||
|
enabled: true # true=ClickHouse, false=MySQL兜底
|
||||||
|
datasource:
|
||||||
|
jdbc-url: jdbc:clickhouse://100.64.0.7:8123/zt
|
||||||
|
username: default
|
||||||
|
password: ZhEnTuAi
|
||||||
|
driver-class-name: com.clickhouse.jdbc.ClickHouseDriver
|
||||||
|
maximum-pool-size: 10
|
||||||
|
minimum-idle: 2
|
||||||
|
|
||||||
# Feign配置(简化版,基于Nacos服务发现)
|
# Feign配置(简化版,基于Nacos服务发现)
|
||||||
feign:
|
feign:
|
||||||
client:
|
client:
|
||||||
@@ -45,3 +56,11 @@ logging:
|
|||||||
|
|
||||||
zhipu:
|
zhipu:
|
||||||
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
||||||
|
|
||||||
|
# 边缘 Worker 接口安全(仅允许 100.64.0.0/24 网段访问)
|
||||||
|
puzzle:
|
||||||
|
edge:
|
||||||
|
worker:
|
||||||
|
security:
|
||||||
|
enabled: true
|
||||||
|
allowed-ip-cidr: 100.64.0.0/24
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ spring:
|
|||||||
lifecycle:
|
lifecycle:
|
||||||
timeout-per-shutdown-phase: 60s
|
timeout-per-shutdown-phase: 60s
|
||||||
|
|
||||||
|
# ClickHouse 配置
|
||||||
|
clickhouse:
|
||||||
|
enabled: true # 设置为 true 启用 ClickHouse,false 使用 MySQL 兜底
|
||||||
|
datasource:
|
||||||
|
jdbc-url: jdbc:clickhouse://100.64.0.7:8123/zt
|
||||||
|
username: default
|
||||||
|
password: ZhEnTuAi
|
||||||
|
driver-class-name: com.clickhouse.jdbc.ClickHouseDriver
|
||||||
|
maximum-pool-size: 20
|
||||||
|
minimum-idle: 5
|
||||||
|
|
||||||
# 生产环境日志级别
|
# 生产环境日志级别
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
@@ -15,3 +26,11 @@ logging:
|
|||||||
|
|
||||||
zhipu:
|
zhipu:
|
||||||
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
api-key: a331e0fcf3f74518818b8e5129b79058.RXuUxUUjKdcxbF4L
|
||||||
|
|
||||||
|
# 边缘 Worker 接口安全(仅允许 100.64.0.0/24 网段访问)
|
||||||
|
puzzle:
|
||||||
|
edge:
|
||||||
|
worker:
|
||||||
|
security:
|
||||||
|
enabled: true
|
||||||
|
allowed-ip-cidr: 100.64.0.0/24
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
</delete>
|
</delete>
|
||||||
<select id="list" resultType="com.ycwl.basic.model.pc.broker.resp.BrokerRespVO">
|
<select id="list" resultType="com.ycwl.basic.model.pc.broker.resp.BrokerRespVO">
|
||||||
select b.id, scenic_id, b.`name`, b.phone, b.broker_enable, b.broker_rate, b.status,
|
select b.id, scenic_id, b.`name`, b.phone, b.broker_enable, b.broker_rate, b.status,
|
||||||
(select count(1) from t_stats_record s where s.action = "CODE_SCAN" and s.identifier = b.id) as broker_scan_count,
|
|
||||||
(select count(1) from broker_record r where r.broker_id = b.id) as broker_order_count,
|
(select count(1) from broker_record r where r.broker_id = b.id) as broker_order_count,
|
||||||
(select sum(order_price) from broker_record r where r.broker_id = b.id) as broker_order_amount,
|
(select sum(order_price) from broker_record r where r.broker_id = b.id) as broker_order_amount,
|
||||||
(select min(r.create_time) from broker_record r where r.broker_id = b.id) as first_broker_date,
|
(select min(r.create_time) from broker_record r where r.broker_id = b.id) as first_broker_date,
|
||||||
|
|||||||
@@ -107,4 +107,30 @@
|
|||||||
</set>
|
</set>
|
||||||
where id = #{id}
|
where id = #{id}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<!-- 按日期统计分销员订单数据(不含扫码统计) -->
|
||||||
|
<select id="getDailyOrderStats" resultType="java.util.HashMap">
|
||||||
|
WITH RECURSIVE
|
||||||
|
date_series AS (SELECT DATE(#{startTime}) AS date
|
||||||
|
UNION ALL
|
||||||
|
SELECT DATE_ADD(date, INTERVAL 1 DAY)
|
||||||
|
FROM date_series
|
||||||
|
WHERE date < DATE(#{endTime}))
|
||||||
|
SELECT ds.date,
|
||||||
|
COALESCE(os.orderCount, 0) AS orderCount,
|
||||||
|
COALESCE(os.totalOrderPrice, 0) AS totalOrderPrice,
|
||||||
|
COALESCE(os.totalBrokerPrice, 0) AS totalBrokerPrice
|
||||||
|
FROM date_series ds
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DATE(create_time) AS date,
|
||||||
|
COUNT(DISTINCT id) AS orderCount,
|
||||||
|
COALESCE(SUM(order_price), 0) AS totalOrderPrice,
|
||||||
|
COALESCE(SUM(broker_price), 0) AS totalBrokerPrice
|
||||||
|
FROM broker_record
|
||||||
|
WHERE broker_id = #{brokerId}
|
||||||
|
AND DATE(create_time) BETWEEN DATE(#{startTime}) AND DATE(#{endTime})
|
||||||
|
GROUP BY DATE(create_time)
|
||||||
|
) os ON ds.date = os.date
|
||||||
|
ORDER BY ds.date
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
108
src/main/resources/mapper/PuzzleEdgeRenderTaskMapper.xml
Normal file
108
src/main/resources/mapper/PuzzleEdgeRenderTaskMapper.xml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ycwl.basic.puzzle.edge.mapper.PuzzleEdgeRenderTaskMapper">
|
||||||
|
|
||||||
|
<resultMap id="BaseResultMap" type="com.ycwl.basic.puzzle.edge.entity.PuzzleEdgeRenderTaskEntity">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="record_id" property="recordId"/>
|
||||||
|
<result column="template_id" property="templateId"/>
|
||||||
|
<result column="template_code" property="templateCode"/>
|
||||||
|
<result column="scenic_id" property="scenicId"/>
|
||||||
|
<result column="face_id" property="faceId"/>
|
||||||
|
<result column="content_hash" property="contentHash"/>
|
||||||
|
<result column="status" property="status"/>
|
||||||
|
<result column="worker_id" property="workerId"/>
|
||||||
|
<result column="lease_expire_time" property="leaseExpireTime"/>
|
||||||
|
<result column="attempt_count" property="attemptCount"/>
|
||||||
|
<result column="output_format" property="outputFormat"/>
|
||||||
|
<result column="output_quality" property="outputQuality"/>
|
||||||
|
<result column="original_object_key" property="originalObjectKey"/>
|
||||||
|
<result column="cropped_object_key" property="croppedObjectKey"/>
|
||||||
|
<result column="payload_json" property="payloadJson"/>
|
||||||
|
<result column="error_message" property="errorMessage"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="update_time" property="updateTime"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
id, record_id, template_id, template_code, scenic_id, face_id, content_hash,
|
||||||
|
status, worker_id, lease_expire_time, attempt_count,
|
||||||
|
output_format, output_quality,
|
||||||
|
original_object_key, cropped_object_key,
|
||||||
|
payload_json, error_message,
|
||||||
|
create_time, update_time
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="getById" resultMap="BaseResultMap">
|
||||||
|
SELECT <include refid="Base_Column_List"/>
|
||||||
|
FROM puzzle_edge_render_task
|
||||||
|
WHERE id = #{id}
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.ycwl.basic.puzzle.edge.entity.PuzzleEdgeRenderTaskEntity"
|
||||||
|
useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO puzzle_edge_render_task (
|
||||||
|
record_id, template_id, template_code, scenic_id, face_id, content_hash,
|
||||||
|
status, worker_id, lease_expire_time, attempt_count,
|
||||||
|
output_format, output_quality,
|
||||||
|
original_object_key, cropped_object_key,
|
||||||
|
payload_json, error_message,
|
||||||
|
create_time, update_time
|
||||||
|
) VALUES (
|
||||||
|
#{recordId}, #{templateId}, #{templateCode}, #{scenicId}, #{faceId}, #{contentHash},
|
||||||
|
#{status}, #{workerId}, #{leaseExpireTime}, #{attemptCount},
|
||||||
|
#{outputFormat}, #{outputQuality},
|
||||||
|
#{originalObjectKey}, #{croppedObjectKey},
|
||||||
|
#{payloadJson}, #{errorMessage},
|
||||||
|
NOW(), NOW()
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="findNextClaimableTaskId" resultType="java.lang.Long">
|
||||||
|
SELECT id
|
||||||
|
FROM puzzle_edge_render_task
|
||||||
|
WHERE status = 0
|
||||||
|
OR (status = 1 AND lease_expire_time IS NOT NULL AND lease_expire_time < NOW())
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="claimTask">
|
||||||
|
UPDATE puzzle_edge_render_task
|
||||||
|
SET worker_id = #{workerId},
|
||||||
|
status = 1,
|
||||||
|
lease_expire_time = #{leaseExpireTime},
|
||||||
|
attempt_count = attempt_count + 1,
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE id = #{taskId}
|
||||||
|
AND (
|
||||||
|
status = 0
|
||||||
|
OR (status = 1 AND lease_expire_time IS NOT NULL AND lease_expire_time < NOW())
|
||||||
|
)
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markSuccess">
|
||||||
|
UPDATE puzzle_edge_render_task
|
||||||
|
SET status = 2,
|
||||||
|
lease_expire_time = NULL,
|
||||||
|
error_message = NULL,
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE id = #{taskId}
|
||||||
|
AND worker_id = #{workerId}
|
||||||
|
AND status = 1
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markFail">
|
||||||
|
UPDATE puzzle_edge_render_task
|
||||||
|
SET status = 3,
|
||||||
|
lease_expire_time = NULL,
|
||||||
|
error_message = #{errorMessage},
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE id = #{taskId}
|
||||||
|
AND worker_id = #{workerId}
|
||||||
|
AND status = 1
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -512,4 +512,15 @@
|
|||||||
SELECT COUNT(*) FROM member_source
|
SELECT COUNT(*) FROM member_source
|
||||||
WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1
|
WHERE face_id = #{faceId} AND `type` = #{type} AND is_free = 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listSourceByFaceRelation" resultType="com.ycwl.basic.model.pc.source.entity.SourceEntity">
|
||||||
|
SELECT s.*
|
||||||
|
FROM member_source ms
|
||||||
|
INNER JOIN source s ON ms.source_id = s.id
|
||||||
|
WHERE ms.face_id = #{faceId}
|
||||||
|
<if test="type != null">
|
||||||
|
AND ms.type = #{type}
|
||||||
|
</if>
|
||||||
|
ORDER BY s.create_time DESC
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -531,4 +531,24 @@
|
|||||||
order by r.create_time desc limit 1
|
order by r.create_time desc limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 统计分销员扫码次数 -->
|
||||||
|
<select id="countBrokerScanCount" resultType="java.lang.Integer">
|
||||||
|
SELECT count(1) AS count
|
||||||
|
FROM t_stats_record
|
||||||
|
WHERE action = 'CODE_SCAN'
|
||||||
|
AND identifier = #{brokerId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按日期统计分销员扫码数据 -->
|
||||||
|
<select id="getDailyScanStats" resultType="java.util.HashMap">
|
||||||
|
SELECT
|
||||||
|
DATE(create_time) AS date,
|
||||||
|
COUNT(DISTINCT id) AS scanCount
|
||||||
|
FROM t_stats_record
|
||||||
|
WHERE action = 'CODE_SCAN'
|
||||||
|
AND identifier = #{brokerId}
|
||||||
|
AND DATE(create_time) BETWEEN DATE(#{startTime}) AND DATE(#{endTime})
|
||||||
|
GROUP BY DATE(create_time)
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<if test="scenicId!= null">scenic_id = #{scenicId}, </if>
|
<if test="scenicId!= null">scenic_id = #{scenicId}, </if>
|
||||||
<if test="taskParams!= null">task_params = #{taskParams}, </if>
|
<if test="taskParams!= null">task_params = #{taskParams}, </if>
|
||||||
<if test="videoUrl!= null">video_url = #{videoUrl}, </if>
|
<if test="videoUrl!= null">video_url = #{videoUrl}, </if>
|
||||||
<if test="status!= null">status = #{status}, </if>
|
<if test="status!= null">`status` = #{status}, </if>
|
||||||
<if test="result!= null">result = #{result}, </if>
|
<if test="result!= null">result = #{result}, </if>
|
||||||
</set>
|
</set>
|
||||||
where id = #{id}
|
where id = #{id}
|
||||||
@@ -151,4 +151,25 @@
|
|||||||
order by create_time desc
|
order by create_time desc
|
||||||
limit 1
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据 face_id 列表统计已完成任务的用户数 -->
|
||||||
|
<select id="countCompletedTaskMembersByFaceIds" resultType="java.lang.Integer">
|
||||||
|
SELECT COUNT(DISTINCT mv.member_id) AS count
|
||||||
|
FROM member_video mv
|
||||||
|
WHERE mv.face_id IN
|
||||||
|
<foreach collection="faceIds" item="faceId" open="(" separator="," close=")">
|
||||||
|
#{faceId}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据 face_id 列表统计已完成任务数 -->
|
||||||
|
<select id="countCompletedTasksByFaceIds" resultType="java.lang.Integer">
|
||||||
|
SELECT COUNT(1) AS count
|
||||||
|
FROM task
|
||||||
|
WHERE status = 1
|
||||||
|
AND face_id IN
|
||||||
|
<foreach collection="faceIds" item="faceId" open="(" separator="," close=")">
|
||||||
|
#{faceId}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.ycwl.basic.integration.common.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.config.IntegrationProperties;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class IntegrationFallbackServiceTest {
|
||||||
|
|
||||||
|
private IntegrationFallbackService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new IntegrationFallbackService(new IntegrationProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnCachedValueWhenCacheExists() {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
String cacheKey = "k1";
|
||||||
|
|
||||||
|
// 第一次调用,缓存结果
|
||||||
|
service.executeWithFallback(serviceName, cacheKey, () -> "cached", String.class);
|
||||||
|
|
||||||
|
// 第二次调用,应直接返回缓存,不调用远程
|
||||||
|
AtomicInteger remoteCalls = new AtomicInteger();
|
||||||
|
String result = service.executeWithFallback(serviceName, cacheKey, () -> {
|
||||||
|
remoteCalls.incrementAndGet();
|
||||||
|
return "remote";
|
||||||
|
}, String.class);
|
||||||
|
|
||||||
|
assertEquals("cached", result);
|
||||||
|
assertEquals(0, remoteCalls.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCallRemoteWhenNoCacheAndCacheResult() {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
String cacheKey = "k2";
|
||||||
|
|
||||||
|
AtomicInteger remoteCalls = new AtomicInteger();
|
||||||
|
String result = service.executeWithFallback(serviceName, cacheKey, () -> {
|
||||||
|
remoteCalls.incrementAndGet();
|
||||||
|
return "remote";
|
||||||
|
}, String.class);
|
||||||
|
|
||||||
|
assertEquals("remote", result);
|
||||||
|
assertEquals(1, remoteCalls.get());
|
||||||
|
assertTrue(service.hasFallbackCache(serviceName, cacheKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowWhenRemoteFailsAndNoCache() {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
String cacheKey = "k3";
|
||||||
|
|
||||||
|
RuntimeException ex = assertThrows(RuntimeException.class, () ->
|
||||||
|
service.executeWithFallback(serviceName, cacheKey, () -> {
|
||||||
|
throw new RuntimeException("boom");
|
||||||
|
}, String.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("boom", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldClearCache() {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
String cacheKey = "k4";
|
||||||
|
|
||||||
|
service.executeWithFallback(serviceName, cacheKey, () -> "value", String.class);
|
||||||
|
assertTrue(service.hasFallbackCache(serviceName, cacheKey));
|
||||||
|
|
||||||
|
service.clearFallbackCache(serviceName, cacheKey);
|
||||||
|
assertFalse(service.hasFallbackCache(serviceName, cacheKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldClearAllCache() {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
|
||||||
|
service.executeWithFallback(serviceName, "key1", () -> "v1", String.class);
|
||||||
|
service.executeWithFallback(serviceName, "key2", () -> "v2", String.class);
|
||||||
|
|
||||||
|
service.clearAllFallbackCache(serviceName);
|
||||||
|
|
||||||
|
assertFalse(service.hasFallbackCache(serviceName, "key1"));
|
||||||
|
assertFalse(service.hasFallbackCache(serviceName, "key2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetStats() {
|
||||||
|
String serviceName = "zt-device";
|
||||||
|
|
||||||
|
service.executeWithFallback(serviceName, "d1", () -> "v1", String.class);
|
||||||
|
service.executeWithFallback(serviceName, "d2", () -> "v2", String.class);
|
||||||
|
|
||||||
|
IntegrationFallbackService.FallbackCacheStats stats = service.getFallbackCacheStats(serviceName);
|
||||||
|
|
||||||
|
assertEquals(serviceName, stats.getServiceName());
|
||||||
|
assertEquals(2, stats.getTotalCacheCount());
|
||||||
|
assertEquals(1, stats.getCacheTtlMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOnlyCallRemoteOnceWithConcurrentRequests() throws InterruptedException {
|
||||||
|
String serviceName = "zt-scenic";
|
||||||
|
String cacheKey = "concurrent-test";
|
||||||
|
int threadCount = 10;
|
||||||
|
|
||||||
|
AtomicInteger remoteCalls = new AtomicInteger();
|
||||||
|
CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
CountDownLatch doneLatch = new CountDownLatch(threadCount);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||||
|
|
||||||
|
// 启动多个线程同时请求
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
startLatch.await(); // 等待同时开始
|
||||||
|
service.executeWithFallback(serviceName, cacheKey, () -> {
|
||||||
|
remoteCalls.incrementAndGet();
|
||||||
|
try {
|
||||||
|
Thread.sleep(50); // 模拟远程调用耗时
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
return "result";
|
||||||
|
}, String.class);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} finally {
|
||||||
|
doneLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startLatch.countDown(); // 同时放行所有线程
|
||||||
|
doneLatch.await(); // 等待所有线程完成
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
|
// 互斥锁生效:只有一个线程真正调用了远程
|
||||||
|
assertEquals(1, remoteCalls.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user