Init
This commit is contained in:
commit
061de4a1ee
|
@ -0,0 +1,7 @@
|
|||
WEB.ADDR=0.0.0.0:9527
|
||||
|
||||
MYSQL.MAX_CONS=5
|
||||
MYSQL.DSN=mysql://root:mysql123!%40%23@127.0.0.1:3306/study
|
||||
|
||||
# 日志级别
|
||||
RUST_LOG=DEBUG
|
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -0,0 +1,8 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,10 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="50" name="Rust" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/axum-sqlx.iml" filepath="$PROJECT_DIR$/.idea/axum-sqlx.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "axum-sqlx"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = "0.12.1"
|
||||
axum = "0.7.4"
|
||||
chrono = { version = "0.4.33", features = ["serde"] }
|
||||
config = "0.13.4"
|
||||
dotenvy = "0.15.7"
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde_json = "1.0.113"
|
||||
sqlx = { version = "0.7.3", features = ["mysql", "runtime-tokio", "tls-rustls", "chrono", "macros"] }
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
|
@ -0,0 +1,33 @@
|
|||
use serde::Deserialize;
|
||||
use crate::{err::Error, Result};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MysqlConfig {
|
||||
pub dsn: String,
|
||||
pub max_cons: u32,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WebConfig {
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub mysql: MysqlConfig,
|
||||
pub web: WebConfig,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
config::Config::builder()
|
||||
.add_source(config::Environment::default())
|
||||
.build()
|
||||
.map_err(Error::from)?
|
||||
.try_deserialize()
|
||||
.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[web]
|
||||
addr = "0.0.0.0:9527"
|
||||
|
||||
[mysql]
|
||||
max_cons = 5
|
||||
dns = "mysql://root:mysql123!%40%23@127.0.0.1:3306/study"
|
|
@ -0,0 +1,155 @@
|
|||
use crate::{err::Error, model, Result};
|
||||
|
||||
use super::{DEFAULT_PAGE_SIZE, Paginate};
|
||||
|
||||
pub async fn list(
|
||||
conn: &sqlx::MySqlPool,
|
||||
page: u32,
|
||||
) -> Result<Paginate<Vec<model::member::Member>>> {
|
||||
let count: (i64, ) = sqlx::query_as("select count(*) from member")
|
||||
.fetch_one(conn)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let mut q = sqlx::QueryBuilder::new("SELECT * FROM member ORDER BY id DESC");
|
||||
q.push(" LIMIT ")
|
||||
.push_bind(DEFAULT_PAGE_SIZE)
|
||||
.push(" OFFSET ")
|
||||
.push_bind(page * DEFAULT_PAGE_SIZE);
|
||||
|
||||
let data = q
|
||||
.build_query_as()
|
||||
.fetch_all(conn)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
Ok(Paginate::new(count.0 as u32, page, data))
|
||||
}
|
||||
|
||||
|
||||
pub async fn find(conn: &sqlx::MySqlPool, id: u32) -> Result<Option<model::member::Member>> {
|
||||
let m = sqlx::query_as("select * from member where id=?")
|
||||
.bind(id)
|
||||
.fetch_optional(conn)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
|
||||
pub async fn exists(conn: &sqlx::MySqlPool, name: &str, id: Option<u32>) -> Result<bool> {
|
||||
let mut q = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM member WHERE name=");
|
||||
q.push_bind(name);
|
||||
|
||||
if let Some(id) = id {
|
||||
q.push(" AND id!=").push_bind(id);
|
||||
};
|
||||
|
||||
let count: (i64, ) = q.build_query_as().fetch_one(conn).await.map_err(Error::from)?;
|
||||
|
||||
Ok(count.0 > 0)
|
||||
}
|
||||
|
||||
|
||||
pub async fn add(
|
||||
conn: &sqlx::MySqlPool,
|
||||
m: &model::member::Member,
|
||||
) -> Result<u32> {
|
||||
if exists(conn, &m.name, None).await? {
|
||||
return Err(Error::exists("同名的会员已存在"));
|
||||
}
|
||||
|
||||
let id = sqlx::query(
|
||||
"INSERT INTO `member` (name, dateline, balance, types,is_del) VALUES(?,?,?,?,?)",
|
||||
)
|
||||
.bind(&m.name)
|
||||
.bind(&m.dateline)
|
||||
.bind(&m.balance)
|
||||
.bind(&m.types)
|
||||
.bind(&m.is_del)
|
||||
.execute(conn)
|
||||
.await
|
||||
.map_err(Error::from)?
|
||||
.last_insert_id();
|
||||
|
||||
Ok(id as u32)
|
||||
}
|
||||
|
||||
pub async fn edit(conn: &sqlx::MySqlPool, m: &model::member::Member) -> Result<u64> {
|
||||
if exists(conn, &m.name, Some(m.id)).await? {
|
||||
return Err(Error::not_found("同名的会员已存在"));
|
||||
}
|
||||
|
||||
let aff = sqlx::query("UPDATE `member` SET name=?,balance=?,types=? WHERE id=?")
|
||||
.bind(&m.name)
|
||||
.bind(&m.balance)
|
||||
.bind(&m.types)
|
||||
.bind(&m.id)
|
||||
.execute(conn)
|
||||
.await.map_err(Error::from)?
|
||||
.rows_affected();
|
||||
|
||||
Ok(aff)
|
||||
}
|
||||
|
||||
pub async fn del(conn: &sqlx::MySqlPool, id: u32) -> Result<u64> {
|
||||
let aff = sqlx::query("update member set is_del=true where id=?")
|
||||
.bind(id)
|
||||
.execute(conn)
|
||||
.await.map_err(Error::from)?
|
||||
.rows_affected();
|
||||
|
||||
Ok(aff)
|
||||
}
|
||||
|
||||
pub async fn real_del(conn: &sqlx::MySqlPool, id: u32) -> Result<u64> {
|
||||
let aff = sqlx::query("delete from member where id=?")
|
||||
.bind(id)
|
||||
.execute(conn)
|
||||
.await.map_err(Error::from)?
|
||||
.rows_affected();
|
||||
|
||||
Ok(aff)
|
||||
}
|
||||
|
||||
|
||||
pub async fn tran(conn: &sqlx::MySqlPool, t: &model::member::Tran) -> Result<(u64, u64)> {
|
||||
let mut tx = conn.begin().await.map_err(Error::from)?;
|
||||
|
||||
let from_query = sqlx::query("UPDATE member SET balance=balance-? WHERE name=? AND balance>=?")
|
||||
.bind(&t.amount)
|
||||
.bind(&t.from_member)
|
||||
.bind(&t.amount)
|
||||
.execute(&mut *tx) // https://github.com/launchbadge/sqlx/issues/2606
|
||||
.await;
|
||||
|
||||
let from_aff = match from_query {
|
||||
Ok(r) => r.rows_affected(),
|
||||
Err(err) => {
|
||||
tx.rollback().await.map_err(Error::from)?;
|
||||
return Err(Error::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
if from_aff < 1 {
|
||||
tx.rollback().await.map_err(Error::from)?;
|
||||
return Err(Error::tran("转账失败,请检查转出账户是否有足够余额"));
|
||||
}
|
||||
|
||||
let to_aff = match sqlx::query("UPDATE member SET balance=balance+? WHERE name=?")
|
||||
.bind(&t.amount)
|
||||
.bind(&t.to_member)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r.rows_affected(),
|
||||
Err(err) => {
|
||||
tx.rollback().await.map_err(Error::from)?;
|
||||
return Err(Error::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
tx.commit().await.map_err(Error::from)?;
|
||||
|
||||
Ok((from_aff, to_aff))
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use serde::Serialize;
|
||||
|
||||
pub mod member;
|
||||
|
||||
const DEFAULT_PAGE_SIZE: u32 = 30;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Paginate<T: Serialize> {
|
||||
pub total: u32,
|
||||
pub total_page: u32,
|
||||
pub page: u32,
|
||||
pub page_size: u32,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
impl<T: Serialize> Paginate<T> {
|
||||
pub fn new(total: u32, page: u32, data: T) -> Self {
|
||||
let total_page = f64::ceil(total as f64 / DEFAULT_PAGE_SIZE as f64) as u32;
|
||||
Self { total, total_page, page, page_size: DEFAULT_PAGE_SIZE, data }
|
||||
}
|
||||
|
||||
pub fn has_prev(&self) -> bool {
|
||||
self.page > 0
|
||||
}
|
||||
|
||||
pub fn has_next(&self) -> bool {
|
||||
self.page < self.last_page()
|
||||
}
|
||||
|
||||
pub fn last_page(&self) -> u32 {
|
||||
self.total_page - 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorKind {
|
||||
Database,
|
||||
Config,
|
||||
Template,
|
||||
NotFound,
|
||||
Exists,
|
||||
Tran,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
pub message: String,
|
||||
pub cause: Option<Box<dyn std::error::Error>>,
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
self.message.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(kind: ErrorKind, message: String, cause: Option<Box<dyn std::error::Error>>) -> Self {
|
||||
Self { kind, message, cause }
|
||||
}
|
||||
|
||||
pub fn with_cause(cause: Box<dyn std::error::Error>, kind: ErrorKind) -> Self {
|
||||
Self::new(kind, cause.to_string(), Some(cause))
|
||||
}
|
||||
|
||||
pub fn not_found(msg: &str) -> Self {
|
||||
Self::new(ErrorKind::NotFound, msg.to_string(), None)
|
||||
}
|
||||
|
||||
pub fn exists(msg: &str) -> Self {
|
||||
Self::new(ErrorKind::Exists, msg.to_string(), None)
|
||||
}
|
||||
|
||||
pub fn tran(msg: &str) -> Self {
|
||||
Self::new(ErrorKind::Tran, msg.to_string(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
Self::with_cause(Box::new(err), ErrorKind::Database)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<config::ConfigError> for Error {
|
||||
fn from(err: config::ConfigError) -> Self {
|
||||
Self::with_cause(Box::new(err), ErrorKind::Config)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<askama::Error> for Error {
|
||||
fn from(err: askama::Error) -> Self {
|
||||
Self::with_cause(Box::new(err), ErrorKind::Template)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::model::member::MemberTypes;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AddAndEdit {
|
||||
pub name: String,
|
||||
pub balance: u32,
|
||||
pub types: MemberTypes,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Tran {
|
||||
pub from_member: String,
|
||||
pub to_member: String,
|
||||
pub amount: u32,
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::Form;
|
||||
use axum::http::{header, HeaderMap, StatusCode};
|
||||
use axum::response::Html;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{db::member, err::Error, form, model, model::state::AppState, Result, view};
|
||||
|
||||
fn get_conn(state: &AppState) -> Arc<sqlx::MySqlPool> {
|
||||
state.pool.clone()
|
||||
}
|
||||
|
||||
|
||||
fn redirect(url: &str) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(header::LOCATION, url.parse().unwrap());
|
||||
|
||||
Ok((StatusCode::FOUND, header, ()))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PageQuery {
|
||||
pub page: Option<u32>,
|
||||
pub msg: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
pub async fn index(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PageQuery>,
|
||||
) -> Result<Html<String>> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
let p = member::list(&conn, q.page.unwrap_or(0)).await?;
|
||||
|
||||
let tpl = view::Home { p, msg: q.msg };
|
||||
let html = tpl.render().map_err(Error::from)?;
|
||||
Ok(Html(html))
|
||||
}
|
||||
|
||||
pub async fn detail(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<u32>,
|
||||
) -> Result<Html<String>> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
let m = member::find(&conn, id).await?;
|
||||
|
||||
match m {
|
||||
None => Err(Error::not_found("不存在的会员")),
|
||||
Some(m) => {
|
||||
let tpl = view::Detail { m };
|
||||
let html = tpl.render().map_err(Error::from)?;
|
||||
Ok(Html(html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn add_ui() -> Result<Html<String>> {
|
||||
let tpl = view::Add;
|
||||
let html = tpl.render().map_err(Error::from)?;
|
||||
Ok(Html(html))
|
||||
}
|
||||
|
||||
|
||||
pub async fn add(
|
||||
State(state): State<AppState>,
|
||||
Form(frm): Form<form::AddAndEdit>,
|
||||
) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
member::add(&conn, &model::member::Member {
|
||||
name: frm.name,
|
||||
balance: frm.balance,
|
||||
types: frm.types,
|
||||
dateline: chrono::Local::now(),
|
||||
..Default::default()
|
||||
}).await?;
|
||||
|
||||
redirect("/?msg=会员添加成功")
|
||||
}
|
||||
|
||||
|
||||
pub async fn edit_ui(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<u32>,
|
||||
) -> Result<Html<String>> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
let m = member::find(&conn, id).await?;
|
||||
|
||||
match m {
|
||||
Some(m) => {
|
||||
let tpl = view::Edit { m };
|
||||
let html = tpl.render().map_err(Error::from)?;
|
||||
Ok(Html(html))
|
||||
}
|
||||
None => Err(Error::not_found("不存在的会员")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn edit(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<u32>,
|
||||
Form(frm): Form<form::AddAndEdit>,
|
||||
) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
member::edit(&conn, &model::member::Member {
|
||||
id,
|
||||
name: frm.name,
|
||||
balance: frm.balance,
|
||||
types: frm.types,
|
||||
..Default::default()
|
||||
}).await?;
|
||||
|
||||
redirect("/?msg=会员修改成功")
|
||||
}
|
||||
|
||||
pub async fn del(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<u32>,
|
||||
) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
member::del(&conn, id).await?;
|
||||
|
||||
redirect("/?msg=逻辑删除成功")
|
||||
}
|
||||
|
||||
pub async fn real_del(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<u32>,
|
||||
) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
member::real_del(&conn, id).await?;
|
||||
|
||||
redirect("/?msg=物理删除成功")
|
||||
}
|
||||
|
||||
|
||||
pub async fn tran_ui() -> Result<Html<String>> {
|
||||
let tpl = view::Tran;
|
||||
let html = tpl.render().map_err(Error::from)?;
|
||||
Ok(Html(html))
|
||||
}
|
||||
|
||||
|
||||
pub async fn tran(
|
||||
State(state): State<AppState>,
|
||||
Form(frm): Form<form::Tran>,
|
||||
) -> Result<(StatusCode, HeaderMap, ())> {
|
||||
let conn = get_conn(&state);
|
||||
|
||||
let aff = member::tran(&conn, &model::member::Tran {
|
||||
from_member: frm.from_member,
|
||||
to_member: frm.to_member,
|
||||
amount: frm.amount,
|
||||
}).await?;
|
||||
|
||||
tracing::debug!("{:?}", aff);
|
||||
|
||||
redirect("/?msg=转账成功")
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod err;
|
||||
pub mod handler;
|
||||
pub mod model;
|
||||
pub mod view;
|
||||
pub mod form;
|
||||
pub type Result<T> = std::result::Result<T, crate::err::Error>;
|
|
@ -0,0 +1,46 @@
|
|||
#![allow(unused, dead_code)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
|
||||
use axum_sqlx::{config::AppConfig, handler, model::state::AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// 解析 .env 文件
|
||||
dotenvy::dotenv().expect("解析.env文件失败");
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let cfg = AppConfig::from_env()
|
||||
.map_err(|e| tracing::error!("初始化配置失败:{}", e.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let pool = MySqlPoolOptions::new()
|
||||
.max_connections(cfg.mysql.max_cons)
|
||||
.connect(&cfg.mysql.dsn)
|
||||
.await
|
||||
.map_err(|e| tracing::error!("数据库连接失败:{}", e.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(handler::index))
|
||||
.route("/detail/:id", get(handler::detail))
|
||||
.route("/add", get(handler::add_ui).post(handler::add))
|
||||
.route("/edit/:id", get(handler::edit_ui).post(handler::edit))
|
||||
.route("/del/:id", get(handler::del))
|
||||
.route("/real_del/:id", get(handler::real_del))
|
||||
.route("/tran", get(handler::tran_ui).post(handler::tran))
|
||||
.with_state(AppState {
|
||||
pool: Arc::new(pool)
|
||||
});
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&cfg.web.addr).await.unwrap();
|
||||
|
||||
tracing::info!("服务器运行于: {}", listener.local_addr().unwrap());
|
||||
|
||||
axum::serve(listener, app).await.unwrap()
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, sqlx::Type, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum MemberTypes {
|
||||
#[default]
|
||||
/// 普通会员
|
||||
Normal,
|
||||
/// 白银会员
|
||||
Silver,
|
||||
/// 黄金会员
|
||||
Gold,
|
||||
/// 钻石会员
|
||||
Diamond,
|
||||
}
|
||||
|
||||
impl Display for MemberTypes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
&Self::Normal => String::from("普通会员"),
|
||||
&Self::Silver => String::from("白银会员"),
|
||||
&Self::Gold => String::from("黄金会员"),
|
||||
&Self::Diamond => String::from("钻石会员"),
|
||||
};
|
||||
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)]
|
||||
pub struct Member {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub dateline: chrono::DateTime<chrono::Local>,
|
||||
pub balance: u32,
|
||||
pub types: MemberTypes,
|
||||
pub is_del: bool,
|
||||
}
|
||||
|
||||
|
||||
pub struct Tran {
|
||||
pub from_member: String,
|
||||
pub to_member: String,
|
||||
pub amount: u32,
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod member;
|
||||
pub mod state;
|
|
@ -0,0 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
// 最外层的state必须实现Clone
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: Arc<sqlx::MySqlPool>,
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use askama::Template;
|
||||
use crate::{db::Paginate, model};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct Home {
|
||||
pub p: Paginate<Vec<model::member::Member>>,
|
||||
pub msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "detail.html")]
|
||||
pub struct Detail {
|
||||
pub m: model::member::Member,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "add.html")]
|
||||
pub struct Add;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "edit.html")]
|
||||
pub struct Edit {
|
||||
pub m: model::member::Member,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "tran.html")]
|
||||
pub struct Tran;
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "./base.html" %} {%block title%}添加{%endblock%} {%block content%}
|
||||
<form method="post" autocomplete="off" action="/add">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">会员名称</label>
|
||||
<input type="text" class="form-control" id="name" name="name" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="balance" class="form-label">账户余额</label>
|
||||
<input type="number" class="form-control" id="balance" name="balance" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="types" class="form-label">会员类型</label>
|
||||
<select class="form-select" name="types" id="types">
|
||||
<option value="Normal">普通会员</option>
|
||||
<option value="Silver">白银会员</option>
|
||||
<option value="Gold">黄金会员</option>
|
||||
<option value="Diamond">钻石会员</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
</form>
|
||||
{%endblock%}
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>AXUM使用SQLx - AXUM中文网</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav
|
||||
class="navbar bg-dark border-bottom border-bottom-dark navbar-expand-lg"
|
||||
data-bs-theme="dark"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="https://axum.rs" target="_blank">
|
||||
<img
|
||||
src="https://file.axum.rs/asset/logo.png"
|
||||
alt="Logo"
|
||||
width="30"
|
||||
class="d-inline-block align-text-middle"
|
||||
/>
|
||||
AXUM.RS
|
||||
</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">列表</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/add">添加</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tran">转账</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container my-3">
|
||||
<h1>{%block title%}{%endblock%}</h1>
|
||||
<div>{%block content%}{%endblock%}</div>
|
||||
</div>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "./base.html" %} {%block title%}详情{%endblock%} {%block content%}
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>会员名称</th>
|
||||
<td>{{m.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>账户余额</th>
|
||||
<td>{{m.balance}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>会员类型</th>
|
||||
<td>{{m.types}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>加入时间</th>
|
||||
<td>{{m.dateline}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="my-3 text-center">
|
||||
<button class="btn btn-primary" onclick="history.back()">返回</button>
|
||||
</div>
|
||||
{%endblock%}
|
|
@ -0,0 +1,50 @@
|
|||
{% extends "./base.html" %} {%block title%}修改{%endblock%} {%block content%}
|
||||
<form method="post" autocomplete="off" action="/edit/{{m.id}}">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">会员名称</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{m.name}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="balance" class="form-label">账户余额</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="balance"
|
||||
name="balance"
|
||||
value="{{m.balance}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="types" class="form-label">会员类型</label>
|
||||
<select class="form-select" name="types" id="types">
|
||||
{%match m.types %} {%when crate::model::member::MemberTypes::Normal%}
|
||||
<option value="Normal" selected>普通会员</option>
|
||||
{%else%}
|
||||
<option value="Normal">普通会员</option>
|
||||
{%endmatch%} {%match m.types %} {%when
|
||||
crate::model::member::MemberTypes::Silver%}
|
||||
<option value="Silver" selected>白银会员</option>
|
||||
{%else%}
|
||||
<option value="Silver">白银会员</option>
|
||||
{%endmatch%} {%match m.types %} {%when
|
||||
crate::model::member::MemberTypes::Gold%}
|
||||
<option value="Gold" selected>黄金会员</option>
|
||||
{%else%}
|
||||
<option value="Gold">黄金会员</option>
|
||||
{%endmatch%} {%match m.types %} {%when
|
||||
crate::model::member::MemberTypes::Diamond%}
|
||||
<option value="Diamond" selected>钻石会员</option>
|
||||
{%else%}
|
||||
<option value="Diamond">钻石会员</option>
|
||||
{%endmatch%}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
</form>
|
||||
{%endblock%}
|
|
@ -0,0 +1,94 @@
|
|||
{% extends "./base.html" %} {%block title%}列表{%endblock%} {%block content%}
|
||||
{%match msg%} {%when Some with (msg)%}
|
||||
<div class="alert alert-success" role="alert">{{msg}}</div>
|
||||
{%when None%} {%endmatch%}
|
||||
|
||||
<div class="my-3">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">名称</th>
|
||||
<th scope="col">余额</th>
|
||||
<th scope="col">类型</th>
|
||||
<th scope="col">时间</th>
|
||||
<th scope="col">已删除</th>
|
||||
<th scope="col">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%for m in p.data%}
|
||||
<tr>
|
||||
<th scope="row">{{ m.id }}</th>
|
||||
<td>{{ m.name }}</td>
|
||||
<td>{{ m.balance }}</td>
|
||||
<td>{{ m.types }}</td>
|
||||
<td>{{ m.dateline }}</td>
|
||||
<td>
|
||||
{% if m.is_del %}
|
||||
<span class="badge text-bg-danger">是</span> {%else%}
|
||||
<span class="badge text-bg-success">否</span> {%endif%}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-secondary btn-sm" href="/detail/{{m.id}}">详情</a>
|
||||
<a class="btn btn-primary btn-sm" href="/edit/{{m.id}}">修改</a>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="/del/{{m.id}}"
|
||||
onclick="if(!confirm('确定删除?')){return false;}"
|
||||
>逻辑删除</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="/real_del/{{m.id}}"
|
||||
onclick="if(!confirm('确定删除?')){return false;}"
|
||||
>物理删除</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{%endfor%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if p.total_page > 0 %}
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
<li class="page-item">
|
||||
{%if p.has_prev()%}
|
||||
<a class="page-link" href="?page={{p.page-1}}">上一页</a>
|
||||
{%else%}
|
||||
<a class="page-link" href="?page=0">上一页</a>
|
||||
{%endif%}
|
||||
</li>
|
||||
{%for i in 0..p.total_page%}
|
||||
<li class="page-item{%if p.page==i%} active{%endif%}">
|
||||
<a class="page-link" href="?page={{i}}">{{i+1}}</a>
|
||||
</li>
|
||||
{%endfor%}
|
||||
<li class="page-item">
|
||||
{%if p.has_next()%}
|
||||
<a class="page-link" href="?page={{p.page+1}}">下一页</a>
|
||||
{%else%}
|
||||
<a class="page-link" href="?page={{p.last_page()}}">下一页</a>
|
||||
{%endif%}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{%endif%} {%endblock%}
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "./base.html" %} {%block title%}转账{%endblock%} {%block content%}
|
||||
<form method="post" autocomplete="off" action="/tran">
|
||||
<div class="mb-3">
|
||||
<label for="from_member" class="form-label">转账人</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="from_member"
|
||||
name="from_member"
|
||||
placeholder="输入转账人的会员名称,比如:张三"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to_member" class="form-label">收款人</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="to_member"
|
||||
name="to_member"
|
||||
placeholder="输入收款人的会员名称,比如:李四"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">转账金额</label>
|
||||
<input type="number" class="form-control" id="amount" name="amount" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
</form>
|
||||
{%endblock%}
|
Loading…
Reference in New Issue