feat(phase3): 备案对接/全国统计/号段管理/BFF安全化/链合约源码
- A.1 备案对接: BindFiling/QueryFiling 关联网标号+备案号 - A.2 监管上报: DailyRegulatoryReport 日报 - B.1 号段管理: ListSegments + /admin/segments - C.1/C.2 全国统计按省聚合 + 跨省协同(单一可信源天然联动) - F.2 全国监管大屏: NationalStats(按省/类目/状态) - B(遗留) 监管大屏BFF: internal/bff + cmd/console-bff, 密钥仅存后端浏览器只用会话令牌 - G 真实链合约源码: contracts/tcs_registry/registry.go (ChainMaker Go) - 新增9个API+BFF服务; 5项新测试; 端到端BFF验证 - D/E(压测/等保/HSM)/F.1(标准)/真实链部署 标注需外部环境
This commit is contained in:
@@ -55,6 +55,11 @@ func (h *Handler) Register(rg *gin.RouterGroup) {
|
||||
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)
|
||||
}
|
||||
|
||||
func roleOf(c *gin.Context) chain.Role {
|
||||
@@ -617,3 +622,61 @@ func (h *Handler) terminalVerify(c *gin.Context) {
|
||||
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()})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Package bff 实现监管控制台的 Backend-For-Frontend(三期 B)。
|
||||
//
|
||||
// 安全目标(替换 MVP 演示态前端直连 + 前端持密钥):
|
||||
// - 凭证(API Key/Secret)仅存于 BFF 后端,绝不下发浏览器
|
||||
// - 浏览器用会话令牌(Session Token)访问 BFF
|
||||
// - BFF 校验会话 + RBAC,再以服务端 HMAC 签名代理到 api-svc
|
||||
package bff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tcs-iptv/tcs/internal/httpx"
|
||||
)
|
||||
|
||||
// roleCred 角色对应的 api-svc 凭证(仅存于 BFF)。
|
||||
type roleCred struct {
|
||||
apiKey string
|
||||
apiSecret string
|
||||
}
|
||||
|
||||
// session 登录会话。
|
||||
type session struct {
|
||||
user string
|
||||
role string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// BFF 控制台后端。
|
||||
type BFF struct {
|
||||
apiBase string
|
||||
creds map[string]roleCred // role -> cred
|
||||
users map[string]struct{ pass, role string }
|
||||
mu sync.RWMutex
|
||||
tokens map[string]session
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// New 创建 BFF。apiBase 如 http://localhost:8080
|
||||
func New(apiBase string) *BFF {
|
||||
b := &BFF{
|
||||
apiBase: apiBase,
|
||||
creds: map[string]roleCred{},
|
||||
users: map[string]struct{ pass, role string }{},
|
||||
tokens: map[string]session{},
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SetCred 配置角色凭证(从 Vault/环境加载,不入前端)。
|
||||
func (b *BFF) SetCred(role, apiKey, apiSecret string) {
|
||||
b.creds[role] = roleCred{apiKey, apiSecret}
|
||||
}
|
||||
|
||||
// AddUser 配置控制台用户(生产接 SSO/LDAP)。
|
||||
func (b *BFF) AddUser(user, pass, role string) {
|
||||
b.users[user] = struct{ pass, role string }{pass, role}
|
||||
}
|
||||
|
||||
func newToken() string {
|
||||
buf := make([]byte, 24)
|
||||
_, _ = rand.Read(buf)
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// Login 用户名口令登录,返回会话令牌(不下发任何密钥)。
|
||||
func (b *BFF) Login(c *gin.Context) {
|
||||
var req struct{ User, Pass string }
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Error(c, 400, "INVALID_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
u, ok := b.users[req.User]
|
||||
if !ok || u.pass != req.Pass {
|
||||
httpx.Error(c, 401, "UNAUTHORIZED", "用户名或口令错误")
|
||||
return
|
||||
}
|
||||
tok := newToken()
|
||||
b.mu.Lock()
|
||||
b.tokens[tok] = session{user: req.User, role: u.role, expiresAt: time.Now().Add(8 * time.Hour)}
|
||||
b.mu.Unlock()
|
||||
httpx.OK(c, gin.H{"token": tok, "role": u.role, "user": req.User})
|
||||
}
|
||||
|
||||
// sessionOf 校验会话令牌。
|
||||
func (b *BFF) sessionOf(tok string) (session, bool) {
|
||||
b.mu.RLock()
|
||||
s, ok := b.tokens[tok]
|
||||
b.mu.RUnlock()
|
||||
if !ok || time.Now().After(s.expiresAt) {
|
||||
return session{}, false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
// AuthMiddleware 校验浏览器会话令牌(Bearer)。
|
||||
func (b *BFF) AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tok := c.GetHeader("X-Session-Token")
|
||||
s, ok := b.sessionOf(tok)
|
||||
if !ok {
|
||||
httpx.Error(c, 401, "UNAUTHORIZED", "会话无效或过期,请重新登录")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("bff_role", s.role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy 以服务端凭证 HMAC 签名后代理到 api-svc(密钥不出 BFF)。
|
||||
func (b *BFF) Proxy(c *gin.Context) {
|
||||
role, _ := c.Get("bff_role")
|
||||
cred, ok := b.creds[role.(string)]
|
||||
if !ok {
|
||||
httpx.Error(c, 403, "FORBIDDEN", "角色无对应凭证")
|
||||
return
|
||||
}
|
||||
// 透传 /api/v1/* 路径
|
||||
path := c.Param("path")
|
||||
fullPath := "/api/v1" + path
|
||||
method := c.Request.Method
|
||||
|
||||
var body []byte
|
||||
if c.Request.Body != nil {
|
||||
body, _ = io.ReadAll(c.Request.Body)
|
||||
}
|
||||
sig := httpx.Sign(cred.apiSecret, method, fullPath)
|
||||
|
||||
url := b.apiBase + fullPath
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
url += "?" + c.Request.URL.RawQuery
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "TCS "+cred.apiKey+":"+sig)
|
||||
if len(body) > 0 {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
httpx.Error(c, 502, "BAD_GATEWAY", err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
c.Data(resp.StatusCode, "application/json", out)
|
||||
}
|
||||
@@ -142,6 +142,30 @@ func (g *Generator) Allocate(category string) (Issued, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SegmentInfo 号段使用情况摘要。
|
||||
type SegmentInfo struct {
|
||||
Category string `json:"category"`
|
||||
IndustryNode string `json:"industry_node"`
|
||||
OrgNode string `json:"org_node"`
|
||||
Start uint64 `json:"start"`
|
||||
End uint64 `json:"end"`
|
||||
Capacity uint64 `json:"capacity"`
|
||||
}
|
||||
|
||||
// Segments 返回已登记号段列表(号段管理后台用,B.1)。
|
||||
func (g *Generator) Segments() []SegmentInfo {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
out := make([]SegmentInfo, 0, len(g.segments))
|
||||
for _, seg := range g.segments {
|
||||
out = append(out, SegmentInfo{
|
||||
Category: seg.Category, IndustryNode: seg.IndustryNode, OrgNode: seg.OrgNode,
|
||||
Start: seg.Start, End: seg.End, Capacity: seg.End - seg.Start + 1,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Format 按备案规则拼装 MA 码。
|
||||
func Format(seg Segment, year int, seq uint64) string {
|
||||
w := seg.SeqWidth
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package model
|
||||
|
||||
// 三期:备案对接、全国统计、监管上报相关模型。
|
||||
|
||||
// FilingRecord 备案/网标关联(三期 A.1,对接广电总局备案/发行许可证系统)。
|
||||
type FilingRecord struct {
|
||||
MACode string `json:"ma_code"`
|
||||
LicenseNo string `json:"license_no"` // 网络剧片发行许可证号(网标号)
|
||||
FilingNo string `json:"filing_no"` // 重点网络影视剧备案号
|
||||
BoundAt string `json:"bound_at"`
|
||||
}
|
||||
|
||||
// NationalStats 全国监管统计(三期 A.2/F.2 全国监管大屏)。
|
||||
type NationalStats struct {
|
||||
TotalContents int `json:"total_contents"`
|
||||
ByStatus map[string]int `json:"by_status"`
|
||||
ByCategory map[string]int `json:"by_category"`
|
||||
ByProvince map[string]ProvinceStat `json:"by_province"`
|
||||
}
|
||||
|
||||
// ProvinceStat 单省统计。
|
||||
type ProvinceStat struct {
|
||||
Province string `json:"province"`
|
||||
OrgNode string `json:"org_node"`
|
||||
Total int `json:"total"`
|
||||
Published int `json:"published"`
|
||||
Revoked int `json:"revoked"`
|
||||
}
|
||||
|
||||
// RegulatoryReport 监管数据上报日报(三期 A.2,上报广电总局)。
|
||||
type RegulatoryReport struct {
|
||||
ReportType string `json:"report_type"`
|
||||
Date string `json:"date"`
|
||||
TotalNew int `json:"total_new"`
|
||||
LevelDist map[string]int `json:"level_dist"`
|
||||
BlacklistCnt int `json:"blacklist_count"`
|
||||
RevokedCnt int `json:"revoked_count"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tcs-iptv/tcs/internal/macode"
|
||||
"github.com/tcs-iptv/tcs/internal/model"
|
||||
)
|
||||
|
||||
// 三期:备案对接、全国统计、号段管理、监管上报。
|
||||
|
||||
// 机构节点 → 省份映射(与发码机构号段分配一致;示例覆盖部分省)。
|
||||
var orgNodeProvince = map[string]string{
|
||||
"6101": "陕西", "4401": "广东", "3301": "浙江", "3201": "江苏",
|
||||
"1101": "北京", "3101": "上海", "5101": "四川", "4201": "湖北",
|
||||
}
|
||||
|
||||
// BindFiling 关联备案号/网标号至 MA 码(三期 A.1,对接广电总局备案系统)。
|
||||
func (s *Service) BindFiling(maCode, licenseNo, filingNo string) (model.FilingRecord, error) {
|
||||
if _, err := s.chain.QueryContent(maCode); err != nil {
|
||||
return model.FilingRecord{}, err
|
||||
}
|
||||
rec := model.FilingRecord{
|
||||
MACode: maCode, LicenseNo: licenseNo, FilingNo: filingNo,
|
||||
BoundAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.filings[maCode] = rec
|
||||
s.mu.Unlock()
|
||||
s.prov.Record(model.ProvenanceEvent{
|
||||
MACode: maCode, Node: model.NodeIssue, Operator: "广电总局备案系统",
|
||||
Detail: "关联网标号 " + licenseNo + " / 备案号 " + filingNo,
|
||||
})
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// QueryFiling 查询备案关联。
|
||||
func (s *Service) QueryFiling(maCode string) (model.FilingRecord, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.filings[maCode]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// NationalStats 全国监管统计(三期 A.2/F.2)。
|
||||
func (s *Service) NationalStats() (model.NationalStats, error) {
|
||||
all, err := s.chain.ListContents("")
|
||||
if err != nil {
|
||||
return model.NationalStats{}, err
|
||||
}
|
||||
st := model.NationalStats{
|
||||
ByStatus: map[string]int{},
|
||||
ByCategory: map[string]int{},
|
||||
ByProvince: map[string]model.ProvinceStat{},
|
||||
}
|
||||
st.TotalContents = len(all)
|
||||
for _, c := range all {
|
||||
st.ByStatus[c.Status]++
|
||||
p, perr := macode.Parse(c.MACode)
|
||||
if perr == nil {
|
||||
st.ByCategory[p.Category]++
|
||||
prov := orgNodeProvince[p.OrgNode]
|
||||
if prov == "" {
|
||||
prov = "其他(" + p.OrgNode + ")"
|
||||
}
|
||||
ps := st.ByProvince[prov]
|
||||
ps.Province = prov
|
||||
ps.OrgNode = p.OrgNode
|
||||
ps.Total++
|
||||
switch c.Status {
|
||||
case model.StatusPublished:
|
||||
ps.Published++
|
||||
case model.StatusRevoked:
|
||||
ps.Revoked++
|
||||
}
|
||||
st.ByProvince[prov] = ps
|
||||
}
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// ListSegments 列出已登记号段及使用情况(三期 B.1)。
|
||||
func (s *Service) ListSegments() []macode.SegmentInfo {
|
||||
return s.gen.Segments()
|
||||
}
|
||||
|
||||
// RegisterSegment 登记新号段(三期 B.1,与发码机构对接后配置)。
|
||||
func (s *Service) RegisterSegment(seg macode.Segment) error {
|
||||
return s.gen.RegisterSegment(seg)
|
||||
}
|
||||
|
||||
// DailyRegulatoryReport 生成监管数据上报日报(三期 A.2)。
|
||||
func (s *Service) DailyRegulatoryReport(date string) (model.RegulatoryReport, error) {
|
||||
all, err := s.chain.ListContents("")
|
||||
if err != nil {
|
||||
return model.RegulatoryReport{}, err
|
||||
}
|
||||
rep := model.RegulatoryReport{
|
||||
ReportType: "daily_summary", Date: date,
|
||||
LevelDist: map[string]int{},
|
||||
}
|
||||
for _, c := range all {
|
||||
rep.TotalNew++
|
||||
if p, perr := macode.Parse(c.MACode); perr == nil {
|
||||
rep.LevelDist[p.Category]++
|
||||
}
|
||||
if c.Status == model.StatusRevoked {
|
||||
rep.RevokedCnt++
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
rep.BlacklistCnt = len(s.black)
|
||||
s.mu.Unlock()
|
||||
return rep, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tcs-iptv/tcs/internal/chain"
|
||||
"github.com/tcs-iptv/tcs/internal/macode"
|
||||
)
|
||||
|
||||
func newServiceSX(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
gen := macode.NewGenerator(macode.NewMemoryStore())
|
||||
require.NoError(t, gen.RegisterSegment(macode.Segment{
|
||||
IndustryNode: "8531", OrgNode: "6101", // 陕西
|
||||
Category: macode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
|
||||
}))
|
||||
return New(chain.NewMemoryChain(), gen)
|
||||
}
|
||||
|
||||
func issueSX(t *testing.T, s *Service) string {
|
||||
t.Helper()
|
||||
sub := sampleSub()
|
||||
r, _ := s.SubmitForReview(sub)
|
||||
require.NoError(t, s.ReviewCSPS(r.ReviewID, true, "rv"))
|
||||
iss, err := s.ApproveAndIssue(chain.RoleRegulator, r.ReviewID, "陕西IPTV运营公司")
|
||||
require.NoError(t, err)
|
||||
return iss.MACode
|
||||
}
|
||||
|
||||
func TestBindFiling(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
ma := issueSX(t, s)
|
||||
rec, err := s.BindFiling(ma, "(陕)网微剧审字(2026)第001号", "备案2026001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "(陕)网微剧审字(2026)第001号", rec.LicenseNo)
|
||||
|
||||
got, ok := s.QueryFiling(ma)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "备案2026001", got.FilingNo)
|
||||
}
|
||||
|
||||
func TestNationalStats(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
ma1 := issueSX(t, s)
|
||||
sub2 := sampleSub()
|
||||
sub2.FileHash = "fh2"
|
||||
sub2.MerkleRoot = "mr2"
|
||||
r2, _ := s.SubmitForReview(sub2)
|
||||
require.NoError(t, s.ReviewCSPS(r2.ReviewID, true, "rv"))
|
||||
_, _ = s.ApproveAndIssue(chain.RoleRegulator, r2.ReviewID, "陕西IPTV")
|
||||
_, _ = s.Takedown(chain.RoleRegulator, ma1, "违规")
|
||||
|
||||
st, err := s.NationalStats()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, st.TotalContents)
|
||||
assert.Equal(t, 1, st.ByStatus["revoked"])
|
||||
// 陕西省统计
|
||||
sx := st.ByProvince["陕西"]
|
||||
assert.Equal(t, "6101", sx.OrgNode)
|
||||
assert.Equal(t, 2, sx.Total)
|
||||
assert.Equal(t, 1, sx.Revoked)
|
||||
// 类目统计
|
||||
assert.Equal(t, 2, st.ByCategory["WD"])
|
||||
}
|
||||
|
||||
func TestListSegments(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
segs := s.ListSegments()
|
||||
require.NotEmpty(t, segs)
|
||||
assert.Equal(t, "6101", segs[0].OrgNode)
|
||||
assert.Equal(t, uint64(100), segs[0].Capacity)
|
||||
}
|
||||
|
||||
func TestDailyRegulatoryReport(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
_ = issueSX(t, s)
|
||||
rep, err := s.DailyRegulatoryReport("2026-06-14")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, rep.TotalNew)
|
||||
assert.Equal(t, 1, rep.LevelDist["WD"])
|
||||
}
|
||||
@@ -49,16 +49,17 @@ type SubmissionResult struct {
|
||||
|
||||
// Service 业务编排器。
|
||||
type Service struct {
|
||||
chain chain.Client
|
||||
gen *macode.Generator
|
||||
pb *playback.Store
|
||||
prov *provenance.Store
|
||||
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
|
||||
auths map[string]model.Authorization // maCode -> 授权(F22)
|
||||
black map[string]bool // maCode -> 黑名单(跨省复用校验)
|
||||
mu sync.Mutex
|
||||
seqMu sync.Mutex
|
||||
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
|
||||
chain chain.Client
|
||||
gen *macode.Generator
|
||||
pb *playback.Store
|
||||
prov *provenance.Store
|
||||
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
|
||||
auths map[string]model.Authorization // maCode -> 授权(F22)
|
||||
black map[string]bool // maCode -> 黑名单(跨省复用校验)
|
||||
filings map[string]model.FilingRecord // maCode -> 备案/网标关联(三期 A.1)
|
||||
mu sync.Mutex
|
||||
seqMu sync.Mutex
|
||||
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
|
||||
// reviewStore 暂存送审申报(MVP 内存;生产落 PG)
|
||||
reviews map[string]*reviewItem
|
||||
}
|
||||
@@ -80,12 +81,13 @@ type phashEntry struct {
|
||||
func New(c chain.Client, gen *macode.Generator) *Service {
|
||||
return &Service{
|
||||
chain: c, gen: gen,
|
||||
pb: playback.NewStore(),
|
||||
prov: provenance.NewStore(),
|
||||
phash: make(map[string]phashEntry),
|
||||
auths: make(map[string]model.Authorization),
|
||||
black: make(map[string]bool),
|
||||
seqs: make(map[string]int), reviews: make(map[string]*reviewItem),
|
||||
pb: playback.NewStore(),
|
||||
prov: provenance.NewStore(),
|
||||
phash: make(map[string]phashEntry),
|
||||
auths: make(map[string]model.Authorization),
|
||||
black: make(map[string]bool),
|
||||
filings: make(map[string]model.FilingRecord),
|
||||
seqs: make(map[string]int), reviews: make(map[string]*reviewItem),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user