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 监管大屏(角色工作台/全流程演示/监管片库) - 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Card, Steps, Button, Space, Input, Select, Form, Tag, Timeline,
|
||||
Descriptions, message, Row, Col, Typography, Divider, Alert, Table,
|
||||
} from 'antd'
|
||||
import {
|
||||
PlayCircleOutlined, RedoOutlined, StopOutlined, SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { api } from './api.js'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
const roleColor = { cp: 'green', regulator: 'red', reviewer: 'blue', operator: 'orange' }
|
||||
const roleLabel = { cp: '内容提供商', regulator: '监管主体', reviewer: '审核/媒资', operator: '运营商' }
|
||||
|
||||
// 七步流水线定义(审核在前,发码在后——审过才发证发码)
|
||||
const STEPS = [
|
||||
{ key: 'register', title: 'CP 送审', role: 'cp', desc: '原片送 CSPS 既有审核渠道 + 哈希包上链(链上不存原片)' },
|
||||
{ key: 'csps', title: 'CSPS 合规审核', role: 'reviewer', desc: '基于原片审画面/台词/声音;验真送审哈希=上链哈希(审播一致)' },
|
||||
{ key: 'issue', title: '审核通过·发码签发', role: 'regulator', desc: '审过才发码:按号段生成 MA 码,1:1 强绑定哈希' },
|
||||
{ key: 'ingest', title: '媒资库入库', role: 'reviewer', desc: '审合格入库,建立媒资编码映射' },
|
||||
{ key: 'publish', title: '发布给运营商', role: 'reviewer', desc: '携 MA 码+哈希证书发布' },
|
||||
{ key: 'inject', title: 'CDN 注入校验', role: 'operator', desc: '注入前哈希比对,匹配放行(防偷换)' },
|
||||
]
|
||||
|
||||
export default function FlowDemo() {
|
||||
const [form] = Form.useForm()
|
||||
const [current, setCurrent] = useState(0)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [logs, setLogs] = useState([])
|
||||
const [ctx, setCtx] = useState({}) // reviewID/ctid/maCode/cert/fileHash/episodeHashes
|
||||
const [done, setDone] = useState(false)
|
||||
const [episodes, setEpisodes] = useState([]) // 集级哈希列表
|
||||
const [epVerify, setEpVerify] = useState({}) // {episode: 'match'|'mismatch'}
|
||||
|
||||
function addLog(role, title, ok, detail) {
|
||||
setLogs((prev) => [...prev, { role, title, ok, detail, t: new Date().toLocaleTimeString() }])
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setCurrent(0); setLogs([]); setCtx({}); setDone(false); setRunning(false)
|
||||
setEpisodes([]); setEpVerify({})
|
||||
}
|
||||
|
||||
// 执行单步,返回是否成功
|
||||
async function runStep(idx, shared) {
|
||||
const step = STEPS[idx]
|
||||
const v = form.getFieldsValue()
|
||||
const fileHash = shared.fileHash
|
||||
let res
|
||||
switch (step.key) {
|
||||
case 'register':
|
||||
// 一剧一码 + 集级哈希:按集数生成每集独立哈希
|
||||
shared.episodeHashes = []
|
||||
const epArr = []
|
||||
const epCount = Number(v.episodes) || 1
|
||||
for (let i = 1; i <= epCount; i++) {
|
||||
const eh = `${fileHash}-E${i}`
|
||||
shared.episodeHashes.push({ episode: i, hash: eh })
|
||||
epArr.push({ episode: i, file_sha256: eh, merkle_root: `mr-${eh}` })
|
||||
}
|
||||
res = await api.register({
|
||||
title: v.title, episode_count: epCount, category: v.category,
|
||||
file_sha256: fileHash, merkle_root: 'mr-' + fileHash, perceptual_hash: 'ph-' + fileHash,
|
||||
episodes: epArr,
|
||||
cp_media_id: v.cpId, cp_name: v.cpName,
|
||||
})
|
||||
if (res.ok) { shared.reviewID = res.data.data.review_id; shared.ctid = res.data.data.content_twin_id }
|
||||
addLog(step.role, step.title, res.ok, res.ok ? `流水号 ${shared.reviewID}(${epCount}集,每集独立哈希)` : res.data.message)
|
||||
break
|
||||
case 'issue':
|
||||
res = await api.issue({ review_id: shared.reviewID, issuer: '陕西IPTV运营公司' })
|
||||
if (res.ok) { shared.maCode = res.data.data.ma_code; shared.cert = res.data.data.certificate }
|
||||
addLog(step.role, step.title, res.ok, res.ok ? `MA码 ${shared.maCode}` : res.data.message)
|
||||
break
|
||||
case 'csps':
|
||||
res = await api.csps({ review_id: shared.reviewID, approved: true, reviewer_id: 'sxiptv-审核01' })
|
||||
addLog(step.role, step.title, res.ok, res.ok ? '审核通过(发码前置)' : res.data.message)
|
||||
break
|
||||
case 'ingest':
|
||||
res = await api.ingest({
|
||||
ma_code: shared.maCode, content_twin_id: shared.ctid,
|
||||
media_asset_id: 'SXMEDIA-' + fileHash, lib_name: '陕西IPTV媒体资源库',
|
||||
})
|
||||
addLog(step.role, step.title, res.ok, res.ok ? '已入媒资库' : res.data.message)
|
||||
break
|
||||
case 'publish':
|
||||
res = await api.publish({ ma_code: shared.maCode, certificate: shared.cert })
|
||||
addLog(step.role, step.title, res.ok, res.ok ? '已发布' : res.data.message)
|
||||
break
|
||||
case 'inject':
|
||||
res = await api.inject({
|
||||
content_twin_id: shared.ctid, ma_code: shared.maCode, file_sha256: fileHash,
|
||||
operator_id: v.opId, cdn_endpoint: v.cdn,
|
||||
})
|
||||
addLog(step.role, step.title, res.ok,
|
||||
res.ok ? `注入成功 ${res.data.data.distribution_id}` : res.data.message)
|
||||
break
|
||||
default:
|
||||
res = { ok: false }
|
||||
}
|
||||
return res.ok
|
||||
}
|
||||
|
||||
// 一键全流程
|
||||
async function runAll() {
|
||||
setRunning(true); setLogs([]); setDone(false); setCurrent(0)
|
||||
const shared = { fileHash: 'fh-' + Date.now().toString(36) }
|
||||
for (let i = 0; i < STEPS.length; i++) {
|
||||
setCurrent(i)
|
||||
const ok = await runStep(i, shared)
|
||||
if (!ok) {
|
||||
message.error(`第 ${i + 1} 步「${STEPS[i].title}」失败,流程中断`)
|
||||
setRunning(false); setCtx(shared)
|
||||
return
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500)) // 放慢便于演示观看
|
||||
}
|
||||
setCurrent(STEPS.length)
|
||||
setCtx(shared); setDone(true); setRunning(false)
|
||||
message.success('全流程跑通:审过即锁定,锁定即通行')
|
||||
await loadEpisodes(shared.maCode)
|
||||
}
|
||||
|
||||
async function loadEpisodes(maCode) {
|
||||
const res = await api.episodes(maCode)
|
||||
if (res.ok) setEpisodes(res.data.data.episodes || [])
|
||||
}
|
||||
|
||||
// 按集验真:correct=true 用正确哈希,false 用篡改哈希
|
||||
async function verifyEp(ep, correct) {
|
||||
const realHash = (ctx.episodeHashes || []).find((e) => e.episode === ep)?.hash
|
||||
const submit = correct ? realHash : 'TAMPERED-' + realHash
|
||||
const res = await api.verifyEpisode(ctx.maCode, ep, submit)
|
||||
const matched = res.ok && res.data.data?.match
|
||||
setEpVerify((prev) => ({ ...prev, [ep]: matched ? 'match' : 'mismatch' }))
|
||||
if (matched) message.success(`第${ep}集验真通过`)
|
||||
else message.warning(`第${ep}集不匹配(疑似该集被替换)`)
|
||||
}
|
||||
|
||||
async function doTakedown() {
|
||||
const res = await api.takedown(ctx.maCode, '监管演示下架')
|
||||
if (res.ok) {
|
||||
addLog('regulator', '违规应急下架', true,
|
||||
`秒级下架,受影响 CDN: ${(res.data.data.cdn_endpoints || []).join(', ')}`)
|
||||
message.success('已全网下架')
|
||||
} else message.error(res.data.message)
|
||||
}
|
||||
|
||||
async function doTamperTest() {
|
||||
const res = await api.inject({
|
||||
content_twin_id: ctx.ctid, ma_code: ctx.maCode, file_sha256: 'TAMPERED-' + ctx.fileHash,
|
||||
operator_id: 'OP-X', cdn_endpoint: 'cdn://x',
|
||||
})
|
||||
addLog('operator', '篡改注入测试', !res.ok, res.ok ? '⚠️ 异常:篡改竟通过' : '✅ 已拒绝:' + res.data.message)
|
||||
if (!res.ok) message.success('篡改内容被正确拦截')
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col span={9}>
|
||||
<Card title="演示参数" size="small" style={{ marginBottom: 16 }}>
|
||||
<Form form={form} layout="vertical" size="small" initialValues={{
|
||||
title: '长安少年行', category: 'WD', episodes: 6,
|
||||
cpId: 'XAQJSL-2026-001', cpName: '西安曲江丝路文化传播有限公司',
|
||||
opId: 'CT-SX-IPTV', cdn: 'cdn://ct-sx/iptv/vod/changan001',
|
||||
}}>
|
||||
<Form.Item label="作品标题" name="title"><Input /></Form.Item>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="类目" name="category">
|
||||
<Select options={[
|
||||
{ value: 'WD', label: '微短剧' }, { value: 'WJ', label: '网络剧' },
|
||||
{ value: 'DY', label: '网络电影' }, { value: 'DH', label: '网络动画' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}><Form.Item label="集数" name="episodes"><Input type="number" /></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item label="内容提供商 (CP)" name="cpName"><Input /></Form.Item>
|
||||
<Form.Item label="CP 媒资编码" name="cpId"><Input /></Form.Item>
|
||||
<Form.Item label="运营商编码" name="opId"><Input /></Form.Item>
|
||||
<Form.Item label="CDN 端点" name="cdn"><Input /></Form.Item>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlayCircleOutlined />} loading={running} onClick={runAll}>
|
||||
一键全流程
|
||||
</Button>
|
||||
<Button icon={<RedoOutlined />} onClick={reset} disabled={running}>重置</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={15}>
|
||||
<Card title="内容生命周期流水线" style={{ marginBottom: 16 }}>
|
||||
<Steps
|
||||
direction="vertical" size="small" current={current}
|
||||
status={running ? 'process' : done ? 'finish' : 'wait'}
|
||||
items={STEPS.map((s) => ({
|
||||
title: <Space>{s.title}<Tag color={roleColor[s.role]}>{roleLabel[s.role]}</Tag></Space>,
|
||||
description: s.desc,
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{done && (
|
||||
<Card title={<Space><SafetyCertificateOutlined />赋码结果(双锚定)</Space>} style={{ marginBottom: 16 }}>
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="MA 码(监管锚点)"><Text strong copyable>{ctx.maCode}</Text></Descriptions.Item>
|
||||
<Descriptions.Item label="文件哈希(技术锚点)">{ctx.fileHash}</Descriptions.Item>
|
||||
<Descriptions.Item label="CTID(机器主键)">{ctx.ctid}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<Space>
|
||||
<Button onClick={doTamperTest}>篡改注入测试(应拒绝)</Button>
|
||||
<Button danger icon={<StopOutlined />} onClick={doTakedown}>违规应急下架</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{done && episodes.length > 0 && (
|
||||
<Card
|
||||
title={<Space>集级面板<Tag color="purple">一剧一码 · {episodes.length} 集独立哈希</Tag></Space>}
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={<Text type="secondary">集级子标识:{ctx.maCode}#E01 …</Text>}
|
||||
>
|
||||
<Table
|
||||
size="small" rowKey="episode" pagination={false}
|
||||
dataSource={episodes}
|
||||
columns={[
|
||||
{ title: '集号', dataIndex: 'episode', width: 70, render: (e) => <Tag>第 {e} 集</Tag> },
|
||||
{ title: '集级子标识', render: (_, r) => <Text code>{`${ctx.maCode}#E${String(r.episode).padStart(2, '0')}`}</Text> },
|
||||
{ title: '该集哈希', dataIndex: 'hash_value', ellipsis: true },
|
||||
{
|
||||
title: '验真状态', width: 110, render: (_, r) => {
|
||||
const st = epVerify[r.episode]
|
||||
if (st === 'match') return <Tag color="green">匹配</Tag>
|
||||
if (st === 'mismatch') return <Tag color="red">不匹配</Tag>
|
||||
return <Tag>未验</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '按集验真', width: 200, render: (_, r) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => verifyEp(r.episode, true)}>正确</Button>
|
||||
<Button size="small" danger onClick={() => verifyEp(r.episode, false)}>模拟篡改</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card title="执行日志" size="small">
|
||||
{logs.length === 0
|
||||
? <Alert type="info" message='点击「一键全流程」开始演示' />
|
||||
: <Timeline items={logs.map((l) => ({
|
||||
color: l.ok ? 'green' : 'red',
|
||||
children: (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Space>
|
||||
<Tag color={roleColor[l.role]}>{roleLabel[l.role]}</Tag>
|
||||
<Text strong>{l.title}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{l.t}</Text>
|
||||
</Space>
|
||||
<Text type={l.ok ? 'success' : 'danger'}>{l.detail}</Text>
|
||||
</Space>
|
||||
),
|
||||
}))} />
|
||||
}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user