🎥 构建一个电影评论程序 - 成为区块链影评人!
🎯 本章目标: 打造一个去中心化的烂番茄网站,让你的影评永远留在区块链上!
🌟 开篇:从观众到影评人的华丽转身!
还记得第一节我们玩过的电影评论程序吗?当时我们只是"观众",现在要成为"导演"了!🎬 今天我们要从零开始构建一个完整的电影评论系统。
🎮 今日任务清单
📍 起点:空白的程序
↓
📝 设计指令结构 → 🔪 反序列化数据 → 🎬 处理评论
↓ ↓ ↓
定义格式 解析数据 保存评论
↓
🎊 部署上链 → 🎯 客户端测试
↓
🏆 你的去中心化影评网站诞生!
💡 有趣的事实: 传统影评网站可以删除你的评论,但区块链上的评论是永恒的!即使是《房间》这样的烂片评论也会永远存在!😄
🏗️ 项目初始化 - 搭建你的电影院!
🎯 两种开发方式
🌐 方式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(())
}
🚢 终极挑战 - 学生介绍程序!
🎯 任务说明
创建一个程序,让学生可以:
- 📝 提交姓名和自我介绍
- 📢 在程序日志中显示信息
- 🔮 为未来的存储功能做准备
💡 提示
// 🎯 指令结构
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 🎬✨🍿