Skip to main content

🎥 构建一个电影评论程序 - 成为区块链影评人!

🎯 本章目标: 打造一个去中心化的烂番茄网站,让你的影评永远留在区块链上!


🌟 开篇:从观众到影评人的华丽转身!

还记得第一节我们玩过的电影评论程序吗?当时我们只是"观众",现在要成为"导演"了!🎬 今天我们要从零开始构建一个完整的电影评论系统。

🎮 今日任务清单

📍 起点:空白的程序

📝 设计指令结构 → 🔪 反序列化数据 → 🎬 处理评论
↓ ↓ ↓
定义格式 解析数据 保存评论

🎊 部署上链 → 🎯 客户端测试

🏆 你的去中心化影评网站诞生!

💡 有趣的事实: 传统影评网站可以删除你的评论,但区块链上的评论是永恒的!即使是《房间》这样的烂片评论也会永远存在!😄


🏗️ 项目初始化 - 搭建你的电影院!

🎯 两种开发方式

🌐 方式1:Solana Playground(推荐新手)

直接在浏览器中开发,零配置!

// 🎯 直接访问
https://beta.solpg.io

// 创建新项目
点击 "Create a new project"
选择 "Native"
项目名: "movie-review-program"

💻 方式2:本地开发(专业玩家)

# 🚀 初始化项目
cargo init movie-review-program --lib
cd movie-review-program

# 📦 安装依赖包
cargo add solana-program # Solana核心库
cargo add borsh # 序列化神器

# 🎨 项目结构
movie-review-program/
├── Cargo.toml # 📋 配置文件
└── src/
├── lib.rs # 🎯 主程序
└── instruction.rs # 📝 指令处理

📋 配置 Cargo.toml

[package]
name = "movie-review-program"
version = "0.1.0"
edition = "2021"

# 📚 依赖包
[dependencies]
borsh = "0.10.3" # 🔄 序列化/反序列化
solana-program = "1.16.10" # 🚀 Solana SDK

# 🎯 重要配置:告诉Rust构建动态库
[lib]
crate-type = ["cdylib", "lib"] # cdylib = C动态库,lib = Rust库

💡 知识点: cdylib让程序能在Solana运行时中执行,lib让其他Rust代码能导入你的库!

🎬 初始化主程序 (lib.rs)

// 🎯 导入必要的Solana组件
use solana_program::{
entrypoint, // 🚪 程序入口宏
entrypoint::ProgramResult, // ✅ 程序结果类型
pubkey::Pubkey, // 🔑 公钥类型
msg, // 📢 日志输出
account_info::AccountInfo, // 📦 账户信息
};

// 🎪 定义程序入口点 - 所有魔法开始的地方!
entrypoint!(process_instruction);

// 🎮 主处理函数 - 程序的大脑
pub fn process_instruction(
program_id: &Pubkey, // 🏷️ 程序ID
accounts: &[AccountInfo], // 🏦 相关账户
instruction_data: &[u8] // 📦 指令数据(原始字节)
) -> ProgramResult {
// 🎬 暂时留空,马上就要填充精彩内容!
msg!("🎬 电影评论程序启动!");
Ok(())
}

🔪 反序列化指令数据 - 解码影评密码!

📝 创建 instruction.rs

// 🎯 导入序列化工具
use borsh::{BorshDeserialize};
use solana_program::{program_error::ProgramError};

// 🎬 定义指令枚举 - 程序能做的所有操作
pub enum MovieInstruction {
AddMovieReview {
title: String, // 🎬 电影标题
rating: u8, // ⭐ 评分 (0-10)
description: String // 📝 评论内容
},
// 🔮 未来可以添加:
// UpdateReview { ... } // ✏️ 更新评论
// DeleteReview { ... } // 🗑️ 删除评论
// LikeReview { ... } // 👍 点赞评论
}

// 📦 负载结构体 - 用于反序列化
#[derive(BorshDeserialize)] // 🪄 自动生成反序列化代码
struct MovieReviewPayload {
title: String, // 电影名称
rating: u8, // 评分
description: String // 评论详情
}

🎨 实现解包逻辑

impl MovieInstruction {
/// 🎯 解包函数:将字节数组转换为指令
///
/// # 工作流程:
/// 1. 分离指令类型(第一个字节)
/// 2. 反序列化剩余数据
/// 3. 构建对应的指令枚举
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// 🔍 步骤1: 提取第一个字节作为指令类型
// split_first() 返回 Option<(&T, &[T])>
// ok_or() 将 None 转换为错误
// ? 操作符:如果错误则提前返回
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;

msg!("📊 指令类型: {}", variant);

// 🎨 步骤2: 反序列化剩余数据
// try_from_slice 尝试将字节切片转换为结构体
// unwrap() 在这里是安全的,因为我们控制输入格式
let payload = MovieReviewPayload::try_from_slice(rest)
.map_err(|_| {
msg!("❌ 反序列化失败!");
ProgramError::InvalidInstructionData
})?;

// 🎯 步骤3: 根据variant构建对应的指令
Ok(match variant {
0 => {
msg!("✅ 解析AddMovieReview指令");
msg!(" 📽️ 电影: {}", payload.title);
msg!(" ⭐ 评分: {}/10", payload.rating);

Self::AddMovieReview {
title: payload.title,
rating: payload.rating,
description: payload.description
}
},
// 🔮 未来可以添加更多指令类型
// 1 => Self::UpdateReview { ... },
// 2 => Self::DeleteReview { ... },
_ => {
msg!("❌ 未知的指令类型: {}", variant);
return Err(ProgramError::InvalidInstructionData)
}
})
}
}

💡 深入理解关键概念

// 🎯 理解 ? 操作符
// 这两种写法是等价的:

// 使用 ? (推荐)
let (&variant, rest) = input.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;

// 不使用 ? (冗长)
let (&variant, rest) = match input.split_first() {
Some(result) => result,
None => return Err(ProgramError::InvalidInstructionData),
};
// 🎯 理解 unwrap()
let payload = MovieReviewPayload::try_from_slice(rest)
.unwrap(); // ⚠️ 如果失败会panic!

// 💡 更安全的做法:
let payload = MovieReviewPayload::try_from_slice(rest)
.map_err(|e| {
msg!("反序列化错误: {:?}", e);
ProgramError::InvalidInstructionData
})?;

🎓 Rust小课堂:

  • Option<T> = 可能有值(Some)或没值(None)
  • Result<T, E> = 成功(Ok)或失败(Err)
  • ? = 错误传播操作符,让错误处理更优雅!

🎬 整合到主程序 - 让电影院开业!

📝 更新 lib.rs

// 🎯 导入所有需要的组件
use solana_program::{
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg,
account_info::AccountInfo,
};

// 📦 导入我们的指令模块
pub mod instruction;
use instruction::{MovieInstruction};

// 🚪 程序入口
entrypoint!(process_instruction);

// 🎮 主处理函数
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
msg!("🎬 电影评论程序 v1.0 启动!");
msg!("📍 程序ID: {}", program_id);

// 🔍 解析指令数据
let instruction = MovieInstruction::unpack(instruction_data)?;

// 🎯 根据指令类型执行相应操作
match instruction {
MovieInstruction::AddMovieReview { title, rating, description } => {
msg!("📝 收到新的电影评论!");
add_movie_review(program_id, accounts, title, rating, description)
}
// 🔮 未来可以添加更多匹配分支
}
}

// 🎬 处理添加电影评论的函数
pub fn add_movie_review(
program_id: &Pubkey,
accounts: &[AccountInfo],
title: String,
rating: u8,
description: String
) -> ProgramResult {
msg!("🎬 ===== 新影评发布 =====");
msg!("📽️ 电影标题: {}", title);
msg!("⭐ 评分: {}/10", rating);
msg!("📝 评论内容: {}", description);
msg!("👤 评论者: {}", accounts[0].key);
msg!("🎬 =====================");

// 🎯 验证评分范围
if rating > 10 {
msg!("❌ 评分必须在0-10之间!");
return Err(ProgramError::InvalidInstructionData.into());
}

// 🎉 暂时只记录日志,下一章会学习存储!
msg!("✅ 影评发布成功!");

Ok(())
}

🚀 部署和测试 - 首映礼!

🎬 部署程序

# 🛠️ 在Solana Playground
1. 点击 "Build" 按钮
2. 等待编译成功 ✅
3. 点击 "Deploy" 按钮
4. 复制程序ID 📋

# 💻 本地开发
cargo build-bpf
solana program deploy target/deploy/movie_review_program.so

🎯 客户端测试

快速设置测试环境

# 🚀 克隆测试客户端
git clone https://github.com/all-in-one-solana/solana-movie-client
cd solana-movie-client
npm install

# 🔧 配置
# 编辑 src/index.js
# 将第94行的程序ID替换为你的程序ID

创建自己的测试脚本

// 🎬 test-movie-review.js
import {
Connection,
PublicKey,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
Keypair
} from '@solana/web3.js';
import * as borsh from 'borsh';

// 🎯 定义数据结构
class MovieReview {
constructor(title, rating, description) {
this.variant = 0; // 指令类型
this.title = title;
this.rating = rating;
this.description = description;
}
}

// 📝 定义序列化模式
const schema = new Map([
[MovieReview, {
kind: 'struct',
fields: [
['variant', 'u8'],
['title', 'string'],
['rating', 'u8'],
['description', 'string']
]
}]
]);

// 🎬 发送影评
async function sendMovieReview() {
// 🌐 连接到网络
const connection = new Connection(
"https://api.devnet.solana.com",
"confirmed"
);

// 🔑 创建钱包(实际使用时应该用你的钱包)
const wallet = Keypair.generate();

// 💰 获取测试币
console.log("💰 获取测试SOL...");
await connection.requestAirdrop(wallet.publicKey, 2e9);

// 🎬 创建影评
const review = new MovieReview(
"星际穿越", // 电影名
10, // 评分
"诺兰的科幻巨作!" // 评论
);

// 📦 序列化数据
const buffer = borsh.serialize(schema, review);

// 🎯 创建指令
const instruction = new TransactionInstruction({
keys: [
{
pubkey: wallet.publicKey,
isSigner: true,
isWritable: true
}
],
programId: new PublicKey("你的程序ID"),
data: Buffer.from(buffer)
});

// 🚀 发送交易
const transaction = new Transaction().add(instruction);
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[wallet]
);

console.log("🎉 影评已发布!");
console.log("🔗 交易签名:", signature);
console.log("👁️ 在浏览器查看:");
console.log(`https://explorer.solana.com/tx/${signature}?cluster=devnet`);
}

// 🎬 执行
sendMovieReview().catch(console.error);

🔍 查看结果

# 运行测试
npm run start

# 输出示例:
🎬 电影评论程序 v1.0 启动!
📍 程序ID: 7xKXtg2...
📝 收到新的电影评论!
🎬 ===== 新影评发布 =====
📽️ 电影标题: 星际穿越
⭐ 评分: 10/10
📝 评论内容: 诺兰的科幻巨作!
👤 评论者: 8xHG9k2...
✅ 影评发布成功!

💡 专业技巧大放送!

🚀 性能优化

// ✅ 提前验证,失败要快
pub fn add_movie_review(...) -> ProgramResult {
// 先验证
if rating > 10 {
return Err(ProgramError::InvalidArgument);
}
if title.len() > 50 {
return Err(ProgramError::InvalidArgument);
}
// 再处理...
}

// ✅ 使用常量避免魔法数字
const MAX_RATING: u8 = 10;
const MAX_TITLE_LENGTH: usize = 50;
const MAX_DESCRIPTION_LENGTH: usize = 500;

🐛 调试技巧

// 🔍 添加详细的调试日志
msg!("Debug: instruction_data.len() = {}", instruction_data.len());
msg!("Debug: first byte = {}", instruction_data[0]);

// 🎯 使用不同的日志级别
msg!("[INFO] Processing instruction");
msg!("[DEBUG] Data: {:?}", instruction_data);
msg!("[ERROR] Invalid rating: {}", rating);

🎓 最佳实践

// 📁 模块化组织
mod instruction; // 指令定义
mod processor; // 业务逻辑
mod state; // 状态管理
mod error; // 错误处理

// 🔒 输入验证
fn validate_review(title: &str, rating: u8) -> ProgramResult {
require!(rating <= 10, ProgramError::InvalidArgument);
require!(title.len() <= 50, ProgramError::InvalidArgument);
Ok(())
}

🚢 终极挑战 - 学生介绍程序!

🎯 任务说明

创建一个程序,让学生可以:

  1. 📝 提交姓名和自我介绍
  2. 📢 在程序日志中显示信息
  3. 🔮 为未来的存储功能做准备

💡 提示

// 🎯 指令结构
pub enum StudentInstruction {
IntroduceStudent {
name: String,
message: String
}
}

// 📦 负载结构
#[derive(BorshDeserialize)]
struct StudentPayload {
name: String,
message: String
}

// 🎬 处理函数
pub fn introduce_student(
name: String,
message: String
) -> ProgramResult {
msg!("👋 新同学报到!");
msg!("📛 姓名: {}", name);
msg!("💬 留言: {}", message);
Ok(())
}

🏆 成功标准

  • ✅ 程序能编译部署
  • ✅ 能接收姓名和消息
  • ✅ 日志正确显示信息
  • ✅ 代码结构清晰

🎁 额外加分项

  • 🌟 添加表情符号支持
  • 🌟 限制消息长度
  • 🌟 添加时间戳
  • 🌟 统计介绍次数

🎊 恭喜完成!

📊 你的成就

🏆 成就解锁:
✅ 掌握指令反序列化
✅ 理解Rust错误处理
✅ 完成程序部署
✅ 实现客户端测试
⏳ 下一步:数据存储

🚀 下一章预告

  • 💾 学习账户数据存储
  • 🔐 实现评论持久化
  • 📊 查询历史评论
  • 👍 添加点赞功能

🌟 激励语: "每个伟大的DApp都始于一个简单的程序。今天的影评程序,明天的去中心化豆瓣!" 🚀

#SolanaMovie #RustDev #Web3Cinema 🎬✨🍿