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 (
({ title: {s.title}{roleLabel[s.role]}, description: s.desc, }))} /> {done && ( 赋码结果(双锚定)} style={{ marginBottom: 16 }}> {ctx.maCode} {ctx.fileHash} {ctx.ctid} )} {done && episodes.length > 0 && ( 集级面板一剧一码 · {episodes.length} 集独立哈希} style={{ marginBottom: 16 }} extra={集级子标识:{ctx.maCode}#E01 …} > 第 {e} 集 }, { title: '集级子标识', render: (_, r) => {`${ctx.maCode}#E${String(r.episode).padStart(2, '0')}`} }, { title: '该集哈希', dataIndex: 'hash_value', ellipsis: true }, { title: '验真状态', width: 110, render: (_, r) => { const st = epVerify[r.episode] if (st === 'match') return 匹配 if (st === 'mismatch') return 不匹配 return 未验 }, }, { title: '按集验真', width: 200, render: (_, r) => ( ), }, ]} /> )} {logs.length === 0 ? : ({ color: l.ok ? 'green' : 'red', children: ( {roleLabel[l.role]} {l.title} {l.t} {l.detail} ), }))} /> } ) }