落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
58 lines
1.6 KiB
Go
58 lines
1.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/service/analytics"
|
|
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
|
)
|
|
|
|
// AnalyticsHandler serves POST /api/v1/analytics/events (DeviceAuth).
|
|
type AnalyticsHandler struct {
|
|
Svc *analytics.Service
|
|
}
|
|
|
|
// Register mounts analytics routes on a DeviceAuth group.
|
|
func (h *AnalyticsHandler) Register(api *gin.RouterGroup) {
|
|
g := api.Group("/analytics")
|
|
g.POST("/events", h.Ingest)
|
|
}
|
|
|
|
func (h *AnalyticsHandler) Ingest(c *gin.Context) {
|
|
userID, ok := middleware.UserIDFromContext(c)
|
|
if !ok {
|
|
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
|
|
return
|
|
}
|
|
deviceKey := c.GetHeader(middleware.DeviceKeyHeader)
|
|
if deviceKey == "" {
|
|
response.Fail(c, http.StatusBadRequest, 40020, "device key required")
|
|
return
|
|
}
|
|
var body struct {
|
|
Items []analytics.EventIn `json:"items"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Fail(c, http.StatusBadRequest, 40021, "invalid body")
|
|
return
|
|
}
|
|
res, err := h.Svc.Ingest(c.Request.Context(), userID, deviceKey, body.Items)
|
|
if err != nil {
|
|
if errors.Is(err, analytics.ErrTooManyItems) {
|
|
response.Fail(c, http.StatusBadRequest, 40022, "too many items")
|
|
return
|
|
}
|
|
if errors.Is(err, analytics.ErrInvalidBatch) {
|
|
response.Fail(c, http.StatusBadRequest, 40023, "invalid batch")
|
|
return
|
|
}
|
|
response.Fail(c, http.StatusInternalServerError, 50020, "ingest failed")
|
|
return
|
|
}
|
|
response.OK(c, res)
|
|
}
|