添加总计

This commit is contained in:
wjsjwr 2025-02-10 22:40:23 +08:00
parent 0ee56aad24
commit 4b052d8b2a
8 changed files with 146 additions and 87 deletions

View File

@ -10,13 +10,15 @@
<cargoProject FILE="$PROJECT_DIR$/bookkeeper/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="更新版本号和部署脚本">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="清仓筛选">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/migration/src/m20220101_000001_create_table.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/migration/src/m20220101_000001_create_table.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/migration/src/m20241230_160326_gain_evolution.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/migration/src/m20241230_160326_gain_evolution.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/nav.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/nav.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -83,7 +85,7 @@
</component>
<component name="RunManager" selected="Cargo.Run bookkeeper">
<configuration name="Run bookkeeper" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="release" />
<option name="buildProfileId" value="dev" />
<option name="command" value="run --package bookkeeper --bin bookkeeper" />
<option name="workingDirectory" value="file://$PROJECT_DIR$/bookkeeper" />
<envs />
@ -154,7 +156,9 @@
<workItem from="1735470376833" duration="16014000" />
<workItem from="1735553493290" duration="16207000" />
<workItem from="1736238929025" duration="646000" />
<workItem from="1736239592084" duration="1828000" />
<workItem from="1736239592084" duration="2278000" />
<workItem from="1737809478583" duration="76000" />
<workItem from="1739197188908" duration="1079000" />
</task>
<task id="LOCAL-00001" summary="Add bookkeeper">
<option name="closed" value="true" />
@ -252,7 +256,15 @@
<option name="project" value="LOCAL" />
<updated>1736241063964</updated>
</task>
<option name="localTasksCounter" value="13" />
<task id="LOCAL-00013" summary="清仓筛选">
<option name="closed" value="true" />
<created>1736241771673</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1736241771673</updated>
</task>
<option name="localTasksCounter" value="14" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -282,7 +294,8 @@
<MESSAGE value="Refine gain algorithm and some UI update" />
<MESSAGE value="Filter by name" />
<MESSAGE value="更新版本号和部署脚本" />
<option name="LAST_COMMIT_MESSAGE" value="更新版本号和部署脚本" />
<MESSAGE value="清仓筛选" />
<option name="LAST_COMMIT_MESSAGE" value="清仓筛选" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

2
bookkeeper/Cargo.lock generated
View File

@ -423,7 +423,7 @@ dependencies = [
[[package]]
name = "bookkeeper"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"chrono",
"entity",

View File

@ -1,6 +1,6 @@
[package]
name = "bookkeeper"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
[dependencies]

View File

@ -6,36 +6,37 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(pk_auto(User::Id))
.col(string(User::Name))
.col(string(User::Password))
.col(string(User::SessionToken))
.to_owned(),
)
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(pk_auto(User::Id))
.col(string(User::Name))
.col(string(User::Password))
.col(string(User::SessionToken))
.to_owned(),
)
.await?;
manager.create_table(
Table::create()
.table(Transaction::Table)
.if_not_exists()
.col(pk_auto(Transaction::Id))
.col(integer(Transaction::Code))
.col(string(Transaction::Name))
.col(date(Transaction::Date))
.col(decimal(Transaction::Buy))
.col(integer(Transaction::Volume))
.col(decimal(Transaction::NetBuy))
.col(json_binary_null(Transaction::SplitPieces))
.col(decimal_null(Transaction::Gain))
.col(decimal_null(Transaction::NetGain))
.col(boolean(Transaction::IsDone))
.to_owned(),
)
manager
.create_table(
Table::create()
.table(Transaction::Table)
.if_not_exists()
.col(pk_auto(Transaction::Id))
.col(integer(Transaction::Code))
.col(string(Transaction::Name))
.col(date(Transaction::Date))
.col(decimal(Transaction::Buy))
.col(integer(Transaction::Volume))
.col(decimal(Transaction::NetBuy))
.col(json_binary_null(Transaction::SplitPieces))
.col(decimal_null(Transaction::Gain))
.col(decimal_null(Transaction::NetGain))
.col(boolean(Transaction::IsDone))
.to_owned(),
)
.await?;
Ok(())
@ -80,4 +81,4 @@ enum Transaction {
Gain,
NetGain,
IsDone,
}
}

View File

@ -1,11 +1,11 @@
use std::str::FromStr;
use sea_orm_migration::prelude::*;
use std::str::FromStr;
use rust_decimal::Decimal;
use sea_orm::entity::prelude::*;
use sea_orm::ActiveValue::Set;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq,)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "transaction")]
pub struct Model {
#[sea_orm(primary_key)]
@ -45,11 +45,14 @@ impl MigrationTrait for Migration {
let mut total_net_sell = Decimal::from_str("0.0").unwrap();
for piece in pieces {
let p = piece.as_object().unwrap();
total_sell += Decimal::from_str(p.get("sell").unwrap().as_str().unwrap()).unwrap() *
Decimal::from(p.get("volume").unwrap().as_i64().unwrap());
total_net_sell += Decimal::from_str(p.get("net_sell").unwrap().as_str().unwrap()).unwrap();
total_sell += Decimal::from_str(p.get("sell").unwrap().as_str().unwrap()).unwrap()
* Decimal::from(p.get("volume").unwrap().as_i64().unwrap());
total_net_sell +=
Decimal::from_str(p.get("net_sell").unwrap().as_str().unwrap()).unwrap();
}
r.gain = Set(Some(total_sell - r.buy.clone().unwrap() * Decimal::from(r.volume.clone().unwrap())));
r.gain = Set(Some(
total_sell - r.buy.clone().unwrap() * Decimal::from(r.volume.clone().unwrap()),
));
r.net_gain = Set(Some(total_net_sell - r.net_buy.clone().unwrap()));
r.update(db).await?;
}

View File

@ -9,6 +9,8 @@ use entity::transaction::Entity as TE;
use migration::MigratorTrait;
use rocket::fairing::{self, AdHoc};
use rocket::form::Form;
use rocket::fs::FileServer;
use rocket::http::Status;
use rocket::response::{Flash, Redirect};
use rocket::{Build, Rocket};
use rocket_dyn_templates::{context, Engines, Template};
@ -17,11 +19,9 @@ use sea_orm::query::*;
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectOptions, DatabaseConnection, EntityTrait};
use sea_orm_rocket::{rocket::figment::Figment, Config, Connection, Database};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::time::Duration;
use rocket::http::Status;
use serde::{Deserialize, Serialize};
use rocket::fs::FileServer;
#[derive(sea_orm_rocket::Database, Debug)]
#[database("bookkeeper")]
@ -58,21 +58,27 @@ impl sea_orm_rocket::Pool for SeaOrmPool {
}
#[get("/?<last>&<name>&<is_done>")]
async fn index(conn: Connection<'_, Db>, last: Option<u32>, name: Option<&str>, is_done: Option<bool>) -> Template {
async fn index(
conn: Connection<'_, Db>,
last: Option<u32>,
name: Option<&str>,
is_done: Option<bool>,
) -> Template {
let mut partial = TE::find();
if let Some(last) = last {
partial = partial.filter(transaction::Column::Date.gte(Local::now() - TimeDelta::days(last as i64)));
partial = partial
.filter(transaction::Column::Date.gte(Local::now() - TimeDelta::days(last as i64)));
}
if let Some(name) = name {
partial = partial.filter(transaction::Column::Name.eq(name));
}
if let Some(done) = is_done {
partial = partial.filter(transaction::Column::IsDone.eq(done));
}
let rows: Vec<transaction::Model> = partial
.order_by_desc(transaction::Column::Date)
.order_by_desc(transaction::Column::Id)
@ -81,11 +87,37 @@ async fn index(conn: Connection<'_, Db>, last: Option<u32>, name: Option<&str>,
.unwrap();
let version = env!("CARGO_PKG_VERSION");
let mut bought = Decimal::from(0);
let mut net_bought = Decimal::from(0);
let mut gain = Decimal::from(0);
let mut net_gain = Decimal::from(0);
for r in &rows {
if let Some(g) = r.gain {
bought += r.buy * Decimal::from(r.volume);
gain += g;
}
if let Some(ng) = r.net_gain {
net_bought += r.net_buy;
net_gain += ng;
}
}
Template::render("index", context! {
rows: rows,
version: version,
})
let pct = gain / bought;
let net_pct = net_gain / net_bought;
Template::render(
"index",
context! {
rows: rows,
version: version,
gain: gain,
net_gain: net_gain,
pct: pct,
net_pct: net_pct,
},
)
}
#[derive(FromForm, Debug)]
@ -110,7 +142,7 @@ struct SplitPieceForm<'r> {
}
#[derive(Deserialize, Serialize)]
struct SplitPiece{
struct SplitPiece {
date: NaiveDate,
sell: Decimal,
net_sell: Decimal,
@ -119,35 +151,37 @@ struct SplitPiece{
#[get("/tx/<id>")]
async fn get_tx(conn: Connection<'_, Db>, id: i32) -> Result<Template, Status> {
let row = TE::find_by_id(id)
.one(conn.into_inner())
.await
.unwrap();
let row = TE::find_by_id(id).one(conn.into_inner()).await.unwrap();
match row {
None => { Err(Status::NotFound) },
Some(row) => {
Ok(Template::render("tx", context! {
None => Err(Status::NotFound),
Some(row) => Ok(Template::render(
"tx",
context! {
r: row,
target: "/tx/".to_owned() + id.to_string().as_str(),
}))
}
},
)),
}
}
#[post("/tx/<id>", data = "<data>")]
async fn post_tx(data: Form<TransModel<'_>>, conn: Connection<'_, Db>, id: i32) -> Result<Redirect, Status> {
async fn post_tx(
data: Form<TransModel<'_>>,
conn: Connection<'_, Db>,
id: i32,
) -> Result<Redirect, Status> {
let mut record = translate(data);
record.id = Set(id);
match record.update(conn.into_inner()).await {
Err(_) => { Err(Status::NotFound) },
Ok(_) => { Ok(Redirect::to("/")) },
Err(_) => Err(Status::NotFound),
Ok(_) => Ok(Redirect::to("/")),
}
}
#[delete("/tx/<id>")]
async fn delete_tx(conn: Connection<'_, Db>, id: i32) -> Result<Redirect, Status> {
match TE::delete_by_id(id).exec(conn.into_inner()).await {
Err(_) => { Err(Status::InternalServerError) },
Err(_) => Err(Status::InternalServerError),
Ok(res) => {
if res.rows_affected == 1 {
Ok(Redirect::to("/"))
@ -160,10 +194,13 @@ async fn delete_tx(conn: Connection<'_, Db>, id: i32) -> Result<Redirect, Status
#[get("/add")]
async fn get_add() -> Template {
Template::render("tx", context! {
r: (),
target: "/add",
})
Template::render(
"tx",
context! {
r: (),
target: "/add",
},
)
}
#[post("/add", data = "<trans>")]
@ -191,7 +228,7 @@ fn translate(trans: Form<TransModel>) -> transaction::ActiveModel {
if let Some(split_pieces) = &trans.split_pieces {
let mut sp: Vec<SplitPiece> = Vec::new();
for split_piece in split_pieces.iter() {
sp.push(SplitPiece{
sp.push(SplitPiece {
date: NaiveDate::parse_from_str(split_piece.date, "%Y-%m-%d").unwrap(),
sell: Decimal::from_str(split_piece.sell).unwrap(),
net_sell: Decimal::from_str(split_piece.net_sell).unwrap(),
@ -199,7 +236,7 @@ fn translate(trans: Form<TransModel>) -> transaction::ActiveModel {
})
}
let total: i32 = sp.iter().fold(0, |acc, e| { acc + e.volume });
let total: i32 = sp.iter().fold(0, |acc, e| acc + e.volume);
if total == trans.volume {
record.is_done = Set(true);
let sell_int = sp.iter().fold(Decimal::new(0, 4), |acc, e| {
@ -208,7 +245,7 @@ fn translate(trans: Form<TransModel>) -> transaction::ActiveModel {
let buy = Decimal::from_str(trans.buy).unwrap() * Decimal::from(total);
record.gain = Set(Some(sell_int - buy));
let net_sell_int = sp.iter().fold(Decimal::from(0), |acc, e| { acc + e.net_sell });
let net_sell_int = sp.iter().fold(Decimal::from(0), |acc, e| acc + e.net_sell);
let net_buy = Decimal::from_str(trans.net_buy).unwrap();
record.net_gain = Set(Some(net_sell_int - net_buy));
}
@ -232,14 +269,19 @@ async fn rocket() -> _ {
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
.attach(Template::custom(|engines: &mut Engines| {
engines.minijinja.add_filter("padLeft", |value: String, length: usize| -> String {
if value.len() < length {
"0".repeat(length - value.len()) + value.as_str()
} else {
value
}
})
engines
.minijinja
.add_filter("padLeft", |value: String, length: usize| -> String {
if value.len() < length {
"0".repeat(length - value.len()) + value.as_str()
} else {
value
}
})
}))
.mount("/", routes![index, add, get_add, get_tx, post_tx, delete_tx])
.mount(
"/",
routes![index, add, get_add, get_tx, post_tx, delete_tx],
)
.mount("/static", FileServer::from("static"))
}
}

View File

@ -15,8 +15,8 @@
<th>卖出价</th>
<th>卖量</th>
<th><abbr title="卖出金额-佣金-各种手续费">实际卖出</abbr></th>
<th>收益</th>
<th>实际收益</th>
<th>收益: {{ gain|float|round(2) }} ({{ ((pct|float)*100)|round(2) }} %)</th>
<th>实际收益: {{ net_gain|float|round(2) }} ({{ ((net_pct|float)*100)|round(2) }} %)</th>
<th>操作</th>
</tr>
</thead>

View File

@ -36,7 +36,7 @@
<th><label class="label">卖出日期</label></th>
<th><label class="label">卖出价</label></th>
<th><label class="label">成交量</label></th>
<th><label class="label">实际卖出</label></th>
<th><label class="label">实际卖出</label></th>
<th style="min-width: 4rem; vertical-align: middle;">操作</th>
</tr>
</thead>