Files
MAcode/tcs-iptv/internal/api/handlers.go
T

833 lines
27 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) // 按CC码聚合可信播放数据(需求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/query-by-hash", h.queryByHash) // 按 Hash 反查标识信息
rg.GET("/content/query-by-provincial-code", h.queryByProvincialCode) // 按省级内容编码反查
rg.GET("/content/query-by-library-id", h.queryByLibraryID) // 按片库文件ID反查
// ---- MA 合并/拆分(方案 MA 管理模块补足)----
rg.POST("/content/merge", h.mergeMA) // MA 码合并(仅监管)
rg.POST("/content/split", h.splitMA) // MA 码拆分(仅监管)
// ---- 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)----
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 {
CCCode 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.CCCode, 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 {
CCCode 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.CCCode, 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.CCCode, "status": "in_library"})
}
type publishReq struct {
CCCode 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{CCCode: req.CCCode, Certificate: req.Certificate}); err != nil {
httpx.Error(c, http.StatusBadRequest, "PUBLISH_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.CCCode, "status": "published"})
}
type injectReq struct {
CTID string `json:"content_twin_id"`
CCCode 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.CCCode, 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 {
CCCode 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.CCCode, req.Reason)
if err != nil {
httpx.Error(c, http.StatusForbidden, "TAKEDOWN_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
type takedownEpisodeReq struct {
CCCode 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.CCCode, 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.CCCode, "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.CCCode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.CCCode, "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.CCCode, req.Episode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_EPISODE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.CCCode, "episode": req.Episode, "revoked": false})
}
func (h *Handler) mappings(c *gin.Context) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
res, err := h.svc.QueryMappings(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
type verifyEpisodeReq struct {
CCCode 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.CCCode, 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) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
eps, err := h.svc.ListEpisodes(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": ccCode, "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 {
CCCode 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{
CCCode: b.CCCode, 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) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.PlaybackSummary(ccCode))
}
type settlementReq struct {
CCCode 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.CCCode, 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) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, gin.H{"ma_code": ccCode, "trail": h.svc.Provenance(ccCode)})
}
func (h *Handler) accountability(c *gin.Context) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.Accountability(ccCode))
}
func (h *Handler) evidence(c *gin.Context) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
ev, err := h.svc.CopyrightEvidence(ccCode)
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 {
CCCode 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.CCCode, 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.CCCode, "authorized": true})
}
type authCheckReq struct {
CCCode 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.CCCode, req.Region, req.Platform))
}
type addEpisodesReq struct {
CCCode 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.CCCode, eps); err != nil {
httpx.Error(c, http.StatusBadRequest, "ADD_EPISODES_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.CCCode, "added": len(eps)})
}
type crossProvinceReq struct {
CCCode 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.CCCode, req.FileHash, req.Province))
}
type terminalVerifyReq struct {
CCCode 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.CCCode, req.Episode, req.SegHash)
httpx.OK(c, gin.H{"ok": ok, "message": msg})
}
// ---- 三期:备案对接/全国统计/监管上报/号段管理 ----
type bindFilingReq struct {
CCCode 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.CCCode, 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) {
ccCode := c.Query("ma_code")
rec, ok := h.svc.QueryFiling(ccCode)
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) {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.Resolve(ccCode))
}
type scanVerifyReq struct {
CCCode 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.CCCode))
}
type purchaseReq struct {
CCCode 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.CCCode, 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 {
CCCode 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.CCCode, req.UserHash, model.ScreenType(req.Screen)))
}
// ---- 多维度标识查询 handlers ----
// queryByHash 按内容哈希反查标识信息。
func (h *Handler) queryByHash(c *gin.Context) {
fileHash := c.Query("file_hash")
if fileHash == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 file_hash 参数")
return
}
res, err := h.svc.QueryByHash(fileHash)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// queryByProvincialCode 按省级内容编码反查标识信息。
func (h *Handler) queryByProvincialCode(c *gin.Context) {
code := c.Query("provincial_code")
if code == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 provincial_code 参数")
return
}
res, err := h.svc.QueryByProvincialCode(code)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// queryByLibraryID 按片库文件 ID 反查标识信息。
func (h *Handler) queryByLibraryID(c *gin.Context) {
libraryID := c.Query("library_file_id")
if libraryID == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 library_file_id 参数")
return
}
res, err := h.svc.QueryByLibraryFileID(libraryID)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// ---- MA 合并/拆分 handlers ----
// mergeMA MA 码合并(仅监管主体)。
func (h *Handler) mergeMA(c *gin.Context) {
var req model.MergeRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.MergeCCCodes(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "MERGE_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
// splitMA MA 码拆分(仅监管主体)。
func (h *Handler) splitMA(c *gin.Context) {
var req model.SplitRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.SplitCCCode(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "SPLIT_FAILED", err.Error())
return
}
httpx.OK(c, res)
}