动态NFT与电影票务:从纸质票到智能合约门票
2002年,史蒂文·斯皮尔伯格的《少数派报告》(Minority Report)展示了一个"未来"场景:主角约翰·安德森在商场中被"眼球扫描仪"识别,个性化的"广告"直接"投射"到他面前。这个场景预见了"数字身份"和"个性化体验"的未来。2026年,动态NFT(Dynamic NFT)正在将电影票务从"纸质票"和"电子票"升级为"智能合约门票"——门票不再只是"入场凭证",而是"动态"的、"可编程"的、"可进化"的"数字资产"。
第一幕:电影票务的"进化"历史
第一场:从"纸质票"到"电子票"
电影票务的"进化"经历了三个阶段:
- 纸质票时代(1900s-2000s):观众在电影院窗口"排队"购买"纸质票"——票面印有"电影名称"、"放映时间"和"座位号"。
- 电子票时代(2000s-2020s):观众在"线上"购买"电子票"——票面以"二维码"或"条形码"的形式"发送"到手机。
- 智能合约门票时代(2020s-):观众在"链上"购买"NFT门票"——门票是"智能合约"中的"不可替代代币"(NFT)。
第二场:传统票务的"问题"
传统票务系统存在"多个"问题:
- 黄牛票:黄牛使用"机器人"批量"抢购"热门电影票,然后以"高价"转售。
- 假票:纸质票和电子票都可以被"伪造"。
- 不可转让:电子票通常"不可转让"——如果观众"无法"观看,票就"浪费"了。
- 数据孤岛:票务数据存储在"中心化"服务器上——观众无法"拥有"自己的"观影记录"。
第三场:动态NFT的"革命"
动态NFT(Dynamic NFT,dNFT)是一种"可以变化"的NFT——它的"元数据"(metadata)可以"根据外部条件"(如"时间"、"事件"、"用户行为")自动"更新"。
在电影票务中,动态NFT门票可以:
- 入场前显示"电影海报"和"座位信息"。
- 入场后自动"更新"为"纪念票"——显示"已观影"、"评分"和"观影时间"。
- 观影后解锁"幕后花絮"、"导演访谈"、"删减片段"等"独家内容"。
- 根据"观影次数"、"评分"等"用户行为"自动"升级"——从"普通会员"升级为"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的"核心"特性是"元数据"可以"自动"更新。在电影票务中:
- 购票时:元数据包含"电影海报"、"放映时间"、"座位号"和"二维码"。
- 入场时:元数据自动"更新"为"已检票"状态,显示"入场时间"。
- 观影后:元数据自动"更新"为"纪念票"状态,显示"电影评分"和"个人影评"。
- 收藏时:元数据可以"生成"一个"独特的"数字艺术版本——"动态"的"电影海报"。
第二场:从"票"到"收藏品"——NFT门票的"二次价值"
传统门票在"入场后"就"失去"了"价值"——票根被"丢弃"或"遗忘"。
NFT门票在"入场后"仍然具有"价值":
- 纪念价值:NFT门票是一个"数字收藏品"——你可以"永久"保存"观影记忆"。
- 社交价值:NFT门票可以"展示"在"数字钱包"或"社交媒体"上——"证明"你看过"这部电影"。
- 实用价值:NFT门票可以"解锁"未来的"优惠"——如"续集电影"的"折扣"、"导演新作"的"优先购票权"等。
第三场:从"个人"到"社区"——NFT门票的"社交"属性
NFT门票的"社交"属性:
- 同场观众:持有"同一场次"NFT门票的观众可以"组成"一个"临时社群"——"讨论"电影、"分享"影评。
- 系列收藏:收藏"同一导演"、"同一系列"或"同一类型"的NFT门票——"展示"你的"电影品味"。
- 成就系统:根据"观影数量"、"评分数量"、"影评质量"等"解锁"不同的"成就徽章"。
# 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门票不仅限于"入场",还可以"扩展"为"完整体验":
- 入场前:NFT门票可以"解锁"电影"预告片"、"幕后花絮"、"导演访谈"。
- 入场时:NFT门票可以"引导"观众到"座位"、"推荐"周边"餐饮"和"商品"。
- 入场后:NFT门票可以"解锁"电影"删减片段"、"多版本结局"、"互动问答"。
第二场:从"个人"到"社交"——NFT门票的"社交"场景
NFT门票的"社交"场景:
- 好友同场:购买"同一场次"的好友可以"共享"一个"专属"的"聊天室"。
- 电影社区:持有"同一电影"NFT门票的观众可以"加入"一个"专属"的"Discord社区"。
- 成就分享:NFT门票的"成就徽章"可以"分享"到"社交媒体"。
第三场:从"票务"到"生态"——NFT门票的"生态系统"
动态NFT门票的"最终"形态是一个"完整的生态系统":
- 票务平台:发行、销售、转让NFT门票。
- 内容平台:提供电影相关的"独家内容"。
- 社交平台:连接"同好"观众。
- 数据平台:分析"观众行为"、"电影偏好"、"市场趋势"。
// 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'));
第四场:结语——从"纸质票"到"智能合约门票"
电影票务的"进化"从"纸质票"到"电子票"再到"NFT门票",不仅是"技术"的进步,更是"文化"的变革。NFT门票不再只是"入场凭证"——它是"数字收藏品"、"社交凭证"、"身份标识"、"记忆容器"。每一次观影,你都"铸造"了一个"不可替代"的"数字记忆"。
在这个万物皆可Token化的时代,技术的迭代往往比镜头切换更快。作为北京城市学院2021级广播电视编导的毕业生,我始终在影像与区块链的交汇处寻找共鸣。感谢阅读,我是王森涛,让我们在视听与去中心化的世界里,继续探索。