Files
MAcode/tcs-iptv/internal/api/handlers.go
T
selfrelease 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 进度
2026-06-14 19:01:26 +08:00

747 lines
24 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"
"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)))
}