Files
digital-psychology/apps/api/internal/handler/analytics.go
T
jackyu66gitandCursor 7ab9add5dd
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 02:26:16 +08:00

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