2cd5fbec6d
- model/rights.go: ScreenType/ParsedMA/ResolveResult/ScanVerifyResult/UserRights/PurchaseRecord/CrossScreenRightsResult - service/phase4.go: ParseMACode + Resolve(C.1/C.2) + ScanVerify(B.2) + RecordPurchase/VerifyCrossScreenRights(D.1) - api/handlers.go: GET /content/resolve, POST /content/scan-verify, /rights/purchase, /rights/verify - service/phase4_test.go: 18 单测全绿 - 同一MA码跨iptv/ott/app统一解析; 任一屏购买全屏通看不重复扣费 - OTT/移动端SDK/C2PA凭证标注需外部环境 - 更新 5-task-IPTV-四期.md 进度
747 lines
24 KiB
Go
747 lines
24 KiB
Go
// Package api 暴露 TCS-IPTV 的 HTTP 接口,串联 service 业务编排。
|
||
// 对应需求17(接口规范)与各业务需求。
|
||
package api
|
||
|
||
import (
|
||
"net/http"
|
||
"time"
|
||
|
||
"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) // 内容队列(待入库/待发布/待注入)
|
||
rg.POST("/data/playback", h.reportPlayback) // 播放数据回传(需求9)
|
||
rg.GET("/data/playback-summary", h.playbackSummary) // 按MA码聚合可信播放数据(需求9/21)
|
||
rg.POST("/settlement/compute", h.computeSettlement) // 基于可信播放数据分账(需求21)
|
||
rg.GET("/content/provenance", h.provenance) // 全链路存证(需求22)
|
||
rg.GET("/content/accountability", h.accountability) // 责任界定取证(需求22)
|
||
rg.GET("/content/evidence", h.evidence) // 版权确权证据链(需求23)
|
||
rg.POST("/content/infringe-match", h.infringeMatch) // 感知哈希侵权比对(需求23)
|
||
rg.POST("/content/authorize", h.authorize) // 登记授权(需求25)
|
||
rg.POST("/content/auth-check", h.authCheck) // 授权核验(需求25)
|
||
rg.POST("/content/add-episodes", h.addEpisodes) // 追更新集(需求24)
|
||
rg.POST("/content/cross-province", h.crossProvince) // 跨省复用准入(需求13)
|
||
rg.POST("/terminal/verify-segment", h.terminalVerify) // 终端片段抽检(需求8)
|
||
rg.POST("/content/bind-filing", h.bindFiling) // 备案/网标关联(三期A.1)
|
||
rg.GET("/content/filing", h.queryFiling) // 查询备案关联
|
||
rg.GET("/regulatory/national-stats", h.nationalStats) // 全国监管统计(三期F.2)
|
||
rg.GET("/regulatory/daily-report", h.dailyReport) // 监管数据上报日报(三期A.2)
|
||
rg.GET("/admin/segments", h.listSegments) // 号段管理(三期B.1)
|
||
// ---- 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)----
|
||
rg.GET("/content/resolve", h.resolve) // MA 跨域解析网关(C.1/C.2)
|
||
rg.POST("/content/scan-verify", h.scanVerify) // 用户扫码验真(B.2)
|
||
rg.POST("/rights/purchase", h.recordPurchase) // 记录跨屏购买(D.1)
|
||
rg.POST("/rights/verify", h.verifyRights) // 跨屏权益核验(D.1)
|
||
}
|
||
|
||
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)})
|
||
}
|
||
|
||
// ---- 二期:播放数据回传与分账(需求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)
|
||
}
|
||
|
||
// ---- 二期:追责取证与确权举证(需求22/23) ----
|
||
|
||
func (h *Handler) provenance(c *gin.Context) {
|
||
maCode := c.Query("ma_code")
|
||
if maCode == "" {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
|
||
return
|
||
}
|
||
httpx.OK(c, gin.H{"ma_code": maCode, "trail": h.svc.Provenance(maCode)})
|
||
}
|
||
|
||
func (h *Handler) accountability(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.Accountability(maCode))
|
||
}
|
||
|
||
func (h *Handler) evidence(c *gin.Context) {
|
||
maCode := c.Query("ma_code")
|
||
if maCode == "" {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
|
||
return
|
||
}
|
||
ev, err := h.svc.CopyrightEvidence(maCode)
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, ev)
|
||
}
|
||
|
||
type infringeReq struct {
|
||
Perceptual string `json:"perceptual_hash"`
|
||
High int `json:"high_threshold"`
|
||
Medium int `json:"medium_threshold"`
|
||
}
|
||
|
||
func (h *Handler) infringeMatch(c *gin.Context) {
|
||
var req infringeReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
if req.High == 0 {
|
||
req.High = 5
|
||
}
|
||
if req.Medium == 0 {
|
||
req.Medium = 10
|
||
}
|
||
matches, err := h.svc.MatchInfringement(req.Perceptual, req.High, req.Medium)
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "MATCH_FAILED", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, gin.H{"matches": matches, "count": len(matches)})
|
||
}
|
||
|
||
// ---- 二期:授权链/追更/跨省/终端抽检(需求25/24/13/8) ----
|
||
|
||
type authorizeReq struct {
|
||
MACode string `json:"ma_code"`
|
||
Regions []string `json:"regions"`
|
||
Platforms []string `json:"platforms"`
|
||
ExpiryAt string `json:"expiry_at"` // RFC3339,空=长期
|
||
}
|
||
|
||
func (h *Handler) authorize(c *gin.Context) {
|
||
var req authorizeReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
var expiry time.Time
|
||
if req.ExpiryAt != "" {
|
||
expiry, _ = time.Parse(time.RFC3339, req.ExpiryAt)
|
||
}
|
||
if err := h.svc.RecordAuthorization(req.MACode, req.Regions, req.Platforms, expiry); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "AUTHORIZE_FAILED", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, gin.H{"ma_code": req.MACode, "authorized": true})
|
||
}
|
||
|
||
type authCheckReq struct {
|
||
MACode string `json:"ma_code"`
|
||
Region string `json:"region"`
|
||
Platform string `json:"platform"`
|
||
}
|
||
|
||
func (h *Handler) authCheck(c *gin.Context) {
|
||
var req authCheckReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, h.svc.CheckAuthorization(req.MACode, req.Region, req.Platform))
|
||
}
|
||
|
||
type addEpisodesReq struct {
|
||
MACode string `json:"ma_code"`
|
||
Episodes []struct {
|
||
Episode int `json:"episode"`
|
||
FileSHA256 string `json:"file_sha256"`
|
||
MerkleRoot string `json:"merkle_root"`
|
||
} `json:"episodes"`
|
||
}
|
||
|
||
func (h *Handler) addEpisodes(c *gin.Context) {
|
||
var req addEpisodesReq
|
||
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})
|
||
}
|
||
if err := h.svc.AddEpisodes(roleOf(c), req.MACode, eps); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "ADD_EPISODES_FAILED", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, gin.H{"ma_code": req.MACode, "added": len(eps)})
|
||
}
|
||
|
||
type crossProvinceReq struct {
|
||
MACode string `json:"ma_code"`
|
||
FileHash string `json:"file_sha256"`
|
||
Province string `json:"province"`
|
||
}
|
||
|
||
func (h *Handler) crossProvince(c *gin.Context) {
|
||
var req crossProvinceReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, h.svc.CrossProvinceAdmit(req.MACode, req.FileHash, req.Province))
|
||
}
|
||
|
||
type terminalVerifyReq struct {
|
||
MACode string `json:"ma_code"`
|
||
Episode int `json:"episode"`
|
||
SegHash string `json:"segment_hash"`
|
||
}
|
||
|
||
func (h *Handler) terminalVerify(c *gin.Context) {
|
||
var req terminalVerifyReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
ok, msg := h.svc.TerminalVerifySegment(req.MACode, req.Episode, req.SegHash)
|
||
httpx.OK(c, gin.H{"ok": ok, "message": msg})
|
||
}
|
||
|
||
// ---- 三期:备案对接/全国统计/监管上报/号段管理 ----
|
||
|
||
type bindFilingReq struct {
|
||
MACode string `json:"ma_code"`
|
||
LicenseNo string `json:"license_no"`
|
||
FilingNo string `json:"filing_no"`
|
||
}
|
||
|
||
func (h *Handler) bindFiling(c *gin.Context) {
|
||
var req bindFilingReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
rec, err := h.svc.BindFiling(req.MACode, req.LicenseNo, req.FilingNo)
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "BIND_FILING_FAILED", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, rec)
|
||
}
|
||
|
||
func (h *Handler) queryFiling(c *gin.Context) {
|
||
maCode := c.Query("ma_code")
|
||
rec, ok := h.svc.QueryFiling(maCode)
|
||
if !ok {
|
||
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", "未关联备案")
|
||
return
|
||
}
|
||
httpx.OK(c, rec)
|
||
}
|
||
|
||
func (h *Handler) nationalStats(c *gin.Context) {
|
||
st, err := h.svc.NationalStats()
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, st)
|
||
}
|
||
|
||
func (h *Handler) dailyReport(c *gin.Context) {
|
||
date := c.Query("date")
|
||
if date == "" {
|
||
date = time.Now().Format("2006-01-02")
|
||
}
|
||
rep, err := h.svc.DailyRegulatoryReport(date)
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, rep)
|
||
}
|
||
|
||
func (h *Handler) listSegments(c *gin.Context) {
|
||
httpx.OK(c, gin.H{"segments": h.svc.ListSegments()})
|
||
}
|
||
|
||
// ---- 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)----
|
||
|
||
func (h *Handler) resolve(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.Resolve(maCode))
|
||
}
|
||
|
||
type scanVerifyReq struct {
|
||
MACode string `json:"ma_code"`
|
||
}
|
||
|
||
func (h *Handler) scanVerify(c *gin.Context) {
|
||
var req scanVerifyReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, h.svc.ScanVerify(req.MACode))
|
||
}
|
||
|
||
type purchaseReq struct {
|
||
MACode string `json:"ma_code"`
|
||
UserHash string `json:"user_hash"`
|
||
Screen string `json:"screen"` // iptv/ott/app
|
||
}
|
||
|
||
func (h *Handler) recordPurchase(c *gin.Context) {
|
||
var req purchaseReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
rec, err := h.svc.RecordPurchase(req.MACode, req.UserHash, model.ScreenType(req.Screen))
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "PURCHASE_FAILED", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, rec)
|
||
}
|
||
|
||
type verifyRightsReq struct {
|
||
MACode string `json:"ma_code"`
|
||
UserHash string `json:"user_hash"`
|
||
Screen string `json:"screen"` // 当前请求屏
|
||
}
|
||
|
||
func (h *Handler) verifyRights(c *gin.Context) {
|
||
var req verifyRightsReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||
return
|
||
}
|
||
httpx.OK(c, h.svc.VerifyCrossScreenRights(req.MACode, req.UserHash, model.ScreenType(req.Screen)))
|
||
}
|