以下是完整的执行脚本,按顺序手工操作即可:
一、创建项目并安装工具
1. 创建项目
cargo new wechat_backend --bin
cd wechat_backend
2. 安装 sqlx-cli(用于数据库迁移)
cargo install sqlx-cli --features mysql
二、配置 Cargo.toml
将 Cargo.toml 替换为以下内容:
rust
[package]
name = "wechat_backend"
version = "0.1.0"
edition = "2021"
[dependencies]
# Web 框架
axum = { version = "0.8", features = ["macros"] }
axum-extra = { version = "0.10", features = ["typed-header"] }
tower = { version = "0.5", features = ["full"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "timeout"] }
# 异步运行时
tokio = { version = "1", features = ["full"] }
# 数据库
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "mysql", "macros", "chrono", "uuid"] }
# 序列化
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# 日志
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# 配置
dotenvy = "0.15"
# 错误处理
thiserror = "1"
anyhow = "1"
# 认证
jsonwebtoken = "9"
argon2 = "0.5"
# 工具
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
三、创建项目目录结构
bash
编辑
mkdir -p src/{api,domain,infrastructure,middleware}
mkdir -p migrations
touch src/{lib.rs,config.rs,error.rs}
touch src/api/{mod.rs,user.rs}
touch src/domain/{mod.rs,user_service.rs}
touch src/infrastructure/{mod.rs,db.rs,models.rs}
touch src/middleware/{mod.rs,auth.rs}
touch migrations/001_create_users.sql
四、创建环境变量文件
在项目根目录创建 .env:
bash
编辑
.env
DATABASE_URL=mysql://root:your_password@localhost:3306/wechat_db
SERVER_PORT=3000
JWT_SECRET=your_jwt_secret_key_change_me
五、创建数据库迁移文件
编辑 migrations/001_create_users.sql:
sql
编辑
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
openid VARCHAR(128) NOT NULL UNIQUE,
nickname VARCHAR(64) DEFAULT '',
avatar_url VARCHAR(255) DEFAULT '',
phone VARCHAR(20) DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_openid (openid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
六、编写核心代码
- src/infrastructure/db.rs --- 数据库连接池
rust
编辑
use sqlx::mysql::MySqlPool;
pub async fn create_pool(database_url: &str) -> Result<MySqlPool, anyhow::Error> {
let pool = MySqlPool::connect(database_url).await?;
Ok(pool)
}
pub async fn run_migrations(pool: &MySqlPool) -> Result<(), anyhow::Error> {
sqlx::migrate!("./migrations").run(pool).await?;
Ok(())
}
- src/infrastructure/models.rs --- 数据模型
rust
编辑
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#derive(Debug, Clone, Serialize, Deserialize, FromRow)
pub struct User {
pub id: String,
pub openid: String,
pub nickname: String,
pub avatar_url: String,
pub phone: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#derive(Debug, Deserialize)
pub struct WxLoginRequest {
pub code: String,
}
#derive(Debug, Serialize)
pub struct WxLoginResponse {
pub token: String,
pub user: User,
}
- src/infrastructure/mod.rs
rust
编辑
pub mod db;
pub mod models;
- src/domain/user_service.rs --- 业务逻辑
rust
编辑
use sqlx::MySqlPool;
use uuid::Uuid;
use crate::infrastructure::models::{User, WxLoginRequest, WxLoginResponse};
use crate::error::AppError;
pub struct UserService {
pool: MySqlPool,
}
impl UserService {
pub fn new(pool: MySqlPool) -> Self {
Self { pool }
}
/// 微信小程序登录:code → openid → 生成/查询用户 → 返回 JWT
pub async fn wx_login(&self, req: WxLoginRequest) -> Result<WxLoginResponse, AppError> {
// TODO: 调用微信接口 https://api.weixin.qq.com/sns/jscode2session
// let openid = fetch_openid_from_wechat(&req.code).await?;
// 这里先用 mock openid 演示流程
let openid = format!("mock_openid_{}", req.code);
// 查询用户是否存在
let user = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE openid = ?"
)
.bind(&openid)
.fetch_optional(&self.pool)
.await?;
let user = match user {
Some(u) => u,
None => {
// 新用户,插入记录
let id = Uuid::new_v4().to_string();
sqlx::query(
"INSERT INTO users (id, openid) VALUES (?, ?)"
)
.bind(&id)
.bind(&openid)
.execute(&self.pool)
.await?;
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
.bind(&id)
.fetch_one(&self.pool)
.await?
}
};
// TODO: 生成 JWT token
let token = format!("mock_token_for_{}", user.id);
Ok(WxLoginResponse { token, user })
}
/// 根据 openid 查询用户
pub async fn get_user_by_openid(&self, openid: &str) -> Result<Option<User>, AppError> {
let user = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE openid = ?"
)
.bind(openid)
.fetch_optional(&self.pool)
.await?;
Ok(user)
}
/// 更新用户信息
pub async fn update_user_info(
&self,
openid: &str,
nickname: &str,
avatar_url: &str,
) -> Result<User, AppError> {
sqlx::query(
"UPDATE users SET nickname = ?, avatar_url = ? WHERE openid = ?"
)
.bind(nickname)
.bind(avatar_url)
.bind(openid)
.execute(&self.pool)
.await?;
let user = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE openid = ?"
)
.bind(openid)
.fetch_one(&self.pool)
.await?;
Ok(user)
}
}
- src/domain/mod.rs
rust
编辑
pub mod user_service;
- src/api/user.rs --- API 路由处理器
rust
编辑
use axum::{
extract::State,
routing::{get, post},
Json, Router,
};
use serde_json::json;
use crate::domain::user_service::UserService;
use crate::infrastructure::models::{WxLoginRequest, WxLoginResponse};
use crate::error::AppError;
pub fn router(user_service: UserService) -> Router {
Router::new()
.route("/wx/login", post(wx_login))
.route("/user/info", get(get_user_info))
.with_state(user_service)
}
/// POST /api/wx/login
async fn wx_login(
State(service): State<UserService>,
Json(req): Json<WxLoginRequest>,
) -> Result<Json<WxLoginResponse>, AppError> {
let resp = service.wx_login(req).await?;
Ok(Json(resp))
}
/// GET /api/user/info?openid=xxx
async fn get_user_info(
State(service): State<UserService>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Result<Json<serde_json::Value>, AppError> {
let openid = params.get("openid").ok_or(AppError::BadRequest("openid is required".into()))?;
let user = service.get_user_by_openid(openid).await?;
match user {
Some(u) => Ok(Json(json!(u))),
None => Err(AppError::NotFound("用户不存在".into())),
}
}
- src/api/mod.rs
rust
编辑
pub mod user;
- src/error.rs --- 统一错误处理
rust
编辑
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#derive(Error, Debug)
pub enum AppError {
#error("数据库错误: {0}")
Db(#from sqlx::Error),
#error("资源不存在: {0}")
NotFound(String),
#error("参数错误: {0}")
BadRequest(String),
#error("内部错误: {0}")
Internal(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::Db(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)),
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
- src/config.rs --- 配置读取
rust
编辑
use dotenvy::dotenv;
use std::env;
pub struct AppConfig {
pub database_url: String,
pub server_port: u16,
pub jwt_secret: String,
}
impl AppConfig {
pub fn from_env() -> Self {
dotenv().ok();
Self {
database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
server_port: env::var("SERVER_PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.unwrap_or(3000),
jwt_secret: env::var("JWT_SECRET").unwrap_or_else(|_| "dev_secret".to_string()),
}
}
}
- src/lib.rs
rust
编辑
pub mod api;
pub mod config;
pub mod domain;
pub mod error;
pub mod infrastructure;
pub mod middleware;
- src/main.rs --- 应用入口
rust
编辑
mod api;
mod config;
mod domain;
mod error;
mod infrastructure;
mod middleware;
use axum::{Router, routing::get};
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
compression::CompressionLayer,
timeout::TimeoutLayer,
};
use std::time::Duration;
use tracing_subscriber;
#tokio::main
async fn main() -> anyhow::Result<()> {
// 初始化日志
tracing_subscriber::fmt::init();
// 加载配置
let config = config::AppConfig::from_env();
// 创建数据库连接池
let pool = infrastructure::db::create_pool(&config.database_url).await?;
tracing::info!("数据库连接成功");
// 执行数据库迁移
infrastructure::db::run_migrations(&pool).await?;
tracing::info!("数据库迁移完成");
// 创建服务层
let user_service = domain::user_service::UserService::new(pool);
// 组装路由
let app = Router::new()
.route("/health", get(|| async { "OK" }))
.nest("/api", api::user::router(user_service))
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(CorsLayer::permissive())
.layer(TimeoutLayer::new(Duration::from_secs(30)));
let addr = format!("0.0.0.0:{}", config.server_port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("服务启动在 http://{}", addr);
axum::serve(listener, app).await?;
Ok(())
}
七、启动 MySQL 并执行迁移
bash
编辑
1. 确保 MySQL 已启动,创建数据库
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS wechat_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
2. 执行数据库迁移
sqlx migrate run
3. 编译并启动服务
cargo run
八、测试接口
bash
编辑
健康检查
curl http://localhost:3000/health
微信小程序登录(mock)
curl -X POST http://localhost:3000/api/wx/login \
-H "Content-Type: application/json" \
-d '{"code": "test_code_123"}'
查询用户
curl "http://localhost:3000/api/user/info?openid=mock_openid_test_code_123"
九、后续需要手工补齐的部分
表格
模块 说明
微信 code2session 在 user_service.rs 中用 reqwest 调用 https://api.weixin.qq.com/sns/jscode2session,换取真实 openid
JWT 认证 用 jsonwebtoken 库生成/验证 token,在 middleware/auth.rs 中实现认证中间件
密码/签名校验 微信小程序通信需要校验签名,防止请求伪造
业务表 根据实际需求在 migrations/ 中添加订单、商品、消息等业务表
执行完以上脚本,你就有了一个可运行的微信小程序后端骨架,包含用户登录、数据库迁移、分层架构和统一错误处理。