You've already forked FrameTour-BE
feat(device): 添加设备拍摄统计数据接口
- 新增设备拍摄统计功能,支持查询拍摄总数、拍摄人数、售出张数等统计信息 - 实现设备拍摄时间线功能,按5分钟分桶统计type=2的拍摄数量 - 添加SourceMapper的数据访问方法,包括getDeviceSourceStats和getDeviceSourceTimeline - 集成日期时间参数处理,支持自定义统计时间段 - 实现时间轴数据补零逻辑,确保时间线图表显示连续性 - 添加相应的响应对象DeviceSourceStatsVO和DeviceSourceTimelineVO
This commit is contained in:
@@ -7,13 +7,19 @@ import com.ycwl.basic.integration.device.dto.status.DeviceStatusDTO;
|
|||||||
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceConfigIntegrationService;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
|
||||||
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
import com.ycwl.basic.integration.device.service.DeviceStatusIntegrationService;
|
||||||
|
import com.ycwl.basic.mapper.SourceMapper;
|
||||||
|
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
|
||||||
|
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
|
||||||
import com.ycwl.basic.utils.ApiResponse;
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -32,6 +38,7 @@ public class DeviceV2Controller {
|
|||||||
private final DeviceIntegrationService deviceIntegrationService;
|
private final DeviceIntegrationService deviceIntegrationService;
|
||||||
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
private final DeviceConfigIntegrationService deviceConfigIntegrationService;
|
||||||
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
|
private final DeviceStatusIntegrationService deviceStatusIntegrationService;
|
||||||
|
private final SourceMapper sourceMapper;
|
||||||
|
|
||||||
// ========== 设备基础 CRUD 操作 ==========
|
// ========== 设备基础 CRUD 操作 ==========
|
||||||
|
|
||||||
@@ -387,4 +394,96 @@ public class DeviceV2Controller {
|
|||||||
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
|
return ApiResponse.fail("获取景区所有设备列表失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备拍摄统计 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/source-stats")
|
||||||
|
public ApiResponse<DeviceSourceStatsVO> getDeviceSourceStats(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
|
||||||
|
try {
|
||||||
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
|
// startDate:归到当天 00:00:00(未传则默认今天)
|
||||||
|
if (startDate != null) {
|
||||||
|
cal.setTime(startDate);
|
||||||
|
}
|
||||||
|
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||||
|
cal.set(java.util.Calendar.MINUTE, 0);
|
||||||
|
cal.set(java.util.Calendar.SECOND, 0);
|
||||||
|
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||||
|
startDate = cal.getTime();
|
||||||
|
// endDate:归到当天 23:59:59(未传则取 startDate 同一天)
|
||||||
|
if (endDate != null) {
|
||||||
|
cal.setTime(endDate);
|
||||||
|
}
|
||||||
|
cal.set(java.util.Calendar.HOUR_OF_DAY, 23);
|
||||||
|
cal.set(java.util.Calendar.MINUTE, 59);
|
||||||
|
cal.set(java.util.Calendar.SECOND, 59);
|
||||||
|
cal.set(java.util.Calendar.MILLISECOND, 999);
|
||||||
|
endDate = cal.getTime();
|
||||||
|
DeviceSourceStatsVO stats = sourceMapper.getDeviceSourceStats(id, startDate, endDate);
|
||||||
|
return ApiResponse.success(stats);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取设备拍摄统计失败, deviceId: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取设备拍摄统计失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备拍摄时间线:按 5 分钟分桶统计 type=2 的拍摄数量,空桶补 0
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/source-timeline")
|
||||||
|
public ApiResponse<List<DeviceSourceTimelineVO>> getDeviceSourceTimeline(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
|
||||||
|
try {
|
||||||
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
|
// startDate:归到当天 08:00:00(未传则默认今天)
|
||||||
|
if (startDate != null) {
|
||||||
|
cal.setTime(startDate);
|
||||||
|
}
|
||||||
|
cal.set(java.util.Calendar.HOUR_OF_DAY, 8);
|
||||||
|
cal.set(java.util.Calendar.MINUTE, 0);
|
||||||
|
cal.set(java.util.Calendar.SECOND, 0);
|
||||||
|
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||||
|
startDate = cal.getTime();
|
||||||
|
// endDate:归到当天 19:59:59(未传则取 startDate 同一天)
|
||||||
|
if (endDate != null) {
|
||||||
|
cal.setTime(endDate);
|
||||||
|
}
|
||||||
|
cal.set(java.util.Calendar.HOUR_OF_DAY, 19);
|
||||||
|
cal.set(java.util.Calendar.MINUTE, 59);
|
||||||
|
cal.set(java.util.Calendar.SECOND, 59);
|
||||||
|
cal.set(java.util.Calendar.MILLISECOND, 999);
|
||||||
|
endDate = cal.getTime();
|
||||||
|
|
||||||
|
// 查询有数据的桶
|
||||||
|
List<DeviceSourceTimelineVO> rawData = sourceMapper.getDeviceSourceTimeline(id, startDate, endDate);
|
||||||
|
|
||||||
|
// 将有数据的桶放入 Map,key 为对齐到 5 分钟的毫秒时间戳
|
||||||
|
Map<Long, Integer> dataMap = new HashMap<>();
|
||||||
|
for (DeviceSourceTimelineVO item : rawData) {
|
||||||
|
long aligned = (item.getTime().getTime() / 300_000) * 300_000;
|
||||||
|
dataMap.put(aligned, item.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成完整时间轴并补零
|
||||||
|
long startMs = (startDate.getTime() / 300_000) * 300_000;
|
||||||
|
long endMs = endDate.getTime();
|
||||||
|
List<DeviceSourceTimelineVO> timeline = new ArrayList<>();
|
||||||
|
for (long ts = startMs; ts <= endMs; ts += 300_000) {
|
||||||
|
timeline.add(new DeviceSourceTimelineVO(new Date(ts), dataMap.getOrDefault(ts, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.success(timeline);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取设备拍摄时间线失败, deviceId: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取设备拍摄时间线失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ycwl.basic.mapper;
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO;
|
||||||
|
import com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO;
|
||||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||||
import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity;
|
import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity;
|
||||||
@@ -203,4 +205,22 @@ public interface SourceMapper {
|
|||||||
List<SourceRespVO> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
|
List<SourceRespVO> pageDeletedByFaceId(SourceReqQuery sourceReqQuery);
|
||||||
|
|
||||||
MemberSourceEntity getMemberSourceById(Long id);
|
MemberSourceEntity getMemberSourceById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备拍摄统计:拍摄总数、拍摄人数、售出张数、赠送张数、售出人数
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 统计结果
|
||||||
|
*/
|
||||||
|
DeviceSourceStatsVO getDeviceSourceStats(Long deviceId, Date startTime, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 5 分钟分桶统计设备 type=2 的拍摄数量(仅返回有数据的桶)
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 有数据的时间桶列表
|
||||||
|
*/
|
||||||
|
List<DeviceSourceTimelineVO> getDeviceSourceTimeline(Long deviceId, Date startTime, Date endTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ycwl.basic.model.pc.device.resp;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备拍摄统计 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DeviceSourceStatsVO {
|
||||||
|
/** 拍摄总数(source type=2 记录数) */
|
||||||
|
private Integer totalShots;
|
||||||
|
/** 拍摄人数(关联的不同 face_id 数) */
|
||||||
|
private Integer totalFaces;
|
||||||
|
/** 售出张数(is_buy=1 的 member_source 记录数) */
|
||||||
|
private Integer soldCount;
|
||||||
|
/** 赠送张数(is_free=1 的 member_source 记录数) */
|
||||||
|
private Integer freeCount;
|
||||||
|
/** 售出人数(is_buy=1 的不同 face_id 数) */
|
||||||
|
private Integer soldFaceCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.model.pc.device.resp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备拍摄时间线数据点(5 分钟一个桶)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DeviceSourceTimelineVO {
|
||||||
|
/** 时间桶起始时刻 */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
|
||||||
|
private Date time;
|
||||||
|
/** 该桶内 source type=2 的记录数 */
|
||||||
|
private Integer count;
|
||||||
|
}
|
||||||
@@ -569,4 +569,30 @@
|
|||||||
<select id="getMemberSourceById" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
<select id="getMemberSourceById" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
|
||||||
SELECT * FROM member_source WHERE id = #{id}
|
SELECT * FROM member_source WHERE id = #{id}
|
||||||
</select>
|
</select>
|
||||||
|
<select id="getDeviceSourceStats" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceStatsVO">
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT so.id) AS totalShots,
|
||||||
|
COUNT(DISTINCT ms.face_id) AS totalFaces,
|
||||||
|
SUM(CASE WHEN ms.is_buy = 1 THEN 1 ELSE 0 END) AS soldCount,
|
||||||
|
SUM(CASE WHEN ms.is_free = 1 THEN 1 ELSE 0 END) AS freeCount,
|
||||||
|
COUNT(DISTINCT CASE WHEN ms.is_buy = 1 THEN ms.face_id END) AS soldFaceCount
|
||||||
|
FROM source so
|
||||||
|
LEFT JOIN member_source ms ON ms.source_id = so.id AND ms.deleted = 0
|
||||||
|
WHERE so.device_id = #{deviceId}
|
||||||
|
AND so.type = 2
|
||||||
|
AND so.create_time >= #{startTime}
|
||||||
|
AND so.create_time <= #{endTime}
|
||||||
|
</select>
|
||||||
|
<select id="getDeviceSourceTimeline" resultType="com.ycwl.basic.model.pc.device.resp.DeviceSourceTimelineVO">
|
||||||
|
SELECT
|
||||||
|
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(create_time) / 300) * 300) AS time,
|
||||||
|
COUNT(*) AS count
|
||||||
|
FROM source
|
||||||
|
WHERE device_id = #{deviceId}
|
||||||
|
AND type = 2
|
||||||
|
AND create_time >= #{startTime}
|
||||||
|
AND create_time <= #{endTime}
|
||||||
|
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
|
||||||
|
ORDER BY time
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user