Files
MAcode/tcs-iptv/web-console/src/FlowDemo.jsx
T
selfrelease a329d4906b 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 监管大屏(角色工作台/全流程演示/监管片库)
- 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
2026-06-14 16:50:31 +08:00

277 lines
12 KiB
React
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.
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>
)
}