Commit a336c43f authored by 陈冲's avatar 陈冲

init

parents
/target
.codex
config.json
test_ds.sh
addons/
This source diff could not be displayed because it is too large. You can view the blob instead.
[package]
name = "backend-ds"
version = "0.1.0"
edition = "2024"
[dependencies]
bytes = "1.10.1"
chrono = { version = "0.4.44", features = ["serde"] }
async-trait = "0.1.89"
sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "chrono", "json"] }
rand = "0.10.0"
rust-embed = { version = "8.11.0", features = ["debug-embed"] }
salvo = { version = "0.90.1", features = ["serve-static", "tower-compat"] }
serde = "1.0.228"
serde_json = "1.0.149"
rmp-serde = "1.3.0"
socketioxide = { version = "0.18.2", features = ["tracing", "extensions", "state"] }
tokio = {version = "1.50.0", features = ["full"]}
tower = "0.5.3"
tower-http = { version = "0.6.8", features = ["cors"] }
dashmap = "6.1.0"
base64 = "0.22.1"
lazy_static = "1.5.0"
aes = "0.8.4"
cbc = { version = "0.1.2", features = ["alloc"] }
md5 = { version = "0.10.6", package = "md-5" }
[[bin]]
name = "backend-ds"
# [target.aarch64-unknown-linux-gnu]
# linker = "aarch64-linux-gnu-gcc"
# cross build --release --target aarch64-unknown-linux-gnu
### 福彩小游戏
\ No newline at end of file
drop table if exists tb_game;
create table tb_game
(
event_date date, -- 活动日期
wechat_id varchar(50), -- 微信id
nickname varchar(50), -- 昵称
avatar varchar(200), -- 头像
round_num int NOT NULL CHECK (round_num between 1 and 3), -- 比赛场次
item_num int not null check (item_num between 1 and 6), -- 游戏项目(1到6)
score int not null default 0 check (score >= 0), -- 得分
create_date timestamptz, -- 创建时间
unique(event_date,wechat_id,round_num,item_num)
);
SELECT * FROM tb_game
DROP TABLE IF EXISTS tb_log;
CREATE TABLE tb_log
(
id BIGSERIAL PRIMARY KEY,
msg TEXT,
params jsonb,
create_date timestamptz
);
SELECT * FROM tb_log
drop table if exists tb_manager;
create table tb_manager
(
id SERIAL PRIMARY KEY,
username VARCHAR(20),
"password" VARCHAR(20),
token VARCHAR(36)
)
INSERT INTO tb_manager(username,"password",token) VALUES('admin','123456','88236aeb-8d3f-6d56-fdf5-3adca3961dfb');
SELECT * FROM tb_manager;
use std::{fs, io::Write};
use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(default)]
pub struct Config {
// 当前服务监听地址,Salvo HTTP 和 socket.io 都挂在这个地址上。
pub host: String,
// 玩家客户端连接的 socket.io namespace。
pub client_ns: String,
// Socket.IO 的 HTTP 传输路径,前后端和 nginx 需要保持一致。
pub socket_path: String,
// 玩家身份校验用的 AES 密钥短语,需和客户端加密逻辑保持一致。
#[serde(skip_serializing_if = "Option::is_none")]
pub aes_passphrase: Option<String>,
// 数据库配置:所有 master/worker 节点都可以直连同一台 DB。
pub db: DbConfig,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(default)]
pub struct DbConfig {
// 数据库类型:mongodb、postgresql、mysql、sqlite。
pub kind: String,
// mongodb://admin:123456@127.0.0.1:27017/backend_ds
// postgresql://admin:123456@localhost:5432/backend_ds
pub uri: String,
}
impl Default for Config {
fn default() -> Self {
Self {
host: "127.0.0.1:8082".to_string(),
client_ns: "/ws".to_string(),
socket_path: "/socket.io".to_string(),
aes_passphrase: None,
db: DbConfig::default(),
}
}
}
impl Default for DbConfig {
fn default() -> Self {
Self {
kind: "postgresql".to_string(),
uri: "postgres://postgres:123456@localhost:5432/backend_ds".to_string(),
}
}
}
impl Config {
pub fn load() -> std::io::Result<Self> {
let path = "config.json";
let err_msg = &format!("<{path}>文件不存在或配置异常");
if !fs::exists(path).expect(err_msg) {
let mut file = fs::File::create(path).expect(err_msg);
let json_str = serde_json::to_string_pretty(&Self::default())?;
file.write_all(json_str.as_bytes())?;
panic!("请配置config.json相关参数后运行");
}
let text = fs::read_to_string(path).expect(err_msg);
let cfg: Self = serde_json::from_str(&text).expect(err_msg);
Ok(cfg)
}
}
pub mod config;
pub mod utils;
use super::config::Config;
use aes::Aes256;
use aes::cipher::BlockDecryptMut;
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockEncryptMut, KeyIvInit};
use base64::{Engine as _, engine::general_purpose};
use cbc::Decryptor;
use md5::{Digest, Md5};
use rand::Rng;
use std::sync::LazyLock;
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load().unwrap());
type Aes256CbcDec = Decryptor<Aes256>;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
pub fn get_aes_passphrase() -> Option<String> {
CONFIG.aes_passphrase.clone()
}
fn evp_bytes_to_key(password: &[u8], salt: &[u8]) -> ([u8; 32], [u8; 16]) {
let mut hasher = Md5::new();
let mut key_iv = Vec::new();
let mut derived_bytes = Vec::new();
while key_iv.len() < 48 {
// 32 bytes for key + 16 bytes for IV
if !derived_bytes.is_empty() {
hasher.update(&derived_bytes);
}
hasher.update(password);
hasher.update(salt);
derived_bytes = hasher.finalize_reset().to_vec();
key_iv.extend_from_slice(&derived_bytes);
}
let mut key = [0u8; 32];
key.copy_from_slice(&key_iv[0..32]);
let mut iv = [0u8; 16];
iv.copy_from_slice(&key_iv[32..48]);
(key, iv)
}
pub fn decrypt_cryptojs_aes(encrypted_base64: &str, passphrase: &str) -> Result<String, String> {
// Decode Base64 input
let encrypted_data = general_purpose::STANDARD
.decode(encrypted_base64)
.map_err(|e| format!("Base64 decode error: {}", e))?;
// Validate data format and extract salt and ciphertext
if encrypted_data.len() < 16 || &encrypted_data[0..8] != b"Salted__" {
return Err("Invalid encrypted data format: missing 'Salted__' prefix".to_string());
}
let salt = &encrypted_data[8..16];
let ciphertext = &encrypted_data[16..];
// Derive key and IV
let (key, iv) = evp_bytes_to_key(passphrase.as_bytes(), salt);
// Initialize AES-256-CBC decryptor
let cipher = Aes256CbcDec::new(&key.into(), &iv.into());
// Decrypt with PKCS#7 padding
let mut buf = ciphertext.to_vec();
let decrypted_bytes = cipher
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| format!("Decryption error: {}", e))?;
// Convert decrypted bytes to UTF-8 string
String::from_utf8(decrypted_bytes.to_vec())
.map_err(|e| format!("UTF-8 conversion error: {}", e))
}
pub fn encrypt_cryptojs_aes(plaintext: &str, passphrase: &str) -> Result<String, String> {
// Validate inputs
if plaintext.is_empty() {
return Err("Empty plaintext".to_string());
}
if passphrase.is_empty() {
return Err("Empty passphrase".to_string());
}
// Generate random 8-byte salt
let mut salt = [0u8; 8];
rand::rng().fill_bytes(&mut salt);
// Derive key and IV
let (key, iv) = evp_bytes_to_key(passphrase.as_bytes(), &salt);
// Debug: Print inputs and derived values
// eprintln!("Plaintext: {}", plaintext);
// eprintln!("Plaintext length: {}", plaintext.len());
// eprintln!(
// "Plaintext bytes (hex): {}",
// hex::encode(plaintext.as_bytes())
// );
// eprintln!("Salt (hex): {}", hex::encode(&salt));
// eprintln!("Key (hex): {}", hex::encode(&key));
// eprintln!("IV (hex): {}", hex::encode(&iv));
// Initialize AES-256-CBC encryptor
let cipher = Aes256CbcEnc::new(&key.into(), &iv.into());
// Encrypt with PKCS#7 padding, using encrypt_padded_vec_mut to handle buffer
let encrypted_bytes = cipher.encrypt_padded_vec_mut::<Pkcs7>(plaintext.as_bytes());
// Debug: Print encrypted bytes
// eprintln!("Encrypted bytes (hex): {}", hex::encode(&encrypted_bytes));
// Construct output: Salted__ + salt + ciphertext
let mut output = Vec::with_capacity(8 + 8 + encrypted_bytes.len());
output.extend_from_slice(b"Salted__");
output.extend_from_slice(&salt);
output.extend_from_slice(&encrypted_bytes);
// Encode to Base64
let base64_output = general_purpose::STANDARD.encode(&output);
// eprintln!("Encrypted Base64: {}", base64_output);
Ok(base64_output)
}
/// 返回当前UTC时间戳,没有毫秒
pub fn get_current_timestamp() -> u64 {
chrono::Local::now().timestamp() as u64
// SystemTime::now()
// .duration_since(UNIX_EPOCH)
// .unwrap()
// .as_secs()
}
pub fn format_timestamp() {}
pub fn add_second(ts: u64, val: u64) -> u64 {
ts + val
}
pub fn add_minute(ts: u64, val: u64) -> u64 {
add_second(ts, val * 60)
}
pub fn add_hour(ts: u64, val: u64) -> u64 {
add_minute(ts, val * 60)
}
use crate::commons::utils::CONFIG as config;
use crate::db::pg::PgDb;
use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::OnceCell;
pub mod pg;
pub mod models;
#[async_trait]
trait DbDriver: Send + Sync {
async fn query(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Value>, String>;
async fn transaction(&self, queries: Vec<Value>) -> Result<Vec<Value>, String>;
}
static DB_CONN: OnceCell<Box<dyn DbDriver>> = OnceCell::const_new();
pub async fn init_db() -> Result<(), String> {
let conn: Box<dyn DbDriver> = match config.db.kind.as_str() {
"postgresql" => Box::new(PgDb::new().await?),
other => {
return Err(format!("unsupported db kind: {other}"));
}
};
DB_CONN
.set(conn)
.map_err(|_| "db already initialized".to_string())
}
pub async fn db_query(sql: &str, params: Vec<Value>) -> Result<Vec<Value>, String> {
let conn = DB_CONN
.get()
.ok_or_else(|| "db not initialized".to_string())?;
conn.query(sql, params).await
}
pub async fn db_transaction(queries: Vec<Value>) -> Result<Vec<Value>, String> {
let conn = DB_CONN
.get()
.ok_or_else(|| "db not initialized".to_string())?;
conn.transaction(queries).await
}
use chrono::{DateTime, NaiveDate, Utc};
use serde_json::json;
use crate::db::models::log::Log;
pub struct Game {
/// 活动日期
pub event_date: NaiveDate,
pub wechat_id: String,
pub nickname: Option<String>,
pub avatar: Option<String>,
/// 比赛场次
pub round_num: i32,
/// 游戏项目(1到6)
pub item_num: i32,
/// 得分
pub score: i32,
/// 创建时间
pub create_date: Option<DateTime<Utc>>,
}
impl Game {
pub async fn insert(&self) -> bool {
let res = crate::db::db_query(
r#"insert into tb_game(event_date,wechat_id,nickname,avatar,round_num,item_num,score,create_date) values(?::date,?,?,?,?,?,?,now());"#,
vec![
json!(&self.event_date),
json!(&self.wechat_id),
json!(&self.nickname),
json!(&self.avatar),
json!(&self.round_num),
json!(&self.item_num),
json!(&self.score),
],
)
.await;
match res {
Ok(res) => {
// dbg!(&res);
true
}
Err(err) => {
// dbg!(&err);
let log = Log {
id: 0,
msg: err,
params: json!({
"event_date": self.event_date,
"wechat_id": self.wechat_id,
"nickname": self.nickname,
"avatar": self.avatar,
"round_num": self.round_num,
"item_num": self.item_num,
"score": self.score,
}),
create_date: None,
};
log.insert().await;
false
}
}
}
}
use chrono::{DateTime, Utc};
use serde_json::{Value, json};
pub struct Log {
pub id: i32,
pub msg: String,
pub params: Value,
pub create_date: Option<DateTime<Utc>>,
}
impl Log {
pub async fn insert(&self) -> bool {
let res = crate::db::db_query(
r#"insert into tb_log(msg,params,create_date) values(?,?::jsonb,now());"#,
vec![json!(&self.msg), json!(&self.params)],
)
.await;
match res {
Ok(res) => {
// dbg!(&res);
true
}
Err(err) => {
// dbg!(&err);
false
}
}
}
}
use serde_json::{Value, json};
use crate::db::models::*;
#[derive(Debug)]
pub struct Manager {
pub id: i32,
pub username: String,
pub password: String,
pub token: String,
}
pub enum ManagerLogin {
/// username / password
unpw(String, String),
token(String),
}
impl Manager {
pub async fn Login(m: ManagerLogin) -> Result<Self, String> {
match m {
ManagerLogin::unpw(username, password) => {
let rows = crate::db::db_query(
r#"select id,username,password,token from tb_manager where username=? and password=? limit 1;"#,
vec![json!(username), json!(password)],
)
.await
.map_err(|err| format!("用户名或密码错误,{err}"))?;
let row = rows.first().ok_or_else(|| "用户名或密码错误".to_string())?;
Ok(Self {
id: get_i32(row, "id")?,
username: get_string(row, "username")?,
password: get_string(row, "password")?,
token: get_string(row, "token")?,
})
}
ManagerLogin::token(token) => {
let rows = crate::db::db_query(
r#"select id,username,password,token from tb_manager where token=? limit 1;"#,
vec![json!(token)],
)
.await
.map_err(|err| format!("token异常,{err}"))?;
let row = rows.first().ok_or_else(|| "token异常".to_string())?;
Ok(Self {
id: get_i32(row, "id")?,
username: get_string(row, "username")?,
password: get_string(row, "password")?,
token: get_string(row, "token")?,
})
}
}
}
}
use serde_json::Value;
pub mod game;
pub mod log;
pub mod manager;
pub fn get_i32(row: &Value, key: &str) -> Result<i32, String> {
row.get(key)
.and_then(|value| value.as_i64())
.and_then(|value| i32::try_from(value).ok())
.ok_or_else(|| format!("{key}字段格式错误"))
}
pub fn get_string(row: &Value, key: &str) -> Result<String, String> {
row.get(key)
.and_then(|value| value.as_str())
.map(|value| value.to_string())
.ok_or_else(|| format!("{key}字段格式错误"))
}
use crate::commons::utils::CONFIG as config;
use crate::db::{DbDriver};
use async_trait::async_trait;
use serde_json::{Value, json};
use sqlx::postgres::{PgPoolOptions, PgRow};
use sqlx::{Column, PgPool, Row, TypeInfo};
pub struct PgDb {
pool: PgPool,
}
impl PgDb {
pub async fn new() -> Result<Self, String> {
let pool = PgPoolOptions::new()
.max_connections(16)
.connect(&config.db.uri)
.await
.map_err(|err| err.to_string())?;
Ok(Self { pool })
}
}
#[async_trait]
impl DbDriver for PgDb {
async fn query(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Value>, String> {
let mut conn = self.pool.acquire().await.map_err(|err| err.to_string())?;
pg_query_rows(&mut *conn, sql, params).await
}
async fn transaction(&self, queries: Vec<Value>) -> Result<Vec<Value>, String> {
let mut tx = self.pool.begin().await.map_err(|err| err.to_string())?;
let mut results = Vec::with_capacity(queries.len());
for query in queries.iter() {
let sql = query
.get("query")
.or_else(|| query.get("sql"))
.and_then(|value| value.as_str())
.unwrap_or("")
.trim();
if sql.is_empty() {
return Err("transaction query sql is empty".to_string());
}
let params = query
.get("params")
.and_then(|value| value.as_array())
.cloned()
.unwrap_or_default();
let rows = pg_query_rows(&mut *tx, sql, params).await?;
results.push(Value::Array(rows));
}
tx.commit().await.map_err(|err| err.to_string())?;
Ok(results)
}
}
async fn pg_query_rows<'e, E>(
executor: E,
sql: &str,
params: Vec<Value>,
) -> Result<Vec<Value>, String>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let sql = normalize_sql_placeholders(sql);
let mut query = sqlx::query(sql.as_str());
for param in params.into_iter() {
query = pg_bind_param(query, param);
}
let rows = query
.fetch_all(executor)
.await
.map_err(|err| err.to_string())?;
rows.into_iter()
.map(pg_row_to_json)
.collect::<Result<Vec<_>, _>>()
}
fn normalize_sql_placeholders(sql: &str) -> String {
let mut index = 1;
let mut out = String::with_capacity(sql.len());
for ch in sql.chars() {
if ch == '?' {
out.push('$');
out.push_str(index.to_string().as_str());
index += 1;
} else {
out.push(ch);
}
}
out
}
fn pg_bind_param<'q>(
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
value: Value,
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
match value {
Value::Bool(v) => query.bind(v),
Value::Number(v) => {
if let Some(v) = v.as_i64() {
query.bind(v)
} else {
let v = v.as_f64().unwrap_or_default();
if v.fract() == 0.0 {
query.bind(v as i64)
} else {
query.bind(v)
}
}
}
Value::String(v) => query.bind(v),
Value::Array(v) => query.bind(Value::Array(v)),
Value::Object(v) => query.bind(Value::Object(v)),
Value::Null => query.bind(Option::<String>::None),
}
}
fn pg_row_to_json(row: PgRow) -> Result<Value, String> {
let mut item = serde_json::Map::new();
for (index, column) in row.columns().iter().enumerate() {
let type_name = column.type_info().name().to_ascii_uppercase();
let value = match type_name.as_str() {
"BOOL" => json!(
row.try_get::<Option<bool>, _>(index)
.map_err(|err| err.to_string())?
),
"INT2" => json!(
row.try_get::<Option<i16>, _>(index)
.map_err(|err| err.to_string())?
),
"INT4" => json!(
row.try_get::<Option<i32>, _>(index)
.map_err(|err| err.to_string())?
),
"INT8" => json!(
row.try_get::<Option<i64>, _>(index)
.map_err(|err| err.to_string())?
),
"FLOAT4" => {
json!(
row.try_get::<Option<f32>, _>(index)
.map_err(|err| err.to_string())?
)
}
"FLOAT8" => {
json!(
row.try_get::<Option<f64>, _>(index)
.map_err(|err| err.to_string())?
)
}
"VARCHAR" | "TEXT" | "BPCHAR" | "NAME" => {
json!(
row.try_get::<Option<String>, _>(index)
.map_err(|err| err.to_string())?
)
}
"JSON" | "JSONB" => row
.try_get::<Option<Value>, _>(index)
.map_err(|err| err.to_string())?
.unwrap_or(Value::Null),
"TIMESTAMPTZ" => {
let value = row
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(index)
.map_err(|err| err.to_string())?
.map(|ts| ts.timestamp_millis())
.unwrap_or(0);
json!(value)
}
_ => json!(type_name),
};
item.insert(column.name().to_string(), value);
}
Ok(Value::Object(item))
}
mod commons;
mod db;
mod protocols;
mod router;
use commons::utils::CONFIG as config;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
pub async fn init_postgresql_schema() -> Result<(), String> {
let pool = PgPoolOptions::new()
.max_connections(4)
.connect(&config.db.uri)
.await
.map_err(|err| err.to_string())?;
sqlx::raw_sql(
"create table if not exists player_profiles (
userid text primary key,
nickname text not null,
level bigint not null default 1,
exp bigint not null default 0,
avatar_id bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists player_wallets (
userid text primary key references player_profiles(userid) on delete cascade,
gold bigint not null default 0,
diamond bigint not null default 0,
stamina bigint not null default 100,
updated_at timestamptz not null default now()
);
create table if not exists player_items (
id bigserial primary key,
userid text not null references player_profiles(userid) on delete cascade,
item_id text not null,
item_count bigint not null default 0,
updated_at timestamptz not null default now(),
unique(userid, item_id)
);
create table if not exists player_mails (
id bigserial primary key,
userid text not null references player_profiles(userid) on delete cascade,
title text not null,
content text not null default '',
attachments jsonb not null default '[]'::jsonb,
status text not null default 'unread',
created_at timestamptz not null default now()
);
create table if not exists task_idempotency (
task_id text primary key,
task_type text not null,
status text not null default 'running',
attempt bigint not null default 1,
result jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table player_profiles alter column level type bigint;
alter table player_profiles alter column avatar_id type bigint;
alter table player_wallets alter column stamina type bigint;",
)
.execute(&pool)
.await
.map_err(|err| err.to_string())?;
migrate_legacy_players(&pool).await
}
async fn migrate_legacy_players(pool: &PgPool) -> Result<(), String> {
let row: (bool,) = sqlx::query_as("select to_regclass('public.players') is not null")
.fetch_one(pool)
.await
.map_err(|err| err.to_string())?;
let has_legacy_table = row.0;
if !has_legacy_table {
return Ok(());
}
sqlx::raw_sql(
"insert into player_profiles (userid, nickname)
select userid, nickname
from players
on conflict (userid)
do update set nickname = excluded.nickname;
insert into player_wallets (userid)
select userid
from players
on conflict (userid) do nothing;
drop table if exists players;",
)
.execute(pool)
.await
.map_err(|err| err.to_string())?;
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::commons::utils;
async fn init_db() {
_ = crate::db::init_db().await;
}
#[tokio::test]
async fn test_db() {
_ = init_db().await;
// if let Ok(res) =
// crate::db::db_query("select * from tb_user where id=?", vec![json!("123")]).await
// {
// dbg!(&res);
// }
// let game = crate::db::models::game::Game {
// event_date: "2026-05-25".parse().unwrap(),
// wechat_id: "test_wechat_id".to_string(),
// nickname: Some("test_nickname".to_string()),
// avatar: Some("test_avatar".to_string()),
// round_num: 1,
// item_num: 2,
// score: 100,
// create_date: None,
// };
// let res = game.insert().await;
// dbg!(&res);
let res = crate::db::models::manager::Manager::Login(
crate::db::models::manager::ManagerLogin::unpw(
"admin".to_string(),
"123456".to_string(),
),
)
.await;
dbg!(&res);
}
fn test_userid() -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|value| value.as_millis())
.unwrap_or(0);
format!("test_userid_{ts}")
}
#[tokio::test]
async fn test_current_timestamp() {
let ts = utils::get_current_timestamp();
dbg!(&ts);
let ts = utils::add_second(ts, 3);
dbg!(&ts);
let now = chrono::Local::now();
println!("{}", now.format("%Y-%m-%d %H:%M:%S"));
let utc_ts = chrono::Utc::now().timestamp();
let local_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
println!("时间戳: {}", utc_ts);
println!("本地时间: {}", local_time);
}
}
use salvo::prelude::*;
mod commons;
mod db;
mod protocols;
mod router;
use commons::utils::CONFIG as config;
use db::init_db;
#[derive(rust_embed::RustEmbed)]
#[folder = "src/web"]
struct Assets;
#[tokio::main]
async fn main() {
if let Err(err) = init_db().await {
panic!("init db failed: {err}");
}
let router = Router::new().push(router::config_router()).push(
Router::with_path("/web/{**path}")
.get(salvo::serve_static::static_embed::<Assets>().fallback("index.html")),
);
let host = config.host.clone();
dbg!(&config);
println!("http://{}", host);
let service = Service::new(router).catcher(router::config_catcher());
let acceptor = TcpListener::new(host).bind().await;
Server::new(acceptor).serve(service).await;
}
This diff is collapsed.
use crate::commons::utils::CONFIG as config;
use crate::protocols;
use salvo::catcher::Catcher;
use salvo::prelude::*;
use serde_json::json;
use socketioxide::SocketIo;
use tower::ServiceBuilder;
use tower_http::cors::CorsLayer;
pub fn config_router() -> Router {
let (layer, io) = SocketIo::new_layer();
let layer = ServiceBuilder::new()
.layer(CorsLayer::permissive())
.layer(layer);
let layer = layer.compat();
let mut r = Router::new().push(
Router::with_path(config.socket_path.as_str())
.hoop(layer)
.goal(hello),
);
io.ns(
config.client_ns.as_str(),
crate::protocols::socket_io::on_connect,
);
r = r.push(Router::with_path("/manager/login").post(crate::protocols::socket_io::login));
r
}
pub fn config_catcher() -> Catcher {
Catcher::default().hoop(rewrite_405_to_404)
}
#[handler]
async fn hello() -> &'static str {
""
}
// 将 405 Method Not Allowed 转换为 404 Not Found,并渲染统一的 404 页面。
#[handler]
async fn rewrite_405_to_404(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
if res.status_code == Some(StatusCode::METHOD_NOT_ALLOWED) {
res.status_code(StatusCode::NOT_FOUND);
}
if res.status_code == Some(StatusCode::NOT_FOUND) {
render_not_found(req, res);
ctrl.skip_rest();
return;
}
ctrl.call_next(req, depot, res).await;
}
// 根据请求的 Accept 头和 URL 路径,返回 JSON 格式的 404 错误信息,或者渲染一个简单的 HTML 404 页面。
fn render_not_found(req: &Request, res: &mut Response) {
let accept = req
.headers()
.get("accept")
.and_then(|value| value.to_str().ok())
.unwrap_or("");
let path = req.uri().path();
if path.starts_with("/api/") || accept.contains("application/json") {
res.render(Json(json!({
"code": 404,
"message": "resource not found",
"path": path,
})));
return;
}
res.render(Text::Html(
r#"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 Not Found</title>
<style>
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7fb;color:#1f2937;font-family:sans-serif;}main{padding:32px;text-align:center;}h1{margin:0 0 12px;font-size:64px;}p{margin:0;font-size:16px;}
</style>
</head>
<body>
<main>
<h1>404</h1>
<p>页面不存在</p>
</main>
</body>
</html>"#,
));
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.DS_Store
temp
library
local
release
node_modules
bin/js/bundles
\ No newline at end of file
{
"perFolder": true,
"includeSubFolders": true,
"maxWidth": 2048,
"maxHeight": 2048,
"eachMaxWidth": 512,
"eachMaxHeight": 512,
"format": 0,
"scale": 1,
"pot": false,
"trimImage": true
}
\ No newline at end of file
{
"uuid": "e6f47f36-13e3-4ecc-bc13-9d63b60062b3"
}
\ No newline at end of file
{
"uuid": "60a9086c-a9c2-4e01-a563-355c117b509e",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "d4cfd6a8-0d0a-475b-ac93-d85eaa646936",
"importer": {
"textureType": 2,
"sizeGrid": [
10,
14,
14,
16,
0
],
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "55da1537-1ec8-4cdf-9b6f-f0d4af0431b0",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "f0c051af-cf42-4abc-82e4-59f42ac888ee",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "e3cd8344-3bd6-4059-b771-a0ea99f96aad",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "d58d2a39-1cb4-49a1-a4fa-0664f5265e1a",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "6aa5b064-ff17-4273-a317-9778b9f4f67a",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "e1e83785-4a8e-42a5-a8ac-51d5ff2cb436",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "f64d4387-f2c7-4e48-bea1-a0dfd22a109d",
"importer": {
"textureType": 2,
"sizeGrid": [
0,
74,
0,
17,
0
],
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "2f7b7200-e363-4ffc-b9a4-62437a2302ab",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "d24c1cbc-0657-41fa-a966-471705de2af1",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "5344e14f-1c62-43c8-a00a-5593ca19bdbf",
"importer": {
"textureType": 2,
"stateNum": 3,
"sizeGrid": [
10,
10,
10,
10,
0
]
}
}
\ No newline at end of file
{
"uuid": "34582d14-0e49-41c3-9bac-3d12def95ef9",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "679b3902-4c06-412d-aeef-7efca529bf41",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "59679c29-d461-4664-be07-bc2bd95e4360",
"importer": {
"textureType": 2,
"sizeGrid": [
9,
9,
9,
9,
0
]
}
}
\ No newline at end of file
{
"uuid": "35922f9e-69f5-4993-b37e-dbfc1504aece",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "051feaa7-15db-4143-958c-161475961a6b",
"importer": {
"textureType": 2,
"sizeGrid": [
9,
9,
9,
9,
0
]
}
}
\ No newline at end of file
{
"uuid": "c13c1b8e-c516-4a0f-98ad-e356f45f0365",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "3225dc27-5bcb-446e-8b66-27df87624835",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "b86ab1e4-4348-47f0-96e7-771387714bfc",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "1fc7a268-7071-4e5d-88be-a125afa0e2c8",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "dda0809b-cfe4-41b3-a392-8bd79c456ee1",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "8daa4e1f-cba7-4117-b0d3-eb5c7ad9eefa",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "2123a790-201a-4900-8885-b7ef9a27cfba",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "5c14adcd-87f9-4670-b165-7a77a56f3e00",
"importer": {
"textureType": 2
}
}
\ No newline at end of file
{
"uuid": "0128a93d-b244-40e4-bc90-48a93a3d87a2",
"importer": {
"textureType": 2,
"sizeGrid": [
0,
18,
0,
23,
0
]
}
}
\ No newline at end of file
{
"uuid": "ae3de75e-ee9f-478d-9f8b-ede75a4fc296",
"importer": {
"textureType": 2,
"sizeGrid": [
0,
21,
0,
17,
0
]
}
}
\ No newline at end of file
{
"uuid": "41488331-0ed3-4c38-959b-6ecc4fdac7e6",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "7db29ccb-50d2-461e-9b1c-8479ff20673f",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "c7ce584a-c256-462a-aa94-a9905f7185aa",
"importer": {
"textureType": 2,
"sizeGrid": [
0,
28,
0,
24,
0
],
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "9eb4836d-78c4-4be3-aa53-70a613fef28d",
"importer": {
"textureType": 2,
"sizeGrid": [
10,
12,
10,
12
]
}
}
\ No newline at end of file
{
"uuid": "87262606-4dfe-490e-8644-7fd6496c2be7",
"importer": {
"textureType": 2,
"sizeGrid": [
10,
12,
10,
12
]
}
}
\ No newline at end of file
{
"uuid": "d79c761b-6bc3-49e0-9e51-bd2ba385cfe0",
"importer": {
"textureType": 2,
"stateNum": 3,
"sizeGrid": [
10,
10,
10,
10,
0
]
}
}
\ No newline at end of file
{
"uuid": "a62dfa59-dd5c-491c-8d7a-346205a51a5b",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "c1da2147-6391-438e-a9a3-6f2dfb6b32d3",
"importer": {
"textureType": 2,
"stateNum": 3
}
}
\ No newline at end of file
{
"uuid": "fc3bb457-8d97-4456-b6b6-304c6b064689",
"importer": {
"textureType": 2,
"sizeGrid": [
11,
9,
11,
9,
0
]
}
}
\ No newline at end of file
{
"uuid": "80000f74-4f4f-4c1c-bb95-49b2cc94c28d",
"importer": {
"textureType": 2,
"stateNum": 3,
"sizeGrid": [
10,
10,
10,
10,
0
]
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment