feat(device): 添加设备拍摄统计数据接口

- 新增设备拍摄统计功能,支持查询拍摄总数、拍摄人数、售出张数等统计信息
- 实现设备拍摄时间线功能,按5分钟分桶统计type=2的拍摄数量
- 添加SourceMapper的数据访问方法,包括getDeviceSourceStats和getDeviceSourceTimeline
- 集成日期时间参数处理,支持自定义统计时间段
- 实现时间轴数据补零逻辑,确保时间线图表显示连续性
- 添加相应的响应对象DeviceSourceStatsVO和DeviceSourceTimelineVO
This commit is contained in:
2026-02-12 16:40:21 +08:00
parent 39bdd02566
commit 55d3d36b81
5 changed files with 187 additions and 0 deletions

View File

@@ -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());
}
}
} }

View File

@@ -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);
} }

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 &gt;= #{startTime}
AND so.create_time &lt;= #{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 &gt;= #{startTime}
AND create_time &lt;= #{endTime}
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
ORDER BY time
</select>
</mapper> </mapper>