feat(phase2): 数据回传聚合与可信分账(F09/F18)

- internal/playback: 播放事件存储/MA码维度聚合/分账结算(CP60/平台34/服务费6)
- service: ReportPlayback(链上状态门禁)/PlaybackSummary/ComputeSettlement
- api: /data/playback, /data/playback-summary, /settlement/compute
- 分账取余兜底无丢分; 未知/已下架MA码回传被拒
- 13项新测试通过; 端到端验证: 回传3条→聚合40元→分账24/13.6/2.4
This commit is contained in:
selfrelease
2026-06-14 17:00:57 +08:00
parent a329d4906b
commit f44c53c5bb
8 changed files with 389 additions and 14 deletions
+64
View File
@@ -4,6 +4,7 @@ package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/tcs-iptv/tcs/internal/chain"
@@ -42,6 +43,9 @@ func (h *Handler) Register(rg *gin.RouterGroup) {
rg.GET("/content/episodes", h.listEpisodes) // 列出集级哈希
rg.GET("/content/reviews", h.listReviews) // 送审待办队列(待审/待发码)
rg.GET("/content/list", h.listContents) // 内容队列(待入库/待发布/待注入)
rg.POST("/data/playback", h.reportPlayback) // 播放数据回传(需求9
rg.GET("/data/playback-summary", h.playbackSummary) // 按MA码聚合可信播放数据(需求9/21)
rg.POST("/settlement/compute", h.computeSettlement) // 基于可信播放数据分账(需求21
}
func roleOf(c *gin.Context) chain.Role {
@@ -386,3 +390,63 @@ func (h *Handler) listContents(c *gin.Context) {
}
httpx.OK(c, gin.H{"contents": list, "count": len(list)})
}
// ---- 二期:播放数据回传与分账(需求9/21) ----
type playbackReq struct {
PlatformID string `json:"platform_id"`
Batch []struct {
MACode string `json:"ma_code"`
Episode int `json:"episode"`
UserHash string `json:"user_hash"`
EventType string `json:"event_type"`
DurationSec int `json:"duration_sec"`
RevenueCent int64 `json:"revenue_cent"`
} `json:"batch"`
}
func (h *Handler) reportPlayback(c *gin.Context) {
var req playbackReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
events := make([]model.PlaybackEvent, 0, len(req.Batch))
for _, b := range req.Batch {
events = append(events, model.PlaybackEvent{
MACode: b.MACode, Episode: b.Episode, PlatformID: req.PlatformID,
UserHash: b.UserHash, EventType: model.PlaybackEventType(b.EventType),
DurationSec: b.DurationSec, RevenueCent: b.RevenueCent, EventTime: time.Now(),
})
}
accepted, rejected := h.svc.ReportPlayback(events)
httpx.OK(c, gin.H{"accepted": accepted, "rejected": rejected})
}
func (h *Handler) playbackSummary(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.PlaybackSummary(maCode))
}
type settlementReq struct {
MACode string `json:"ma_code"`
Period string `json:"period"`
}
func (h *Handler) computeSettlement(c *gin.Context) {
var req settlementReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
st, err := h.svc.ComputeSettlement(req.MACode, req.Period)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "SETTLEMENT_FAILED", err.Error())
return
}
httpx.OK(c, st)
}