This commit is contained in:
tsuki 2025-07-30 21:33:14 +08:00
commit 8c67595ca9
12 changed files with 2717 additions and 0 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
DATABASE_URL=postgresql://mmap:yjhcfzXWrzslzl1331@8.217.64.157:5433/mmap
JWT_SECRET="JvGpWgGWLHAhvhxN7BuOVtUWfMXm6xAqjClaTwOcAnI="
RUST_LOG=debug
PORT=3060

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n id,\n ingestion_time,\n data_time,\n source,\n storage_url\n FROM data_ingestion\n WHERE data_time = $1 AND source = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "ingestion_time",
"type_info": "Timestamp"
},
{
"ordinal": 2,
"name": "data_time",
"type_info": "Timestamp"
},
{
"ordinal": 3,
"name": "source",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "storage_url",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Timestamp",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "61e3837493638fe3f72448ed0d03e907812280d443959f3b093226394fc98db0"
}

2433
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "mapp-tile"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.4"
chrono = "0.4.41"
dotenvy = "0.15.7"
log = "0.4.27"
lru = "0.16.0"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] }
tokio = { version = "1.47.0", features = ["full"] }
tower-http = {version = "0.6.6" , features = ["cors"]}
uuid = { version = "1.17.0", features = ["v4"] }

82
src/app.rs Normal file
View File

@ -0,0 +1,82 @@
use axum::{Error, response::Response};
use std::num::NonZero;
use crate::{config::Config, model::inputs::TileInfo, service::Service};
use axum::{
Router, ServiceExt,
extract::{FromRef, Query, State},
http::StatusCode,
response::{Html, IntoResponse},
routing::{get, post},
};
use chrono::{DateTime, Utc};
use lru::LruCache;
use sqlx::postgres::PgPool;
use tower_http::cors::CorsLayer;
#[derive(Clone)]
pub struct AppState {
pub service: Service,
pub pool: PgPool,
pub cache: LruCache<DateTime<Utc>, Vec<u8>>,
}
pub async fn create_router(config: &Config) -> Router {
let pool = PgPool::connect(&config.database_url).await.unwrap();
let cache = LruCache::new(NonZero::new(10).unwrap());
let service = Service::new(pool.clone());
let app_state = AppState {
service,
pool,
cache,
};
Router::new()
.route("/api/v1/health", get(health_handler))
.route("/api/v1/data", get(data_handler))
.layer(CorsLayer::permissive())
.with_state(app_state)
}
async fn health_handler(State(state): State<AppState>) -> impl IntoResponse {
Html("OK")
}
async fn data_handler(
Query(params): Query<TileInfo>,
State(mut state): State<AppState>,
) -> impl IntoResponse {
match chrono::NaiveDateTime::parse_from_str(&params.datetime, "%Y%m%d%H%M%S") {
Ok(naive_datetime) => {
let datetime = naive_datetime.and_utc();
if let Some(tile_info) = state.cache.get(&datetime) {
return Html(format!("tile_info: {:?}", tile_info)).into_response();
} else {
match state.service.get_tile_data(datetime, "").await {
Ok(tile_info) => {
// state.cache.put(datetime, tile_info.clone());
(StatusCode::OK, Html(format!("tile_info: {:?}", tile_info)))
.into_response()
}
Err(e) => {
// 方式1: 返回错误状态码和错误信息
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("数据库错误: {:?}", e),
)
.into_response();
}
}
}
}
Err(e) => {
// 方式2: 返回400错误状态码
return (
StatusCode::BAD_REQUEST,
format!("日期格式错误: {}. 期望格式: YYYYMMDDHHMMSS", e),
)
.into_response();
}
}
}

23
src/config.rs Normal file
View File

@ -0,0 +1,23 @@
use std::env;
#[derive(Debug, Clone)]
pub struct Config {
pub database_url: String,
pub jwt_secret: String,
pub port: u16,
}
impl Config {
pub fn from_env() -> Result<Self, env::VarError> {
dotenvy::dotenv().ok();
Ok(Config {
database_url: env::var("DATABASE_URL")?,
jwt_secret: env::var("JWT_SECRET")?,
port: env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.unwrap_or(3000),
})
}
}

12
src/db/mod.rs Normal file
View File

@ -0,0 +1,12 @@
use chrono::{DateTime, Utc};
use sqlx::prelude::FromRow;
use uuid::Uuid;
#[derive(FromRow)]
pub struct TileInfo {
pub id: Uuid,
pub ingestion_time: DateTime<Utc>,
pub data_time: DateTime<Utc>,
pub source: String,
pub url: String,
}

14
src/main.rs Normal file
View File

@ -0,0 +1,14 @@
mod app;
mod config;
mod db;
mod model;
mod service;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = config::Config::from_env().unwrap();
let app = app::create_router(&config).await;
let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{}", config.port)).await?;
axum::serve(listener, app).await?;
Ok(())
}

40
src/model/inputs.rs Normal file
View File

@ -0,0 +1,40 @@
use std::ops::Deref;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub struct Datetime(String);
impl Deref for Datetime {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for Datetime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'de> serde::Deserialize<'de> for Datetime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let re = Regex::new(r"^[0-9]{14}$").unwrap();
if !re.is_match(&s) {
return Err(serde::de::Error::custom("Invalid datetime format"));
}
Ok(Datetime(s))
}
}
#[derive(Debug, Deserialize)]
pub struct TileInfo {
pub datetime: Datetime,
pub area: Option<String>,
}

1
src/model/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod inputs;

42
src/service/mod.rs Normal file
View File

@ -0,0 +1,42 @@
use axum::Error;
use chrono::{DateTime, Utc};
use sqlx::PgPool;
#[derive(Clone)]
pub struct Service {
pub pool: PgPool,
}
impl Service {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
pub async fn get_tile_data(
&self,
datetime: DateTime<Utc>,
area: &str,
) -> Result<Vec<u8>, Error> {
let primitive_datetime = datetime.format("%Y%m%d%H%M%S").to_string();
let record = sqlx::query!(
r#"
SELECT
id,
ingestion_time,
data_time,
source,
storage_url
FROM data_ingestion
WHERE data_time = $1 AND source = $2
"#,
datetime.naive_utc(),
area
)
.fetch_optional(&self.pool)
.await
.map_err(|e| Error::new(format!("Database error: {}", e)))?;
Ok(Vec::new())
}
}