feat: C3 埋点事件上报(/app/events 匿名落库)

This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-21 06:25:25 +08:00
1 parent fa4977a71d
commit 6d3997cbae
6 files changed
+168

No files matched your search

@@ -0,0 +1,35 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppEventService;
import com.fruitshorts.app.vo.AppEventReqVO;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.web.WebUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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;
/** 客户端埋点上报。 */
@Tag(name = "埋点")
@RestController
@RequestMapping("/app")
@RequiredArgsConstructor
public class AppEventController {
private final AppEventService eventService;
@Operation(summary = "埋点事件批量上报")
@PostMapping("/events")
public Result<Void> report(@Valid @RequestBody AppEventReqVO req, HttpServletRequest request) {
eventService.save(req, SecurityUtils.getUserIdOrNull(), WebUtils.getClientIp(request));
return Result.ok();
}
}
@@ -0,0 +1,33 @@
// Author: fruit.com.cn
package com.fruitshorts.app.dal.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
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.time.LocalDateTime;
/** 埋点事件落库记录,为后续分析/漏斗预留原始数据。 */
@Data
@TableName("app_event_log")
public class AppEventLog {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String eventName;
private LocalDateTime eventTime;
private Long userId;
private String deviceId;
private String sessionId;
private String page;
private String properties;
private String clientIp;
private LocalDateTime createdAt;
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,10 @@
// Author: fruit.com.cn
package com.fruitshorts.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.app.dal.entity.AppEventLog;
/** 埋点事件 Mapper。 */
public interface AppEventLogMapper extends BaseMapper<AppEventLog> {
}
@@ -0,0 +1,40 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fruitshorts.app.dal.entity.AppEventLog;
import com.fruitshorts.app.dal.mapper.AppEventLogMapper;
import com.fruitshorts.app.vo.AppEventReqVO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/** 埋点事件接收:批量落库,事件名与属性暂不校验、透传,口径后续与运营对齐。 */
@Service
@RequiredArgsConstructor
public class AppEventService {
private final AppEventLogMapper eventLogMapper;
private final ObjectMapper objectMapper;
@SneakyThrows
public void save(AppEventReqVO req, Long userId, String clientIp) {
for (AppEventReqVO.EventItem item : req.getEvents()) {
AppEventLog log = new AppEventLog();
log.setEventName(item.getEventName());
log.setEventTime(item.getEventTime() == null ? LocalDateTime.now() : item.getEventTime());
log.setUserId(userId);
log.setDeviceId(req.getDeviceId());
log.setSessionId(req.getSessionId());
log.setPage(item.getPage());
log.setProperties(item.getProperties() == null ? null
: objectMapper.writeValueAsString(item.getProperties()));
log.setClientIp(clientIp);
log.setCreatedAt(LocalDateTime.now());
eventLogMapper.insert(log);
}
}
}
@@ -0,0 +1,48 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/** 埋点事件批量上报请求。 */
@Data
public class AppEventReqVO {
@Schema(description = "设备ID")
private String deviceId;
@Schema(description = "会话ID")
private String sessionId;
@Schema(description = "事件列表")
@NotEmpty(message = "事件列表不能为空")
@Size(max = 50, message = "单次最多上报 50 条事件")
@Valid
private List<EventItem> events;
@Data
public static class EventItem {
@Schema(description = "事件名,如 playback_start/playback_end/pay_success")
@NotBlank(message = "事件名不能为空")
private String eventName;
@Schema(description = "事件发生时间,缺省用服务端接收时间")
private LocalDateTime eventTime;
@Schema(description = "页面/场景标识")
private String page;
@Schema(description = "事件属性(key-value)")
private Map<String, Object> properties;
}
}
@@ -44,6 +44,8 @@ public class SecurityConfig {
/* 启动配置与公告:客户端登录前就要拿(POST /app/creator/apply 等仍要登录) */
"/app/config",
"/app/notices",
/* 埋点上报:游客浏览也要记录,匿名可访问 */
"/app/events",
/* 媒资:封面与视频用 <img>/<video> 直接访问,无法带请求头。
片源的防盗链由 fruitshorts.media.signature-required 控制。 */
"/files/**",