Files
MAcode/tcs-iptv/internal/api/handlers.go
T
selfrelease a329d4906b init: AIGC-Hub/AVCC 方案文档 + TCS-IPTV 内容可信锁定系统 MVP
- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务
- tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权)
- web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库)
- 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
2026-06-14 16:50:31 +08:00

389 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package api 暴露 TCS-IPTV 的 HTTP 接口,串联 service 业务编排。
// 对应需求17(接口规范)与各业务需求。
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/httpx"
"github.com/tcs-iptv/tcs/internal/model"
"github.com/tcs-iptv/tcs/internal/service"
)
// Handler 持有业务服务。
type Handler struct {
svc *service.Service
}
// NewHandler 创建 API 处理器。
func NewHandler(svc *service.Service) *Handler {
return &Handler{svc: svc}
}
// Register 注册路由(受鉴权中间件保护的路由组由调用方组装)。
func (h *Handler) Register(rg *gin.RouterGroup) {
rg.POST("/content/register", h.register) // CP 送审上链(需求2
rg.POST("/content/csps-result", h.cspsResult) // CSPS 合规审核(发码前,需求5)
rg.POST("/content/issue", h.issue) // 审核通过后发码签发(需求3
rg.POST("/content/verify", h.verify) // 哈希验真(需求4/7
rg.POST("/content/transcoded", h.bindTranscoded) // 转码版绑定(需求5
rg.POST("/content/ingest", h.ingest) // 媒资库入库(需求6
rg.POST("/content/publish", h.publish) // 发布给运营商(需求6
rg.POST("/content/inject", h.inject) // CDN 注入校验(需求7
rg.POST("/content/version-change", h.versionChange) // 版本变更重审(需求12
rg.POST("/content/takedown", h.takedown) // 应急下架(需求11
rg.POST("/content/takedown-episode", h.takedownEpisode) // 集级下架(只下架某集)
rg.POST("/content/restore", h.restore) // 恢复上架整剧
rg.POST("/content/restore-episode", h.restoreEpisode) // 恢复上架某集
rg.GET("/content/mappings", h.mappings) // 映射查询(需求11/17
rg.POST("/content/verify-episode", h.verifyEpisode) // 集级验真(一剧多集)
rg.GET("/content/episodes", h.listEpisodes) // 列出集级哈希
rg.GET("/content/reviews", h.listReviews) // 送审待办队列(待审/待发码)
rg.GET("/content/list", h.listContents) // 内容队列(待入库/待发布/待注入)
}
func roleOf(c *gin.Context) chain.Role {
return chain.Role(httpx.RoleFromContext(c))
}
// --- handlers ---
type episodeHashReq struct {
Episode int `json:"episode"`
FileSHA256 string `json:"file_sha256"`
MerkleRoot string `json:"merkle_root"`
Perceptual string `json:"perceptual_hash"`
Duration int `json:"duration"`
Resolution string `json:"resolution"`
}
type registerReq struct {
Title string `json:"title"`
EpisodeCount int `json:"episode_count"`
Category string `json:"category"`
FileHash string `json:"file_sha256"`
MerkleRoot string `json:"merkle_root"`
Perceptual string `json:"perceptual_hash"`
Episodes []episodeHashReq `json:"episodes"`
CPMediaID string `json:"cp_media_id"`
CPName string `json:"cp_name"`
}
func (h *Handler) register(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
eps := make([]model.EpisodeHash, 0, len(req.Episodes))
for _, e := range req.Episodes {
eps = append(eps, model.EpisodeHash{
Episode: e.Episode, FileSHA256: e.FileSHA256, MerkleRoot: e.MerkleRoot,
Perceptual: e.Perceptual, Duration: e.Duration, Resolution: e.Resolution,
})
}
res, err := h.svc.SubmitForReview(service.Submission{
Title: req.Title, EpisodeCount: req.EpisodeCount, Category: req.Category,
FileHash: req.FileHash, MerkleRoot: req.MerkleRoot, Perceptual: req.Perceptual,
Episodes: eps,
CPMediaID: req.CPMediaID, CPName: req.CPName,
})
if err != nil {
httpx.Error(c, http.StatusBadRequest, "REGISTER_FAILED", err.Error())
return
}
httpx.Accepted(c, res)
}
type issueReq struct {
ReviewID string `json:"review_id"`
Issuer string `json:"issuer"`
}
func (h *Handler) issue(c *gin.Context) {
var req issueReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.ApproveAndIssue(roleOf(c), req.ReviewID, req.Issuer)
if err != nil {
httpx.Error(c, http.StatusForbidden, "ISSUE_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
type verifyReq struct {
MACode string `json:"ma_code"`
FileHash string `json:"file_sha256"`
}
func (h *Handler) verify(c *gin.Context) {
var req verifyReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.Verify(req.MACode, req.FileHash)
if err != nil {
// 验真不匹配也返回结果体,便于调用方据 match 处理
httpx.Error(c, http.StatusBadRequest, "VERIFY_MISMATCH", err.Error())
return
}
httpx.OK(c, res)
}
type cspsReq struct {
ReviewID string `json:"review_id"`
Approved bool `json:"approved"`
ReviewerID string `json:"reviewer_id"`
}
func (h *Handler) cspsResult(c *gin.Context) {
var req cspsReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.ReviewCSPS(req.ReviewID, req.Approved, req.ReviewerID); err != nil {
httpx.Error(c, http.StatusBadRequest, "CSPS_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"review_id": req.ReviewID, "approved": req.Approved})
}
type transcodedReq struct {
CTID string `json:"content_twin_id"`
ParentFileHash string `json:"parent_file_hash"`
TranscodedHash string `json:"transcoded_hash"`
Format string `json:"format"`
Resolution string `json:"resolution"`
Version string `json:"version"`
}
func (h *Handler) bindTranscoded(c *gin.Context) {
var req transcodedReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
tx, err := h.svc.BindTranscoded(roleOf(c), req.CTID, req.ParentFileHash,
req.TranscodedHash, req.Format, req.Resolution, req.Version)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "TRANSCODE_BIND_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"tx_id": tx})
}
type ingestReq struct {
MACode string `json:"ma_code"`
CTID string `json:"content_twin_id"`
MediaAssetID string `json:"media_asset_id"`
LibName string `json:"lib_name"`
}
func (h *Handler) ingest(c *gin.Context) {
var req ingestReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.IngestToLibrary(roleOf(c), req.MACode, req.CTID, req.MediaAssetID, req.LibName); err != nil {
httpx.Error(c, http.StatusBadRequest, "INGEST_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "in_library"})
}
type publishReq struct {
MACode string `json:"ma_code"`
Certificate string `json:"certificate"`
}
func (h *Handler) publish(c *gin.Context) {
var req publishReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.PublishToOperator(service.PublishRequest{MACode: req.MACode, Certificate: req.Certificate}); err != nil {
httpx.Error(c, http.StatusBadRequest, "PUBLISH_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "published"})
}
type injectReq struct {
CTID string `json:"content_twin_id"`
MACode string `json:"ma_code"`
FileHash string `json:"file_sha256"`
OperatorID string `json:"operator_id"`
CDNEndpoint string `json:"cdn_endpoint"`
}
func (h *Handler) inject(c *gin.Context) {
var req injectReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.InjectToCDN(roleOf(c), req.CTID, req.MACode, req.FileHash, req.OperatorID, req.CDNEndpoint)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "INJECT_REJECTED", err.Error())
return
}
httpx.OK(c, res)
}
type versionChangeReq struct {
CTID string `json:"content_twin_id"`
Reason string `json:"reason"`
PrevHash string `json:"prev_hash"`
NewHash string `json:"new_hash"`
OldSegments []string `json:"old_segments"`
NewSegments []string `json:"new_segments"`
}
func (h *Handler) versionChange(c *gin.Context) {
var req versionChangeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
episodes, err := h.svc.ReportVersionChange(req.CTID, req.Reason, req.PrevHash, req.NewHash, req.OldSegments, req.NewSegments)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "VERSION_CHANGE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"reaudit_required": true, "affected_episodes": episodes})
}
type takedownReq struct {
MACode string `json:"ma_code"`
Reason string `json:"reason"`
}
func (h *Handler) takedown(c *gin.Context) {
var req takedownReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.Takedown(roleOf(c), req.MACode, req.Reason)
if err != nil {
httpx.Error(c, http.StatusForbidden, "TAKEDOWN_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
type takedownEpisodeReq struct {
MACode string `json:"ma_code"`
Episode int `json:"episode"`
Reason string `json:"reason"`
}
func (h *Handler) takedownEpisode(c *gin.Context) {
var req takedownEpisodeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.TakedownEpisode(roleOf(c), req.MACode, req.Episode, req.Reason); err != nil {
httpx.Error(c, http.StatusForbidden, "TAKEDOWN_EPISODE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "episode": req.Episode, "revoked": true})
}
func (h *Handler) restore(c *gin.Context) {
var req takedownReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.Restore(roleOf(c), req.MACode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "published"})
}
func (h *Handler) restoreEpisode(c *gin.Context) {
var req takedownEpisodeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.RestoreEpisode(roleOf(c), req.MACode, req.Episode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_EPISODE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "episode": req.Episode, "revoked": false})
}
func (h *Handler) mappings(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
res, err := h.svc.QueryMappings(maCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
type verifyEpisodeReq struct {
MACode string `json:"ma_code"`
Episode int `json:"episode"`
FileHash string `json:"file_sha256"`
}
func (h *Handler) verifyEpisode(c *gin.Context) {
var req verifyEpisodeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.VerifyEpisode(req.MACode, req.Episode, req.FileHash)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "VERIFY_MISMATCH", err.Error())
return
}
httpx.OK(c, res)
}
func (h *Handler) listEpisodes(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
eps, err := h.svc.ListEpisodes(maCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": maCode, "episodes": eps, "count": len(eps)})
}
func (h *Handler) listReviews(c *gin.Context) {
httpx.OK(c, gin.H{"reviews": h.svc.ListReviews(c.Query("status"))})
}
func (h *Handler) listContents(c *gin.Context) {
list, err := h.svc.ListContentsByStatus(c.Query("status"))
if err != nil {
httpx.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
httpx.OK(c, gin.H{"contents": list, "count": len(list)})
}