王森涛
发布于 2026-08-03 / 0 阅读
0
0

动态NFT与电影票务:从纸质票到智能合约门票

动态NFT与电影票务:从纸质票到智能合约门票

2002年,史蒂文·斯皮尔伯格的《少数派报告》(Minority Report)展示了一个"未来"场景:主角约翰·安德森在商场中被"眼球扫描仪"识别,个性化的"广告"直接"投射"到他面前。这个场景预见了"数字身份"和"个性化体验"的未来。2026年,动态NFT(Dynamic NFT)正在将电影票务从"纸质票"和"电子票"升级为"智能合约门票"——门票不再只是"入场凭证",而是"动态"的、"可编程"的、"可进化"的"数字资产"。

第一幕:电影票务的"进化"历史

第一场:从"纸质票"到"电子票"

电影票务的"进化"经历了三个阶段:

  1. 纸质票时代(1900s-2000s):观众在电影院窗口"排队"购买"纸质票"——票面印有"电影名称"、"放映时间"和"座位号"。
  2. 电子票时代(2000s-2020s):观众在"线上"购买"电子票"——票面以"二维码"或"条形码"的形式"发送"到手机。
  3. 智能合约门票时代(2020s-):观众在"链上"购买"NFT门票"——门票是"智能合约"中的"不可替代代币"(NFT)。

第二场:传统票务的"问题"

传统票务系统存在"多个"问题:

  1. 黄牛票:黄牛使用"机器人"批量"抢购"热门电影票,然后以"高价"转售。
  2. 假票:纸质票和电子票都可以被"伪造"。
  3. 不可转让:电子票通常"不可转让"——如果观众"无法"观看,票就"浪费"了。
  4. 数据孤岛:票务数据存储在"中心化"服务器上——观众无法"拥有"自己的"观影记录"。

第三场:动态NFT的"革命"

动态NFT(Dynamic NFT,dNFT)是一种"可以变化"的NFT——它的"元数据"(metadata)可以"根据外部条件"(如"时间"、"事件"、"用户行为")自动"更新"。

在电影票务中,动态NFT门票可以:

  1. 入场前显示"电影海报"和"座位信息"。
  2. 入场后自动"更新"为"纪念票"——显示"已观影"、"评分"和"观影时间"。
  3. 观影后解锁"幕后花絮"、"导演访谈"、"删减片段"等"独家内容"。
  4. 根据"观影次数"、"评分"等"用户行为"自动"升级"——从"普通会员"升级为"VIP会员"。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract DynamicMovieTicket is ERC721, AccessControl, ReentrancyGuard {
    using Counters for Counters.Counter;
    using Strings for uint256;

    bytes32 public constant THEATER_ROLE = keccak256("THEATER_ROLE");
    bytes32 public constant DISTRIBUTOR_ROLE = keccak256("DISTRIBUTOR_ROLE");

    Counters.Counter private _tokenIdCounter;

    enum TicketStatus {
        MINTED,         // 已铸造
        SCANNED,        // 已扫描
        ACTIVE,         // 已入场
        USED,           // 已使用
        EXPIRED,        // 已过期
        REFUNDED        // 已退款
    }

    enum TicketTier {
        STANDARD,       // 普通
        PREMIUM,        // 高级
        VIP,            // VIP
        COLLECTOR       // 收藏版
    }

    struct Movie {
        uint256 movieId;
        string title;
        string director;
        uint256 releaseYear;
        uint256 duration;  // minutes
        string ipfsPosterURI;
        string ipfsTrailerURI;
        string ipfsMetadataURI;
        bool isActive;
    }

    struct Screening {
        uint256 screeningId;
        uint256 movieId;
        uint256 theaterId;
        uint256 startTime;
        uint256 endTime;
        string screenName;
        uint256 maxSeats;
        uint256 availableSeats;
        uint256 basePrice;
        bool isActive;
    }

    struct DynamicTicket {
        uint256 tokenId;
        uint256 screeningId;
        uint256 seatNumber;
        TicketStatus status;
        TicketTier tier;
        string holderName;
        uint256 mintedAt;
        uint256 scannedAt;
        uint256 usedAt;
        string ipfsPreURI;
        string ipfsPostURI;
        string ipfsCollectibleURI;
        uint256 rating;
        string review;
    }

    struct Theater {
        uint256 theaterId;
        string name;
        string location;
        uint256 screenCount;
        bool isActive;
    }

    uint256 private _movieCounter;
    uint256 private _screeningCounter;
    uint256 private _theaterCounter;

    mapping(uint256 => Movie) public movies;
    mapping(uint256 => Screening) public screenings;
    mapping(uint256 => Theater) public theaters;
    mapping(uint256 => DynamicTicket) public tickets;
    mapping(uint256 => uint256) public screeningTickets;
    mapping(uint256 => uint256[]) public userTickets;
    mapping(uint256 => mapping(uint256 => bool)) public seatOccupied;

    uint256 public constant PLATFORM_FEE = 200; // 2%
    uint256 public constant MAX_RATING = 5;

    event MovieRegistered(uint256 indexed movieId, string title, string director);
    event ScreeningCreated(uint256 indexed screeningId, uint256 indexed movieId, uint256 startTime);
    event TicketMinted(uint256 indexed tokenId, uint256 indexed screeningId, uint256 seat);
    event TicketScanned(uint256 indexed tokenId, address indexed scanner);
    event TicketUpgraded(uint256 indexed tokenId, TicketTier newTier);
    event RatingSubmitted(uint256 indexed tokenId, uint256 rating);

    constructor() ERC721("DynamicMovieTicket", "DMT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function registerMovie(
        string memory _title,
        string memory _director,
        uint256 _releaseYear,
        uint256 _duration,
        string memory _posterURI,
        string memory _trailerURI,
        string memory _metadataURI
    ) external onlyRole(DISTRIBUTOR_ROLE) returns (uint256) {
        uint256 movieId = _movieCounter++;
        movies[movieId] = Movie({
            movieId: movieId,
            title: _title,
            director: _director,
            releaseYear: _releaseYear,
            duration: _duration,
            ipfsPosterURI: _posterURI,
            ipfsTrailerURI: _trailerURI,
            ipfsMetadataURI: _metadataURI,
            isActive: true
        });
        emit MovieRegistered(movieId, _title, _director);
        return movieId;
    }

    function createScreening(
        uint256 _movieId,
        uint256 _theaterId,
        uint256 _startTime,
        string memory _screenName,
        uint256 _maxSeats,
        uint256 _basePrice
    ) external onlyRole(THEATER_ROLE) returns (uint256) {
        require(movies[_movieId].isActive, "Movie not active");
        uint256 screeningId = _screeningCounter++;
        screenings[screeningId] = Screening({
            screeningId: screeningId,
            movieId: _movieId,
            theaterId: _theaterId,
            startTime: _startTime,
            endTime: _startTime + movies[_movieId].duration * 1 minutes,
            screenName: _screenName,
            maxSeats: _maxSeats,
            availableSeats: _maxSeats,
            basePrice: _basePrice,
            isActive: true
        });
        emit ScreeningCreated(screeningId, _movieId, _startTime);
        return screeningId;
    }

    function mintTicket(
        uint256 _screeningId,
        uint256 _seatNumber,
        TicketTier _tier,
        string memory _holderName
    ) external payable nonReentrant returns (uint256) {
        Screening storage screening = screenings[_screeningId];
        require(screening.isActive, "Screening not active");
        require(block.timestamp < screening.startTime, "Screening started");
        require(screening.availableSeats > 0, "No seats available");
        require(!seatOccupied[_screeningId][_seatNumber], "Seat occupied");
        require(msg.value >= screening.basePrice * (uint256(_tier) + 1), "Insufficient payment");

        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();

        DynamicTicket memory ticket = DynamicTicket({
            tokenId: tokenId,
            screeningId: _screeningId,
            seatNumber: _seatNumber,
            status: TicketStatus.MINTED,
            tier: _tier,
            holderName: _holderName,
            mintedAt: block.timestamp,
            scannedAt: 0,
            usedAt: 0,
            ipfsPreURI: string(abi.encodePacked("ipfs://pre/", tokenId.toString(), ".json")),
            ipfsPostURI: string(abi.encodePacked("ipfs://post/", tokenId.toString(), ".json")),
            ipfsCollectibleURI: string(abi.encodePacked("ipfs://collectible/", tokenId.toString(), ".json")),
            rating: 0,
            review: ""
        });

        tickets[tokenId] = ticket;
        screening.availableSeats--;
        seatOccupied[_screeningId][_seatNumber] = true;
        _safeMint(msg.sender, tokenId);

        uint256 platformFee = msg.value * PLATFORM_FEE / 10000;
        uint256 theaterRevenue = msg.value - platformFee;
        payable(screenings[_screeningId].theaterId).transfer(theaterRevenue);

        emit TicketMinted(tokenId, _screeningId, _seatNumber);
        return tokenId;
    }

    function scanTicket(uint256 _tokenId) external onlyRole(THEATER_ROLE) {
        DynamicTicket storage ticket = tickets[_tokenId];
        require(ticket.status == TicketStatus.MINTED, "Ticket not minted");
        Screening storage screening = screenings[ticket.screeningId];
        require(block.timestamp >= screening.startTime, "Screening not started");
        require(block.timestamp <= screening.endTime, "Screening ended");

        ticket.status = TicketStatus.SCANNED;
        ticket.scannedAt = block.timestamp;
        emit TicketScanned(_tokenId, msg.sender);
    }

    function useTicket(uint256 _tokenId) external {
        DynamicTicket storage ticket = tickets[_tokenId];
        require(ownerOf(_tokenId) == msg.sender, "Not the owner");
        require(ticket.status == TicketStatus.SCANNED, "Ticket not scanned");

        ticket.status = TicketStatus.USED;
        ticket.usedAt = block.timestamp;
        // Update metadata URI to post-screening collectible
        ticket.ipfsPreURI = ticket.ipfsPostURI;
    }

    function submitRating(uint256 _tokenId, uint256 _rating, string memory _review) external {
        require(ownerOf(_tokenId) == msg.sender, "Not the owner");
        require(tickets[_tokenId].status == TicketStatus.USED, "Ticket not used");
        require(_rating >= 1 && _rating <= MAX_RATING, "Invalid rating");
        tickets[_tokenId].rating = _rating;
        tickets[_tokenId].review = _review;
        emit RatingSubmitted(_tokenId, _rating);
    }

    function upgradeTicket(uint256 _tokenId, TicketTier _newTier) external payable nonReentrant {
        DynamicTicket storage ticket = tickets[_tokenId];
        require(ownerOf(_tokenId) == msg.sender, "Not the owner");
        require(ticket.status == TicketStatus.MINTED, "Ticket already used");
        require(_newTier > ticket.tier, "New tier must be higher");
        uint256 priceDiff = (uint256(_newTier) - uint256(ticket.tier)) * screenings[ticket.screeningId].basePrice;
        require(msg.value >= priceDiff, "Insufficient payment");
        ticket.tier = _newTier;
        emit TicketUpgraded(_tokenId, _newTier);
    }

    function getTicketMetadata(uint256 _tokenId) external view returns (DynamicTicket memory) {
        return tickets[_tokenId];
    }

    function getUserTickets(address _user) external view returns (uint256[] memory) {
        uint256 balance = balanceOf(_user);
        uint256[] memory result = new uint256[](balance);
        uint256 index = 0;
        for (uint256 i = 0; i < _tokenIdCounter.current(); i++) {
            if (ownerOf(i) == _user) {
                result[index] = i;
                index++;
            }
        }
        return result;
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, AccessControl) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

第二幕:动态NFT的"智能"特性

第一场:元数据的"动态"更新

动态NFT的"核心"特性是"元数据"可以"自动"更新。在电影票务中:

  1. 购票时:元数据包含"电影海报"、"放映时间"、"座位号"和"二维码"。
  2. 入场时:元数据自动"更新"为"已检票"状态,显示"入场时间"。
  3. 观影后:元数据自动"更新"为"纪念票"状态,显示"电影评分"和"个人影评"。
  4. 收藏时:元数据可以"生成"一个"独特的"数字艺术版本——"动态"的"电影海报"。

第二场:从"票"到"收藏品"——NFT门票的"二次价值"

传统门票在"入场后"就"失去"了"价值"——票根被"丢弃"或"遗忘"。

NFT门票在"入场后"仍然具有"价值":

  1. 纪念价值:NFT门票是一个"数字收藏品"——你可以"永久"保存"观影记忆"。
  2. 社交价值:NFT门票可以"展示"在"数字钱包"或"社交媒体"上——"证明"你看过"这部电影"。
  3. 实用价值:NFT门票可以"解锁"未来的"优惠"——如"续集电影"的"折扣"、"导演新作"的"优先购票权"等。

第三场:从"个人"到"社区"——NFT门票的"社交"属性

NFT门票的"社交"属性:

  1. 同场观众:持有"同一场次"NFT门票的观众可以"组成"一个"临时社群"——"讨论"电影、"分享"影评。
  2. 系列收藏:收藏"同一导演"、"同一系列"或"同一类型"的NFT门票——"展示"你的"电影品味"。
  3. 成就系统:根据"观影数量"、"评分数量"、"影评质量"等"解锁"不同的"成就徽章"。
# Dynamic NFT Movie Ticket System
# Smart contract tickets with dynamic metadata

import hashlib
import json
import time
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum

class TicketStatus(Enum):
    MINTED = "minted"
    SCANNED = "scanned"
    ACTIVE = "active"
    USED = "used"
    EXPIRED = "expired"
    REFUNDED = "refunded"

class TicketTier(Enum):
    STANDARD = 0
    PREMIUM = 1
    VIP = 2
    COLLECTOR = 3

@dataclass
class Movie:
    movie_id: int
    title: str
    director: str
    release_year: int
    duration: int
    ipfs_poster: str
    ipfs_trailer: str

@dataclass
class Screening:
    screening_id: int
    movie_id: int
    theater: str
    screen: str
    start_time: float
    end_time: float
    max_seats: int
    available_seats: int
    base_price: float

@dataclass
class DynamicTicket:
    token_id: int
    screening_id: int
    seat: int
    status: TicketStatus
    tier: TicketTier
    holder: str
    minted_at: float
    scanned_at: float
    used_at: float
    rating: int
    review: str
    metadata: Dict

class DynamicMovieTicketSystem:
    PLATFORM_FEE = 0.02

    def __init__(self):
        self.movies: Dict[int, Movie] = {}
        self.screenings: Dict[int, Screening] = {}
        self.tickets: Dict[int, DynamicTicket] = {}
        self.user_tickets: Dict[str, List[int]] = {}
        self.movie_counter = 0
        self.screening_counter = 0
        self.ticket_counter = 0

    def register_movie(self, title: str, director: str, release_year: int,
                      duration: int, poster: str, trailer: str) -> Movie:
        movie_id = self.movie_counter
        self.movie_counter += 1
        movie = Movie(movie_id=movie_id, title=title, director=director,
                     release_year=release_year, duration=duration,
                     ipfs_poster=poster, ipfs_trailer=trailer)
        self.movies[movie_id] = movie
        print(f"[MOVIE] Registered: {title} by {director}")
        return movie

    def create_screening(self, movie_id: int, theater: str, screen: str,
                        start_time: float, max_seats: int, base_price: float) -> Screening:
        if movie_id not in self.movies:
            raise ValueError(f"Movie {movie_id} not found")
        movie = self.movies[movie_id]
        screening_id = self.screening_counter
        self.screening_counter += 1
        screening = Screening(screening_id=screening_id, movie_id=movie_id,
                            theater=theater, screen=screen, start_time=start_time,
                            end_time=start_time + movie.duration * 60,
                            max_seats=max_seats, available_seats=max_seats,
                            base_price=base_price)
        self.screenings[screening_id] = screening
        print(f"[SCREENING] Created: {movie.title} at {theater}")
        return screening

    def mint_ticket(self, screening_id: int, seat: int, tier: TicketTier,
                   holder: str, payment: float) -> DynamicTicket:
        if screening_id not in self.screenings:
            raise ValueError(f"Screening {screening_id} not found")
        screening = self.screenings[screening_id]
        if screening.available_seats <= 0:
            raise ValueError("No seats available")
        if payment < screening.base_price * (tier.value + 1):
            raise ValueError("Insufficient payment")

        token_id = self.ticket_counter
        self.ticket_counter += 1

        metadata = self._generate_pre_metadata(screening, seat, tier)
        ticket = DynamicTicket(token_id=token_id, screening_id=screening_id,
                              seat=seat, status=TicketStatus.MINTED, tier=tier,
                              holder=holder, minted_at=time.time(),
                              scanned_at=0, used_at=0, rating=0, review="",
                              metadata=metadata)
        self.tickets[token_id] = ticket
        screening.available_seats -= 1

        if holder not in self.user_tickets:
            self.user_tickets[holder] = []
        self.user_tickets[holder].append(token_id)

        print(f"[TICKET] Minted #{token_id}: {seat} ({tier.name}) for {holder}")
        return ticket

    def _generate_pre_metadata(self, screening: Screening, seat: int, tier: TicketTier) -> Dict:
        movie = self.movies[screening.movie_id]
        return {
            "name": f"{movie.title} - {screening.screen}",
            "description": f"Ticket for {movie.title}",
            "image": movie.ipfs_poster,
            "attributes": [
                {"trait_type": "Movie", "value": movie.title},
                {"trait_type": "Theater", "value": screening.theater},
                {"trait_type": "Screen", "value": screening.screen},
                {"trait_type": "Seat", "value": str(seat)},
                {"trait_type": "Tier", "value": tier.name},
                {"trait_type": "Time", "value": time.strftime('%Y-%m-%d %H:%M', time.localtime(screening.start_time))},
                {"trait_type": "Status", "value": "Minted"}
            ]
        }

    def scan_ticket(self, token_id: int) -> bool:
        if token_id not in self.tickets:
            raise ValueError(f"Ticket {token_id} not found")
        ticket = self.tickets[token_id]
        if ticket.status != TicketStatus.MINTED:
            raise ValueError("Ticket not minted")
        ticket.status = TicketStatus.SCANNED
        ticket.scanned_at = time.time()
        ticket.metadata["attributes"].append({"trait_type": "Status", "value": "Scanned"})
        print(f"[SCAN] Ticket #{token_id} scanned")
        return True

    def use_ticket(self, token_id: int, user: str) -> bool:
        if token_id not in self.tickets:
            raise ValueError(f"Ticket {token_id} not found")
        ticket = self.tickets[token_id]
        if ticket.holder != user:
            raise ValueError("Not the owner")
        if ticket.status != TicketStatus.SCANNED:
            raise ValueError("Ticket not scanned")
        ticket.status = TicketStatus.USED
        ticket.used_at = time.time()
        ticket.metadata = self._generate_post_metadata(ticket)
        print(f"[USE] Ticket #{token_id} used")
        return True

    def _generate_post_metadata(self, ticket: DynamicTicket) -> Dict:
        screening = self.screenings[ticket.screening_id]
        movie = self.movies[screening.movie_id]
        return {
            "name": f"{movie.title} - Collectible Ticket",
            "description": f"Thank you for watching {movie.title}!",
            "image": movie.ipfs_poster.replace("poster", "collectible"),
            "attributes": [
                {"trait_type": "Movie", "value": movie.title},
                {"trait_type": "Theater", "value": screening.theater},
                {"trait_type": "Seat", "value": str(ticket.seat)},
                {"trait_type": "Tier", "value": ticket.tier.name},
                {"trait_type": "Rating", "value": str(ticket.rating)},
                {"trait_type": "Status", "value": "Collectible"}
            ]
        }

    def submit_rating(self, token_id: int, user: str, rating: int, review: str):
        if token_id not in self.tickets:
            raise ValueError(f"Ticket {token_id} not found")
        ticket = self.tickets[token_id]
        if ticket.holder != user:
            raise ValueError("Not the owner")
        if ticket.status != TicketStatus.USED:
            raise ValueError("Ticket not used")
        if rating < 1 or rating > 5:
            raise ValueError("Rating must be 1-5")
        ticket.rating = rating
        ticket.review = review
        ticket.metadata["attributes"].append({"trait_type": "Rating", "value": str(rating)})
        print(f"[RATING] Ticket #{token_id}: {rating}/5")

    def get_user_collection(self, user: str) -> List[Dict]:
        if user not in self.user_tickets:
            return []
        result = []
        for token_id in self.user_tickets[user]:
            ticket = self.tickets[token_id]
            screening = self.screenings[ticket.screening_id]
            movie = self.movies[screening.movie_id]
            result.append({
                "token_id": token_id,
                "movie": movie.title,
                "theater": screening.theater,
                "seat": ticket.seat,
                "tier": ticket.tier.name,
                "status": ticket.status.value,
                "rating": ticket.rating,
                "date": time.strftime('%Y-%m-%d', time.localtime(screening.start_time))
            })
        return result

# Example
system = DynamicMovieTicketSystem()
movie = system.register_movie("Dune: Part Three", "Denis Villeneuve", 2026, 165,
                             "ipfs://poster", "ipfs://trailer")
screening = system.create_screening(movie.movie_id, "IMAX Theater", "Screen 1",
                                   time.time() + 86400, 200, 15.0)
ticket = system.mint_ticket(screening.screening_id, 42, TicketTier.VIP, "0xUSER", 45.0)
system.scan_ticket(ticket.token_id)
system.use_ticket(ticket.token_id, "0xUSER")
system.submit_rating(ticket.token_id, "0xUSER", 5, "Amazing film!")
collection = system.get_user_collection("0xUSER")
print(f"Collection: {json.dumps(collection, indent=2, ensure_ascii=False)}")

第三幕:动态NFT票务的"未来"场景

第一场:从"入场"到"体验"——NFT门票的"体验"扩展

动态NFT门票不仅限于"入场",还可以"扩展"为"完整体验":

  1. 入场前:NFT门票可以"解锁"电影"预告片"、"幕后花絮"、"导演访谈"。
  2. 入场时:NFT门票可以"引导"观众到"座位"、"推荐"周边"餐饮"和"商品"。
  3. 入场后:NFT门票可以"解锁"电影"删减片段"、"多版本结局"、"互动问答"。

第二场:从"个人"到"社交"——NFT门票的"社交"场景

NFT门票的"社交"场景:

  1. 好友同场:购买"同一场次"的好友可以"共享"一个"专属"的"聊天室"。
  2. 电影社区:持有"同一电影"NFT门票的观众可以"加入"一个"专属"的"Discord社区"。
  3. 成就分享:NFT门票的"成就徽章"可以"分享"到"社交媒体"。

第三场:从"票务"到"生态"——NFT门票的"生态系统"

动态NFT门票的"最终"形态是一个"完整的生态系统":

  1. 票务平台:发行、销售、转让NFT门票。
  2. 内容平台:提供电影相关的"独家内容"。
  3. 社交平台:连接"同好"观众。
  4. 数据平台:分析"观众行为"、"电影偏好"、"市场趋势"。
// Dynamic NFT Movie Ticket System API
// Smart contract tickets with dynamic metadata

class DynamicMovieTicketSystem {
    constructor() {
        this.movies = new Map();
        this.screenings = new Map();
        this.tickets = new Map();
        this.userTickets = new Map();
        this.movieCounter = 0;
        this.screeningCounter = 0;
        this.ticketCounter = 0;
    }

    registerMovie(title, director, releaseYear, duration, poster, trailer) {
        const movieId = this.movieCounter++;
        const movie = { movieId, title, director, releaseYear, duration, poster, trailer };
        this.movies.set(movieId, movie);
        console.log(`[MOVIE] Registered: ${title}`);
        return movie;
    }

    createScreening(movieId, theater, screen, startTime, maxSeats, basePrice) {
        const movie = this.movies.get(movieId);
        if (!movie) throw new Error('Movie not found');
        const screeningId = this.screeningCounter++;
        const screening = {
            screeningId, movieId, theater, screen, startTime,
            endTime: startTime + movie.duration * 60000,
            maxSeats, availableSeats: maxSeats, basePrice
        };
        this.screenings.set(screeningId, screening);
        console.log(`[SCREENING] Created: ${movie.title} at ${theater}`);
        return screening;
    }

    mintTicket(screeningId, seat, tier, holder, payment) {
        const screening = this.screenings.get(screeningId);
        if (!screening) throw new Error('Screening not found');
        if (screening.availableSeats <= 0) throw new Error('No seats');
        const tierValues = {STANDARD: 0, PREMIUM: 1, VIP: 2, COLLECTOR: 3};
        if (payment < screening.basePrice * (tierValues[tier] + 1)) throw new Error('Insufficient payment');

        const tokenId = this.ticketCounter++;
        const movie = this.movies.get(screening.movieId);
        const metadata = {
            name: `${movie.title} - ${screening.screen}`,
            attributes: [
                {trait_type: 'Movie', value: movie.title},
                {trait_type: 'Theater', value: screening.theater},
                {trait_type: 'Seat', value: seat.toString()},
                {trait_type: 'Tier', value: tier},
                {trait_type: 'Status', value: 'Minted'}
            ]
        };

        const ticket = { tokenId, screeningId, seat, status: 'minted', tier, holder,
            mintedAt: Date.now(), scannedAt: 0, usedAt: 0, rating: 0, review: '', metadata };
        this.tickets.set(tokenId, ticket);
        screening.availableSeats--;

        if (!this.userTickets.has(holder)) this.userTickets.set(holder, []);
        this.userTickets.get(holder).push(tokenId);

        console.log(`[TICKET] Minted #${tokenId}: ${seat} (${tier}) for ${holder}`);
        return ticket;
    }

    scanTicket(tokenId) {
        const ticket = this.tickets.get(tokenId);
        if (!ticket) throw new Error('Ticket not found');
        if (ticket.status !== 'minted') throw new Error('Not minted');
        ticket.status = 'scanned';
        ticket.scannedAt = Date.now();
        ticket.metadata.attributes.push({trait_type: 'Status', value: 'Scanned'});
        console.log(`[SCAN] Ticket #${tokenId} scanned`);
    }

    useTicket(tokenId, user) {
        const ticket = this.tickets.get(tokenId);
        if (!ticket) throw new Error('Ticket not found');
        if (ticket.holder !== user) throw new Error('Not the owner');
        if (ticket.status !== 'scanned') throw new Error('Not scanned');
        ticket.status = 'used';
        ticket.usedAt = Date.now();
        ticket.metadata.name = `${ticket.metadata.name} - Collectible`;
        ticket.metadata.attributes = ticket.metadata.attributes.filter(a => a.trait_type !== 'Status');
        ticket.metadata.attributes.push({trait_type: 'Status', value: 'Collectible'});
        console.log(`[USE] Ticket #${tokenId} used`);
    }

    submitRating(tokenId, user, rating, review) {
        const ticket = this.tickets.get(tokenId);
        if (!ticket) throw new Error('Ticket not found');
        if (ticket.holder !== user) throw new Error('Not the owner');
        if (ticket.status !== 'used') throw new Error('Not used');
        ticket.rating = rating;
        ticket.review = review;
        ticket.metadata.attributes.push({trait_type: 'Rating', value: rating.toString()});
        console.log(`[RATING] Ticket #${tokenId}: ${rating}/5`);
    }

    getUserCollection(user) {
        const ids = this.userTickets.get(user) || [];
        return ids.map(id => {
            const t = this.tickets.get(id);
            const s = this.screenings.get(t.screeningId);
            const m = this.movies.get(s.movieId);
            return { tokenId: id, movie: m.title, theater: s.theater, seat: t.seat,
                tier: t.tier, status: t.status, rating: t.rating };
        });
    }
}

// Example
const system = new DynamicMovieTicketSystem();
const movie = system.registerMovie('Dune: Part Three', 'Denis Villeneuve', 2026, 165, 'ipfs://poster', 'ipfs://trailer');
const screening = system.createScreening(movie.movieId, 'IMAX Theater', 'Screen 1', Date.now() + 86400000, 200, 15);
const ticket = system.mintTicket(screening.screeningId, 42, 'VIP', '0xUSER', 45);
system.scanTicket(ticket.tokenId);
system.useTicket(ticket.tokenId, '0xUSER');
system.submitRating(ticket.tokenId, '0xUSER', 5, 'Amazing!');
console.log('Collection:', system.getUserCollection('0xUSER'));

Dynamic NFT movie tickets

第四场:结语——从"纸质票"到"智能合约门票"

电影票务的"进化"从"纸质票"到"电子票"再到"NFT门票",不仅是"技术"的进步,更是"文化"的变革。NFT门票不再只是"入场凭证"——它是"数字收藏品"、"社交凭证"、"身份标识"、"记忆容器"。每一次观影,你都"铸造"了一个"不可替代"的"数字记忆"。

在这个万物皆可Token化的时代,技术的迭代往往比镜头切换更快。作为北京城市学院2021级广播电视编导的毕业生,我始终在影像与区块链的交汇处寻找共鸣。感谢阅读,我是王森涛,让我们在视听与去中心化的世界里,继续探索。


评论